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

Merge branch 'master' into i/6007

# Conflicts:
#	packages/ckeditor5-markdown-gfm/package.json
Maciej Gołaszewski 5 лет назад
Родитель
Сommit
3014d1e588
100 измененных файлов с 3279 добавлено и 1006 удалено
  1. 39 39
      CHANGELOG.md
  2. 0 8
      docs/_snippets/features/wproofreader.html
  3. 47 1
      docs/_snippets/features/wproofreader.js
  4. 2 2
      docs/builds/guides/development/custom-builds.md
  5. 8 2
      docs/builds/guides/frameworks/vuejs.md
  6. 3 3
      docs/builds/guides/integration/advanced-setup.md
  7. 137 136
      docs/builds/guides/integration/content-styles.md
  8. 5 4
      docs/builds/guides/integration/installing-plugins.md
  9. 1 1
      docs/builds/guides/migrate.md
  10. 2 2
      docs/features/image-upload.md
  11. 58 39
      docs/features/spelling-and-grammar-checking.md
  12. 1 11
      docs/features/ui-language.md
  13. 19 19
      docs/framework/guides/architecture/editing-engine.md
  14. 1 1
      docs/framework/guides/contributing/development-environment.md
  15. 2 2
      docs/framework/guides/quick-start.md
  16. 10 9
      package.json
  17. 1 1
      packages/ckeditor5-adapter-ckfinder/lang/translations/ko.po
  18. 4 4
      packages/ckeditor5-alignment/lang/translations/ko.po
  19. 1 1
      packages/ckeditor5-alignment/lang/translations/zh.po
  20. 3 2
      packages/ckeditor5-autoformat/docs/features/autoformat.md
  21. 2 2
      packages/ckeditor5-autoformat/tests/blockautoformatediting.js
  22. 1 1
      packages/ckeditor5-autosave/lang/translations/ko.po
  23. 1 1
      packages/ckeditor5-basic-styles/lang/translations/ko.po
  24. 1 1
      packages/ckeditor5-ckfinder/docs/features/ckfinder.md
  25. 37 0
      packages/ckeditor5-ckfinder/lang/translations/ko.po
  26. 23 0
      packages/ckeditor5-clipboard/src/clipboard.js
  27. 110 0
      packages/ckeditor5-clipboard/tests/clipboard.js
  28. 25 0
      packages/ckeditor5-code-block/lang/translations/ko.po
  29. 25 0
      packages/ckeditor5-code-block/lang/translations/zh.po
  30. 1 0
      packages/ckeditor5-code-block/package.json
  31. 3 37
      packages/ckeditor5-code-block/src/converters.js
  32. 62 0
      packages/ckeditor5-code-block/tests/codeblock-integration.js
  33. 1 1
      packages/ckeditor5-core/lang/translations/ko.po
  34. 2 1
      packages/ckeditor5-core/src/editor/utils/dataapimixin.js
  35. 0 0
      packages/ckeditor5-engine/docs/_snippets/framework/build-custom-element-converter-source.html
  36. 14 0
      packages/ckeditor5-engine/docs/_snippets/framework/build-custom-element-converter-source.js
  37. 1 1
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-unsafe-link-class.html
  38. 2 2
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-div-attributes.html
  39. 61 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-custom-element-converter.html
  40. 165 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-custom-element-converter.js
  41. 53 54
      packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion-extending-output.md
  42. 10 10
      packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion-introduction.md
  43. 43 43
      packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion-preserving-custom-content.md
  44. 310 0
      packages/ckeditor5-engine/docs/framework/guides/deep-dive/custom-element-conversion.md
  45. 312 11
      packages/ckeditor5-engine/docs/framework/guides/deep-dive/schema.md
  46. 33 16
      packages/ckeditor5-engine/src/controller/datacontroller.js
  47. 1 1
      packages/ckeditor5-engine/src/controller/editingcontroller.js
  48. 62 57
      packages/ckeditor5-engine/src/conversion/downcastdispatcher.js
  49. 75 54
      packages/ckeditor5-engine/src/conversion/downcasthelpers.js
  50. 239 85
      packages/ckeditor5-engine/src/conversion/upcastdispatcher.js
  51. 78 100
      packages/ckeditor5-engine/src/conversion/upcasthelpers.js
  52. 4 4
      packages/ckeditor5-engine/src/dev-utils/model.js
  53. 2 2
      packages/ckeditor5-engine/src/dev-utils/view.js
  54. 2 2
      packages/ckeditor5-engine/src/model/documentselection.js
  55. 1 1
      packages/ckeditor5-engine/src/model/markercollection.js
  56. 15 8
      packages/ckeditor5-engine/src/model/model.js
  57. 81 2
      packages/ckeditor5-engine/src/model/schema.js
  58. 77 0
      packages/ckeditor5-engine/src/model/utils/autoparagraphing.js
  59. 16 13
      packages/ckeditor5-engine/src/model/utils/modifyselection.js
  60. 12 13
      packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js
  61. 15 14
      packages/ckeditor5-engine/src/view/domconverter.js
  62. 21 20
      packages/ckeditor5-engine/src/view/downcastwriter.js
  63. 21 21
      packages/ckeditor5-engine/src/view/rawelement.js
  64. 126 2
      packages/ckeditor5-engine/tests/controller/datacontroller.js
  65. 40 64
      packages/ckeditor5-engine/tests/conversion/downcasthelpers.js
  66. 279 5
      packages/ckeditor5-engine/tests/conversion/upcastdispatcher.js
  67. 54 9
      packages/ckeditor5-engine/tests/conversion/upcasthelpers.js
  68. 26 0
      packages/ckeditor5-engine/tests/model/documentselection.js
  69. 25 3
      packages/ckeditor5-engine/tests/model/model.js
  70. 113 0
      packages/ckeditor5-engine/tests/model/schema.js
  71. 1 1
      packages/ckeditor5-engine/tests/model/selection.js
  72. 8 3
      packages/ckeditor5-engine/tests/model/utils/deletecontent.js
  73. 70 0
      packages/ckeditor5-engine/tests/model/utils/modifyselection.js
  74. 30 4
      packages/ckeditor5-engine/tests/model/utils/selection-post-fixer.js
  75. 1 0
      packages/ckeditor5-enter/package.json
  76. 1 1
      packages/ckeditor5-enter/src/shiftenter.js
  77. 22 3
      packages/ckeditor5-enter/tests/shiftenter-integration.js
  78. 4 4
      packages/ckeditor5-font/lang/translations/ko.po
  79. 1 1
      packages/ckeditor5-font/lang/translations/zh.po
  80. 1 1
      packages/ckeditor5-font/src/fontfamily/fontfamilyediting.js
  81. 1 1
      packages/ckeditor5-font/src/fontsize/fontsizeediting.js
  82. 1 1
      packages/ckeditor5-font/src/utils.js
  83. 1 1
      packages/ckeditor5-font/tests/utils.js
  84. 7 7
      packages/ckeditor5-heading/lang/translations/ko.po
  85. 2 2
      packages/ckeditor5-heading/lang/translations/zh.po
  86. 2 3
      packages/ckeditor5-heading/src/title.js
  87. 53 0
      packages/ckeditor5-highlight/lang/translations/ko.po
  88. 1 1
      packages/ckeditor5-highlight/lang/translations/zh.po
  89. 21 0
      packages/ckeditor5-horizontal-line/lang/translations/zh.po
  90. 9 9
      packages/ckeditor5-horizontal-line/src/horizontallineediting.js
  91. 1 1
      packages/ckeditor5-horizontal-line/tests/horizontallinecommand.js
  92. 2 2
      packages/ckeditor5-image/docs/_snippets/features/image-caption.html
  93. 2 2
      packages/ckeditor5-image/docs/_snippets/features/image-link.html
  94. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-resize-buttons-dropdown.html
  95. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-resize-buttons.html
  96. 2 2
      packages/ckeditor5-image/docs/_snippets/features/image-resize-px.html
  97. 2 2
      packages/ckeditor5-image/docs/_snippets/features/image-resize.html
  98. 2 2
      packages/ckeditor5-image/docs/_snippets/features/image-style-presentational.html
  99. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-style.html
  100. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-toolbar.html

+ 39 - 39
CHANGELOG.md

@@ -7,25 +7,25 @@ Changelog
 
 We are happy to announce the release of CKEditor 5 v21.0.0.
 
-This release packs quite a few all around improvements, including:
+This release packs quite a few all-around improvements, including:
 
-* [A convenient UI for changing the image's width to a predefined size](https://github.com/ckeditor/ckeditor5/issues/5201).
-* [Autolinking URLs and emails in editor content](https://github.com/ckeditor/ckeditor5/issues/4715).
-* [Distinguishing between inside and outside `<code>`](https://github.com/ckeditor/ckeditor5/issues/6722).
+* [A convenient UI for changing the image width to a predefined size](https://github.com/ckeditor/ckeditor5/issues/5201).
+* [Autolinking URLs and e-mails in the editor content](https://github.com/ckeditor/ckeditor5/issues/4715).
+* [Distinguishing between the inside and the outside of `<code>`](https://github.com/ckeditor/ckeditor5/issues/6722).
 * [Better experience when replacing (typing over) a link text](https://github.com/ckeditor/ckeditor5/issues/4762).
 
-We fixed also a handful of bug fixes, for example:
+We have also fixed a handful of bugs, for example:
 
-* Calling [the `editor.setData()` will now also clear the undo stack](https://github.com/ckeditor/ckeditor5/issues/4060).
-* [Linking to a part of to-do list item](https://github.com/ckeditor/ckeditor5/issues/5779).
+* Calling the [`editor.setData()` method will now also clear the undo stack](https://github.com/ckeditor/ckeditor5/issues/4060).
+* [Linking to a part of a to-do list item](https://github.com/ckeditor/ckeditor5/issues/5779).
 * [Automatic link decorators in case of a linked image](https://github.com/ckeditor/ckeditor5/issues/7519).
 
-Finally, we also took care of some developer experience-oriented improvements:
+Finally, we also took care of some of the developer experience-oriented improvements:
 
-* [We changed marker conversion, so that it doesn't break HTML structure in some cases](https://github.com/ckeditor/ckeditor5/issues/7556).
+* [We changed marker conversion so that it does not break the HTML structure in some cases](https://github.com/ckeditor/ckeditor5/issues/7556).
 * Introduced a new [`RawElement`](https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_view_rawelement-RawElement.html) class to make it simpler to [implement features like "embedding raw HTML"](https://github.com/ckeditor/ckeditor5/issues/4469).
 
-Please note that there are some **major breaking changes**, be sure to review them before upgrading.
+Please note that there are some **major breaking changes**. Be sure to review them before upgrading.
 
 Blog post link coming soon…
 
@@ -36,36 +36,36 @@ The CKEditor 5 Collaboration features changelog can be found here: https://ckedi
 ### MAJOR BREAKING CHANGES [ℹ️](https://ckeditor.com/docs/ckeditor5/latest/framework/guides/support/versioning-policy.html#major-and-minor-breaking-changes)
 
 * The `editor.setData()` method now clears the undo and redo stacks.
-* **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: The [`Text#is()`](https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_model_text-Text.html#function-is) and [`TextProxy#is()`](https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_model_textproxy-TextProxy.html#function-is) methods (in the model and view) now expect to be called with `'$text'` instead of `'text'` and `'$textProxy'` instead of `'textProxy'`.
-* **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: The `is()` method (e.g. [`Element#is()`](https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_model_element-Element.html#function-is), [`Text#is()`](https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_model_text-Text.html#function-is), [`AttributeElement#is()`](https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_view_attributeelement-AttributeElement.html#function-is) or [`ContainerElement#is()`](https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_view_containerelement-ContainerElement.html#function-is)) in both, the model and the view, no longer treats the first argument as an element name. To check the element's name use the second argument instead (`node.is( 'element', 'paragraph' )` instead of `node.is( 'paragraph' )`).
-* **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: Marker conversion to data has been revamped. The data format changed, the new conversion helpers were introduced and a new rule has been implemented that a comma (`,`) is not allowed in a marker name. See the GitHub issue for a [walkthrough and example migration path](https://github.com/ckeditor/ckeditor5/issues/7556#issuecomment-665579653).
-* **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: The `DomConverter#getParentUIElement()` method was renamed to `DomConverter#getHostViewElement()` because now it supports both `UIElement` and `RawElement` (see [#4469](https://github.com/ckeditor/ckeditor5/issues/4469)).
+* **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: The [`Text#is()`](https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_model_text-Text.html#function-is) and [`TextProxy#is()`](https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_model_textproxy-TextProxy.html#function-is) methods (in the model and the view) now expect to be called with `'$text'` instead of `'text'` and `'$textProxy'` instead of `'textProxy'`.
+* **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: The `is()` method (e.g. [`Element#is()`](https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_model_element-Element.html#function-is), [`Text#is()`](https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_model_text-Text.html#function-is), [`AttributeElement#is()`](https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_view_attributeelement-AttributeElement.html#function-is) or [`ContainerElement#is()`](https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_view_containerelement-ContainerElement.html#function-is)) in both the model and the view no longer treats the first argument as an element name. To check the element name, use the second argument instead (`node.is( 'element', 'paragraph' )` instead of `node.is( 'paragraph' )`).
+* **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: The marker-to-data conversion was revamped. The data format changed, the new conversion helpers were introduced and a new rule was implemented that a comma (`,`) is not allowed in the marker name. See the GitHub issue for a [walkthrough and example migration path](https://github.com/ckeditor/ckeditor5/issues/7556#issuecomment-665579653).
+* **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: The `DomConverter#getParentUIElement()` method was renamed to [`DomConverter#getHostViewElement()`](https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_view_domconverter-DomConverter.html#function-getHostViewElement) because now it supports both `UIElement` and `RawElement` (see [#4469](https://github.com/ckeditor/ckeditor5/issues/4469)).
 
 ### MINOR BREAKING CHANGES [ℹ️](https://ckeditor.com/docs/ckeditor5/latest/framework/guides/support/versioning-policy.html#major-and-minor-breaking-changes)
 
-* **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: The `bindTwoStepCaretToAttribute()` utility function has been removed. Use `editor.plugins.get( TwoStepCaretMovement ).registerAttribute()` instead.
+* **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: The `bindTwoStepCaretToAttribute()` utility function was removed. Use `editor.plugins.get( TwoStepCaretMovement ).registerAttribute()` instead.
 * **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: The `findAncestor()` utility function was removed.
-* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: The parameters of `TableUtils#createTable()` has changed. Use `options` object to pass number of `rows` and `columns`.
-* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: The `removeEmptyRows()` and `removeEmptyRowsColumns()` utility functions does not require the `batch` parameter any more.
-* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: The `downcastTableHeadingRowsChange()` downcast converter has been removed. It is no longer possible to override `headingRows` attribute change in a single converter. This behavior can be customized using a table downcast converter. See [#7601](https://github.com/ckeditor/ckeditor5/issues/7601).
+* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: The parameters of `TableUtils#createTable()` have changed. Use the `options` object to pass the number of `rows` and `columns`.
+* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: The `removeEmptyRows()` and `removeEmptyRowsColumns()` utility functions do not require the `batch` parameter anymore.
+* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: The `downcastTableHeadingRowsChange()` downcast converter was removed. It is no longer possible to override the `headingRows` attribute change in a single converter. This behavior can be customized using the table downcast converter. See [#7601](https://github.com/ckeditor/ckeditor5/issues/7601).
 
 ### Features
 
 * **[autoformat](https://www.npmjs.com/package/@ckeditor/ckeditor5-autoformat)**: Block autoformat can also be triggered in blocks other than a paragraph. Closes [#6170](https://github.com/ckeditor/ckeditor5/issues/6170). ([commit](https://github.com/ckeditor/ckeditor5/commit/5866d4199dad1b70b5329c83dd4b3974716f04a5))
-* **[autoformat](https://www.npmjs.com/package/@ckeditor/ckeditor5-autoformat)**: Enabled autoformat feature also for blocks that are not empty. ([commit](https://github.com/ckeditor/ckeditor5/commit/5866d4199dad1b70b5329c83dd4b3974716f04a5))
+* **[autoformat](https://www.npmjs.com/package/@ckeditor/ckeditor5-autoformat)**: Enabled the autoformatting feature also for blocks that are not empty. ([commit](https://github.com/ckeditor/ckeditor5/commit/5866d4199dad1b70b5329c83dd4b3974716f04a5))
 * **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: Implemented the view `RawElement`. Added the `DowncastWriter#createRawElement()` method. Closes [#4469](https://github.com/ckeditor/ckeditor5/issues/4469). ([commit](https://github.com/ckeditor/ckeditor5/commit/bff38e366517a2801ffdd136bcff3afbfe671fd6))
-* **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: The method `DataController#set()` is now decorated so plugins can listen to `editor.setData()` calls. ([commit](https://github.com/ckeditor/ckeditor5/commit/4a12d38094803f62d351e467a37ecba2b9c957fd))
-* **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: Introduced a new marker conversion helpers that produce semantic HTML data output. See `DowncastHelpers#markerToData()` and `UpcastHelpers#dataToMarker()`. Closes [#7556](https://github.com/ckeditor/ckeditor5/issues/7556). ([commit](https://github.com/ckeditor/ckeditor5/commit/b68d310d7ca779c2e6da5072e46fb5a13fb1e4f0))
+* **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: The `DataController#set()` method is now decorated so plugins can listen to `editor.setData()` calls. ([commit](https://github.com/ckeditor/ckeditor5/commit/4a12d38094803f62d351e467a37ecba2b9c957fd))
+* **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: Introduced new marker conversion helpers that produce semantic HTML data output. See `DowncastHelpers#markerToData()` and `UpcastHelpers#dataToMarker()`. Closes [#7556](https://github.com/ckeditor/ckeditor5/issues/7556). ([commit](https://github.com/ckeditor/ckeditor5/commit/b68d310d7ca779c2e6da5072e46fb5a13fb1e4f0))
 * **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: Added model `Position#findAncestor()` and `Element#findAncestor()` methods. Closes [#3233](https://github.com/ckeditor/ckeditor5/issues/3233). ([commit](https://github.com/ckeditor/ckeditor5/commit/a349af57c6a0ceeea1f7cfebf28a138065f15189))
 * **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: Changed the visibility scope of `Mapper#findPositionIn()` from `private` to `public`. ([commit](https://github.com/ckeditor/ckeditor5/commit/3d260151f833e84cbdccc9deeff6415ae8b0c6e1))
-* **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: Added `Range#getJoined()` method for joining ranges. ([commit](https://github.com/ckeditor/ckeditor5/commit/1264e63947c88123fdb2b9a8c301d100476e83a8))
-* **[image](https://www.npmjs.com/package/@ckeditor/ckeditor5-image)**: Introduced the UI for a manual image resizing via a dropdown or standalone buttons. Closes [#5201](https://github.com/ckeditor/ckeditor5/issues/5201). ([commit](https://github.com/ckeditor/ckeditor5/commit/70e0b4102511a272cfef710379e8fcde40e53ac6))
-* **[image](https://www.npmjs.com/package/@ckeditor/ckeditor5-image)**: Introduced the UI for restoring original image size.  Closes [#5197](https://github.com/ckeditor/ckeditor5/issues/5197). ([commit](https://github.com/ckeditor/ckeditor5/commit/70e0b4102511a272cfef710379e8fcde40e53ac6))
-* **[link](https://www.npmjs.com/package/@ckeditor/ckeditor5-link)**: Added an icon indicating in the top-right corner of an image that informs the image is linked. Closes [#7457](https://github.com/ckeditor/ckeditor5/issues/7457). ([commit](https://github.com/ckeditor/ckeditor5/commit/9887b7fcf148a72ad393c05f7278cf572c62a31a))
+* **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: Added the `Range#getJoined()` method for joining ranges. ([commit](https://github.com/ckeditor/ckeditor5/commit/1264e63947c88123fdb2b9a8c301d100476e83a8))
+* **[image](https://www.npmjs.com/package/@ckeditor/ckeditor5-image)**: Introduced the UI for manual image resizing via a dropdown or standalone buttons. Closes [#5201](https://github.com/ckeditor/ckeditor5/issues/5201). ([commit](https://github.com/ckeditor/ckeditor5/commit/70e0b4102511a272cfef710379e8fcde40e53ac6))
+* **[image](https://www.npmjs.com/package/@ckeditor/ckeditor5-image)**: Introduced the UI for restoring the original image size.  Closes [#5197](https://github.com/ckeditor/ckeditor5/issues/5197). ([commit](https://github.com/ckeditor/ckeditor5/commit/70e0b4102511a272cfef710379e8fcde40e53ac6))
+* **[link](https://www.npmjs.com/package/@ckeditor/ckeditor5-link)**: Added an icon in the top-right corner of an image indicating that the image is linked. Closes [#7457](https://github.com/ckeditor/ckeditor5/issues/7457). ([commit](https://github.com/ckeditor/ckeditor5/commit/9887b7fcf148a72ad393c05f7278cf572c62a31a))
 * **[link](https://www.npmjs.com/package/@ckeditor/ckeditor5-link)**: Typing over the selected link will not remove the link itself. Instead, the typed text will replace the link text. Closes [#4762](https://github.com/ckeditor/ckeditor5/issues/4762). ([commit](https://github.com/ckeditor/ckeditor5/commit/de476bb365aabb17d81f18cbe27d47b4baa32a0d))
-* **[link](https://www.npmjs.com/package/@ckeditor/ckeditor5-link)**: Added the `AutoLink` feature which replaces a plain text with a link if typed or pasted content is the link. Closes [#4715](https://github.com/ckeditor/ckeditor5/issues/4715). ([commit](https://github.com/ckeditor/ckeditor5/commit/c3f307848dbefdd943376d06dcdc750e1f97eed9))
-* **[page-break](https://www.npmjs.com/package/@ckeditor/ckeditor5-page-break)**: Added support for pasting page breaks from MS Word. Closes [#2508](https://github.com/ckeditor/ckeditor5/issues/2508). ([commit](https://github.com/ckeditor/ckeditor5/commit/d921aabf5e57e0daafec4e0be086b4ff18493c2d))
-* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Added option to set heading rows and columns for `insertTable` command and `TableUtils#createTable()`. Closes [#6768](https://github.com/ckeditor/ckeditor5/issues/6768). ([commit](https://github.com/ckeditor/ckeditor5/commit/392f61ffd1681ad6c5d7994d2339f46e317064bb))
+* **[link](https://www.npmjs.com/package/@ckeditor/ckeditor5-link)**: Added the `AutoLink` feature which replaces a plain text with a URL or e-mail address if the typed or pasted content is a link. Closes [#4715](https://github.com/ckeditor/ckeditor5/issues/4715). ([commit](https://github.com/ckeditor/ckeditor5/commit/c3f307848dbefdd943376d06dcdc750e1f97eed9))
+* **[page-break](https://www.npmjs.com/package/@ckeditor/ckeditor5-page-break)**: Added support for pasting page breaks from Microsoft Word. Closes [#2508](https://github.com/ckeditor/ckeditor5/issues/2508). ([commit](https://github.com/ckeditor/ckeditor5/commit/d921aabf5e57e0daafec4e0be086b4ff18493c2d))
+* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Added an option to set heading rows and columns for the `insertTable` command and `TableUtils#createTable()`. Closes [#6768](https://github.com/ckeditor/ckeditor5/issues/6768). ([commit](https://github.com/ckeditor/ckeditor5/commit/392f61ffd1681ad6c5d7994d2339f46e317064bb))
 * **[typing](https://www.npmjs.com/package/@ckeditor/ckeditor5-typing)**: Introduced the `TwoStepCaretMovement` plugin. See [#7444](https://github.com/ckeditor/ckeditor5/issues/7444). ([commit](https://github.com/ckeditor/ckeditor5/commit/d40bd5832084821b54c6962462ec909e47e28168))
 * **[utils](https://www.npmjs.com/package/@ckeditor/ckeditor5-utils)**: Introduced the `Collection#addMany()` method for adding multiple items in a single call. Closes [#7627](https://github.com/ckeditor/ckeditor5/issues/7627). ([commit](https://github.com/ckeditor/ckeditor5/commit/a1f0efd3c09fe52b73dca92107ef035175704d31))
 * **[utils](https://www.npmjs.com/package/@ckeditor/ckeditor5-utils)**: Introduced the `Collection#change` event. See [#7627](https://github.com/ckeditor/ckeditor5/issues/7627). ([commit](https://github.com/ckeditor/ckeditor5/commit/a1f0efd3c09fe52b73dca92107ef035175704d31))
@@ -77,11 +77,11 @@ The CKEditor 5 Collaboration features changelog can be found here: https://ckedi
 * **[link](https://www.npmjs.com/package/@ckeditor/ckeditor5-link)**: After backspacing into a link, the caret should still stay outside the link. Closes [#7521](https://github.com/ckeditor/ckeditor5/issues/7521). ([commit](https://github.com/ckeditor/ckeditor5/commit/c175e1c62d358a58dddd24e048cf71aa1603781e))
 * **[link](https://www.npmjs.com/package/@ckeditor/ckeditor5-link)**: Manual and automatic decorators will work properly with a link on an image. Closes [#7519](https://github.com/ckeditor/ckeditor5/issues/7519). ([commit](https://github.com/ckeditor/ckeditor5/commit/d38b5e526709d69024df3bc1ca0ebf7cf10306b0))
 * **[link](https://www.npmjs.com/package/@ckeditor/ckeditor5-link)**: Fake visual selection should not be added to the editor's data. Closes [#7614](https://github.com/ckeditor/ckeditor5/issues/7614). ([commit](https://github.com/ckeditor/ckeditor5/commit/84e2042181fdff0d60d97e6bcbf0a6d26a9c9f41))
-* **[list](https://www.npmjs.com/package/@ckeditor/ckeditor5-list)**: Editor should not crash on the <kbd>Enter</kbd> keypress inside a to-do list item containing soft-breaks. Closes [#5866](https://github.com/ckeditor/ckeditor5/issues/5866), [#6585](https://github.com/ckeditor/ckeditor5/issues/6585). ([commit](https://github.com/ckeditor/ckeditor5/commit/3d260151f833e84cbdccc9deeff6415ae8b0c6e1))
+* **[list](https://www.npmjs.com/package/@ckeditor/ckeditor5-list)**: The editor should not crash on the <kbd>Enter</kbd> keypress inside a to-do list item containing soft-breaks. Closes [#5866](https://github.com/ckeditor/ckeditor5/issues/5866), [#6585](https://github.com/ckeditor/ckeditor5/issues/6585). ([commit](https://github.com/ckeditor/ckeditor5/commit/3d260151f833e84cbdccc9deeff6415ae8b0c6e1))
 * **[list](https://www.npmjs.com/package/@ckeditor/ckeditor5-list)**: Links inside a to-do list item should be properly converted to HTML. Closes [#5779](https://github.com/ckeditor/ckeditor5/issues/5779). ([commit](https://github.com/ckeditor/ckeditor5/commit/3d260151f833e84cbdccc9deeff6415ae8b0c6e1))
 * **[media-embed](https://www.npmjs.com/package/@ckeditor/ckeditor5-media-embed)**: The editor's placeholder should disappear after inserting media into an empty editor. Closes [#1684](https://github.com/ckeditor/ckeditor5/issues/1684). ([commit](https://github.com/ckeditor/ckeditor5/commit/bff38e366517a2801ffdd136bcff3afbfe671fd6))
-* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Pasting a table into an existing table should not set multi-cell selection if the `TableSelection` plugin is disabled. Closes [#7486](https://github.com/ckeditor/ckeditor5/issues/7486). ([commit](https://github.com/ckeditor/ckeditor5/commit/e50a4e19ddbdf2e90f08da0f568916d117f1fdea))
-* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Pasting a table into an existing table with headings should not break table layout. Closes [#7453](https://github.com/ckeditor/ckeditor5/issues/7453). ([commit](https://github.com/ckeditor/ckeditor5/commit/df4485fb17e28f2ddb2d3c24253c2b23c9e11249))
+* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Pasting a table into an existing table should not set the multi-cell selection if the `TableSelection` plugin is disabled. Closes [#7486](https://github.com/ckeditor/ckeditor5/issues/7486). ([commit](https://github.com/ckeditor/ckeditor5/commit/e50a4e19ddbdf2e90f08da0f568916d117f1fdea))
+* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Pasting a table into an existing table with headings should not break the table layout. Closes [#7453](https://github.com/ckeditor/ckeditor5/issues/7453). ([commit](https://github.com/ckeditor/ckeditor5/commit/df4485fb17e28f2ddb2d3c24253c2b23c9e11249))
 * **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: The table structure should not be changed when removing the heading row. Closes [#7454](https://github.com/ckeditor/ckeditor5/issues/7454), [#7601](https://github.com/ckeditor/ckeditor5/issues/7601). ([commit](https://github.com/ckeditor/ckeditor5/commit/8b83c9bcdd09e5d66c66df35fd2ee8252cecc26e))
 * **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Merging cells of multiple whole rows or columns should not crash the editor. ([commit](https://github.com/ckeditor/ckeditor5/commit/8b83c9bcdd09e5d66c66df35fd2ee8252cecc26e))
 * **[ui](https://www.npmjs.com/package/@ckeditor/ckeditor5-ui)**: Removing the first hidden (grouped) toolbar button should not throw an exception. Closes [#7655](https://github.com/ckeditor/ckeditor5/issues/7655). ([commit](https://github.com/ckeditor/ckeditor5/commit/266dfda77fe51ca824195e22d84ad7517840777d))
@@ -89,21 +89,21 @@ The CKEditor 5 Collaboration features changelog can be found here: https://ckedi
 * **[widget](https://www.npmjs.com/package/@ckeditor/ckeditor5-widget)**: `Resizer#redraw()` should not change the editing view unless a different size should be set. Closes [#7633](https://github.com/ckeditor/ckeditor5/issues/7633). ([commit](https://github.com/ckeditor/ckeditor5/commit/978dd711c9db4022cfc89eff4a1de4f148bd65c8))
 * **[widget](https://www.npmjs.com/package/@ckeditor/ckeditor5-widget)**: Triple-clicking inside an image caption should not crash the editor in Firefox. Closes [#7542](https://github.com/ckeditor/ckeditor5/issues/7542). ([commit](https://github.com/ckeditor/ckeditor5/commit/ef4b1f92dbd5a816a6be49f997726df1fd7d6eae))
 * **[widget](https://www.npmjs.com/package/@ckeditor/ckeditor5-widget)**: Triple-clicking a link inside an image caption should not crash the editor in Safari. Closes [#6021](https://github.com/ckeditor/ckeditor5/issues/6021). ([commit](https://github.com/ckeditor/ckeditor5/commit/ef4b1f92dbd5a816a6be49f997726df1fd7d6eae))
-* **[widget](https://www.npmjs.com/package/@ckeditor/ckeditor5-widget)**: The resizing mechanism will not trigger other `view.Document#mousedown` events. Thanks to that while resizing an image inside a cell, the mouse will not trigger the table's actions. Closes [#6755](https://github.com/ckeditor/ckeditor5/issues/6755). ([commit](https://github.com/ckeditor/ckeditor5/commit/27fce4e3c37bd52da6cad913defa6571618bd350))
+* **[widget](https://www.npmjs.com/package/@ckeditor/ckeditor5-widget)**: The resizing mechanism will not trigger other `view.Document#mousedown` events. Thanks to that, when resizing an image inside a cell, the mouse will not trigger the table's actions. Closes [#6755](https://github.com/ckeditor/ckeditor5/issues/6755). ([commit](https://github.com/ckeditor/ckeditor5/commit/27fce4e3c37bd52da6cad913defa6571618bd350))
 
 ### Other changes
 
 * **[core](https://www.npmjs.com/package/@ckeditor/ckeditor5-core)**: Added icons that represent different sizes of an object (`object-size-*.svg`) (see [#7559](https://github.com/ckeditor/ckeditor5/issues/7559)). ([commit](https://github.com/ckeditor/ckeditor5/commit/565628a6e6faa0efdeb4aee7c6a9b63e8a429dd7))
-* **[core](https://www.npmjs.com/package/@ckeditor/ckeditor5-core)**: The `Editor`, `CommandCollection` and `MultiCommand`'s `execute()` method will the result of the called `command.execute()`. Closes [#7647](https://github.com/ckeditor/ckeditor5/issues/7647). ([commit](https://github.com/ckeditor/ckeditor5/commit/152ffc911c5345c8a5ac8536a48458847414c72c))
-* **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: Changed arguments of `Element#is()`, `Text#is()`, `TextProxy#is()`, `AttributeElement#is()`, `ContainerElement#is()`, `EditableElement#is()`, `EmptyElement#is()`, `UIElement#is()` methods and all it's usages. Closes [#7608](https://github.com/ckeditor/ckeditor5/issues/7608). ([commit](https://github.com/ckeditor/ckeditor5/commit/dbee47989aad166fff054e55cd294446772153af))
-* **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: Added `model.Schema` instance to downcast conversion API, available under `conversionApi.schema`. ([commit](https://github.com/ckeditor/ckeditor5/commit/b68d310d7ca779c2e6da5072e46fb5a13fb1e4f0))
-* **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: `UpcastHelpers#elementToMarker()` is now deprecated. Use `UpcastHelpers#dataToMarker()` instead. `DowncastHelpers#markerToElement()` should be used only for editing downcast. ([commit](https://github.com/ckeditor/ckeditor5/commit/b68d310d7ca779c2e6da5072e46fb5a13fb1e4f0))
-* **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: Table cells should not be filled with single spaces while pasting table with empty cells. Closes [#7487](https://github.com/ckeditor/ckeditor5/issues/7487). ([commit](https://github.com/ckeditor/ckeditor5/commit/284c7c1b4f3fba9d4133db273706a15db7454725))
+* **[core](https://www.npmjs.com/package/@ckeditor/ckeditor5-core)**: The `Editor`, `CommandCollection` and `MultiCommand`'s `execute()` method will return the result of the called `command.execute()`. Closes [#7647](https://github.com/ckeditor/ckeditor5/issues/7647). ([commit](https://github.com/ckeditor/ckeditor5/commit/152ffc911c5345c8a5ac8536a48458847414c72c))
+* **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: Changed arguments of the `Element#is()`, `Text#is()`, `TextProxy#is()`, `AttributeElement#is()`, `ContainerElement#is()`, `EditableElement#is()`, `EmptyElement#is()`, `UIElement#is()` methods and all their usages. Closes [#7608](https://github.com/ckeditor/ckeditor5/issues/7608). ([commit](https://github.com/ckeditor/ckeditor5/commit/dbee47989aad166fff054e55cd294446772153af))
+* **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: Added the `model.Schema` instance to the downcast conversion API, available under `conversionApi.schema`. ([commit](https://github.com/ckeditor/ckeditor5/commit/b68d310d7ca779c2e6da5072e46fb5a13fb1e4f0))
+* **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: `UpcastHelpers#elementToMarker()` is now deprecated. Use `UpcastHelpers#dataToMarker()` instead. `DowncastHelpers#markerToElement()` should only be used for editing downcast. ([commit](https://github.com/ckeditor/ckeditor5/commit/b68d310d7ca779c2e6da5072e46fb5a13fb1e4f0))
+* **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: Table cells should not be filled with single spaces when pasting a table with empty cells. Closes [#7487](https://github.com/ckeditor/ckeditor5/issues/7487). ([commit](https://github.com/ckeditor/ckeditor5/commit/284c7c1b4f3fba9d4133db273706a15db7454725))
 * **[engine](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)**: The `bindTwoStepCaretToAttribute()` engine's utility was removed. See [#7444](https://github.com/ckeditor/ckeditor5/issues/7444). ([commit](https://github.com/ckeditor/ckeditor5/commit/d40bd5832084821b54c6962462ec909e47e28168))
-* **[image](https://www.npmjs.com/package/@ckeditor/ckeditor5-image)**: Allow to configure `ImageResize` in a more granular way. For example, by combining `ImageResizeEditing` with `ImageResizeHandles` or `ImageResizeButtons` to resize image by handles or by image toolbar UI components (dropdown or standalone buttons) respectively. Closes [#7579](https://github.com/ckeditor/ckeditor5/issues/7579). ([commit](https://github.com/ckeditor/ckeditor5/commit/3396d4e4c0e481b6c7927c73b88e065d61e81e49))
+* **[image](https://www.npmjs.com/package/@ckeditor/ckeditor5-image)**: Allow to configure `ImageResize` in a more granular way. For example, by combining `ImageResizeEditing` with `ImageResizeHandles` or `ImageResizeButtons` to resize an image with handles or with the image toolbar UI components (dropdown or standalone buttons) respectively. Closes [#7579](https://github.com/ckeditor/ckeditor5/issues/7579). ([commit](https://github.com/ckeditor/ckeditor5/commit/3396d4e4c0e481b6c7927c73b88e065d61e81e49))
 * **[image](https://www.npmjs.com/package/@ckeditor/ckeditor5-image)**: Image alignment styles (`alignLeft`, `alignCenter` and `alignRight`) no longer set `max-width: 50%` of the `<figure>` element.  If you wish them to still do so, add [these styles](https://github.com/ckeditor/ckeditor5/pull/7625/files#diff-960e3b5e24794dab54cce5dd955c2db2L11-L16) to your content styles. ([commit](https://github.com/ckeditor/ckeditor5/commit/a4cbcf11c0f387ad815a94b6a39b8932387d9ec8))
 * **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Restoring the document selection to the ranges as they were before undoing table cells merge. Closes [#6639](https://github.com/ckeditor/ckeditor5/issues/6639). ([commit](https://github.com/ckeditor/ckeditor5/commit/1264e63947c88123fdb2b9a8c301d100476e83a8))
-* **[ui](https://www.npmjs.com/package/@ckeditor/ckeditor5-ui)**: Improved toolbar rendering time when multiple items are added or removed at once (e.g. during editor initialization). Closes [#6194](https://github.com/ckeditor/ckeditor5/issues/6194). ([commit](https://github.com/ckeditor/ckeditor5/commit/266dfda77fe51ca824195e22d84ad7517840777d))
+* **[ui](https://www.npmjs.com/package/@ckeditor/ckeditor5-ui)**: Improved toolbar rendering time when multiple items are added or removed at once (e.g. during the editor initialization). Closes [#6194](https://github.com/ckeditor/ckeditor5/issues/6194). ([commit](https://github.com/ckeditor/ckeditor5/commit/266dfda77fe51ca824195e22d84ad7517840777d))
 * Link's attribute element highlight is now `inlineHighlight()` - a public utility. ([commit](https://github.com/ckeditor/ckeditor5/commit/fc59dc4f9790c709fda493e00e6db41f4a1ae6be))
 
 ### Released packages

+ 0 - 8
docs/_snippets/features/wproofreader.html

@@ -1,11 +1,3 @@
-<script type="text/javascript" src="https://svc.webspellchecker.net/spellcheck31/wscbundle/wscbundle.js"></script>
-<script>
-	window.WEBSPELLCHECKER_CONFIG = {
-		autoSearch: true,
-		enableGrammar: true,
-		serviceId: '1:Eebp63-lWHbt2-ASpHy4-AYUpy2-fo3mk4-sKrza1-NsuXy4-I1XZC2-0u2F54-aqYWd1-l3Qf14-umd'
-	};
-</script>
 <div id="snippet-wproofreader">
 	<p>Typos hapen. We striving to correct them. Hover on the marked words for instant correction suggestions or click the dialog icon in the bottom right corner to have the whole text proofread at once.</p>
 	<p>You can also paste your own text here to have its spelling and grammar checked.</p>

+ 47 - 1
docs/_snippets/features/wproofreader.js

@@ -3,15 +3,61 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-/* globals ClassicEditor, console, window, document */
+/* globals console, window, document */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage';
+import WProofreader from '@webspellchecker/wproofreader-ckeditor5/src/wproofreader';
 
 import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-wproofreader' ), {
+		plugins: [ ArticlePluginSet, EasyImage, WProofreader ],
+		wproofreader: {
+			serviceId: '1:Eebp63-lWHbt2-ASpHy4-AYUpy2-fo3mk4-sKrza1-NsuXy4-I1XZC2-0u2F54-aqYWd1-l3Qf14-umd',
+			srcUrl: 'https://svc.webspellchecker.net/spellcheck31/wscbundle/wscbundle.js'
+		},
 		cloudServices: CS_CONFIG,
 		toolbar: {
+			items: [
+				'heading',
+				'|',
+				'bold',
+				'italic',
+				'bulletedList',
+				'numberedList',
+				'|',
+				'outdent',
+				'indent',
+				'|',
+				'blockQuote',
+				'link',
+				'mediaEmbed',
+				'insertTable',
+				'|',
+				'undo',
+				'redo'
+			],
 			viewportTopOffset: window.getViewportTopOffsetConfig()
+		},
+		image: {
+			styles: [
+				'full',
+				'alignLeft',
+				'alignRight'
+			],
+			toolbar: [
+				'imageStyle:alignLeft',
+				'imageStyle:full',
+				'imageStyle:alignRight',
+				'|',
+				'imageTextAlternative'
+			]
+		},
+		table: {
+			contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ]
 		}
 	} )
 	.then( editor => {

+ 2 - 2
docs/builds/guides/development/custom-builds.md

@@ -29,8 +29,8 @@ Some of the reasons for creating custom builds are:
 
 In order to start developing CKEditor 5 you will require:
 
-* [Node.js](https://nodejs.org/en/) 6.9.0+
-* npm 4+ (**note:** some npm 5+ versions were known to cause [problems](https://github.com/npm/npm/issues/16991), especially with deduplicating packages; upgrade npm when in doubt)
+* [Node.js](https://nodejs.org/en/) 12.0.0+
+* npm 5.7.1+ (**note:** some npm 5+ versions were known to cause [problems](https://github.com/npm/npm/issues/16991), especially with deduplicating packages; upgrade npm when in doubt)
 * [Git](https://git-scm.com/)
 
 ## Forking an existing build

+ 8 - 2
docs/builds/guides/frameworks/vuejs.md

@@ -216,7 +216,10 @@ module.exports = {
 			// CKEditor needs its own plugin to be built using webpack.
 			new CKEditorWebpackPlugin( {
 				// See https://ckeditor.com/docs/ckeditor5/latest/features/ui-language.html
-				language: 'en'
+				language: 'en',
+
+				// Append translations to the file matching the `app` name.
+				translationsOutputFile: /app/
 			} )
 		]
 	},
@@ -455,7 +458,10 @@ module.exports = {
 			// CKEditor needs its own plugin to be built using webpack.
 			new CKEditorWebpackPlugin( {
 				// The UI language. Language codes follow the https://en.wikipedia.org/wiki/ISO_639-1 format.
-				language: 'de'
+				language: 'de',
+
+				// Append translations to the file matching the `app` name.
+				translationsOutputFile: /app/
 			} )
 		]
 	},

+ 3 - 3
docs/builds/guides/integration/advanced-setup.md

@@ -13,8 +13,8 @@ In this guide, we would like to show you ways to closer integrate CKEditor 5 wit
 
 In order to start developing CKEditor 5 you will require:
 
-* [Node.js](https://nodejs.org/en/) 6.9.0+
-* npm 4+ (**note:** some npm 5+ versions were known to cause [problems](https://github.com/npm/npm/issues/16991), especially with deduplicating packages; upgrade npm when in doubt)
+* [Node.js](https://nodejs.org/en/) 12.0.0+
+* npm 5.7.1+ (**note:** some npm 5+ versions were known to cause [problems](https://github.com/npm/npm/issues/16991), especially with deduplicating packages; upgrade npm when in doubt)
 * [Git](https://git-scm.com/)
 
 ## Bundler
@@ -513,7 +513,7 @@ If you want to load two different editors on one page you need to make sure that
 
 There is no limit for how many editor classes a single build can export. By default, the official builds export a single editor class only. However, they can easily import more.
 
-You can start from forking (or copying) an existing build like in the {@link builds/guides/development/custom-builds "Creating custom builds"} guide. Let's say you forked and cloned the [`ckeditor5`](http://github.com/ckeditor /ckeditor5) repository and want to add {@link module:editor-inline/inlineeditor~InlineEditor} to the classic build:
+You can start from forking (or copying) an existing build like in the {@link builds/guides/development/custom-builds "Creating custom builds"} guide. Let's say you forked and cloned the [`ckeditor5`](http://github.com/ckeditor/ckeditor5) repository and want to add {@link module:editor-inline/inlineeditor~InlineEditor} to the classic build:
 
 ```bash
 git clone -b stable git@github.com:<your-username>/ckeditor5.git

+ 137 - 136
docs/builds/guides/integration/content-styles.md

@@ -48,12 +48,14 @@ Below there is a full list of content styles used by the editor features. You ca
 
 ```css
 /*
- * CKEditor 5 (v19.1.1) content styles.
- * Generated on Fri, 19 Jun 2020 01:26:44 GMT.
+ * CKEditor 5 (v21.0.0) content styles.
+ * Generated on Wed, 29 Jul 2020 12:14:43 GMT.
  * For more information, check out https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/content-styles.html
  */
 
 :root {
+	--ck-color-mention-background: hsla(341, 100%, 30%, 0.1);
+	--ck-color-mention-text: hsl(341, 100%, 30%);
 	--ck-highlight-marker-blue: hsl(201, 97%, 72%);
 	--ck-highlight-marker-green: hsl(120, 93%, 68%);
 	--ck-highlight-marker-pink: hsl(345, 96%, 73%);
@@ -64,6 +66,81 @@ Below there is a full list of content styles used by the editor features. You ca
 	--ck-todo-list-checkmark-size: 16px;
 }
 
+/* ckeditor5-image/theme/image.css */
+.ck-content .image {
+	display: table;
+	clear: both;
+	text-align: center;
+	margin: 1em auto;
+}
+/* ckeditor5-image/theme/image.css */
+.ck-content .image img {
+	display: block;
+	margin: 0 auto;
+	max-width: 100%;
+	min-width: 50px;
+}
+/* ckeditor5-image/theme/imagecaption.css */
+.ck-content .image > figcaption {
+	display: table-caption;
+	caption-side: bottom;
+	word-break: break-word;
+	color: hsl(0, 0%, 20%);
+	background-color: hsl(0, 0%, 97%);
+	padding: .6em;
+	font-size: .75em;
+	outline-offset: -1px;
+}
+/* ckeditor5-image/theme/imageresize.css */
+.ck-content .image.image_resized {
+	max-width: 100%;
+	display: block;
+	box-sizing: border-box;
+}
+/* ckeditor5-image/theme/imageresize.css */
+.ck-content .image.image_resized img {
+	width: 100%;
+}
+/* ckeditor5-image/theme/imageresize.css */
+.ck-content .image.image_resized > figcaption {
+	display: block;
+}
+/* ckeditor5-image/theme/imagestyle.css */
+.ck-content .image-style-side {
+	float: right;
+	margin-left: var(--ck-image-style-spacing);
+	max-width: 50%;
+}
+/* ckeditor5-image/theme/imagestyle.css */
+.ck-content .image-style-align-left {
+	float: left;
+	margin-right: var(--ck-image-style-spacing);
+}
+/* ckeditor5-image/theme/imagestyle.css */
+.ck-content .image-style-align-center {
+	margin-left: auto;
+	margin-right: auto;
+}
+/* ckeditor5-image/theme/imagestyle.css */
+.ck-content .image-style-align-right {
+	float: right;
+	margin-left: var(--ck-image-style-spacing);
+}
+/* ckeditor5-block-quote/theme/blockquote.css */
+.ck-content blockquote {
+	overflow: hidden;
+	padding-right: 1.5em;
+	padding-left: 1.5em;
+	margin-left: 0;
+	margin-right: 0;
+	font-style: italic;
+	border-left: solid 5px hsl(0, 0%, 80%);
+}
+/* ckeditor5-block-quote/theme/blockquote.css */
+.ck-content[dir="rtl"] blockquote {
+	border-left: 0;
+	border-right: solid 5px hsl(0, 0%, 80%);
+}
 /* ckeditor5-list/theme/todolist.css */
 .ck-content .todo-list {
 	list-style: none;
@@ -131,12 +208,54 @@ Below there is a full list of content styles used by the editor features. You ca
 .ck-content .todo-list .todo-list__label .todo-list__label__description {
 	vertical-align: middle;
 }
-/* ckeditor5-media-embed/theme/mediaembed.css */
-.ck-content .media {
-	clear: both;
-	margin: 1em 0;
-	display: block;
-	min-width: 15em;
+/* ckeditor5-horizontal-line/theme/horizontalline.css */
+.ck-content hr {
+	margin: 15px 0;
+	height: 4px;
+	background: hsl(0, 0%, 87%);
+	border: 0;
+}
+/* ckeditor5-highlight/theme/highlight.css */
+.ck-content .marker-yellow {
+	background-color: var(--ck-highlight-marker-yellow);
+}
+/* ckeditor5-highlight/theme/highlight.css */
+.ck-content .marker-green {
+	background-color: var(--ck-highlight-marker-green);
+}
+/* ckeditor5-highlight/theme/highlight.css */
+.ck-content .marker-pink {
+	background-color: var(--ck-highlight-marker-pink);
+}
+/* ckeditor5-highlight/theme/highlight.css */
+.ck-content .marker-blue {
+	background-color: var(--ck-highlight-marker-blue);
+}
+/* ckeditor5-highlight/theme/highlight.css */
+.ck-content .pen-red {
+	color: var(--ck-highlight-pen-red);
+	background-color: transparent;
+}
+/* ckeditor5-highlight/theme/highlight.css */
+.ck-content .pen-green {
+	color: var(--ck-highlight-pen-green);
+	background-color: transparent;
+}
+/* ckeditor5-font/theme/fontsize.css */
+.ck-content .text-tiny {
+	font-size: .7em;
+}
+/* ckeditor5-font/theme/fontsize.css */
+.ck-content .text-small {
+	font-size: .85em;
+}
+/* ckeditor5-font/theme/fontsize.css */
+.ck-content .text-big {
+	font-size: 1.4em;
+}
+/* ckeditor5-font/theme/fontsize.css */
+.ck-content .text-huge {
+	font-size: 1.8em;
 }
 /* ckeditor5-basic-styles/theme/code.css */
 .ck-content code {
@@ -144,21 +263,6 @@ Below there is a full list of content styles used by the editor features. You ca
 	padding: .15em;
 	border-radius: 2px;
 }
-/* ckeditor5-block-quote/theme/blockquote.css */
-.ck-content blockquote {
-	overflow: hidden;
-	padding-right: 1.5em;
-	padding-left: 1.5em;
-	margin-left: 0;
-	margin-right: 0;
-	font-style: italic;
-	border-left: solid 5px hsl(0, 0%, 80%);
-}
-/* ckeditor5-block-quote/theme/blockquote.css */
-.ck-content[dir="rtl"] blockquote {
-	border-left: 0;
-	border-right: solid 5px hsl(0, 0%, 80%);
-}
 /* ckeditor5-table/theme/table.css */
 .ck-content .table {
 	margin: 1em auto;
@@ -192,98 +296,6 @@ Below there is a full list of content styles used by the editor features. You ca
 .ck-content[dir="ltr"] .table th {
 	text-align: left;
 }
-/* ckeditor5-image/theme/imageresize.css */
-.ck-content .image.image_resized {
-	max-width: 100%;
-	display: block;
-	box-sizing: border-box;
-}
-/* ckeditor5-image/theme/imageresize.css */
-.ck-content .image.image_resized img {
-	width: 100%;
-}
-/* ckeditor5-image/theme/imageresize.css */
-.ck-content .image.image_resized > figcaption {
-	display: block;
-}
-/* ckeditor5-image/theme/image.css */
-.ck-content .image {
-	display: table;
-	clear: both;
-	text-align: center;
-	margin: 1em auto;
-}
-/* ckeditor5-image/theme/image.css */
-.ck-content .image img {
-	display: block;
-	margin: 0 auto;
-	max-width: 100%;
-	min-width: 50px;
-}
-/* ckeditor5-image/theme/imagecaption.css */
-.ck-content .image > figcaption {
-	display: table-caption;
-	caption-side: bottom;
-	word-break: break-word;
-	color: hsl(0, 0%, 20%);
-	background-color: hsl(0, 0%, 97%);
-	padding: .6em;
-	font-size: .75em;
-	outline-offset: -1px;
-}
-/* ckeditor5-highlight/theme/highlight.css */
-.ck-content .marker-yellow {
-	background-color: var(--ck-highlight-marker-yellow);
-}
-/* ckeditor5-highlight/theme/highlight.css */
-.ck-content .marker-green {
-	background-color: var(--ck-highlight-marker-green);
-}
-/* ckeditor5-highlight/theme/highlight.css */
-.ck-content .marker-pink {
-	background-color: var(--ck-highlight-marker-pink);
-}
-/* ckeditor5-highlight/theme/highlight.css */
-.ck-content .marker-blue {
-	background-color: var(--ck-highlight-marker-blue);
-}
-/* ckeditor5-highlight/theme/highlight.css */
-.ck-content .pen-red {
-	color: var(--ck-highlight-pen-red);
-	background-color: transparent;
-}
-/* ckeditor5-highlight/theme/highlight.css */
-.ck-content .pen-green {
-	color: var(--ck-highlight-pen-green);
-	background-color: transparent;
-}
-/* ckeditor5-image/theme/imagestyle.css */
-.ck-content .image-style-side,
-.ck-content .image-style-align-left,
-.ck-content .image-style-align-center,
-.ck-content .image-style-align-right {
-	max-width: 50%;
-}
-/* ckeditor5-image/theme/imagestyle.css */
-.ck-content .image-style-side {
-	float: right;
-	margin-left: var(--ck-image-style-spacing);
-}
-/* ckeditor5-image/theme/imagestyle.css */
-.ck-content .image-style-align-left {
-	float: left;
-	margin-right: var(--ck-image-style-spacing);
-}
-/* ckeditor5-image/theme/imagestyle.css */
-.ck-content .image-style-align-center {
-	margin-left: auto;
-	margin-right: auto;
-}
-/* ckeditor5-image/theme/imagestyle.css */
-.ck-content .image-style-align-right {
-	float: right;
-	margin-left: var(--ck-image-style-spacing);
-}
 /* ckeditor5-page-break/theme/pagebreak.css */
 .ck-content .page-break {
 	position: relative;
@@ -320,21 +332,12 @@ Below there is a full list of content styles used by the editor features. You ca
 	-ms-user-select: none;
 	user-select: none;
 }
-/* ckeditor5-font/theme/fontsize.css */
-.ck-content .text-tiny {
-	font-size: .7em;
-}
-/* ckeditor5-font/theme/fontsize.css */
-.ck-content .text-small {
-	font-size: .85em;
-}
-/* ckeditor5-font/theme/fontsize.css */
-.ck-content .text-big {
-	font-size: 1.4em;
-}
-/* ckeditor5-font/theme/fontsize.css */
-.ck-content .text-huge {
-	font-size: 1.8em;
+/* ckeditor5-media-embed/theme/mediaembed.css */
+.ck-content .media {
+	clear: both;
+	margin: 1em 0;
+	display: block;
+	min-width: 15em;
 }
 /* ckeditor5-code-block/theme/codeblock.css */
 .ck-content pre {
@@ -356,12 +359,10 @@ Below there is a full list of content styles used by the editor features. You ca
 	padding: 0;
 	border-radius: 0;
 }
-/* ckeditor5-horizontal-line/theme/horizontalline.css */
-.ck-content hr {
-	margin: 15px 0;
-	height: 4px;
-	background: hsl(0, 0%, 87%);
-	border: 0;
+/* ckeditor5-mention/theme/mention.css */
+.ck-content .mention {
+	background: var(--ck-color-mention-background);
+	color: var(--ck-color-mention-text);
 }
 @media print {
 	/* ckeditor5-page-break/theme/pagebreak.css */

+ 5 - 4
docs/builds/guides/integration/installing-plugins.md

@@ -21,8 +21,8 @@ In this guide you can learn how to add plugins to your editor in the two most co
 
 In order to start developing CKEditor 5 you will require:
 
-* [Node.js](https://nodejs.org/en/) 6.9.0+
-* npm 4+ (**note:** some npm 5+ versions were known to cause [problems](https://github.com/npm/npm/issues/16991), especially with deduplicating packages; upgrade npm when in doubt)
+* [Node.js](https://nodejs.org/en/) 12.0.0+
+* npm 5.7.1+ (**note:** some npm 5+ versions were known to cause [problems](https://github.com/npm/npm/issues/16991), especially with deduplicating packages; upgrade npm when in doubt)
 
 ## Adding a plugin to a build
 
@@ -34,8 +34,9 @@ Adding plugins to existing builds is done through their customization. Editor bu
 4. Bundle the build.
 
 ```bash
-git clone -b stable https://github.com/ckeditor/ckeditor5-build-classic.git
-cd ckeditor5-build-classic
+git clone -b stable https://github.com/ckeditor/ckeditor5
+
+cd ckeditor5/packages/ckeditor5-build-classic
 npm install
 ```
 

+ 1 - 1
docs/builds/guides/migrate.md

@@ -118,7 +118,7 @@ Note: The number of options was reduced on purpose. We understood that configuri
 		</tr>
 		<tr>
 			<td><a href="../../../../ckeditor4/latest/api/CKEDITOR_config.html#cfg-autolink_commitKeystrokes">autolink_commitKeystrokes</a> <br> <a href="../../../../ckeditor4/latest/api/CKEDITOR_config.html#cfg-autolink_emailRegex">autolink_emailRegex</a> <br> <a href="../../../../ckeditor4/latest/api/CKEDITOR_config.html#cfg-autolink_urlRegex">autolink_urlRegex</a></td>
-			<td>N/A</td>
+			<td>Refer to the {@link features/link#autolink-feature Autolink section} of the Link guide to learn more about support for automatic linking in CKEditor 5.</td>
 		</tr>
 		<tr>
 			<td><a href="../../../../ckeditor4/latest/api/CKEDITOR_config.html#cfg-baseFloatZIndex">baseFloatZIndex</a></td>

+ 2 - 2
docs/features/image-upload.md

@@ -40,7 +40,7 @@ The demo below uses the {@link builds/guides/overview#classic-editor Classic edi
 
 CKEditor 5 introduces a new way of handling images, with a strong focus on the end–user experience. This feature is called {@link features/easy-image Easy Image} and its goal is to make the image upload as effortless and intuitive as possible.
 
-Easy Image is part of the [CKEditor Cloud Services](https://ckeditor.com/ckeditor-cloud-services/) offer. It is a <abbr title="Software as a service">SaaS</abbr> product that:
+Easy Image is part of the commercial [CKEditor Cloud Services](https://ckeditor.com/ckeditor-cloud-services/) offer. It is a <abbr title="Software as a service">SaaS</abbr> product that:
 
 * securely uploads images,
 * takes care of rescaling and optimizing them as well as providing various image sizes (responsive images),
@@ -52,7 +52,7 @@ All that with virtually zero server setup.
 
 ### CKFinder
 
-The {@link features/ckfinder CKFinder feature} provides a bridge between the rich-text editor and [CKFinder](https://ckeditor.com/ckfinder/), a browser-based file uploader with its server-side connectors (PHP, Java and ASP.NET).
+The {@link features/ckfinder CKFinder feature} provides a bridge between the rich-text editor and [CKFinder](https://ckeditor.com/ckfinder/), a browser-based commercial file uploader with its server-side connectors (PHP, Java and ASP.NET).
 
 There are two ways you can integrate CKEditor 5 with the CKFinder file manager:
 

+ 58 - 39
docs/features/spelling-and-grammar-checking.md

@@ -5,8 +5,6 @@ menu-title: Spelling and grammar checking
 
 # Proofreading, spelling and grammar checking
 
-{@snippet build-classic-source}
-
 <info-box>
 	The spell checker for CKEditor 5 is a commercial solution provided by our partner, [WebSpellChecker](https://webspellchecker.com/). You can report any issues in its [GitHub repository](https://github.com/WebSpellChecker/wproofreader). The license can be purchased [here](https://ckeditor.com/contact/).
 </info-box>
@@ -29,63 +27,84 @@ There are also over 150 additional languages and specialized dictionaries such a
 
 ## Installation
 
-WProofreader is installed separately from CKEditor 5 and does not need to be combined into an editor build as other features. To use this tool, it is necessary to load the WProofreader script on your page and provide the configuration.
+WProofreader is delivered as a CKEditor 5 plugin, so it could be combined into an editor build as other features. To add this feature to your rich-text editor, install the [`@webspellchecker/wproofreader-ckeditor5`](https://www.npmjs.com/package/@webspellchecker/wproofreader-ckeditor5) package:
+
+```bash
+npm install --save @webspellchecker/wproofreader-ckeditor5
+```
+
+Then, add it to your plugin list:
+
+```js
+import WProofreader from '@webspellchecker/wproofreader-ckeditor5/src/wproofreader';
+// ...
+
+ClassicEditor
+	.create( editorElement, {
+		plugins: [ ..., WProofreader ],
+		// ...
+	} )
+	.then( ... )
+	.catch( ... );
+```
+
+<info-box info>
+	Read more about {@link builds/guides/integration/installing-plugins installing plugins}.
+</info-box>
 
-The proofreader can be used either as a [cloud solution](#wproofreader-cloud) or [hosted on your own server](#wproofreader-server).
+At this step, it is required to provide a proper configuration. The proofreader can be used either as a [cloud solution](#wproofreader-cloud) or [hosted on your own server](#wproofreader-server).
 
 ### WProofreader Cloud
 
 After signing up for a [trial or paid version](https://ckeditor.com/contact/), you will receive your service ID which is used to activate the service.
 
-Add the following configuration to your page:
-
-```html
-<script>
-	window.WEBSPELLCHECKER_CONFIG = {
-		autoSearch: true,
-		enableGrammar: true,
-		serviceId: 'your-service-ID'
-	};
-</script>
-```
+Add the following configuration to your editor:
 
-And then load the proofreader script:
+```js
+import WProofreader from '@webspellchecker/wproofreader-ckeditor5/src/wproofreader';
+// ...
 
-```html
-<script src="https://svc.webspellchecker.net/spellcheck31/wscbundle/wscbundle.js"></script>
+ClassicEditor
+	.create( editorElement, {
+		plugins: [ ..., WProofreader ],
+		wproofreader: {
+			serviceId: 'your-service-ID',
+			srcUrl: 'https://svc.webspellchecker.net/spellcheck31/wscbundle/wscbundle.js'
+		}
+	} )
 ```
 
-Refer to the [official documentation](https://github.com/WebSpellChecker/wproofreader#wproofreader-cloud) for more details about the cloud setup and available configuration options.
+Refer to the [official documentation](https://github.com/WebSpellChecker/wproofreader-ckeditor5#install-instructions) for more details about the cloud setup and available configuration options.
 
 ### WProofreader Server
 
 After signing up for a [trial or paid version](https://ckeditor.com/contact/), you will receive access to the WebSpellChecker Server package to install on your own server.
 
-You will need to add the following configuration to your page:
-
-```html
-<script>
-	window.WEBSPELLCHECKER_CONFIG = {
-		autoSearch: true,
-		enableGrammar: true,
-		servicePort: '2880',
-		servicePath: '/'
-	};
-</script>
-```
-
-Then specify the path to the service on your page:
-
-```html
-<script src="http(s)://your_host_name/spellcheck/wscbundle/wscbundle.js"></script>
+You will need to add the following configuration to your editor:
+
+```js
+import WProofreader from '@webspellchecker/wproofreader-ckeditor5/src/wproofreader';
+// ...
+
+ClassicEditor
+	.create( editorElement, {
+		plugins: [ ..., WProofreader ],
+		wproofreader: {
+			serviceProtocol: 'https',
+			serviceHost: 'localhost',
+			servicePort: '2880',
+			servicePath: '/',
+			srcUrl: 'https://host_name/virtual_directory/wscbundle/wscbundle.js'
+		}
+	} )
 ```
 
-Refer to the [official documentation](https://github.com/WebSpellChecker/wproofreader#wproofreader-server) for more details about the server setup and available configuration options.
+Refer to the [official documentation](https://github.com/WebSpellChecker/wproofreader-ckeditor5#install-instructions) for more details about the server setup and available configuration options.
 
 ## Configuration
 
-WProofreader configuration is set outside the CKEditor 5 configuration. Refer to the [WProofreader API](http://dev.webspellchecker.net/api/wscbundle/) for further information.
+WProofreader configuration is set inside the CKEditor 5 configuration in `wproofreader` object. Refer to the [WProofreader API](https://webspellchecker.com/docs/api/wscbundle/Options.html) for further information.
 
 ## Contribute
 
-You can report issues and request features in the [official WProofreader repository](https://github.com/WebSpellChecker/wproofreader/issues).
+You can report issues and request features in the [official WProofreader for CKEditor 5 repository](https://github.com/WebSpellChecker/wproofreader-ckeditor5/issues).

+ 1 - 11
docs/features/ui-language.md

@@ -128,17 +128,7 @@ If you build CKEditor 5 from scratch or integrate it directly into your applicat
 				// The bundle is optimized for one language when this option is omitted.
 				additionalLanguages: 'all',
 
-				// An optional directory for emitted translations. Relative to the webpack's output.
-				// Defaults to `'translations'`.
-				// outputDirectory: 'ckeditor5-translations',
-
-				// Whether the build process should fail if an error occurs.
-				// Defaults to `false`.
-				// strict: true,
-
-				// Whether to log all warnings to the console.
-				// Defaults to `false`.
-				// verbose: true
+				// For more advanced options see https://github.com/ckeditor/ckeditor5-dev/tree/master/packages/ckeditor5-dev-webpack-plugin.
 			} ),
 
 			// Other webpack plugins...

+ 19 - 19
docs/framework/guides/architecture/editing-engine.md

@@ -17,15 +17,15 @@ The editing engine implements an MVC architecture. The shape of it is not enforc
 
 [{@img assets/img/framework-architecture-engine-diagram.png Diagram of the engine's MVC architecture.}](%BASE_PATH%/assets/img/framework-architecture-engine-diagram.png)
 
-What you can see are three layers: **model**, **controller** and **view**. There is one **model document** which is **converted** to two views &mdash; the **editing view** and the **data view**. These two views represent, respectively, the content that the user is editing (the DOM structure that you see in the browser) and the editor input and output data (in a format that the plugged data processor understands). Both views feature virtual DOM structures (custom, DOM-like structures) on which converters and features work and which are then **rendered** to the DOM.
+What you can see are three layers: **model**, **controller** and **view**. There is one **model document** which is **converted** into separate views &mdash; the **editing view** and the **data view**. These two views represent, respectively, the content that the user is editing (the DOM structure that you see in the browser) and the editor input and output data (in a format that the plugged data processor understands). Both views feature virtual DOM structures (custom, DOM-like structures) on which converters and features work and which are then **rendered** to the DOM.
 
-The green blocks are the code introduced by editor features (plugins). So features control what changes are done to the model, how they are converted to the view and how the model needs to be changed based on fired events (view's and model's ones).
+The green blocks are the code introduced by editor features (plugins). These features control what changes are made to the model, how they are converted to the view and how the model needs to be changed based on fired events (the view's and model's ones).
 
 Let's now talk about each layer separately.
 
 ## Model
 
-The model is implemented by a DOM-like tree structure of {@link module:engine/model/element~Element elements} and {@link module:engine/model/text~Text text nodes}. Unlike in the DOM, in the model, both elements and text nodes can have attributes.
+The model is implemented by a DOM-like tree structure of {@link module:engine/model/element~Element elements} and {@link module:engine/model/text~Text text nodes}. Unlike in the actual DOM, in the model, both elements and text nodes can have attributes.
 
 Like in the DOM, the model structure is contained within a {@link module:engine/model/document~Document document} that contains {@link module:engine/model/document~Document#roots root elements} (the model, as well as the view, may have multiple roots). The document also holds its {@link module:engine/model/documentselection~DocumentSelection selection} and the {@link module:engine/model/history~History history of its changes}.
 
@@ -72,12 +72,12 @@ editor.model.change( writer => {
 ```
 
 <info-box>
-	All changes made to the document structure are done by applying {@link module:engine/model/operation/operation~Operation operations}. The concept of operations comes from [Operational Transformation](https://en.wikipedia.org/wiki/Operational_transformation) (in short: OT), a technology enabling collaboration functionality. Since OT requires that a system is able to transform every operation by every other one (to figure out the result of concurrently applied operations), the set of operations needs to be small. CKEditor 5 features a non-linear model (normally, OT implementations use flat, array-like models while CKEditor 5 uses a tree structure), hence the set of potential semantic changes is more complex. Operations are grouped in {@link module:engine/model/batch~Batch batches}. A batch can be understood as a single undo step.
+	All changes made to the document structure are done by applying {@link module:engine/model/operation/operation~Operation operations}. The concept of operations comes from [Operational Transformation](https://en.wikipedia.org/wiki/Operational_transformation) (in short: OT), a technology enabling collaboration functionality. Since OT requires a system to be able to transform every operation by every other one (to figure out the result of concurrently applied operations), the set of operations needs to be small. CKEditor 5 features a non-linear model (normally, OT implementations use flat, array-like models while CKEditor 5 uses a tree structure), hence the set of potential semantic changes is more complex. Operations are grouped in {@link module:engine/model/batch~Batch batches}. A batch may be understood as a single undo step.
 </info-box>
 
 ### Text attributes
 
-Text styles such as "bold" and "italic" are not kept in the model as elements but as text attributes (think &mdash; like element attributes). The following DOM structure:
+Text styles such as "bold" and "italic" are kept in the model not as elements but as text attributes (think &mdash; like element attributes). The following DOM structure:
 
 ```html
 <p>
@@ -148,8 +148,8 @@ On the other hand, offset `x` in `<paragraph>` translates to:
 The engine also defines three levels of classes that operate on offsets:
 
 * A {@link module:engine/model/position~Position} instance contains an {@link module:engine/model/position~Position#path array of offsets} (which is called a "path"). See the examples in the {@link module:engine/model/position~Position#path `Position#path` API documentation} to better understand how paths work.
-* {@link module:engine/model/range~Range} contains two positions: {@link module:engine/model/range~Range#start start} and {@link module:engine/model/range~Range#end end} ones.
-* Finally, there is a {@link module:engine/model/selection~Selection} which contains one or more ranges, attributes, and has a direction (whether it was done from left to right or right to left). You can make as many instances of it as you need and you can freely modify it whenever you want.  Additionally, there is a single {@link module:engine/model/documentselection~DocumentSelection}. It represents the document's selection and can only be changed through the {@link module:engine/model/writer~Writer model writer}. It is automatically updated when the document's structure is changed.
+* A {@link module:engine/model/range~Range} contains two positions: {@link module:engine/model/range~Range#start start} and {@link module:engine/model/range~Range#end end} ones.
+* Finally, there is a {@link module:engine/model/selection~Selection} which contains one or more ranges, attributes, and has a direction (whether it was done from left to right or right to left). You can make as many instances of it as you need and you can freely modify it whenever you want. Additionally, there is a single {@link module:engine/model/documentselection~DocumentSelection}. It represents the document's selection and can only be changed through the {@link module:engine/model/writer~Writer model writer}. It is automatically updated when the document's structure is changed.
 
 ### Markers
 
@@ -159,11 +159,11 @@ Markers are a special type of ranges.
 * They can only be created and changed through the {@link module:engine/model/writer~Writer model writer}.
 * They can be synchronized over the network with other collaborating clients.
 * They are automatically updated when the document's structure is changed.
-* They can be converted to the editing view, to show them in the editor (as {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToHighlight highlights} or {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToElement elements}).
-* They can be {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToData converted to the data view}, to store them with the document data.
+* They can be converted to the editing view to show them in the editor (as {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToHighlight highlights} or {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToElement elements}).
+* They can be {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToData converted to the data view} to store them with the document data.
 * They can be {@link module:engine/conversion/upcasthelpers~UpcastHelpers#dataToMarker loaded with the document data}.
 
-Markers are ideal for storing and maintaining additional data related to portions of the document &mdash; such as comments or selections of other users.
+Markers are perfect for storing and maintaining additional data related to portions of the document such as comments or selections of other users.
 
 ### Schema
 
@@ -173,7 +173,7 @@ The {@link module:engine/model/schema~Schema model's schema} defines several asp
 * What attributes are allowed for a certain node (e.g. `image` can have the `src` and `alt` attributes).
 * Additional semantics of model nodes (e.g. `image` is of the "object" type and paragraph of the "block" type).
 
-This information is then used by features and the engine to make decisions how to process the model. For instance, the information from the schema will affect:
+This information is then used by the features and the engine to make decisions on how to process the model. For instance, the information from the schema will affect:
 
 * What happens with the pasted content and what is filtered out (note: in case of pasting the other important mechanism is the conversion. HTML elements and attributes which are not upcasted by any of the registered converters are filtered out before they even become model nodes, so the schema is not applied to them; the conversion will be covered later in this guide).
 * To which elements the heading feature can be applied (which blocks can be turned to headings and which elements are blocks in the first place).
@@ -196,7 +196,7 @@ Let's again take a look at the editing engine's architecture:
 
 [{@img assets/img/framework-architecture-engine-diagram.png Diagram of the engine's MVC architecture.}](%BASE_PATH%/assets/img/framework-architecture-engine-diagram.png)
 
-So far, we talked about the topmost layer of this diagram  the model. The role of the model layer is to create an abstraction over the data. Its format was designed to allow storing and modifying the data in the most convenient way, while enabling implementation of complex features. Most features operate (read from it and change it) on the model.
+So far, we talked about the topmost layer of this diagram &mdash; the model. The role of the model layer is to create an abstraction over the data. Its format was designed to allow storing and modifying the data in the most convenient way, while enabling implementation of complex features. Most features operate on the model (read from it and change it).
 
 The view, on the other hand, is an abstract representation of the DOM structure which should be presented to the user (for editing) and which should (in most cases) represent the editor's input and output (i.e. the data returned by `editor.getData()`, the data set by `editor.setData()`, pasted content, etc.).
 
@@ -221,23 +221,23 @@ editor.data;                    // The data pipeline (DataController).
 
 	It is much simpler than the editing pipeline and in the following part of this section we will be talking about the editing view.
 
-	Check out the {@link module:engine/controller/editingcontroller~EditingController}'s and {@link module:engine/controller/datacontroller~DataController}'s API.
+	Check out the {@link module:engine/controller/editingcontroller~EditingController}'s and {@link module:engine/controller/datacontroller~DataController}'s API for more details.
 </info-box>
 
 ### Element types and custom data
 
-The structure of the view resembles the structure in the DOM very closely. The semantics of HTML is defined in its specification. The view structure comes "DTD-free", so in order to provide additional information and better express the semantics of the content, the view structure implements 6 element types ({@link module:engine/view/containerelement~ContainerElement}, {@link module:engine/view/attributeelement~AttributeElement}, {@link module:engine/view/emptyelement~EmptyElement}, {@link module:engine/view/rawelement~RawElement}, {@link module:engine/view/uielement~UIElement}, and {@link module:engine/view/editableelement~EditableElement}) and so called {@link module:engine/view/element~Element#getCustomProperty "custom properties"} (i.e. custom element properties which are not rendered). This additional information provided by editor features is then used by the {@link module:engine/view/renderer~Renderer} and [converters](#conversion).
+The structure of the view resembles the structure in the DOM very closely. The semantics of HTML is defined in its specification. The view structure comes "DTD-free", so in order to provide additional information and to better express the semantics of the content, the view structure implements 6 element types ({@link module:engine/view/containerelement~ContainerElement}, {@link module:engine/view/attributeelement~AttributeElement}, {@link module:engine/view/emptyelement~EmptyElement}, {@link module:engine/view/rawelement~RawElement}, {@link module:engine/view/uielement~UIElement}, and {@link module:engine/view/editableelement~EditableElement}) and so called {@link module:engine/view/element~Element#getCustomProperty "custom properties"} (i.e. custom element properties which are not rendered). This additional information provided by the editor features is then used by the {@link module:engine/view/renderer~Renderer} and [converters](#conversion).
 
 The element types can be defined as follows:
 
 * **Container element** &ndash; The elements that build the structure of the content. Used for block elements such as `<p>`, `<h1>`, `<blockQuote>`, `<li>`, etc.
-* **Attribute element** &ndash; The elements that cannot contain container elements inside them. Most model text attributes are converted to view attribute elements. They are used mostly for inline styling elements such as `<strong>`, `<i>`, `<a>`, `<code>`. Similar attribute elements are flattened by the view writer, so e.g. `<a href="..."><a class="bar">x</a></a>` would automatically be optimized to `<a href="..." class="bar">x</a>`.
+* **Attribute element** &ndash; The elements that cannot hold container elements inside them. Most model text attributes are converted to view attribute elements. They are used mostly for inline styling elements such as `<strong>`, `<i>`, `<a>`, `<code>`. Similar attribute elements are flattened by the view writer, so e.g. `<a href="..."><a class="bar">x</a></a>` would automatically be optimized to `<a href="..." class="bar">x</a>`.
 * **Empty element** &ndash; The elements that must not have any child nodes, for example `<img>`.
-* **UI elements** &ndash; The elements that are not a part of the "data" but need to be "inlined" in the content. They are ignored by the selection (it jumps over them) and the view writer in general. The contents of these elements and events coming from them are filtered out, too.
+* **UI element** &ndash; The elements that are not a part of the "data" but need to be "inlined" in the content. They are ignored by the selection (it jumps over them) and the view writer in general. The contents of these elements and events coming from them are filtered out, too.
 * **Raw element** &ndash; The elements that work as data containers ("wrappers", "sandboxes") but their children are transparent to the editor. Useful when non-standard data must be rendered but the editor should not be concerned what it is and how it works. Users cannot put the selection inside a raw element, split it into smaller chunks or directly modify its content.
 * **Editable element** &ndash; The elements used as "nested editables" of non-editable fragments of the content, for example a caption in the image widget, where the `<figure>` wrapping the image is not editable (it is a widget) and the `<figcaption>` inside it is an editable element.
 
-Additionally, you can define {@link module:engine/view/element~Element#getCustomProperty custom properties} which can be used to store information like:
+Additionally, you can define {@link module:engine/view/element~Element#getCustomProperty custom properties} which may be used to store information like:
 
 * Whether an element is a widget (added by {@link module:widget/utils~toWidget `toWidget()`}).
 * How an element should be marked when a [marker](#markers) highlights it.
@@ -274,7 +274,7 @@ editor.data.view.change( writer => {
 
 ### Positions
 
-Just like [in the model](#positions-ranges-and-selections), in the view there are 3 levels of classes that describe points in the view structure: **positions**, **ranges** and **selections**. A position is a single point in the document. A range consists of two positions (start and end). And selection consists of one or more ranges and has a direction (whether it was done from left to right or from right to left).
+Just like [in the model](#positions-ranges-and-selections), there are 3 levels of classes in the view that describe points in the view structure: **positions**, **ranges** and **selections**. A position is a single point in the document. A range consists of two positions (start and end). A selection consists of one or more ranges and has a direction (whether it was done from left to right or from right to left).
 
 A view range is very similar to its [DOM counterpart](https://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html) as view positions are represented by a parent and an offset in that parent. This means that, unlike model offsets, view offsets describe:
 
@@ -372,4 +372,4 @@ Let's take a look at the diagram of the engine's MVC architecture and see where
 -->
 ## Read next
 
-Once you learnt how to implement editing features, it is time to add a UI for them. You can read about the CKEditor 5 standard UI framework and UI library in the {@link framework/guides/architecture/ui-library UI library} guide.
+Once you have learnt how to implement editing features, it is time to add a UI for them. You can read about the CKEditor 5 standard UI framework and UI library in the {@link framework/guides/architecture/ui-library UI library} guide.

+ 1 - 1
docs/framework/guides/contributing/development-environment.md

@@ -25,7 +25,7 @@ You can find all the official packages listed in the [CKEditor 5 repository's RE
 
 In order to start developing CKEditor 5 you will require:
 
-* [Node.js](https://nodejs.org/en/) 8.0.0+
+* [Node.js](https://nodejs.org/en/) 12.0.0+
 * [Git](https://git-scm.com/)
 
 ## Setting up the CKEditor development environment

+ 2 - 2
docs/framework/guides/quick-start.md

@@ -11,8 +11,8 @@ This guide will show you how to initialize CKEditor 5 rich-text editor from sour
 
 The CKEditor 5 Framework is made of several [npm packages](https://npmjs.com). To install it you need:
 
-* [Node.js](https://nodejs.org/en/) 6.9.0+
-* npm 4+ (**note:** some npm 5+ versions were known to cause [problems](https://github.com/npm/npm/issues/16991), especially with deduplicating packages; upgrade npm when in doubt)
+* [Node.js](https://nodejs.org/en/) 12.0.0+
+* npm 5.7.1+ (**note:** some npm 5+ versions were known to cause [problems](https://github.com/npm/npm/issues/16991), especially with deduplicating packages; upgrade npm when in doubt)
 
 Besides Node.js and npm you also need [webpack@4](https://webpack.js.org) with a few additional packages to use the framework. They are needed to bundle the source code. Read more about building CKEditor 5 in the {@link builds/guides/integration/advanced-setup CKEditor 5 Builds Advanced setup} guide.
 

+ 10 - 9
package.json

@@ -76,16 +76,17 @@
     "@ckeditor/ckeditor5-word-count": "^21.0.0"
   },
   "devDependencies": {
-    "@ckeditor/ckeditor5-comments": "^20.0.0",
-    "@ckeditor/ckeditor5-dev-docs": "^23.0.0",
-    "@ckeditor/ckeditor5-dev-env": "^23.0.0",
-    "@ckeditor/ckeditor5-dev-tests": "^23.0.0",
-    "@ckeditor/ckeditor5-dev-utils": "^23.0.0",
-    "@ckeditor/ckeditor5-dev-webpack-plugin": "^23.0.0",
-    "@ckeditor/ckeditor5-inspector": "^2.1.0",
+    "@ckeditor/ckeditor5-comments": "^21.0.0",
+    "@ckeditor/ckeditor5-dev-docs": "^23.1.1",
+    "@ckeditor/ckeditor5-dev-env": "^23.1.1",
+    "@ckeditor/ckeditor5-dev-tests": "^23.1.1",
+    "@ckeditor/ckeditor5-dev-utils": "^23.1.1",
+    "@ckeditor/ckeditor5-dev-webpack-plugin": "^23.1.1",
+    "@ckeditor/ckeditor5-inspector": "^2.2.0",
     "@ckeditor/ckeditor5-react": "^2.1.0",
-    "@ckeditor/ckeditor5-real-time-collaboration": "^20.0.0",
-    "@ckeditor/ckeditor5-track-changes": "^20.0.0",
+    "@ckeditor/ckeditor5-real-time-collaboration": "^21.0.0",
+    "@ckeditor/ckeditor5-track-changes": "^21.0.0",
+    "@webspellchecker/wproofreader-ckeditor5": "^1.0.5",
     "@wiris/mathtype-ckeditor5": "7.20.0",
     "babel-standalone": "^6.26.0",
     "coveralls": "^3.1.0",

+ 1 - 1
packages/ckeditor5-adapter-ckfinder/lang/translations/ko.po

@@ -18,4 +18,4 @@ msgstr ""
 
 msgctxt "A generic error message displayed on upload failure. The file name is concatenated to this text."
 msgid "Cannot upload file:"
-msgstr "파일 업로드 불가"
+msgstr "파일 업로드할 수 없음: "

+ 4 - 4
packages/ckeditor5-alignment/lang/translations/ko.po

@@ -18,19 +18,19 @@ msgstr ""
 
 msgctxt "Toolbar button tooltip for aligning the text to the left."
 msgid "Align left"
-msgstr "왼쪽 맞춤"
+msgstr "왼쪽 정렬"
 
 msgctxt "Toolbar button tooltip for aligning the text to the right."
 msgid "Align right"
-msgstr "오른쪽 맞춤"
+msgstr "오른쪽 정렬"
 
 msgctxt "Toolbar button tooltip for aligning the text to center."
 msgid "Align center"
-msgstr "가운데 맞춤"
+msgstr "가운데 정렬"
 
 msgctxt "Toolbar button tooltip for making the text justified."
 msgid "Justify"
-msgstr "양쪽 맞춤"
+msgstr "양쪽 정렬"
 
 msgctxt "Dropdown button tooltip for the text alignment feature."
 msgid "Text alignment"

+ 1 - 1
packages/ckeditor5-alignment/lang/translations/zh.po

@@ -38,4 +38,4 @@ msgstr "文字對齊"
 
 msgctxt "Label used by assistive technologies describing the text alignment feature toolbar."
 msgid "Text alignment toolbar"
-msgstr ""
+msgstr "文字對齊"

+ 3 - 2
packages/ckeditor5-autoformat/docs/features/autoformat.md

@@ -42,8 +42,9 @@ Example:
 
 In addition to enabling automatic text formatting, you may want to check the following productivity features:
 
-* {@link features/text-transformation Automatic text transformation} &ndash; It enables automatic turning snippets such as `(tm)` into `™` and `"foo"` into `“foo”`.
-* {@link features/mentions Mentions} &ndash; It brings support for smart autocompletion.
+* {@link features/text-transformation Automatic text transformation} &ndash; Enables automatic turning snippets such as `(tm)` into `™` and `"foo"` into `“foo”`.
+* {@link features/link#autolink-feature Autolink} &ndash; Turns the links and email addresses typed or pasted into the editor into active URLs.
+* {@link features/mentions Mentions} &ndash; Brings support for smart autocompletion.
 
 ## Installation
 

+ 2 - 2
packages/ckeditor5-autoformat/tests/blockautoformatediting.js

@@ -167,7 +167,7 @@ describe( 'blockAutoformatEditing', () => {
 			editor.conversion.for( 'downcast' )
 				.elementToElement( {
 					model: 'softBreak',
-					view: ( modelElement, viewWriter ) => viewWriter.createEmptyElement( 'br' )
+					view: ( modelElement, { writer } ) => writer.createEmptyElement( 'br' )
 				} );
 
 			const spy = testUtils.sinon.spy();
@@ -207,7 +207,7 @@ describe( 'blockAutoformatEditing', () => {
 			editor.conversion.for( 'downcast' )
 				.elementToElement( {
 					model: 'softBreak',
-					view: ( modelElement, viewWriter ) => viewWriter.createEmptyElement( 'br' )
+					view: ( modelElement, { writer } ) => writer.createEmptyElement( 'br' )
 				} );
 
 			const spy = testUtils.sinon.spy();

+ 1 - 1
packages/ckeditor5-autosave/lang/translations/ko.po

@@ -18,4 +18,4 @@ msgstr ""
 
 msgctxt "A message that the data is being saved."
 msgid "Saving changes"
-msgstr "변경사항 저장"
+msgstr "변경된 내용을 저장하고 있습니다"

+ 1 - 1
packages/ckeditor5-basic-styles/lang/translations/ko.po

@@ -30,7 +30,7 @@ msgstr "밑줄"
 
 msgctxt "Toolbar button tooltip for the Code feature."
 msgid "Code"
-msgstr "소스"
+msgstr "코드"
 
 msgctxt "Toolbar button tooltip for the Strikethrough feature."
 msgid "Strikethrough"

+ 1 - 1
packages/ckeditor5-ckfinder/docs/features/ckfinder.md

@@ -8,7 +8,7 @@ order: 30
 
 # CKFinder file manager integration
 
-This feature allows you to insert images as well as links to files into the rich-text editor content. It is a bridge between the CKEditor 5 WYSIWYG editor and the [CKFinder](https://ckeditor.com/ckfinder) file manager and uploader.
+This feature allows you to insert images as well as links to files into the rich-text editor content. It is a bridge between the CKEditor 5 WYSIWYG editor and the [CKFinder](https://ckeditor.com/ckfinder) file manager and uploader. CKFinder is a commercial application that was designed with CKEditor compatibility in mind. It is currently available as version 3.x for PHP, ASP.NET and Java and version 2.x for ASP and ColdFusion.
 
 <info-box>
 	Check out the comprehensive {@link features/image-upload Image upload overview} to learn about other ways to upload images into CKEditor 5.

+ 37 - 0
packages/ckeditor5-ckfinder/lang/translations/ko.po

@@ -0,0 +1,37 @@
+# Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+#
+#                                     !!! IMPORTANT !!!
+#
+#         Before you edit this file, please keep in mind that contributing to the project
+#                translations is possible ONLY via the Transifex online service.
+#
+#         To submit your translations, visit https://www.transifex.com/ckeditor/ckeditor5.
+#
+#                   To learn more, check out the official contributor's guide:
+#     https://ckeditor.com/docs/ckeditor5/latest/framework/guides/contributing/contributing.html
+#
+msgid ""
+msgstr ""
+"Language-Team: Korean (https://www.transifex.com/ckeditor/teams/11143/ko/)\n"
+"Language: ko\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+msgctxt "Toolbar button tooltip for inserting an image or file via a CKFinder file browser."
+msgid "Insert image or file"
+msgstr "사진이나 파일을 삽입"
+
+msgctxt "Error message displayed when inserting a resized version of an image failed."
+msgid "Could not obtain resized image URL."
+msgstr "크기가 조절된 사진의 URL을 가져오지 못했습니다."
+
+msgctxt "Title of a notification displayed when inserting a resized version of an image failed."
+msgid "Selecting resized image failed"
+msgstr "크기가 조절된 이미지 선택 실패"
+
+msgctxt "Error message displayed when an image cannot be inserted at the current position."
+msgid "Could not insert image at the current position."
+msgstr "현재 위치에 사진을 삽입할 수 없습니다."
+
+msgctxt "Title of a notification displayed when an image cannot be inserted at the current position."
+msgid "Inserting image failed"
+msgstr "사진 삽입 실패"

+ 23 - 0
packages/ckeditor5-clipboard/src/clipboard.js

@@ -103,6 +103,15 @@ export default class Clipboard extends Plugin {
 					return;
 				}
 
+				// While pasting plain text, apply selection attributes on the text.
+				if ( isPlainText( modelFragment ) ) {
+					const node = modelFragment.getChild( 0 );
+
+					model.change( writer => {
+						writer.setAttributes( modelDocument.selection.getAttributes(), node );
+					} );
+				}
+
 				model.insertContent( modelFragment );
 				evt.stop();
 			}
@@ -198,3 +207,17 @@ export default class Clipboard extends Plugin {
  *
  * @member {'copy'|'cut'} module:clipboard/clipboard~ClipboardOutputEventData#method
  */
+
+// Returns true if specified `documentFragment` represents a plain text.
+//
+// @param {module:engine/view/documentfragment~DocumentFragment} documentFragment
+// @returns {Boolean}
+function isPlainText( documentFragment ) {
+	if ( documentFragment.childCount > 1 ) {
+		return false;
+	}
+
+	const child = documentFragment.getChild( 0 );
+
+	return [ ...child.getAttributeKeys() ].length == 0;
+}

+ 110 - 0
packages/ckeditor5-clipboard/tests/clipboard.js

@@ -343,6 +343,116 @@ describe( 'Clipboard feature', () => {
 			expect( spy.callCount ).to.equal( 1 );
 		} );
 
+		// https://github.com/ckeditor/ckeditor5/issues/1006
+		describe( 'pasting plain text', () => {
+			let model;
+
+			beforeEach( () => {
+				model = editor.model;
+
+				model.schema.extend( '$text', { allowAttributes: 'bold' } );
+			} );
+
+			it( 'should inherit selection attributes (collapsed selection)', () => {
+				const insertContent = model.insertContent.bind( model );
+				let insertedNode;
+
+				sinon.stub( model, 'insertContent' ).callsFake( documentFragment => {
+					insertedNode = documentFragment.getChild( 0 );
+
+					return insertContent( documentFragment );
+				} );
+
+				setModelData( model, '<paragraph><$text bold="true">Bolded []text.</$text></paragraph>' );
+
+				const dataTransferMock = createDataTransfer( { 'text/plain': 'foo' } );
+
+				viewDocument.fire( 'paste', {
+					dataTransfer: dataTransferMock,
+					stopPropagation() {},
+					preventDefault() {}
+				} );
+
+				expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">Bolded foo[]text.</$text></paragraph>' );
+				expect( insertedNode.getAttribute( 'bold' ) ).to.equal( true );
+			} );
+
+			it( 'should inherit selection attributes (non-collapsed selection)', () => {
+				const insertContent = model.insertContent.bind( model );
+				let insertedNode;
+
+				sinon.stub( model, 'insertContent' ).callsFake( documentFragment => {
+					insertedNode = documentFragment.getChild( 0 );
+
+					return insertContent( documentFragment );
+				} );
+
+				setModelData( model, '<paragraph><$text bold="true">Bolded [text.]</$text></paragraph>' );
+
+				const dataTransferMock = createDataTransfer( { 'text/plain': 'foo' } );
+
+				viewDocument.fire( 'paste', {
+					dataTransfer: dataTransferMock,
+					stopPropagation() {},
+					preventDefault() {}
+				} );
+
+				expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">Bolded foo[]</$text></paragraph>' );
+				expect( insertedNode.getAttribute( 'bold' ) ).to.equal( true );
+			} );
+
+			it( 'should inherit selection attributes while pasting a plain text as text/html', () => {
+				setModelData( model, '<paragraph><$text bold="true">Bolded []text.</$text></paragraph>' );
+
+				const dataTransferMock = createDataTransfer( {
+					'text/html': 'foo',
+					'text/plain': 'foo'
+				} );
+
+				viewDocument.fire( 'paste', {
+					dataTransfer: dataTransferMock,
+					stopPropagation() {},
+					preventDefault() {}
+				} );
+
+				expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">Bolded foo[]text.</$text></paragraph>' );
+			} );
+
+			it( 'should inherit selection attributes while pasting a plain text as text/html (Chrome style)', () => {
+				setModelData( model, '<paragraph><$text bold="true">Bolded []text.</$text></paragraph>' );
+
+				const dataTransferMock = createDataTransfer( {
+					'text/html': '<meta http-equiv="content-type" content="text/html; charset=utf-8">foo',
+					'text/plain': 'foo'
+				} );
+
+				viewDocument.fire( 'paste', {
+					dataTransfer: dataTransferMock,
+					stopPropagation() {},
+					preventDefault() {}
+				} );
+
+				expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">Bolded foo[]text.</$text></paragraph>' );
+			} );
+
+			it( 'should inherit selection attributes while pasting HTML with unsupported attributes', () => {
+				setModelData( model, '<paragraph><$text bold="true">Bolded []text.</$text></paragraph>' );
+
+				const dataTransferMock = createDataTransfer( {
+					'text/html': '<i>foo</i>',
+					'text/plain': 'foo'
+				} );
+
+				viewDocument.fire( 'paste', {
+					dataTransfer: dataTransferMock,
+					stopPropagation() {},
+					preventDefault() {}
+				} );
+
+				expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">Bolded foo[]text.</$text></paragraph>' );
+			} );
+		} );
+
 		function createDataTransfer( data ) {
 			return {
 				getData( type ) {

+ 25 - 0
packages/ckeditor5-code-block/lang/translations/ko.po

@@ -0,0 +1,25 @@
+# Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+#
+#                                     !!! IMPORTANT !!!
+#
+#         Before you edit this file, please keep in mind that contributing to the project
+#                translations is possible ONLY via the Transifex online service.
+#
+#         To submit your translations, visit https://www.transifex.com/ckeditor/ckeditor5.
+#
+#                   To learn more, check out the official contributor's guide:
+#     https://ckeditor.com/docs/ckeditor5/latest/framework/guides/contributing/contributing.html
+#
+msgid ""
+msgstr ""
+"Language-Team: Korean (https://www.transifex.com/ckeditor/teams/11143/ko/)\n"
+"Language: ko\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+msgctxt "A label of the button that allows inserting a new code block into the editor content."
+msgid "Insert code block"
+msgstr "코드 블럭 삽입"
+
+msgctxt "A language of the code block in the editor content when no specific programming language is associated with it."
+msgid "Plain text"
+msgstr "평문"

+ 25 - 0
packages/ckeditor5-code-block/lang/translations/zh.po

@@ -0,0 +1,25 @@
+# Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+#
+#                                     !!! IMPORTANT !!!
+#
+#         Before you edit this file, please keep in mind that contributing to the project
+#                translations is possible ONLY via the Transifex online service.
+#
+#         To submit your translations, visit https://www.transifex.com/ckeditor/ckeditor5.
+#
+#                   To learn more, check out the official contributor's guide:
+#     https://ckeditor.com/docs/ckeditor5/latest/framework/guides/contributing/contributing.html
+#
+msgid ""
+msgstr ""
+"Language-Team: Chinese (Taiwan) (https://www.transifex.com/ckeditor/teams/11143/zh_TW/)\n"
+"Language: zh_TW\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+msgctxt "A label of the button that allows inserting a new code block into the editor content."
+msgid "Insert code block"
+msgstr "插入程式碼區塊"
+
+msgctxt "A language of the code block in the editor content when no specific programming language is associated with it."
+msgid "Plain text"
+msgstr "純文字"

+ 1 - 0
packages/ckeditor5-code-block/package.json

@@ -24,6 +24,7 @@
     "@ckeditor/ckeditor5-editor-classic": "^21.0.0",
     "@ckeditor/ckeditor5-engine": "^21.0.0",
     "@ckeditor/ckeditor5-indent": "^21.0.0",
+    "@ckeditor/ckeditor5-markdown-gfm": "^21.0.0",
     "@ckeditor/ckeditor5-paragraph": "^21.0.0",
     "@ckeditor/ckeditor5-undo": "^21.0.0"
   },

+ 3 - 37
packages/ckeditor5-code-block/src/converters.js

@@ -193,48 +193,14 @@ export function dataViewToModelCodeBlockInsertion( editingView, languageDefs ) {
 
 		writer.append( fragment, codeBlock );
 
-		// Let's see if the codeBlock can be inserted the current modelCursor.
-		const splitResult = conversionApi.splitToAllowedParent( codeBlock, 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 ) {
+		// Let's try to insert code block.
+		if ( !conversionApi.safeInsert( codeBlock, data.modelCursor ) ) {
 			return;
 		}
 
-		// Insert element on allowed position.
-		writer.insert( codeBlock, splitResult.position );
-
 		consumable.consume( viewItem, { name: true } );
 		consumable.consume( viewChild, { name: true } );
 
-		const parts = conversionApi.getSplitParts( codeBlock );
-
-		// Set conversion result range.
-		data.modelRange = writer.createRange(
-			conversionApi.writer.createPositionBefore( codeBlock ),
-			conversionApi.writer.createPositionAfter( parts[ parts.length - 1 ] )
-		);
-
-		// If we had to split parent to insert our element then we want to continue conversion inside
-		// the split parent.
-		//
-		// before split:
-		//
-		//		<allowed><notAllowed>[]</notAllowed></allowed>
-		//
-		// after split:
-		//
-		//		<allowed>
-		//			<notAllowed></notAllowed>
-		//			<converted></converted>
-		//			<notAllowed>[]</notAllowed>
-		//		</allowed>
-		if ( splitResult.cursorParent ) {
-			data.modelCursor = writer.createPositionAt( splitResult.cursorParent, 0 );
-		} else {
-			// Otherwise just continue after the inserted element.
-			data.modelCursor = data.modelRange.end;
-		}
+		conversionApi.updateConversionResult( codeBlock, data );
 	};
 }

+ 62 - 0
packages/ckeditor5-code-block/tests/codeblock-integration.js

@@ -0,0 +1,62 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import Enter from '@ckeditor/ckeditor5-enter/src/enter';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import GFMDataProcessor from '@ckeditor/ckeditor5-markdown-gfm/src/gfmdataprocessor';
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+
+import CodeBlockEditing from '../src/codeblockediting';
+
+// A simple plugin that enables the GFM data processor.
+class CodeBlockIntegration extends Plugin {
+	constructor( editor ) {
+		super( editor );
+		editor.data.processor = new GFMDataProcessor( editor.data.viewDocument );
+	}
+}
+
+function getEditor( initialData = '' ) {
+	return ClassicTestEditor
+		.create( initialData, {
+			plugins: [ CodeBlockIntegration, CodeBlockEditing, Enter, Paragraph ]
+		} );
+}
+
+describe( 'CodeBlock - integration', () => {
+	describe( 'with Markdown GFM', () => {
+		it( 'should be loaded and returned from the editor (for plain text)', async () => {
+			const editor = await getEditor(
+				'```\n' +
+				'test()\n' +
+				'```'
+			);
+
+			expect( editor.getData() ).to.equal(
+				'```plaintext\n' +
+				'test()\n' +
+				'```'
+			);
+
+			await editor.destroy();
+		} );
+		it( 'should be loaded and returned from the editor (for defined language)', async () => {
+			const editor = await getEditor(
+				'```javascript\n' +
+				'test()\n' +
+				'```'
+			);
+
+			expect( editor.getData() ).to.equal(
+				'```javascript\n' +
+				'test()\n' +
+				'```'
+			);
+
+			await editor.destroy();
+		} );
+	} );
+} );

+ 1 - 1
packages/ckeditor5-core/lang/translations/ko.po

@@ -26,4 +26,4 @@ msgstr "취소"
 
 msgctxt "The label used by a button next to the color palette in the color picker that removes the color (resets it to an empty value, example usages in font color or table properties)."
 msgid "Remove color"
-msgstr "색상 지우기"
+msgstr "색깔 제거"

+ 2 - 1
packages/ckeditor5-core/src/editor/utils/dataapimixin.js

@@ -71,7 +71,8 @@ export default DataApiMixin;
  * the right format for you.
  *
  * @method #getData
- * @param {Object} [options]
+ * @param {Object} [options] Additional configuration for the retrieved data.
+ * Editor features may introduce more configuration options that can be set through this parameter.
  * @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 that whenever editor content is considered empty, an empty string is returned. To turn off trimming

+ 0 - 0
packages/ckeditor5-engine/docs/_snippets/framework/build-custom-element-converter-source.html


+ 14 - 0
packages/ckeditor5-engine/docs/_snippets/framework/build-custom-element-converter-source.js

@@ -0,0 +1,14 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals window */
+
+import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor';
+
+import { toWidget, toWidgetEditable } from '@ckeditor/ckeditor5-widget/src/utils';
+
+window.ClassicEditor = ClassicEditor;
+window.toWidget = toWidget;
+window.toWidgetEditable = toWidgetEditable;

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

@@ -8,6 +8,6 @@
 
 <div id="snippet-link-unsafe-classes">
 	<p>All links in this <a href="https://ckeditor.com">editor</a> that do not use the <a href="http://developer.mozilla.org/en-US/docs/Glossary/https">HTTPS</a> protocol
-	have a custom <code>.unsafe-link</code> CSS <a href="http://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class">class</a> that marks them red.</p>
+	have a custom <code>.unsafe-link</code> CSS <a href="http://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class">class</a> that marks them with a predefined graphic differentiator.</p>
 	<p>Edit the URL of the links using "http://" or "https://" to see them marked as "safe" or "unsafe".</p>
 </div>

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

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

+ 61 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-custom-element-converter.html

@@ -0,0 +1,61 @@
+<style>
+	.info-box {
+		background: hsl(0, 0%, 45%);
+		border: 1px solid hsl(0, 0%, 80%);
+		padding: 1em;
+		border-radius: 8px;
+	}
+
+	.info-box-title {
+		margin-bottom: 0.5em;
+		font-size: 1.4em;
+		font-weight: bold;
+		color: inherit;
+	}
+
+	.info-box-warning {
+		background: hsl(48, 100%, 87%);
+		border: 1px solid hsl(47, 100%, 62%);
+	}
+
+	.info-box-warning .info-box-title  {
+		color: hsl(47, 100%, 32%)
+	}
+
+	.info-box-info {
+		background: hsl(205, 100%, 95%);
+		border: 1px solid hsl(205, 91%, 82%);
+	}
+
+	.info-box-info .info-box-title  {
+		color: hsl(204, 79%, 41%)
+	}
+
+	.info-box-content {
+		padding: 8px 10px;
+		background: hsl(0, 0%, 100%);
+		border-radius: 6px;
+	}
+
+	.info-box-content p:only-child,
+	.info-box-content p:last-child {
+		margin-bottom: 0;
+	}
+</style>
+
+<div id="editor-custom-element-converter">
+	<p>Info:</p>
+	<div class="info-box info-box-info">
+		<div class="info-box-title">Info</div>
+		<div class="info-box-content">
+			<p>Editable content of the <strong>info box</strong>.</p>
+		</div>
+	</div>
+	<p>Warning:</p>
+	<div class="info-box info-box-warning">
+		<div class="info-box-title">Warning</div>
+		<div class="info-box-content">
+			<p>Editable content of the <strong>info box</strong>.</p>
+		</div>
+	</div>
+</div>

+ 165 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-custom-element-converter.js

@@ -0,0 +1,165 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals ClassicEditor, toWidget, toWidgetEditable, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+class InfoBox {
+	constructor( editor ) {
+		// Schema definition
+		editor.model.schema.register( 'infoBox', {
+			allowWhere: '$block',
+			allowContentOf: '$root',
+			isObject: true,
+			allowAttributes: [ 'infoBoxType' ]
+		} );
+
+		// Upcast converter.
+		editor.conversion.for( 'upcast' )
+			.add( dispatcher => dispatcher.on( 'element:div', upcastConverter ) );
+
+		// The downcast conversion must be split as we need a widget in the editing pipeline.
+		editor.conversion.for( 'editingDowncast' )
+			.add( dispatcher => dispatcher.on( 'insert:infoBox', editingDowncastConverter ) );
+		editor.conversion.for( 'dataDowncast' )
+			.add( dispatcher => dispatcher.on( 'insert:infoBox', dataDowncastConverter ) );
+	}
+}
+
+function upcastConverter( event, data, conversionApi ) {
+	const viewInfoBox = data.viewItem;
+
+	// Detect that view element is an info-box div.
+	// Otherwise, it should be handled by another converter.
+	if ( !viewInfoBox.hasClass( 'info-box' ) ) {
+		return;
+	}
+
+	// Create a model structure.
+	const modelElement = conversionApi.writer.createElement( 'infoBox', {
+		infoBoxType: getTypeFromViewElement( viewInfoBox )
+	} );
+
+	// Try to safely insert element - if it returns false the element can't be safely inserted
+	// into the content, and the conversion process must stop.
+	if ( !conversionApi.safeInsert( modelElement, data.modelCursor ) ) {
+		return;
+	}
+
+	// Mark info-box div as handled by this converter.
+	conversionApi.consumable.consume( viewInfoBox, { name: true } );
+
+	// Let's assume that the HTML structure is always the same.
+	const viewInfoBoxTitle = viewInfoBox.getChild( 0 );
+	const viewInfoBoxContent = viewInfoBox.getChild( 1 );
+
+	// Mark info-box inner elements as handled by this converter.
+	conversionApi.consumable.consume( viewInfoBoxTitle, { name: true } );
+	conversionApi.consumable.consume( viewInfoBoxContent, { name: true } );
+
+	// Let the editor handle children of the info-box content conversion.
+	conversionApi.convertChildren( viewInfoBoxContent, modelElement );
+
+	// Conversion requires updating result data structure properly.
+	conversionApi.updateConversionResult( modelElement, data );
+}
+
+function editingDowncastConverter( event, data, conversionApi ) {
+	let { infoBox, infoBoxContent, infoBoxTitle } = createViewElements( data, conversionApi );
+
+	// Decorate view items as widgets.
+	infoBox = toWidget( infoBox, conversionApi.writer, { label: 'simple box widget' } );
+	infoBoxContent = toWidgetEditable( infoBoxContent, conversionApi.writer );
+
+	insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBoxContent );
+}
+
+function dataDowncastConverter( event, data, conversionApi ) {
+	const { infoBox, infoBoxContent, infoBoxTitle } = createViewElements( data, conversionApi );
+
+	insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBoxContent );
+}
+
+function createViewElements( data, conversionApi ) {
+	const type = data.item.getAttribute( 'infoBoxType' );
+
+	const infoBox = conversionApi.writer.createContainerElement( 'div', {
+		class: `info-box info-box-${ type.toLowerCase() }`
+	} );
+	const infoBoxContent = conversionApi.writer.createEditableElement( 'div', {
+		class: 'info-box-content'
+	} );
+
+	const infoBoxTitle = conversionApi.writer.createUIElement( 'div',
+		{ class: 'info-box-title' },
+		function( domDocument ) {
+			const domElement = this.toDomElement( domDocument );
+
+			domElement.innerText = type;
+
+			return domElement;
+		} );
+
+	return { infoBox, infoBoxContent, infoBoxTitle };
+}
+
+function insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBoxContent ) {
+	conversionApi.consumable.consume( data.item, 'insert' );
+
+	conversionApi.writer.insert(
+		conversionApi.writer.createPositionAt( infoBox, 0 ),
+		infoBoxTitle
+	);
+	conversionApi.writer.insert(
+		conversionApi.writer.createPositionAt( infoBox, 1 ),
+		infoBoxContent
+	);
+
+	conversionApi.mapper.bindElements( data.item, infoBox );
+	conversionApi.mapper.bindElements( data.item, infoBoxContent );
+
+	conversionApi.writer.insert(
+		conversionApi.mapper.toViewPosition( data.range.start ),
+		infoBox
+	);
+}
+
+ClassicEditor
+	.create( document.querySelector( '#editor-custom-element-converter' ), {
+		cloudServices: CS_CONFIG,
+		extraPlugins: [ InfoBox ],
+		image: {
+			toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
+		},
+		table: {
+			contentToolbar: [
+				'tableColumn',
+				'tableRow',
+				'mergeTableCells'
+			]
+		},
+		toolbar: {
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+
+function getTypeFromViewElement( viewElement ) {
+	if ( viewElement.hasClass( 'info-box-info' ) ) {
+		return 'Info';
+	}
+
+	if ( viewElement.hasClass( 'info-box-warning' ) ) {
+		return 'Warning';
+	}
+
+	return 'None';
+}

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

@@ -16,17 +16,17 @@ If you want to learn how to load some extra content (element, attributes, classe
 
 ### Code architecture
 
-It is recommended that the code that customizes the editor data and editing pipelines is delivered as {@link framework/guides/architecture/core-editor-architecture#plugins plugins} and all examples in this guide follow this convention.
+It is recommended for the code that customizes the editor data and editing pipelines to be delivered as {@link framework/guides/architecture/core-editor-architecture#plugins plugins} and all examples in this guide follow this convention.
 
 Also for the sake of simplicity all examples use the same {@link module:editor-classic/classiceditor~ClassicEditor `ClassicEditor`}, but keep in mind that code snippets will work with other editors, too.
 
-Finally, none of the converters covered in this guide require to import any module from CKEditor 5 Framework, hence, you can write them without rebuilding the editor. In other words, such converters can easily be added to existing {@link builds/guides/overview CKEditor 5 builds}.
+Finally, none of the converters covered in this guide requires to import any modules from CKEditor 5 Framework, hence, you can write them without rebuilding the editor. In other words, such converters can easily be added to existing {@link builds/guides/overview CKEditor 5 builds}.
 
 ### Granular converters
 
 You can create separate converters for the data and editing (downcast) pipelines. The former (`dataDowncast`) will customize the data in the editor output (e.g. when {@link builds/guides/integration/saving-data#manually-retrieving-the-data obtaining the editor data}). The latter (`editingDowncast`) will only work for the content of the editor when editing.
 
-If you do not want to complicate your conversion, you can just add a single (`downcast`) converter which will apply both to the data and the editing view. We did that in all examples to keep them simple but keep in mind you have options:
+If you do not want to complicate your conversion, you can just add a single (`downcast`) converter which will apply both to the data and the editing view. We did that in all the examples to keep them simple but keep in mind you have several options:
 
 ```js
 // Adds a conversion dispatcher for the editing downcast pipeline only.
@@ -47,32 +47,32 @@ editor.conversion.for( 'downcast' ).add( dispatcher => {
 
 ### CKEditor 5 inspector
 
-{@link framework/guides/development-tools#ckeditor-5-inspector CKEditor 5 inspector} is an invaluable help when working with the model and view structures. It allows browsing their structure and checking selection positions like in typical browser developer tools. Make sure to enable the inspector when playing with CKEditor 5.
+The {@link framework/guides/development-tools#ckeditor-5-inspector CKEditor 5 inspector} is an invaluable help when working with the model and view structures. It allows browsing their structure and checking selection positions like in typical browser developer tools. Make sure to enable the inspector when playing with CKEditor 5.
 
 ## Adding a CSS class to inline elements
 
 In this example all links (`<a href="...">...</a>`) get the `.my-green-link` CSS class. This includes all links in the editor output (`editor.getData()`) and all links in the edited content (existing and future ones).
 
-<info-box>
-	Note that the same behavior can be obtained with {@link features/link#custom-link-attributes-decorators link decorators}:
-
-	```js
-	ClassicEditor
-		.create( ..., {
-			// ...
-			link: {
-				decorators: {
-					addGreenLink: {
-						mode: 'automatic',
-						attributes: {
-							class: 'my-green-link'
-						}
+
+Note that the same behavior can be obtained with {@link features/link#custom-link-attributes-decorators link decorators}:
+
+```js
+ClassicEditor
+	.create( ..., {
+		// ...
+		link: {
+			decorators: {
+				addGreenLink: {
+					mode: 'automatic',
+					attributes: {
+						class: 'my-green-link'
 					}
 				}
 			}
-		} )
-	```
-</info-box>
+		}
+	} )
+```
+
 
 {@snippet framework/extending-content-add-link-class}
 
@@ -136,21 +136,20 @@ Add some CSS styles for `.my-green-link` to see the customization in action:
 
 ## Adding an HTML attribute to certain inline elements
 
-In this example all links (`<a href="...">...</a>`) that do not have "ckeditor.com" in their `href="..."` get the `target="_blank"` attribute. This includes all links in the editor output (`editor.getData()`) and all links in the edited content (existing and future ones).
+In this example all the links (`<a href="...">...</a>`) that do not have "ckeditor.com" in their `href="..."` get the `target="_blank"` attribute. This includes all links in the editor output (`editor.getData()`) and all links in the edited content (existing and future ones).
 
-<info-box>
-	Note that similar behavior can be obtained with {@link module:link/link~LinkConfig#addTargetToExternalLinks link decorators}:
-
-	```js
-	ClassicEditor
-		.create( ..., {
-			// ...
-			link: {
-				addTargetToExternalLinks: true
-			}
-		} )
-	```
-</info-box>
+
+Note that similar behavior can be obtained with {@link module:link/link~LinkConfig#addTargetToExternalLinks link decorators}:
+
+```js
+ClassicEditor
+	.create( ..., {
+		// ...
+		link: {
+			addTargetToExternalLinks: true
+		}
+	} )
+```
 
 {@snippet framework/extending-content-add-external-link-target}
 
@@ -216,27 +215,27 @@ a[target="_blank"]::after {
 
 In this example all links (`<a href="...">...</a>`) that do not have `https://` in their `href="..."` attribute get the `.unsafe-link` CSS class. This includes all links in the editor output (`editor.getData()`) and all links in the edited content (existing and future ones).
 
-<info-box>
-	Note that the same behavior can be obtained with {@link features/link#custom-link-attributes-decorators link decorators}:
-
-	```js
-	ClassicEditor
-		.create( ..., {
-			// ...
-			link: {
-				decorators: {
-					markUnsafeLink: {
-						mode: 'automatic',
-						callback: url => /^(http:)?\/\//.test( url ),
-						attributes: {
-							class: 'unsafe-link'
-						}
+
+Note that the same behavior can be obtained with {@link features/link#custom-link-attributes-decorators link decorators}:
+
+```js
+ClassicEditor
+	.create( ..., {
+		// ...
+		link: {
+			decorators: {
+				markUnsafeLink: {
+					mode: 'automatic',
+					callback: url => /^(http:)?\/\//.test( url ),
+					attributes: {
+						class: 'unsafe-link'
 					}
 				}
 			}
-		} )
-	```
-</info-box>
+		}
+	} )
+```
+
 
 {@snippet framework/extending-content-add-unsafe-link-class}
 
@@ -302,7 +301,7 @@ Add some CSS styles for "unsafe" links to make them visible:
 
 ## Adding a CSS class to block elements
 
-In this example all second–level headings (`<h2>...</h2>`) get the `.my-heading` CSS class. This includes all heading elements in the editor output (`editor.getData()`) and in the edited content (existing and future ones).
+In this example all second–level headings (`<h2>...</h2>`) get the `.my-heading` CSS class. This includes all the heading elements in the editor output (`editor.getData()`) and in the edited content (existing and future ones).
 
 {@snippet framework/extending-content-add-heading-class}
 

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

@@ -8,17 +8,17 @@ order: 10
 # For now, due to lack of content, it is called "advanced concepts".
 ---
 
-# Advanced conversion concepts
+# Advanced conversion concepts &mdash; attributes
 
 This guide extends the {@link framework/guides/architecture/editing-engine introduction to CKEditor 5 editing engine architecture}. Therefore, we highly recommend reading the former guide first.
 
-In this guide we will dive deeper into some of the conversion concepts.
+In this guide you will dive deeper into some of the conversion concepts.
 
 ## Inline and block content
 
-Generally speaking, there are two main types of the content in the editor view and data output: inline and block.
+Generally speaking, there are two main types of content in the editor view and data output: inline and block.
 
-The inline content means elements like `<strong>`, `<a>` or `<span>`. Unlike `<p>`, `<blockquote>` or `<div>`, inline elements do not structure the data. Instead, they mark some text in a specific (visual and semantical) way. These elements are a characteristic of a text. For instance, you could say that some part of the text is bold, or is linked, etc.. This concept has its reflection in the model of the rich-text editor where `<a>` or `<strong>` are not represented as elements. Instead, they are attributes of the text.
+The inline content means elements like `<strong>`, `<a>` or `<span>`. Unlike `<p>`, `<blockquote>` or `<div>`, inline elements do not structure the data. Instead, they format some text in a specific (visual and semantical) way. These elements are a characteristic of a text. For instance, you could say that some part of the text is bold, or is linked, etc. This concept has its reflection in the model of the rich-text editor where `<a>` or `<strong>` are not represented as elements. Instead, they are the attributes of the text.
 
 For example &mdash; in the model, you might have a `<paragraph>` element with the "Foo bar" text, where "bar" has the `bold` attribute set to `true`. A pseudo–code of this *model* data structure could look as follows:
 
@@ -30,7 +30,7 @@ For example &mdash; in the model, you might have a `<paragraph>` element with th
 ```
 
 <info-box>
-	Throughout the rest of this guide the following, shorter convention will be used to represent model text attributes:
+	Throughout the rest of this guide the following, shorter convention will be used to represent model text attributes for the sake of clarity:
 
 	```html
 	<paragraph>Foo <$text bold="true">bar</$text></paragraph>
@@ -39,11 +39,11 @@ For example &mdash; in the model, you might have a `<paragraph>` element with th
 
 Note that there is no `<strong>` or any other additional element there, it is just some text with an attribute.
 
-So, when does this text become wrapped with a `<strong>` element? This happens during the conversion to the view. It is also important to know which type of a view element needs to be used. In case of elements that represent inline formatting, this should be an {@link module:engine/view/attributeelement~AttributeElement}.
+So, when does this text become wrapped with a `<strong>` element? This happens during the conversion to the view. It is also important to know what type of a view element needs to be used. In the case of the elements that represent inline formatting, this should be an {@link module:engine/view/attributeelement~AttributeElement}.
 
 ## Conversion of multiple text attributes
 
-A model text node may have multiple attributes (e.g. be bolded and linked) and all of them are converted to their respective view elements by independent converters.
+A model text node may have multiple attributes (e.g. be bolded and linked) and all of them are converted into their respective view elements by independent converters.
 
 Keep in mind that in the model, attributes do not have any specific order. This is contrary to the editor view or HTML output, where inline elements are nested in one another. Fortunately, the nesting happens automatically during the conversion from the model to the view. This makes working in the model simpler, as features do not need to take care of breaking or rearranging elements in the model.
 
@@ -73,7 +73,7 @@ Note that the `<a>` element is converted in such way that it always becomes the
 </p>
 ```
 
-There are two links with the same `href` attribute next to each other in the generated view (editor output), which is semantically wrong. To make sure that it never happens, the view element that represents a link must have a *priority* defined. Most elements, like for instance `<strong>`, do not care about it and stick to the default priority (`10`). The {@link features/link link feature} ensures that all view `<a>` elements have the priority set to `5` so they are kept outside other elements.
+There are two links with the same `href` attribute next to each other in the generated view (editor output), which is semantically wrong. To make sure that it never happens, the view element that represents a link must have a *priority* defined. Most elements, like for instance `<strong>`, do not care about it and stick to the default priority (`10`). The {@link features/link link feature} ensures that all `<a>` view elements have the priority set to `5` therefore they are kept outside other elements.
 
 ## Merging attribute elements during conversion
 
@@ -112,9 +112,9 @@ But this is not the most optimal output you can get from the rich-text editor. W
 </p>
 ```
 
-Obviously a single `<span>` makes more sense. And thanks to the merging mechanism built into the conversion process, this would be the actual result of the conversion.
+Obviously a single `<span>` makes more sense. And thanks to the merging mechanism built into the conversion process, this would be the actual output of the conversion.
 
-Why is it so? In the above scenario, two model attributes are converted to `<span>` elements. When the first attribute (say, `fontFamily`) is converted, there is no `<span>` in the view yet. So the first `<span>` is added with the `style` attribute. But then, when `fontSize` is converted, the `<span>` is already in the view. The {@link module:engine/view/downcastwriter~DowncastWriter downcast writer} recognizes it and checks whether the elements can be merged, following these 3 rules:
+Why is it so? In the above scenario, two model attributes are converted to `<span>` elements. When the first attribute (say, `fontFamily`) is converted, there is no `<span>` in the view yet. So the first `<span>` is added with the `style` attribute. But then, when `fontSize` is converted, the `<span>` is already in the view. The {@link module:engine/view/downcastwriter~DowncastWriter downcast writer} recognizes it and checks whether these elements can be merged, following these 3 rules:
 
 1. Both elements must have the same {@link module:engine/view/element~Element#name name}.
 2. Both elements must have the same {@link module:engine/view/attributeelement~AttributeElement#priority priority}.

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

@@ -16,42 +16,41 @@ Eventually, this knowledge will allow you to create your custom features on top
 
 ### Code architecture
 
-It is recommended that the code that customizes the editor data and editing pipelines is delivered as {@link framework/guides/architecture/core-editor-architecture#plugins plugins} and all examples in this guide follow this convention.
+It is recommended for the code that customizes the editor data and editing pipelines to be delivered as {@link framework/guides/architecture/core-editor-architecture#plugins plugins} and all examples in this guide follow this convention.
 
 Also for the sake of simplicity all examples use the same {@link module:editor-classic/classiceditor~ClassicEditor `ClassicEditor`}, but keep in mind that code snippets will work with other editors, too.
 
-Finally, none of the converters covered in this guide require to import any module from CKEditor 5 Framework, hence, you can write them without rebuilding the editor. In other words, such converters can easily be added to existing {@link builds/guides/overview CKEditor 5 builds}.
+Finally, none of the converters covered in this guide requires to import any modules from CKEditor 5 Framework, hence, you can write them without rebuilding the editor. In other words, such converters can easily be added to existing {@link builds/guides/overview CKEditor 5 builds}.
 
 ### CKEditor 5 inspector
 
-{@link framework/guides/development-tools#ckeditor-5-inspector CKEditor 5 inspector} is an invaluable help when working with the model and view structures. It allows browsing their structure and checking selection positions like in typical browser developer tools. Make sure to enable the inspector when playing with CKEditor 5.
+The {@link framework/guides/development-tools#ckeditor-5-inspector CKEditor 5 inspector} is an invaluable help when working with the model and view structures. It allows browsing their structure and checking selection positions like in typical browser developer tools. Make sure to enable the inspector when playing with CKEditor 5.
 
 ## Loading content with a custom attribute
 
-In this example links (`<a href="...">...</a>`) loaded into the editor content will preserve their `target` attribute, which is not supported by the {@link features/link Link} feature. The DOM `target` attribute will be stored in the editor model as a `linkTarget` attribute.
+In this example the links (`<a href="...">...</a>`) loaded into the editor content will preserve their `target` attribute, which is by default *not* supported by the {@link features/link Link} feature. The DOM `target` attribute will be stored in the editor model as a `linkTarget` attribute.
 
-Unlike the {@link framework/guides/deep-dive/conversion-extending-output#adding-an-html-attribute-to-certain-inline-elements downcast–only solution}, this approach does not change the content loaded into the editor. Links without the `target` attribute will not get one and links with the attribute will preserve its value.
+Unlike the {@link framework/guides/deep-dive/conversion-extending-output#adding-an-html-attribute-to-certain-inline-elements downcast–only solution}, this approach does not change the content loaded into the editor. Any links without the `target` attribute will not get one while all the links with the attribute will preserve its value.
 
-<info-box>
-	Note that the same behavior can be obtained with {@link features/link#custom-link-attributes-decorators link decorators}:
-
-	```js
-	ClassicEditor
-		.create( ..., {
-			// ...
-			link: {
-				decorators: {
-					addGreenLink: {
-						mode: 'automatic',
-						attributes: {
-							class: 'my-green-link'
-						}
+
+Note that the same behavior can be obtained with {@link features/link#custom-link-attributes-decorators link decorators}:
+
+```js
+ClassicEditor
+	.create( ..., {
+		// ...
+		link: {
+			decorators: {
+				addGreenLink: {
+					mode: 'automatic',
+					attributes: {
+						class: 'my-green-link'
 					}
 				}
 			}
-		} )
-	```
-</info-box>
+		}
+	} )
+```
 
 {@snippet framework/extending-content-allow-link-target}
 
@@ -119,7 +118,7 @@ a[target]::after {
 
 ## Loading content with all attributes
 
-In this example `<div>` elements (`<div>...</div>`) loaded into the editor content will preserve their attributes. All the DOM attributes will be stored in the editor model as corresponding attributes.
+In this example the `<div>` elements (`<div>...</div>`) loaded into the editor content will preserve their attributes. All the DOM attributes will be stored in the editor model as corresponding attributes.
 
 {@snippet framework/extending-content-allow-div-attributes}
 
@@ -128,10 +127,10 @@ All attributes are allowed on `<div>` elements thanks to custom "upcast" and "do
 Allowing every possible attribute on a `<div>` element in the model is done by adding an {@link module:engine/model/schema~Schema#addAttributeCheck addAttributeCheck()} callback.
 
 <info-box>
-	Allowing every attribute on `<div>` elements might introduce security issues &mdash; including XSS attacks. The production code should use only application-related attributes and/or properly encode data.
+	Allowing every attribute on `<div>` elements might introduce security issues &mdash; including XSS attacks. The production code should use only application-related attributes and/or properly encode the data.
 </info-box>
 
-Adding "upcast" and "downcast" converters for the `<div>` element is enough for cases where its attributes do not change. If the attributes in the model are modified, these `elementToElement()` converters will not be called as the `<div>` is already converted. To overcome this, a lower-level API is used.
+Adding "upcast" and "downcast" converters for the `<div>` element is enough for these cases where its attributes do not change. If the attributes in the model are modified, however, these `elementToElement()` converters will not be called as the `<div>` is already converted. To overcome this, a lower-level API is used.
 
 Instead of using predefined converters, the {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event-attribute `attribute`} event listener is registered for the "downcast" dispatcher.
 
@@ -150,7 +149,7 @@ function ConvertDivAttributes( editor ) {
 		}
 	} );
 
-	// View-to-model converter converting a view <div> with all its attributes to the model.
+	// The view-to-model converter converting a view <div> with all its attributes to the model.
 	editor.conversion.for( 'upcast' ).elementToElement( {
 		view: 'div',
 		model: ( viewElement, modelWriter ) => {
@@ -158,13 +157,13 @@ function ConvertDivAttributes( editor ) {
 		}
 	} );
 
-	// Model-to-view converter for the <div> element (attributes are converted separately).
+	// The model-to-view converter for the <div> element (attributes are converted separately).
 	editor.conversion.for( 'downcast' ).elementToElement( {
 		model: 'div',
 		view: 'div'
 	} );
 
-	// Model-to-view converter for <div> attributes.
+	// The model-to-view converter for <div> attributes.
 	// Note that a lower-level, event-based API is used here.
 	editor.conversion.for( 'downcast' ).add( dispatcher => {
 		dispatcher.on( 'attribute', ( evt, data, conversionApi ) => {
@@ -229,8 +228,8 @@ function HandleFontSizeValue( editor ) {
 			value: viewElement => {
 				const value = parseFloat( viewElement.getStyle( 'font-size' ) ).toFixed( 0 );
 
-				// It might be needed to further convert the value to meet business requirements.
-				// In the sample the font size is configured to handle only the sizes:
+				// It might be necessary to further convert the value to meet business requirements.
+				// In the sample the font size is configured to handle only these sizes:
 				// 12, 14, 'default', 18, 20, 22, 24, 26, 28, 30
 				// Other sizes will be converted to the model but the UI might not be aware of them.
 
@@ -241,7 +240,7 @@ function HandleFontSizeValue( editor ) {
 		converterPriority: 'high'
 	} );
 
-	// Add a special converter for the font size feature to convert all (even not configured)
+	// Add a special converter for the font size feature to convert all (even the not configured)
 	// model attribute values.
 	editor.conversion.for( 'downcast' ).attributeToElement( {
 		model: {
@@ -278,7 +277,7 @@ ClassicEditor
 
 ## Adding extra attributes to elements contained in a figure
 
-The {@link features/image Image} and {@link features/table Table} features wrap view elements (`<img>` for Image nad `<table>` for Table) in `<figure>`. During the downcast conversion, the model element is mapped to `<figure>` and not the inner element. In such cases the default `conversion.attributeToAttribute()` conversion helpers could lose information about the element that the attribute should be set on.
+The {@link features/image image} and {@link features/table table} features wrap view elements (`<img>` for image and `<table>` for table, respectively) in a `<figure>` element. During the downcast conversion, the model element is mapped to `<figure>` and not the inner element. In such cases the default `conversion.attributeToAttribute()` conversion helpers could lose information about the element that the attribute should be set on.
 
 To overcome this limitation it is sufficient to write a custom converter that adds custom attributes to elements already converted by base features. The key point is to add these converters with a lower priority than the base converters so they will be called after the base ones.
 
@@ -288,11 +287,11 @@ The sample below is extensible. To add your own attributes to preserve, just add
 
 ```js
 /**
- * Plugin that converts custom attributes for elements that are wrapped in <figure> in the view.
+ * A plugin that converts custom attributes for elements that are wrapped in <figure> in the view.
  */
 class CustomFigureAttributes {
 	/**
-	 * Plugin's constructor - receives editor instance on creation.
+	 * Plugin's constructor - receives an editor instance on creation.
 	 */
 	constructor( editor ) {
 		// Save reference to the editor.
@@ -300,9 +299,9 @@ class CustomFigureAttributes {
 	}
 
 	/**
-	 * Setups conversion and extends table & image features schema.
+	 * Sets the conversion up and extends the table & image features schema.
 	 *
-	 * Schema extending must be done in the “afterInit()” call because plugins define their schema in “init()“.
+	 * Schema extending must be done in the "afterInit()" call because plugins define their schema in "init()".
 	 */
 	afterInit() {
 		const editor = this.editor;
@@ -320,23 +319,24 @@ class CustomFigureAttributes {
 }
 
 /**
- * Sets up a conversion that preservers classes on <img> and <table> elements.
+ * Sets up a conversion that preserves classes on <img> and <table> elements.
  */
 function setupCustomClassConversion( viewElementName, modelElementName, editor ) {
-	// The 'customClass' attribute will store custom classes from the data in the model so schema definitions allow this attribute.
+	// The 'customClass' attribute stores custom classes from the data in the model so that schema definitions allow this attribute.
 	editor.model.schema.extend( modelElementName, { allowAttributes: [ 'customClass' ] } );
 
-	// Define upcast converters for the <img> and <table> elements with a "low" priority so they are run after the default converters.
+	// Defines upcast converters for the <img> and <table> elements with a "low" priority so they are run after the default converters.
 	editor.conversion.for( 'upcast' ).add( upcastCustomClasses( viewElementName ), { priority: 'low' } );
 
-	// Define downcast converters for a model element with a "low" priority so they are run after the default converters.
-	// Use `downcastCustomClassesToFigure` if you'd like to keep your classes on <figure> element or `downcastCustomClassesToChild` if you'd like to keep your classes on a <figure> child element, i.e. <img>.
+	// Defines downcast converters for a model element with a "low" priority so they are run after the default converters.
+	// Use `downcastCustomClassesToFigure` if you want to keep your classes on <figure> element or `downcastCustomClassesToChild`
+	// if you would like to keep your classes on a <figure> child element, i.e. <img>.
 	editor.conversion.for( 'downcast' ).add( downcastCustomClassesToFigure( modelElementName ), { priority: 'low' } );
 	// editor.conversion.for( 'downcast' ).add( downcastCustomClassesToChild( viewElementName, modelElementName ), { priority: 'low' } );
 }
 
 /**
- * Sets up a conversion for a custom attribute on view elements contained inside a <figure>.
+ * Sets up a conversion for a custom attribute on the view elements contained inside a <figure>.
  *
  * This method:
  * - Adds proper schema rules.
@@ -344,7 +344,7 @@ function setupCustomClassConversion( viewElementName, modelElementName, editor )
  * - Adds a downcast converter.
  */
 function setupCustomAttributeConversion( viewElementName, modelElementName, viewAttribute, editor ) {
-	// Extend the schema to store an attribute in the model.
+	// Extends the schema to store an attribute in the model.
 	const modelAttribute = `custom${ viewAttribute }`;
 
 	editor.model.schema.extend( modelElementName, { allowAttributes: [ modelAttribute ] } );

+ 310 - 0
packages/ckeditor5-engine/docs/framework/guides/deep-dive/custom-element-conversion.md

@@ -0,0 +1,310 @@
+---
+category: framework-deep-dive-conversion
+menu-title: Custom element conversion
+order: 40
+---
+
+{@snippet framework/build-custom-element-converter-source}
+
+There are three levels on which elements can be converted:
+
+* By using the two-way converter: {@link module:engine/conversion/conversion~Conversion#elementToElement `conversion.elementToElement()`}. It is a fully declarative API. It is the least powerful option but it is the easiest one to use.
+* By using one-way converters: for example {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `conversion.for( 'downcast' ).elementToElement()`} and {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToElement `conversion.for( 'upcast' ).elementToElement()`}. In this case, you need to define at least two converters (for upcast and downcast), but the "how" part becomes a callback, and hence you gain more control over it.
+* Finally, by using event-based converters. In this case, you need to listen to events fired by {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} and {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}. This method has the full access to every bit of logic that a converter needs to implement and therefore it can be used to write the most complex conversion methods.
+
+In this guide, we will show you how to migrate from a simple two-way converter to an event-based converters as the requirements regarding the feature get more complex.
+
+## Introduction
+
+Let's assume that content in your application contains "info boxes". As for now, it was only required to wrap part of a content in a `<div>` element that would look in the data and editing views like this:
+
+```html
+<div class="info-box">
+	<!-- any editable content -->
+	<p>This is <strong>important!</strong></p>
+</div>
+```
+
+This data is represented in the model as the following structure:
+
+```html
+<infoBox>
+	<!-- any $block content: -->
+	<paragraph><$text>This is </$text><$text bold="true">important!</$text></paragraph>
+</infoBox>
+```
+
+This can be easily done with the below schema and converters in a simple `InfoBox` plugin:
+
+```js
+class InfoBox {
+	constructor( editor ) {
+		// 1. Define infoBox as an object that can be contained any other content.
+		editor.model.schema.register( 'infoBox', {
+			allowWhere: '$block',
+			allowContentOf: '$root',
+			isObject: true
+		} );
+
+		// 2. Conversion is straight forward:
+		editor.conversion.elementToElement( {
+			model: 'infoBox',
+			view: {
+				name: 'div',
+				classes: 'info-box'
+			}
+		} );
+	}
+}
+```
+
+## Migrating to an event-based converter
+
+Let's now assume that the requirements have changed and there is a need for adding an additional element in the data and editing views that will display the type of the info box (warning, error, info, etc.).
+
+The new info box structure:
+
+```html
+<div class="info-box info-box-warning">
+	<div class="info-box-title">Warning</div>
+	<div class="info-box-content">
+		<!-- any editable content -->
+		<p>This is <strong>important!</strong></p>
+	</div>
+</div>
+```
+
+The "Warning" part should not be editable. It defines a type of the info box so we can store this  bit of information as an attribute of the `<infoBox>` element:
+
+```html
+<infoBox infoBoxType="warning">
+	<!-- any $block content: -->
+	<paragraph><$text>This is </$text><$text bold="true">important!</$text></paragraph>
+</infoBox>
+```
+
+Let's see how to update our basic implementation to cover these requirements.
+
+### Demo
+
+Below is a demo of the editor with the example info box.
+
+{@snippet framework/extending-content-custom-element-converter}
+
+### Schema
+
+The type of the box is defined by the additional class on the main `<div>` but it is also represented as text in `<div class="info-box-title">`. All the info box content must be now placed inside `<div class="info-box-content">` instead of the main wrapper.
+
+For the above requirements we can see that the model structure of the `infoBox` does not need to change much. We can still use a single element in the model. The only addition to the model is an attribute that will hold information about the info box type:
+
+```js
+editor.model.schema.register( 'infoBox', {
+	allowWhere: '$block',
+	allowContentOf: '$root',
+	isObject: true,
+	allowAttributes: [ 'infoBoxType' ] // Added
+} );
+```
+
+### Event-based upcast converter
+
+The conversion of the type of the box itself could be achieved by using {@link module:engine/conversion/conversion~Conversion#attributeToAttribute `attributeToAttribute()`} (`info-box-*` CSS classes to the `infoBoxType` model attribute). However, two more changes were made to the data format that we need to handle:
+
+* There is the new `<div class="info-box-title">` element that should be ignored during upcast conversion as it duplicates the information conveyed by the main element's CSS class.
+* The content of the info box is now located inside another element (previously it was located directly in the main wrapper).
+
+Neither two-way nor one-way converters can handle such conversion. Therefore, we need to use an event-based converter with the following behavior:
+
+1. Create model `<infoBox>` element with `infoBoxType` attribute.
+1. Skip conversion of `<div class="info-box-title">` as the information about type can be obtained from the wrapper's CSS classes.
+1. Convert children of `<div class="info-box-content">` and insert them directly into `<infoBox>`.
+
+```js
+function upcastConverter( event, data, conversionApi ) {
+	const viewInfoBox = data.viewItem;
+
+	// Check whether the view element is an info box <div>.
+	// Otherwise, it should be handled by another converter.
+	if ( !viewInfoBox.hasClass( 'info-box' ) ) {
+		return;
+	}
+
+	// Create a model structure.
+	const modelElement = conversionApi.writer.createElement( 'infoBox', {
+		infoBoxType: getTypeFromViewElement( viewInfoBox )
+	} );
+
+	// Try to safely insert the element into the model structure.
+	// If `safeInsert()` returns `false` the element cannot be safely inserted
+	// into the content, and the conversion process must stop.
+	// This may happen if the data that we are converting has incorrect structure
+	// (e.g. was copied from an external website).
+	if ( !conversionApi.safeInsert( modelElement, data.modelCursor ) ) {
+		return;
+	}
+
+	// Mark the info box <div> as handled by this converter.
+	conversionApi.consumable.consume( viewInfoBox, { name: true } );
+
+	// Let's assume that the HTML structure is always the same.
+	// Note: for full bulletproofing this converter we should also check
+	// whether these elements are the right ones.
+	const viewInfoBoxTitle = viewInfoBox.getChild( 0 );
+	const viewInfoBoxContent = viewInfoBox.getChild( 1 );
+
+	// Mark info box inner elements (title and content <div>s) as handled by this converter.
+	conversionApi.consumable.consume( viewInfoBoxTitle, { name: true } );
+	conversionApi.consumable.consume( viewInfoBoxContent, { name: true } );
+
+	// Let the editor handle children of <div class="info-box-content">.
+	conversionApi.convertChildren( viewInfoBoxContent, modelElement );
+
+	// Finally, update the conversion's modelRange and modelCursor.
+	conversionApi.updateConversionResult( modelElement, data );
+}
+
+// Helper function to read the type from the view classes.
+function getTypeFromViewElement( viewElement ) {
+	if ( viewElement.hasClass( 'info-box-info' ) ) {
+		return 'Info';
+	}
+
+	if ( viewElement.hasClass( 'info-box-warning' ) ) {
+		return 'Warning';
+	}
+
+	return 'None';
+}
+```
+
+This upcast converter callback can now be plugged by adding a listener to the {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#element `UpcastDispatcher#element` event}. We will listen to `element:div` to ensure that the callback is called only for `<div>` elements.
+
+```js
+editor.conversion.for( 'upcast' )
+	.add( dispatcher => dispatcher.on( 'element:div', upcastConverter ) );
+```
+
+### Event-based downcast converter
+
+The missing bit are the downcast converters for the editing and data pipelines. We want to use the widget system to make the info box behave like an "object". The other aspect that we need to take care of is the fact that the view structure has more elements than the model structure. In this case, we could actually use one-way converters. However, we will showcase how an event-based converter would look.
+
+<info-box>
+	See the {@link framework/guides/tutorials/implementing-a-block-widget Implementing a block widget} to learn about the widget system.
+</info-box>
+
+The remaining downcast converters:
+
+```js
+function editingDowncastConverter( event, data, conversionApi ) {
+	let { infoBox, infoBoxContent, infoBoxTitle } = createViewElements( data, conversionApi );
+
+	// Decorate view items as a widget and widget editable area.
+	infoBox = toWidget( infoBox, conversionApi.writer, { label: 'info box widget' } );
+	infoBoxContent = toWidgetEditable( infoBoxContent, conversionApi.writer );
+
+	insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBoxContent );
+}
+
+function dataDowncastConverter( event, data, conversionApi ) {
+	const { infoBox, infoBoxContent, infoBoxTitle } = createViewElements( data, conversionApi );
+
+	insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBoxContent );
+}
+
+function createViewElements( data, conversionApi ) {
+	const type = data.item.getAttribute( 'infoBoxType' );
+
+	const infoBox = conversionApi.writer.createContainerElement( 'div', {
+		class: `info-box info-box-${ type.toLowerCase() }`
+	} );
+	const infoBoxContent = conversionApi.writer.createEditableElement( 'div', {
+		class: 'info-box-content'
+	} );
+
+	const infoBoxTitle = conversionApi.writer.createUIElement( 'div',
+		{ class: 'info-box-title' },
+		function( domDocument ) {
+			const domElement = this.toDomElement( domDocument );
+
+			domElement.innerText = type;
+
+			return domElement;
+		} );
+
+	return { infoBox, infoBoxContent, infoBoxTitle };
+}
+
+function insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBoxContent ) {
+	conversionApi.consumable.consume( data.item, 'insert' );
+
+	conversionApi.writer.insert(
+		conversionApi.writer.createPositionAt( infoBox, 0 ),
+		infoBoxTitle
+	);
+	conversionApi.writer.insert(
+		conversionApi.writer.createPositionAt( infoBox, 1 ),
+		infoBoxContent
+	);
+
+	// The default mapping between the model <infoBox> and its view representation.
+	conversionApi.mapper.bindElements( data.item, infoBox );
+	// However, since the model <infoBox> content need to end up in the inner
+	// <div class="info-box-content"> we need to bind one with another overriding
+	// part of the default binding.
+	conversionApi.mapper.bindElements( data.item, infoBoxContent );
+
+	conversionApi.writer.insert(
+		conversionApi.mapper.toViewPosition( data.range.start ),
+		infoBox
+	);
+}
+```
+
+These two converters need to be plugged as listeners to the {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#insert `DowncastDispatcher#insert` event}:
+
+```js
+editor.conversion.for( 'editingDowncast' )
+	.add( dispatcher => dispatcher.on( 'insert:infoBox', editingDowncastConverter ) );
+editor.conversion.for( 'dataDowncast' )
+	.add( dispatcher => dispatcher.on( 'insert:infoBox', dataDowncastConverter ) );
+```
+
+### Updated plugin code
+
+The updated `InfoBox` plugin that glues all this together:
+
+```js
+class InfoBox {
+	constructor( editor ) {
+		// Schema definition
+		editor.model.schema.register( 'infoBox', {
+			allowWhere: '$block',
+			allowContentOf: '$root',
+			isObject: true,
+			allowAttributes: [ 'infoBoxType' ]
+		} );
+
+		// Upcast converter.
+		editor.conversion.for( 'upcast' )
+			.add( dispatcher => dispatcher.on( 'element:div', upcastConverter ) );
+
+		// The downcast conversion must be split as we need a widget in the editing pipeline.
+		editor.conversion.for( 'editingDowncast' )
+			.add( dispatcher => dispatcher.on( 'insert:infoBox', editingDowncastConverter ) );
+		editor.conversion.for( 'dataDowncast' )
+			.add( dispatcher => dispatcher.on( 'insert:infoBox', dataDowncastConverter ) );
+	}
+}
+
+function upcastConverter() {
+	// ...
+}
+
+function editingDowncastConverter() {
+	// ...
+}
+
+function dataDowncastConverter() {
+	// ...
+}
+```

+ 312 - 11
packages/ckeditor5-engine/docs/framework/guides/deep-dive/schema.md

@@ -1,5 +1,6 @@
 ---
 category: framework-deep-dive
+classes: schema-deep-dive
 ---
 
 # Schema
@@ -10,9 +11,9 @@ This article assumes that you have already read the {@link framework/guides/arch
 
 The editor's schema is available in the {@link module:engine/model/model~Model#schema `editor.model.schema`} property. It defines allowed model structures (how model elements can be nested), allowed attributes (of both elements and text nodes), and other characteristics (inline vs. block, atomicity in regards of external actions). This information is later used by editing features and the editing engine to decide how to process the model, where to enable features, etc.
 
-Schema rules can be defined by using the {@link module:engine/model/schema~Schema#register `Schema#register()`} or {@link module:engine/model/schema~Schema#extend `Schema#extend()`} methods. The former can be used only once for a given item name which ensures that only a single editing feature can introduce this item. Similarly, `extend()` can only be used for defined items.
+Schema rules can be defined by using the {@link module:engine/model/schema~Schema#register `Schema#register()`} or the {@link module:engine/model/schema~Schema#extend `Schema#extend()`} methods. The former can be used only once for a given item name which ensures that only a single editing feature can introduce this item. Similarly, `extend()` can only be used for defined items.
 
-Elements and attributes are checked by features separately by using the {@link module:engine/model/schema~Schema#checkChild `Schema#checkChild()`} and {@link module:engine/model/schema~Schema#checkAttribute `Schema#checkAttribute()`} methods.
+Elements and attributes are checked by features separately by using the {@link module:engine/model/schema~Schema#checkChild `Schema#checkChild()`} and the {@link module:engine/model/schema~Schema#checkAttribute `Schema#checkAttribute()`} methods.
 
 ## Defining allowed structures
 
@@ -46,7 +47,223 @@ While this would be incorrect:
 
 ## Defining additional semantics
 
-In addition to setting allowed structures, the schema can also define additional traits of model elements. By using the `is*` properties, a feature author may declare how a certain element should be treated by other features and the engine.
+In addition to setting allowed structures, the schema can also define additional traits of model elements. By using the `is*` properties, a feature author may declare how a certain element should be treated by other features and by the engine.
+
+Here is a table listing various model elements and their properties registered in the schema:
+
+<table>
+	<thead>
+		<tr>
+			<th rowspan="2">Schema entry</th>
+			<th colspan="6">Properties in the <a href="#defining-allowed-structures">definition</a></th>
+		</tr>
+		<tr>
+			<th><a href="#block-elements"><code>isBlock</code></a></th>
+			<th><a href="#limit-elements"><code>isLimit</code></a></th>
+			<th><a href="#object-elements"><code>isObject</code></a></th>
+			<th><a href="#inline-elements"><code>isInline</code></a></th>
+			<th><a href="#selectable-elements"><code>isSelectable</code></a></th>
+			<th><a href="#content-elements"><code>isContent</code></a></th>
+		</tr>
+	</thead>
+	<tbody>
+		<tr>
+			<td><code>$block</code></td>
+			<td class="value_positive"><code>true</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+		</tr>
+		<tr>
+			<td><code>$clipboardHolder</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_positive"><code>true</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+		</tr>
+		<tr>
+			<td><code>$marker</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+		</tr>
+		<tr>
+			<td><code>$root</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_positive"><code>true</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+		</tr>
+		<tr>
+			<td><code>$text</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_positive"><code>true</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_positive"><code>true</code></td>
+		</tr>
+		<tr>
+			<td><code>blockQuote</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+		</tr>
+		<tr>
+			<td><code>caption</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_positive"><code>true</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+		</tr>
+		<tr>
+			<td><code>codeBlock</code></td>
+			<td class="value_positive"><code>true</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+		</tr>
+		<tr>
+			<td><code>heading1</code></td>
+			<td class="value_positive"><code>true</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+		</tr>
+		<tr>
+			<td><code>heading2</code></td>
+			<td class="value_positive"><code>true</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+		</tr>
+		<tr>
+			<td><code>heading3</code></td>
+			<td class="value_positive"><code>true</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+		</tr>
+		<tr>
+			<td><code>horizontalLine</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_positive_inherited"><code>true</code><a href="#inherited1"><sup>[1]</sup></a></td>
+			<td class="value_positive"><code>true</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_positive_inherited"><code>true</code><a href="#inherited2"><sup>[2]</sup></a></td>
+			<td class="value_positive_inherited"><code>true</code><a href="#inherited3"><sup>[3]</sup></a></td>
+		</tr>
+		<tr>
+			<td><code>image</code></td>
+			<td class="value_positive"><code>true</code></td>
+			<td class="value_positive_inherited"><code>true</code><a href="#inherited1"><sup>[1]</sup></a></td>
+			<td class="value_positive"><code>true</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_positive_inherited"><code>true</code><a href="#inherited2"><sup>[2]</sup></a></td>
+			<td class="value_positive_inherited"><code>true</code><a href="#inherited3"><sup>[3]</sup></a></td>
+		</tr>
+		<tr>
+			<td><code>listItem</code></td>
+			<td class="value_positive"><code>true</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+		</tr>
+		<tr>
+			<td><code>media</code></td>
+			<td class="value_positive"><code>true</code></td>
+			<td class="value_positive_inherited"><code>true</code><a href="#inherited1"><sup>[1]</sup></a></td>
+			<td class="value_positive"><code>true</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_positive_inherited"><code>true</code><a href="#inherited2"><sup>[2]</sup></a></td>
+			<td class="value_positive_inherited"><code>true</code><a href="#inherited3"><sup>[3]</sup></a></td>
+		</tr>
+		<tr>
+			<td><code>pageBreak</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_positive_inherited"><code>true</code><a href="#inherited1"><sup>[1]</sup></a></td>
+			<td class="value_positive"><code>true</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_positive_inherited"><code>true</code><a href="#inherited2"><sup>[2]</sup></a></td>
+			<td class="value_positive_inherited"><code>true</code><a href="#inherited3"><sup>[3]</sup></a></td>
+		</tr>
+		<tr>
+			<td><code>paragraph</code></td>
+			<td class="value_positive"><code>true</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+		</tr>
+		<tr>
+			<td><code>softBreak</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_positive"><code>true</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+		</tr>
+		<tr>
+			<td><code>table</code></td>
+			<td class="value_positive"><code>true</code></td>
+			<td class="value_positive_inherited"><code>true</code><a href="#inherited1"><sup>[1]</sup></a></td>
+			<td class="value_positive"><code>true</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_positive_inherited"><code>true</code><a href="#inherited2"><sup>[2]</sup></a></td>
+			<td class="value_positive_inherited"><code>true</code><a href="#inherited3"><sup>[3]</sup></a></td>
+		</tr>
+		<tr>
+			<td><code>tableRow</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_positive"><code>true</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+		</tr>
+		<tr>
+			<td><code>tableCell</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_positive"><code>true</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_negative"><code>false</code></td>
+			<td class="value_positive"><code>true</code></td>
+			<td class="value_negative"><code>false</code></td>
+		</tr>
+	</tbody>
+</table>
+
+<info-box>
+	* <span id="inherited1">[1]</span> The value of `isLimit` is `true` for this element because all [objects](#object-elements) are automatically [limit elements](#limit-elements),
+	* <span id="inherited2">[2]</span> The value of `isSelectable` is `true` for this element because all [objects](#object-elements) are automatically [selectable elements](#selectable-elements),
+	* <span id="inherited3">[3]</span> The value of `isContent` is `true` for this element because all [objects](#object-elements) are automatically [content elements](#content-elements).
+</info-box>
 
 ### Limit elements
 
@@ -75,7 +292,7 @@ The engine and various features then check it via {@link module:engine/model/sch
 
 For an image caption like in the example above it does not make much sense to select the caption box, then copy or drag it somewhere else.
 
-A caption without the image that it describes makes little sense. However, the image is more self-sufficient. Most likely users should be able to select the entire image (with all its internals), then copy or move it around. The {@link module:engine/model/schema~SchemaItemDefinition#isObject `isObject`} property should be used to mark such behavior.
+A caption without the image it describes makes little sense. The image, however, is more self-sufficient. Most likely users should be able to select the entire image (with all its internals), then copy or move it around. The {@link module:engine/model/schema~SchemaItemDefinition#isObject `isObject`} property should be used to mark such behavior.
 
 ```js
 schema.register( 'myImage', {
@@ -86,9 +303,11 @@ schema.register( 'myImage', {
 The {@link module:engine/model/schema~Schema#isObject `Schema#isObject()`} can later be used to check this property.
 
 <info-box>
-	Every "object" is also a "limit" element.
+	Every "object" is automatically also:
 
-	It means that for every element with `isObject` set to `true`, {@link module:engine/model/schema~Schema#isLimit `Schema#isLimit( element )`} will always return `true`.
+	* a [limit element](#limit-elements) – for every element with `isObject` set `true`, {@link module:engine/model/schema~Schema#isLimit `Schema#isLimit( element )`} will always return `true`.
+	* a [selectable element](#selectable-elements) – for every element with `isObject` set `true`, {@link module:engine/model/schema~Schema#isSelectable `Schema#isSelectable( element )`} will always return `true`.
+	* a [content element](#content-elements) – for every element with `isObject` set `true`, {@link module:engine/model/schema~Schema#isContent `Schema#isContent( element )`} will always return `true`.
 </info-box>
 
 ### Block elements
@@ -105,10 +324,46 @@ It is important to remember that a block should not allow another block inside.
 
 In the editor, all HTML formatting elements such as `<strong>` or `<code>` are represented by text attributes. Therefore, inline model elements are not supposed to be used for these scenarios.
 
-Currently, the {@link module:engine/model/schema~SchemaItemDefinition#isInline `isInline`} property is used for the `$text` token (so, text nodes) and elements such as `<softBreak>` or placeholder elements such as in the {@link framework/guides/tutorials/implementing-an-inline-widget Implementing an inline widget} tutorial.
+Currently, the {@link module:engine/model/schema~SchemaItemDefinition#isInline `isInline`} property is used for the `$text` token (so, text nodes) and elements such as `<softBreak>` or placeholder elements such as described in the {@link framework/guides/tutorials/implementing-an-inline-widget Implementing an inline widget} tutorial.
 
 The support for inline elements in CKEditor 5 is so far limited to self-contained elements. Because of this, all elements marked with `isInline` should also be marked with `isObject`.
 
+### Selectable elements
+
+Elements that users can select as a whole (with all their internals) and then, for instance, copy them or apply formatting, are marked with the {@link module:engine/model/schema~SchemaItemDefinition#isSelectable `isSelectable`} property in the schema:
+
+```js
+schema.register( 'mySelectable', {
+	isSelectable: true
+} );
+```
+
+The {@link module:engine/model/schema~Schema#isSelectable `Schema#isSelectable()`} method can later be used to check this property.
+
+<info-box>
+	All [object elements](#object-elements) are selectable by default. There are other selectable elements registered in the editor, though. For instance, there is also the `tableCell` model element (rendered as a `<td>` in the editing view) that is selectable while **not** registered as an object. The {@link features/table#table-selection table selection} plugin takes advantage of this fact and allows users create rectangular selections made of multiple table cells.
+</info-box>
+
+### Content elements
+
+You can tell content model elements from other elements by looking at their representation in the editor data (you can use {@link module:editor-classic/classiceditor~ClassicEditor#getData `editor.getData()`} or {@link module:engine/model/model~Model#hasContent Model#hasContent()} to check this out).
+
+Elements such as images or media will **always** find their way into editor data and this is what makes them content elements. They are marked with the {@link module:engine/model/schema~SchemaItemDefinition#isContent `isContent`} property in the schema:
+
+```js
+schema.register( 'myImage', {
+	isContent: true
+} );
+```
+
+The {@link module:engine/model/schema~Schema#isContent `Schema#isContent()`} method can later be used to check this property.
+
+At the same time, elements like paragraphs, list items, or headings **are not** content elements because they are skipped in the editor output when they are empty. From the data perspective they are transparent unless they contain other content elements (an empty paragraph is as good as no paragraph).
+
+<info-box>
+	[Object elements](#object-elements) and [`$text`](#generic-items) are content by default.
+</info-box>
+
 ## Generic items
 
 There are three basic generic items: `$root`, `$block` and `$text`. They are defined as follows:
@@ -146,7 +401,7 @@ schema.register( 'paragraph', {
 } );
 ```
 
-Which can be read as:
+And this can be read as:
 
 * The `<paragraph>` element will be allowed in elements in which `<$block>` is allowed (e.g. in `<$root>`).
 * The `<paragraph>` element will allow all nodes that are allowed in `<$block>` (e.g. `$text`).
@@ -172,7 +427,7 @@ The side effect of such a definition inheritance is that now `<blockQuote>` is a
 
 ## Defining advanced rules in `checkChild()` callbacks
 
-The {@link module:engine/model/schema~Schema#checkChild `Schema#checkChild()`} method which is the base method used to check whether some element is allowed in a given structure is {@link module:utils/observablemixin~ObservableMixin#decorate a decorated method}. It means that you can add listeners to implement your specific rules which are not limited by the {@link module:engine/model/schema~SchemaItemDefinition declarative `SchemaItemDefinition` API}.
+The {@link module:engine/model/schema~Schema#checkChild `Schema#checkChild()`} method which is the a base method used to check whether some element is allowed in a given structure is {@link module:utils/observablemixin~ObservableMixin#decorate a decorated method}. It means that you can add listeners to implement your specific rules which are not limited by the {@link module:engine/model/schema~SchemaItemDefinition declarative `SchemaItemDefinition` API}.
 
 These listeners can be added either by listening directly to the {@link module:engine/model/schema~Schema#event:checkChild} event or by using the handy {@link module:engine/model/schema~Schema#addChildCheck `Schema#addChildCheck()`} method.
 
@@ -230,7 +485,7 @@ While this is a relatively simple scenario (unlike most real-time collaborative
 
 Therefore, if your editor needs to implement such rules, you should do that through {@link module:engine/model/document~Document#registerPostFixer model's post-fixers} fixing incorrect content or actively prevent such situations (e.g. by disabling certain features). It means that these constraints will be defined specifically for your scenario by your code which makes their implementation much easier.
 
-To sum up, the answer to who and how should implement additional constraints is: Your features or your editor through the CKEditor 5 API.
+To sum up, the answer to who and how should implement additional constraints is: your features or your editor through the CKEditor 5 API.
 
 ## Who checks the schema?
 
@@ -267,4 +522,50 @@ Finally, the schema plays a crucial role during the conversion from the view to
 	Some features may miss schema checks. If you happen to find such a scenario, do not hesitate to [report it to us](https://github.com/ckeditor/ckeditor5/issues).
 </info-box>
 
-
+<style>
+.schema-deep-dive table {
+	text-align: center;
+}
+
+.schema-deep-dive table td,
+.schema-deep-dive table th {
+	border-color: hsl(72deg 6% 16%);
+}
+
+.schema-deep-dive table thead th {
+	font-weight: bold;
+	vertical-align: middle;
+}
+
+.schema-deep-dive table thead th code {
+	white-space: nowrap;
+}
+
+.schema-deep-dive table tbody td.value_negative {
+	background: hsl(354deg, 100%, 90%);
+}
+
+.schema-deep-dive table tbody td.value_positive {
+	background: hsl(88deg, 50%, 60%);
+}
+
+.schema-deep-dive table tbody td.value_negative code,
+.schema-deep-dive table tbody td.value_positive code,
+.schema-deep-dive table tbody td.value_positive_inherited code {
+	background: none;
+	text-shadow: 0px 0px 2px hsl(0deg, 0%, 100%);
+}
+
+.schema-deep-dive table tbody td.value_positive_inherited {
+	background-image: linear-gradient(45deg, hsl(88deg, 50%, 60%) 25%, hsl(89deg, 58% ,71%) 25%, hsl(89deg, 58%, 71%) 50%, hsl(88deg, 50%, 60%) 50%, hsl(88deg, 50%, 60%) 75%, hsl(89deg, 58%, 71%) 75%, hsl(89deg, 58%, 71%) 100%);
+	background-size: 3px 3px;
+}
+
+.schema-deep-dive table tbody td sup {
+	top: -0.5em;
+	position: relative;
+	font-size: 75%;
+	line-height: 0;
+	vertical-align: baseline;
+}
+</style>

+ 33 - 16
packages/ckeditor5-engine/src/controller/datacontroller.js

@@ -24,6 +24,7 @@ import ViewDocument from '../view/document';
 import ViewDowncastWriter from '../view/downcastwriter';
 
 import ModelRange from '../model/range';
+import { autoParagraphEmptyRoots } from '../model/utils/autoparagraphing';
 
 /**
  * Controller for the data pipeline. The data pipeline controls how data is retrieved from the document
@@ -126,8 +127,8 @@ export default class DataController {
 		// Define default converters for text and elements.
 		//
 		// Note that if there is no default converter for the element it will be skipped, for instance `<b>foo</b>` will be
-		// converted to nothing. We add `convertToModelFragment` as a last converter so it converts children of that
-		// element to the document fragment so `<b>foo</b>` will be converted to `foo` if there is no converter for `<b>`.
+		// converted to nothing. We therefore add `convertToModelFragment` as a last converter so it converts children of that
+		// element to the document fragment and so `<b>foo</b>` will be converted to `foo` if there is no converter for `<b>`.
 		this.upcastDispatcher.on( 'text', convertText(), { priority: 'lowest' } );
 		this.upcastDispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
 		this.upcastDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
@@ -135,26 +136,33 @@ export default class DataController {
 		this.decorate( 'init' );
 		this.decorate( 'set' );
 
-		// Fire `ready` event when initialisation has completed. Such low level listener gives possibility
-		// to plug into initialisation pipeline without interrupting the initialisation flow.
+		// Fire the `ready` event when the initialization has completed. Such low-level listener gives possibility
+		// to plug into the initialization pipeline without interrupting the initialization flow.
 		this.on( 'init', () => {
 			this.fire( 'ready' );
 		}, { priority: 'lowest' } );
+
+		// Fix empty roots after DataController is 'ready' (note that init method could be decorated and stopped).
+		// We need to handle this event because initial data could be empty and post-fixer would not get triggered.
+		this.on( 'ready', () => {
+			this.model.enqueueChange( 'transparent', autoParagraphEmptyRoots );
+		}, { priority: 'lowest' } );
 	}
 
 	/**
 	 * Returns the model's data converted by downcast dispatchers attached to {@link #downcastDispatcher} and
 	 * formatted by the {@link #processor data processor}.
 	 *
-	 * @param {Object} [options]
+	 * @param {Object} [options] Additional configuration for the retrieved data. `DataController` provides two optional
+	 * properties: `rootName` and `trim`. Other properties of this object are specified by various editor features.
 	 * @param {String} [options.rootName='main'] Root name.
 	 * @param {String} [options.trim='empty'] Whether returned data should be trimmed. This option is set to `empty` by default,
 	 * which means whenever editor content is considered empty, an empty string will be returned. To turn off trimming completely
 	 * use `'none'`. In such cases exact content will be returned (for example `<p>&nbsp;</p>` for an empty editor).
 	 * @returns {String} Output data.
 	 */
-	get( options ) {
-		const { rootName = 'main', trim = 'empty' } = options || {};
+	get( options = {} ) {
+		const { rootName = 'main', trim = 'empty' } = options;
 
 		if ( !this._checkIfRootsExists( [ rootName ] ) ) {
 			/**
@@ -177,7 +185,7 @@ export default class DataController {
 			return '';
 		}
 
-		return this.stringify( root );
+		return this.stringify( root, options );
 	}
 
 	/**
@@ -187,11 +195,12 @@ export default class DataController {
 	 *
 	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} modelElementOrFragment
 	 * Element whose content will be stringified.
+	 * @param {Object} [options] Additional configuration passed to the conversion process.
 	 * @returns {String} Output data.
 	 */
-	stringify( modelElementOrFragment ) {
+	stringify( modelElementOrFragment, options ) {
 		// Model -> view.
-		const viewDocumentFragment = this.toView( modelElementOrFragment );
+		const viewDocumentFragment = this.toView( modelElementOrFragment, options );
 
 		// View -> data.
 		return this.processor.toData( viewDocumentFragment );
@@ -205,9 +214,11 @@ export default class DataController {
 	 *
 	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} modelElementOrFragment
 	 * Element or document fragment whose content will be converted.
+	 * @param {Object} [options] Additional configuration that will be available through
+	 * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi#options} during the conversion process.
 	 * @returns {module:engine/view/documentfragment~DocumentFragment} Output view DocumentFragment.
 	 */
-	toView( modelElementOrFragment ) {
+	toView( modelElementOrFragment, options ) {
 		const viewDocument = this.viewDocument;
 		const viewWriter = this._viewWriter;
 
@@ -220,7 +231,10 @@ export default class DataController {
 
 		this.mapper.bindElements( modelElementOrFragment, viewDocumentFragment );
 
-		// We have no view controller and rendering do DOM in DataController so view.change() block is not used here.
+		// Make additional options available during conversion process through `conversionApi`.
+		this.downcastDispatcher.conversionApi.options = options;
+
+		// We have no view controller and rendering to DOM in DataController so view.change() block is not used here.
 		this.downcastDispatcher.convertInsert( modelRange, viewWriter );
 
 		if ( !modelElementOrFragment.is( 'documentFragment' ) ) {
@@ -233,6 +247,9 @@ export default class DataController {
 			}
 		}
 
+		// Clean `conversionApi`.
+		delete this.downcastDispatcher.conversionApi.options;
+
 		return viewDocumentFragment;
 	}
 
@@ -439,17 +456,17 @@ export default class DataController {
 	}
 
 	/**
-	 * Event fired once data initialisation has finished.
+	 * Event fired once the data initialization 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}
+	 * Event fired after the {@link #init `init()` method} was run. It can be {@link #listenTo listened to} in order to adjust or modify
+	 * the initialization 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.
+	 * The `init` event is fired by the decorated {@link #init} method.
 	 * See {@link module:utils/observablemixin~ObservableMixin#decorate} for more information and samples.
 	 *
 	 * @event init

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

@@ -73,7 +73,7 @@ export default class EditingController {
 		const selection = doc.selection;
 		const markers = this.model.markers;
 
-		// When plugins listen on model changes (on selection change, post fixers, etc) and change the view as a result of
+		// When plugins listen on model changes (on selection change, post fixers, etc.) and change the view as a result of
 		// model's change, they might trigger view rendering before the conversion is completed (e.g. before the selection
 		// is converted). We disable rendering for the length of the outermost model change() block to prevent that.
 		//

+ 62 - 57
packages/ckeditor5-engine/src/conversion/downcastdispatcher.js

@@ -11,87 +11,86 @@ import Consumable from './modelconsumable';
 import Range from '../model/range';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
-import { extend } from 'lodash-es';
 
 /**
- * `DowncastDispatcher` is a central point of downcasting (conversion from model to view), which is a process of reacting to changes
- * in the model and firing a set of events. Callbacks listening to those events are called converters. Those
- * converters role is to convert the model changes to changes in view (for example, adding view nodes or
+ * Downcast dispatcher is a central point of downcasting (conversion from the model to the view), which is a process of reacting to changes
+ * in the model and firing a set of events. Callbacks listening to these events are called converters. The
+ * converters' role is to convert the model changes to changes in view (for example, adding view nodes or
  * changing attributes on view elements).
  *
- * During conversion process, `DowncastDispatcher` fires events, basing on state of the model and prepares
- * data for those events. It is important to understand that those events are connected with changes done on model,
- * for example: "node has been inserted" or "attribute has changed". This is in a contrary to upcasting (view to model conversion),
- * where we convert view state (view nodes) to a model tree.
+ * During the conversion process, downcast dispatcher fires events basing on the state of the model and prepares
+ * data for these events. It is important to understand that the events are connected with the changes done on the model,
+ * for example: "a node has been inserted" or "an attribute has changed". This is in contrary to upcasting (a view-to-model conversion)
+ * where you convert the view state (view nodes) to a model tree.
  *
  * The events are prepared basing on a diff created by {@link module:engine/model/differ~Differ Differ}, which buffers them
- * and then passes to `DowncastDispatcher` as a diff between old model state and new model state.
+ * and then passes to the downcast dispatcher as a diff between the old model state and the new model state.
  *
- * Note, that because changes are converted there is a need to have a mapping between model structure and view structure.
- * To map positions and elements during downcast (model to view conversion) use {@link module:engine/conversion/mapper~Mapper}.
+ * Note that because the changes are converted, there is a need to have a mapping between the model structure and the view structure.
+ * To map positions and elements during the downcast (a model-to-view conversion), use {@link module:engine/conversion/mapper~Mapper}.
  *
- * `DowncastDispatcher` fires following events for model tree changes:
+ * Downcast dispatcher fires the following events for model tree changes:
  *
- * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert insert}
- * if a range of nodes has been inserted to the model tree,
- * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:remove remove}
- * if a range of nodes has been removed from the model tree,
- * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute attribute}
- * if attribute has been added, changed or removed from a model node.
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert `insert`} &ndash;
+ * If a range of nodes was inserted to the model tree.
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:remove `remove`} &ndash;
+ * If a range of nodes was removed from the model tree.
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute `attribute`} &ndash;
+ * If an attribute was added, changed or removed from a model node.
  *
- * For {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert insert}
- * and {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute attribute},
- * `DowncastDispatcher` generates {@link module:engine/conversion/modelconsumable~ModelConsumable consumables}.
- * These are used to have a control over which changes has been already consumed. It is useful when some converters
- * overwrite other or converts multiple changes (for example converts insertion of an element and also converts that
- * element's attributes during insertion).
+ * For {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert `insert`}
+ * and {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute `attribute`},
+ * downcast dispatcher generates {@link module:engine/conversion/modelconsumable~ModelConsumable consumables}.
+ * These are used to have control over which changes have already been consumed. It is useful when some converters
+ * overwrite others or convert multiple changes (for example, it converts an insertion of an element and also converts that
+ * element's attributes during the insertion).
  *
- * Additionally, `DowncastDispatcher` fires events for {@link module:engine/model/markercollection~Marker marker} changes:
+ * Additionally, downcast dispatcher fires events for {@link module:engine/model/markercollection~Marker marker} changes:
  *
- * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker} if a marker has been added,
- * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:removeMarker} if a marker has been removed.
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker} &ndash; If a marker was added.
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:removeMarker} &ndash; If a marker was removed.
  *
- * Note, that changing a marker is done through removing the marker from the old range, and adding on the new range,
- * so both those events are fired.
+ * Note that changing a marker is done through removing the marker from the old range and adding it on the new range,
+ * so both events are fired.
  *
- * Finally, `DowncastDispatcher` also handles firing events for {@link module:engine/model/selection model selection}
+ * Finally, downcast dispatcher also handles firing events for the {@link module:engine/model/selection model selection}
  * conversion:
  *
  * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:selection}
- * which converts selection from model to view,
+ * &ndash; Converts the selection from the model to the view.
  * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute}
- * which is fired for every selection attribute,
+ * &ndash; Fired for every selection attribute.
  * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}
- * which is fired for every marker which contains selection.
+ * &ndash; Fired for every marker that contains a selection.
  *
  * Unlike model tree and markers, events for selection are not fired for changes but for selection state.
  *
- * When providing custom listeners for `DowncastDispatcher` remember to check whether given change has not been
+ * When providing custom listeners for downcast dispatcher, remember to check whether a given change has not been
  * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed} yet.
  *
- * When providing custom listeners for `DowncastDispatcher` keep in mind that any callback that had
+ * When providing custom listeners for downcast dispatcher, keep in mind that any callback that has
  * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed} a value from a consumable and
  * converted the change should also stop the event (for efficiency purposes).
  *
- * When providing custom listeners for `DowncastDispatcher` remember to use provided
+ * When providing custom listeners for downcast dispatcher, remember to use the provided
  * {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer} to apply changes to the view document.
  *
- * Example of a custom converter for `DowncastDispatcher`:
+ * An example of a custom converter for the downcast dispatcher:
  *
- *		// We will convert inserting "paragraph" model element into the model.
+ *		// You will convert inserting a "paragraph" model element into the model.
  *		downcastDispatcher.on( 'insert:paragraph', ( evt, data, conversionApi ) => {
  *			// Remember to check whether the change has not been consumed yet and consume it.
  *			if ( conversionApi.consumable.consume( data.item, 'insert' ) ) {
  *				return;
  *			}
  *
- *			// Translate position in model to position in view.
+ *			// Translate the position in the model to a position in the view.
  *			const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
  *
- *			// Create <p> element that will be inserted in view at `viewPosition`.
+ *			// Create a <p> element that will be inserted into the view at the `viewPosition`.
  *			const viewElement = conversionApi.writer.createContainerElement( 'p' );
  *
- *			// Bind the newly created view element to model element so positions will map accordingly in future.
+ *			// Bind the newly created view element to the model element so positions will map accordingly in the future.
  *			conversionApi.mapper.bindElements( data.item, viewElement );
  *
  *			// Add the newly created view element to the view.
@@ -103,27 +102,27 @@ import { extend } from 'lodash-es';
  */
 export default class DowncastDispatcher {
 	/**
-	 * Creates a `DowncastDispatcher` instance.
+	 * Creates a downcast dispatcher instance.
 	 *
 	 * @see module:engine/conversion/downcastdispatcher~DowncastConversionApi
-	 * @param {Object} conversionApi Additional properties for interface that will be passed to events fired
-	 * by `DowncastDispatcher`.
+	 * @param {Object} conversionApi Additional properties for an interface that will be passed to events fired
+	 * by the downcast dispatcher.
 	 */
 	constructor( conversionApi ) {
 		/**
-		 * Interface passed by dispatcher to the events callbacks.
+		 * An interface passed by the dispatcher to the event callbacks.
 		 *
 		 * @member {module:engine/conversion/downcastdispatcher~DowncastConversionApi}
 		 */
-		this.conversionApi = extend( { dispatcher: this }, conversionApi );
+		this.conversionApi = Object.assign( { dispatcher: this }, conversionApi );
 	}
 
 	/**
-	 * Takes {@link module:engine/model/differ~Differ model differ} object with buffered changes and fires conversion basing on it.
+	 * Takes a {@link module:engine/model/differ~Differ model differ} object with buffered changes and fires conversion basing on it.
 	 *
-	 * @param {module:engine/model/differ~Differ} differ Differ object with buffered changes.
-	 * @param {module:engine/model/markercollection~MarkerCollection} markers Markers connected with converted model.
-	 * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
+	 * @param {module:engine/model/differ~Differ} differ The differ object with buffered changes.
+	 * @param {module:engine/model/markercollection~MarkerCollection} markers Markers connected with the converted model.
+	 * @param {module:engine/view/downcastwriter~DowncastWriter} writer The view writer that should be used to modify the view document.
 	 */
 	convertChanges( differ, markers, writer ) {
 		// Before the view is updated, remove markers which have changed.
@@ -157,15 +156,15 @@ export default class DowncastDispatcher {
 	}
 
 	/**
-	 * Starts conversion of a range insertion.
+	 * Starts a conversion of a range insertion.
 	 *
-	 * For each node in the range, {@link #event:insert insert event is fired}. For each attribute on each node,
-	 * {@link #event:attribute attribute event is fired}.
+	 * For each node in the range, {@link #event:insert `insert` event is fired}. For each attribute on each node,
+	 * {@link #event:attribute `attribute` event is fired}.
 	 *
 	 * @fires insert
 	 * @fires attribute
-	 * @param {module:engine/model/range~Range} range Inserted range.
-	 * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
+	 * @param {module:engine/model/range~Range} range The inserted range.
+	 * @param {module:engine/view/downcastwriter~DowncastWriter} writer The view writer that should be used to modify the view document.
 	 */
 	convertInsert( range, writer ) {
 		this.conversionApi.writer = writer;
@@ -554,7 +553,7 @@ export default class DowncastDispatcher {
 	 */
 
 	/**
-	 * Fired when a new marker is added to the model. Also fired when collapsed model selection that is inside marker is converted.
+	 * Fired when a new marker is added to the model. Also fired when collapsed model selection that is inside a marker is converted.
 	 *
 	 * `addMarker` is a namespace for a class of events. Names of actually called events follow this pattern:
 	 * `addMarker:markerName`. By specifying certain marker names, you can make the events even more gradual. For example,
@@ -634,7 +633,7 @@ function shouldMarkerChangeBeConverted( modelPosition, marker, mapper ) {
 /**
  * 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.
+ * fires its events.
  *
  * @interface module:engine/conversion/downcastdispatcher~DowncastConversionApi
  */
@@ -669,3 +668,9 @@ function shouldMarkerChangeBeConverted( modelPosition, marker, mapper ) {
  *
  * @member {module:engine/view/downcastwriter~DowncastWriter} #writer
  */
+
+/**
+ * An object with an additional configuration which can be used during conversion process. Available only for data downcast conversion.
+ *
+ * @member {Object} #options
+ */

+ 75 - 54
packages/ckeditor5-engine/src/conversion/downcasthelpers.js

@@ -52,8 +52,10 @@ export default class DowncastHelpers extends ConversionHelpers {
 	 *
 	 *		editor.conversion.for( 'downcast' ).elementToElement( {
 	 *			model: 'heading',
-	 *			view: ( modelElement, viewWriter ) => {
-	 *				return viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
+	 *			view: ( modelElement, conversionApi ) => {
+	 *				const { writer } = conversionApi;
+	 *
+	 *				return writer.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) );
 	 *			}
 	 *		} );
 	 *
@@ -64,7 +66,7 @@ export default class DowncastHelpers extends ConversionHelpers {
 	 * @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}
+	 * that takes the model element and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
 	 * as parameters and returns a view container element.
 	 * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
 	 */
@@ -120,8 +122,10 @@ export default class DowncastHelpers extends ConversionHelpers {
 	 *
 	 *		editor.conversion.for( 'downcast' ).attributeToElement( {
 	 *			model: 'bold',
-	 *			view: ( modelAttributeValue, viewWriter ) => {
-	 *				return viewWriter.createAttributeElement( 'span', {
+	 *			view: ( modelAttributeValue, conversionApi ) => {
+	 *				const { writer } = conversionApi;
+	 *
+	 *				return writer.createAttributeElement( 'span', {
 	 *					style: 'font-weight:' + modelAttributeValue
 	 *				} );
 	 *			}
@@ -132,8 +136,10 @@ export default class DowncastHelpers extends ConversionHelpers {
 	 *				key: 'color',
 	 *				name: '$text'
 	 *			},
-	 *			view: ( modelAttributeValue, viewWriter ) => {
-	 *				return viewWriter.createAttributeElement( 'span', {
+	 *			view: ( modelAttributeValue, conversionApi ) => {
+	 *				const { writer } = conversionApi;
+	 *
+	 *				return writer.createAttributeElement( 'span', {
 	 *					style: 'color:' + modelAttributeValue
 	 *				} );
 	 *			}
@@ -147,9 +153,10 @@ export default class DowncastHelpers extends ConversionHelpers {
 	 * @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.
+	 * that takes the model attribute value and
+	 * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} 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}
 	 */
@@ -201,7 +208,10 @@ export default class DowncastHelpers extends ConversionHelpers {
 	 *
 	 *		editor.conversion.for( 'downcast' ).attributeToAttribute( {
 	 *			model: 'styled',
-	 *			view: modelAttributeValue => ( { key: 'class', value: 'styled-' + modelAttributeValue } )
+	 *			view: modelAttributeValue => ( {
+	 *				key: 'class',
+	 *				value: 'styled-' + modelAttributeValue
+	 *			} )
 	 *		} );
 	 *
 	 * **Note**: Downcasting to a style property requires providing `value` as an object:
@@ -225,7 +235,8 @@ export default class DowncastHelpers extends ConversionHelpers {
 	 * @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
+	 * the model attribute value and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
+	 * as parameters 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.
@@ -269,8 +280,10 @@ export default class DowncastHelpers extends ConversionHelpers {
 	 *
 	 *		editor.conversion.for( 'editingDowncast' ).markerToElement( {
 	 *			model: 'search',
-	 *			view: ( markerData, viewWriter ) => {
-	 *				return viewWriter.createUIElement( 'span', {
+	 *			view: ( markerData, conversionApi ) => {
+	 *				const { writer } = conversionApi;
+	 *
+	 *				return writer.createUIElement( 'span', {
 	 *					'data-marker': 'search',
 	 *					'data-start': markerData.isOpening
 	 *				} );
@@ -278,7 +291,8 @@ export default class DowncastHelpers extends ConversionHelpers {
 	 *		} );
 	 *
 	 * 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
+	 * receives the `data` object and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
+	 * as a parameters 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,
@@ -291,8 +305,9 @@ export default class DowncastHelpers extends ConversionHelpers {
 	 * @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:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function that
+	 * takes the model marker data and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
+	 * as a parameters and returns a view UI element.
 	 * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
 	 * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
 	 */
@@ -329,7 +344,7 @@ export default class DowncastHelpers extends ConversionHelpers {
 	 *
 	 *		editor.conversion.for( 'downcast' ).markerToHighlight( {
 	 *			model: 'comment',
-	 *			view: data => {
+	 *			view: ( data, converstionApi ) => {
 	 *				// Assuming that the marker name is in a form of comment:commentType.
 	 *				const commentType = data.markerName.split( ':' )[ 1 ];
 	 *
@@ -340,7 +355,8 @@ export default class DowncastHelpers extends ConversionHelpers {
 	 *		} );
 	 *
 	 * 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
+	 * receives the `data` object and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
+	 * as a parameters 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}.
 	 *
@@ -351,7 +367,9 @@ export default class DowncastHelpers extends ConversionHelpers {
 	 * @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.
+	 * that will be used for highlighting or a function that takes the model marker data and
+	 * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as a parameters
+	 * and returns a highlight descriptor.
 	 * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
 	 * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
 	 */
@@ -364,39 +382,40 @@ export default class DowncastHelpers extends ConversionHelpers {
 	 *
 	 * This conversion creates a representation for model marker boundaries in the view:
 	 *
-	 * * if the marker boundary is at a position where text nodes are allowed, then a view element with specified tag name
-	 * and `name` attribute is added at that position,
-	 * * in other cases, a specified attribute is set on a view element that is before/after marker boundary.
+	 * * If the marker boundary is at a position where text nodes are allowed, then a view element with the specified tag name
+	 * and `name` attribute is added at this position.
+	 * * In other cases, a specified attribute is set on a view element that is before or after the marker boundary.
 	 *
-	 * Typically, the marker names use `group:uniqueId:otherData` convention. For example: `comment:e34zfk9k2n459df53sjl34:zx32c`.
-	 * The default configuration for this conversion is that the first part is `group` part and the rest of
-	 * the marker name becomes `name` part.
+	 * Typically, marker names use the `group:uniqueId:otherData` convention. For example: `comment:e34zfk9k2n459df53sjl34:zx32c`.
+	 * The default configuration for this conversion is that the first part is the `group` part and the rest of
+	 * the marker name becomes the `name` part.
 	 *
 	 * Tag and attribute names and values are generated from the marker name:
 	 *
-	 * * templates for attributes are `data-[group]-start-before="[name]"`, `data-[group]-start-after="[name]"`,
-	 * `data-[group]-end-before="[name]"` and `data-[group]-end-after="[name]"`,
-	 * * templates for view elements are `<[group]-start name="[name]">` and `<[group]-end name="[name]">`.
+	 * * Templates for attributes are `data-[group]-start-before="[name]"`, `data-[group]-start-after="[name]"`,
+	 * `data-[group]-end-before="[name]"` and `data-[group]-end-after="[name]"`.
+	 * * Templates for view elements are `<[group]-start name="[name]">` and `<[group]-end name="[name]">`.
 	 *
-	 * Attributes mark whether given marker start or end boundary is before or after given element.
+	 * Attributes mark whether the given marker's start or end boundary is before or after the given element.
 	 * Attributes `data-[group]-start-before` and `data-[group]-end-after` are favored.
 	 * The other two are used when the former two cannot be used.
 	 *
 	 * The conversion configuration can take a function that will generate different group and name parts.
 	 * If such function is set as the `config.view` parameter, it is passed a marker name and it is expected to return an object with two
-	 * properties: `group` and `name`. If the function returns falsy value, the conversion will not take place.
+	 * properties: `group` and `name`. If the function returns a falsy value, the conversion will not take place.
 	 *
 	 * Basic usage:
 	 *
 	 *		// Using the default conversion.
-	 *		// In this case, all markers which name starts with 'comment:' will be converted.
+	 *		// In this case, all markers whose name starts with 'comment:' will be converted.
 	 *		// The `group` parameter will be set to `comment`.
 	 *		// The `name` parameter will be the rest of the marker name (without `:`).
 	 *		editor.conversion.for( 'dataDowncast' ).markerToData( {
 	 *			model: 'comment'
 	 *		} );
 	 *
-	 * An example of a view that may be generated by this conversion (assuming a marker with name `comment:commentId:uid` marked by `[]`):
+	 * An example of a view that may be generated by this conversion (assuming a marker with the name `comment:commentId:uid` marked
+	 * by `[]`):
 	 *
 	 *		// Model:
 	 *		<paragraph>Foo[bar</paragraph>
@@ -408,12 +427,12 @@ export default class DowncastHelpers extends ConversionHelpers {
 	 *
 	 * In the example above, the comment starts before "bar" and ends after the image.
 	 *
-	 * If `name` part is empty, following view may be generated:
+	 * If the `name` part is empty, the following view may be generated:
 	 *
 	 *		<p>Foo <myMarker-start></myMarker-start>bar</p>
 	 *		<figure data-myMarker-end-after="" class="image"><img src="abc.jpg" /></figure>
 	 *
-	 * **Note:** situation when some markers have `name` part and some don't is incorrect and should be avoided.
+	 * **Note:** A situation where some markers have the `name` part and some do not have it is incorrect and should be avoided.
 	 *
 	 * Examples where `data-group-start-after` and `data-group-end-before` are used:
 	 *
@@ -423,7 +442,7 @@ export default class DowncastHelpers extends ConversionHelpers {
 	 * 		// View:
 	 *		<blockquote><p data-group-end-before="name" data-group-start-before="name">Foo</p></blockquote>
 	 *
-	 * Similarly, when marker is collapsed after the last element:
+	 * Similarly, when a marker is collapsed after the last element:
 	 *
 	 *		// Model:
 	 *		<blockQuote><paragraph>Foo</paragraph>[]</blockQuote>
@@ -436,7 +455,7 @@ export default class DowncastHelpers extends ConversionHelpers {
 	 *
 	 * Other examples of usage:
 	 *
-	 *		// Using custom function which is the same as the default conversion:
+	 *		// Using a custom function which is the same as the default conversion:
 	 *		editor.conversion.for( 'dataDowncast' ).markerToData( {
 	 *			model: 'comment'
 	 *			view: markerName => ( {
@@ -445,7 +464,7 @@ export default class DowncastHelpers extends ConversionHelpers {
 	 *			} )
 	 *		} );
 	 *
-	 *		// Using converter priority:
+	 *		// Using the converter priority:
 	 *		editor.conversion.for( 'dataDowncast' ).markerToData( {
 	 *			model: 'comment'
 	 *			view: markerName => ( {
@@ -463,8 +482,9 @@ export default class DowncastHelpers extends ConversionHelpers {
 	 * @method #markerToData
 	 * @param {Object} config Conversion configuration.
 	 * @param {String} config.model The name of the model marker (or model marker group) to convert.
-	 * @param {Function} [config.view] Function that takes the model marker name as a parameter and returns an object with `group`
-	 * and `name` properties.
+	 * @param {Function} [config.view] A function that takes the model marker name and
+	 * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as a parameters
+	 * and returns an object with the `group` and `name` properties.
 	 * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
 	 * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
 	 */
@@ -702,10 +722,10 @@ 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 );
+		const oldViewElement = elementCreator( data.attributeOldValue, conversionApi );
 
 		// Create node to wrap with.
-		const newViewElement = elementCreator( data.attributeNewValue, conversionApi.writer );
+		const newViewElement = elementCreator( data.attributeNewValue, conversionApi );
 
 		if ( !oldViewElement && !newViewElement ) {
 			return;
@@ -765,7 +785,7 @@ export function wrap( elementCreator ) {
  */
 export function insertElement( elementCreator ) {
 	return ( evt, data, conversionApi ) => {
-		const viewElement = elementCreator( data.item, conversionApi.writer );
+		const viewElement = elementCreator( data.item, conversionApi );
 
 		if ( !viewElement ) {
 			return;
@@ -802,10 +822,10 @@ export function insertUIElement( elementCreator ) {
 		// 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 );
+		const viewStartElement = elementCreator( data, conversionApi );
 
 		data.isOpening = false;
-		const viewEndElement = elementCreator( data, conversionApi.writer );
+		const viewEndElement = elementCreator( data, conversionApi );
 
 		if ( !viewStartElement || !viewEndElement ) {
 			return;
@@ -845,7 +865,7 @@ export function insertUIElement( elementCreator ) {
 }
 
 // Function factory that returns a default downcast converter for removing a {@link module:engine/view/uielement~UIElement UI element}
-// basing on marker remove change.
+// based on marker remove change.
 //
 // This converter unbinds elements from the marker name.
 //
@@ -879,7 +899,7 @@ function removeUIElement() {
 // @returns {Function} Add marker converter.
 function insertMarkerData( viewCreator ) {
 	return ( evt, data, conversionApi ) => {
-		const viewMarkerData = viewCreator( data.markerName );
+		const viewMarkerData = viewCreator( data.markerName, conversionApi );
 
 		if ( !viewMarkerData ) {
 			return;
@@ -960,7 +980,7 @@ function insertMarkerAsElement( position, isStart, conversionApi, data, viewMark
 // @returns {Function} Remove marker converter.
 function removeMarkerData( viewCreator ) {
 	return ( evt, data, conversionApi ) => {
-		const viewData = viewCreator( data.markerName );
+		const viewData = viewCreator( data.markerName, conversionApi );
 
 		if ( !viewData ) {
 			return;
@@ -1035,8 +1055,8 @@ function removeMarkerData( viewCreator ) {
 // @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 );
+		const oldAttribute = attributeCreator( data.attributeOldValue, conversionApi );
+		const newAttribute = attributeCreator( data.attributeNewValue, conversionApi );
 
 		if ( !oldAttribute && !newAttribute ) {
 			return;
@@ -1486,7 +1506,7 @@ function normalizeToElementConfig( view, viewElementType ) {
 		return view;
 	}
 
-	return ( modelData, viewWriter ) => createViewElementFromDefinition( view, viewWriter, viewElementType );
+	return ( modelData, conversionApi ) => createViewElementFromDefinition( view, conversionApi, viewElementType );
 }
 
 // Creates a view element instance from the provided {@link module:engine/view/elementdefinition~ElementDefinition} and class.
@@ -1495,13 +1515,14 @@ function normalizeToElementConfig( view, viewElementType ) {
 // @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter
 // @param {'container'|'attribute'|'ui'} viewElementType
 // @returns {module:engine/view/element~Element}
-function createViewElementFromDefinition( viewElementDefinition, viewWriter, viewElementType ) {
+function createViewElementFromDefinition( viewElementDefinition, conversionApi, 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 viewWriter = conversionApi.writer;
 	const attributes = Object.assign( {}, viewElementDefinition.attributes );
 
 	if ( viewElementType == 'container' ) {
@@ -1542,11 +1563,11 @@ function createViewElementFromDefinition( viewElementDefinition, viewWriter, vie
 
 function getFromAttributeCreator( config ) {
 	if ( config.model.values ) {
-		return ( modelAttributeValue, viewWriter ) => {
+		return ( modelAttributeValue, conversionApi ) => {
 			const view = config.view[ modelAttributeValue ];
 
 			if ( view ) {
-				return view( modelAttributeValue, viewWriter );
+				return view( modelAttributeValue, conversionApi );
 			}
 
 			return null;

+ 239 - 85
packages/ckeditor5-engine/src/conversion/upcastdispatcher.js

@@ -11,34 +11,41 @@ import ViewConsumable from './viewconsumable';
 import ModelRange from '../model/range';
 import ModelPosition from '../model/position';
 import { SchemaContext } from '../model/schema';
+import { isParagraphable, wrapInParagraph } from '../model/utils/autoparagraphing';
 
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 
 /**
- * `UpcastDispatcher` is a central point of {@link module:engine/view/view view} conversion, which is a process of
+ * `UpcastDispatcher` is a central point of the view to model conversion, which is a process of
  * converting given {@link module:engine/view/documentfragment~DocumentFragment view document fragment} or
- * {@link module:engine/view/element~Element} into another structure.
- * In default application, {@link module:engine/view/view view} is converted to {@link module:engine/model/model}.
+ * {@link module:engine/view/element~Element view element} into a correct model structure.
  *
- * During conversion process, for all {@link module:engine/view/node~Node view nodes} from the converted view document fragment,
- * `UpcastDispatcher` fires corresponding events. Special callbacks called "converters" should listen to
- * `UpcastDispatcher` for those events.
+ * During the conversion process, the dispatcher fires events for all {@link module:engine/view/node~Node view nodes}
+ * from the converted view document fragment.
+ * Special callbacks called "converters" should listen to these events in order to convert these view nodes.
  *
- * Each callback, as the second argument, is passed a special object `data` that has `viewItem`, `modelCursor` and
- * `modelRange` properties. `viewItem` property contains {@link module:engine/view/node~Node view node} or
- * {@link module:engine/view/documentfragment~DocumentFragment view document fragment}
- * that is converted at the moment and might be handled by the callback. `modelRange` property should be used to save the result
- * of conversion and is always a {@link module:engine/model/range~Range} when conversion result is correct.
- * `modelCursor` property is a {@link module:engine/model/position~Position position} on which conversion result will be inserted
- * and is a context according to {@link module:engine/model/schema~Schema schema} will be checked before the conversion.
- * See also {@link ~UpcastDispatcher#convert}. It is also shared by reference by all callbacks listening to given event.
+ * The second parameter of the callback is the `data` object with the following properties:
  *
- * The third parameter passed to a callback is an instance of {@link ~UpcastDispatcher}
+ * * `data.viewItem` contains {@link module:engine/view/node~Node view node} or
+ * {@link module:engine/view/documentfragment~DocumentFragment view document fragment}
+ * that is converted at the moment and might be handled by the callback.
+ * * `data.modelRange` is used to point to the result
+ * of the current conversion (e.g. the element that is being inserted)
+ * and is always a {@link module:engine/model/range~Range} when the succeeds.
+ * * `data.modelCursor` is a {@link module:engine/model/position~Position position} on which the converter should insert
+ * newly created items.
+ *
+ * The third parameter of the callback is an instance of {@link module:engine/conversion/upcastdispatcher~UpcastConversionApi}
  * which provides additional tools for converters.
  *
- * Examples of providing callbacks for `UpcastDispatcher`:
+ * You can read more about conversion in the following guides:
+ *
+ * * {@glink framework/guides/deep-dive/conversion/conversion-introduction Advanced conversion concepts &mdash; attributes}
+ * * {@glink framework/guides/deep-dive/conversion/custom-element-conversion Custom element conversion}
+ *
+ * Examples of event-based converters:
  *
  *		// Converter for links (<a>).
  *		editor.data.upcastDispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
@@ -76,42 +83,31 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
  *			}
  *		}, { priority: 'low' } );
  *
- *		// Convert all elements which have no custom converter into paragraph (autoparagraphing).
- *  	editor.data.upcastDispatcher.on( 'element', ( evt, data, conversionApi ) => {
- *  	 	// When element is already consumed by higher priority converters then do nothing.
- *  	 	if ( conversionApi.consumable.test( data.viewItem, { name: data.viewItem.name } ) ) {
- *  	 			const paragraph = conversionApi.writer.createElement( 'paragraph' );
- *
- *  	 			// Find allowed parent for paragraph that we are going to insert. If current parent does not allow
- *  	 			// to insert paragraph but one of the ancestors does then split nodes to allowed parent.
- *  	 			const splitResult = conversionApi.splitToAllowedParent( paragraph, data.modelCursor );
- *
- *  	 			// When there is no split result it means that we can't insert paragraph in this position.
- *  	 			if ( splitResult ) {
- *  	 				// Insert paragraph in allowed position.
- *  	 				conversionApi.writer.insert( paragraph, splitResult.position );
- *
- *  	 				// Convert children to paragraph.
- *  	 				const { modelRange } = conversionApi.convertChildren(
- *  	 					data.viewItem,
- *  	 					conversionApi.writer.createPositionAt( paragraph, 0 )
- *  	 				);
- *
- * 						// Set as conversion result, attribute converters may use this property.
- *  	 				data.modelRange = conversionApi.writer.createRange(
- *  	 					conversionApi.writer.createPositionBefore( paragraph ),
- *  	 					modelRange.end
- *  	 				);
- *
- *  	 				// Continue conversion inside paragraph.
- *  	 				data.modelCursor = data.modelRange.end;
- *  	 			}
- *  	 		}
- *  	 	}
- *  	 }, { priority: 'low' } );
- *
- * Before each conversion process, `UpcastDispatcher` fires {@link ~UpcastDispatcher#event:viewCleanup}
- * event which can be used to prepare tree view for conversion.
+ *		// Convert all elements which have no custom converter into a paragraph (autoparagraphing).
+ *		editor.data.upcastDispatcher.on( 'element', ( evt, data, conversionApi ) => {
+ *			// Check if element can be converted.
+ *			if ( !conversionApi.consumable.test( data.viewItem, { name: data.viewItem.name } ) ) {
+ *				// When element is already consumed by higher priority converters then do nothing.
+ *				return;
+ *			}
+ *
+ *			const paragraph = conversionApi.writer.createElement( 'paragraph' );
+ *
+ *			// Try to safely insert paragraph at model cursor - it will find an allowed parent for a current element.
+ *			if ( !conversionApi.safeInsert( paragraph, data.modelCursor ) ) {
+ *				// When element was not inserted it means that we can't insert paragraph at this position.
+ *				return;
+ *			}
+ *
+ *			// Consume the inserted element.
+ *			conversionApi.consumable.consume( data.viewItem, { name: data.viewItem.name } ) );
+ *
+ *			// Convert children to paragraph.
+ *			const { modelRange } = conversionApi.convertChildren( data.viewItem,  paragraph ) );
+ *
+ *			// Update `modelRange` and `modelCursor` in a `data` as a conversion result.
+ *			conversionApi.updateConversionResult( paragraph, data );
+ *		}, { priority: 'low' } );
  *
  * @mixes module:utils/emittermixin~EmitterMixin
  * @fires viewCleanup
@@ -138,6 +134,16 @@ export default class UpcastDispatcher {
 		 */
 		this._splitParts = new Map();
 
+		/**
+		 * List of cursor parent elements that were created during splitting.
+		 *
+		 * After conversion process the list is cleared.
+		 *
+		 * @private
+		 * @type {Map.<module:engine/model/element~Element,Array.<module:engine/model/element~Element>>}
+		 */
+		this._cursorParents = new Map();
+
 		/**
 		 * Position in the temporary structure where the converted content is inserted. The structure reflect the context of
 		 * the target position where the content will be inserted. This property is build based on the context parameter of the
@@ -155,10 +161,13 @@ export default class UpcastDispatcher {
 		 */
 		this.conversionApi = Object.assign( {}, conversionApi );
 
-		// `convertItem`, `convertChildren` and `splitToAllowedParent` are bound to this `UpcastDispatcher`
-		// instance and set on `conversionApi`. This way only a part of `UpcastDispatcher` API is exposed.
+		// The below methods are bound to this `UpcastDispatcher` instance and set on `conversionApi`.
+		// This way only a part of `UpcastDispatcher` API is exposed.
 		this.conversionApi.convertItem = this._convertItem.bind( this );
 		this.conversionApi.convertChildren = this._convertChildren.bind( this );
+		this.conversionApi.safeInsert = this._safeInsert.bind( this );
+		this.conversionApi.updateConversionResult = this._updateConversionResult.bind( this );
+		// Advanced API - use only if custom position handling is needed.
 		this.conversionApi.splitToAllowedParent = this._splitToAllowedParent.bind( this );
 		this.conversionApi.getSplitParts = this._getSplitParts.bind( this );
 	}
@@ -217,8 +226,9 @@ export default class UpcastDispatcher {
 		// Clear context position.
 		this._modelCursor = null;
 
-		// Clear split elements lists.
+		// Clear split elements & parents lists.
 		this._splitParts.clear();
+		this._cursorParents.clear();
 
 		// Clear conversion API.
 		this.conversionApi.writer = null;
@@ -262,9 +272,11 @@ export default class UpcastDispatcher {
 	 * @private
 	 * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#convertChildren
 	 */
-	_convertChildren( viewItem, modelCursor ) {
-		const modelRange = new ModelRange( modelCursor );
-		let nextModelCursor = modelCursor;
+	_convertChildren( viewItem, elementOrModelCursor ) {
+		let nextModelCursor = elementOrModelCursor.is( 'position' ) ?
+			elementOrModelCursor : ModelPosition._createAt( elementOrModelCursor, 0 );
+
+		const modelRange = new ModelRange( nextModelCursor );
 
 		for ( const viewChild of Array.from( viewItem.getChildren() ) ) {
 			const result = this._convertItem( viewChild, nextModelCursor );
@@ -278,27 +290,92 @@ export default class UpcastDispatcher {
 		return { modelRange, modelCursor: nextModelCursor };
 	}
 
+	/**
+	 * @private
+	 * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#safeInsert
+	 */
+	_safeInsert( modelElement, position ) {
+		// 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 = this._splitToAllowedParent( modelElement, position );
+
+		// 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 false;
+		}
+
+		// Insert element on allowed position.
+		this.conversionApi.writer.insert( modelElement, splitResult.position );
+
+		return true;
+	}
+
+	/**
+	 * @private
+	 * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#updateConversionResult
+	 */
+	_updateConversionResult( modelElement, data ) {
+		const parts = this._getSplitParts( modelElement );
+
+		const writer = this.conversionApi.writer;
+
+		// Set conversion result range - only if not set already.
+		if ( !data.modelRange ) {
+			data.modelRange = writer.createRange(
+				writer.createPositionBefore( modelElement ),
+				writer.createPositionAfter( parts[ parts.length - 1 ] )
+			);
+		}
+
+		const savedCursorParent = this._cursorParents.get( modelElement );
+
+		// Now we need to check where the `modelCursor` should be.
+		if ( savedCursorParent ) {
+			// 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 = writer.createPositionAt( savedCursorParent, 0 );
+		} else {
+			// Otherwise just continue after inserted element.
+
+			data.modelCursor = data.modelRange.end;
+		}
+	}
+
 	/**
 	 * @private
 	 * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#splitToAllowedParent
 	 */
 	_splitToAllowedParent( node, modelCursor ) {
+		const { schema, writer } = this.conversionApi;
+
 		// Try to find allowed parent.
-		const allowedParent = this.conversionApi.schema.findAllowedParent( modelCursor, node );
+		let allowedParent = schema.findAllowedParent( modelCursor, node );
 
-		// When there is no parent that allows to insert node then return `null`.
-		if ( !allowedParent ) {
-			return null;
-		}
+		if ( allowedParent ) {
+			// When current position parent allows to insert node then return this position.
+			if ( allowedParent === modelCursor.parent ) {
+				return { position: modelCursor };
+			}
 
-		// When current position parent allows to insert node then return this position.
-		if ( allowedParent === modelCursor.parent ) {
-			return { position: modelCursor };
+			// When allowed parent is in context tree (it's outside the converted tree).
+			if ( this._modelCursor.parent.getAncestors().includes( allowedParent ) ) {
+				allowedParent = null;
+			}
 		}
 
-		// When allowed parent is in context tree.
-		if ( this._modelCursor.parent.getAncestors().includes( allowedParent ) ) {
-			return null;
+		if ( !allowedParent ) {
+			// Check if the node wrapped with a paragraph would be accepted by the schema.
+			if ( !isParagraphable( modelCursor, node, schema ) ) {
+				return null;
+			}
+
+			return {
+				position: wrapInParagraph( modelCursor, writer )
+			};
 		}
 
 		// Split element to allowed parent.
@@ -332,9 +409,12 @@ export default class UpcastDispatcher {
 			}
 		}
 
+		const cursorParent = splitResult.range.end.parent;
+		this._cursorParents.set( node, cursorParent );
+
 		return {
 			position: splitResult.position,
-			cursorParent: splitResult.range.end.parent
+			cursorParent
 		};
 	}
 
@@ -415,14 +495,9 @@ export default class UpcastDispatcher {
 	 * all elements conversion or to conversion of specific elements.
 	 *
 	 * @event element
-	 * @param {Object} data Conversion data. Keep in mind that this object is shared by reference between all
-	 * callbacks that will be called. This means that callbacks can override values if needed, and those values will
-	 * be available in other callbacks.
-	 * @param {module:engine/view/item~Item} data.viewItem Converted item.
-	 * @param {module:engine/model/position~Position} data.modelCursor Position where a converter should start changes.
-	 * Change this value for the next converter to tell where the conversion should continue.
-	 * @param {module:engine/model/range~Range} data.modelRange The current state of conversion result. Every change to
-	 * converted element should be reflected by setting or modifying this property.
+	 * @param {module:engine/conversion/upcastdispatcher~UpcastConversionData} data Conversion data. Keep in mind that this object is shared
+	 * by reference between all callbacks that will be called. This means that callbacks can override values if needed, and those values
+	 * will be available in other callbacks.
 	 * @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion utilities to be used by callback.
 	 */
 
@@ -507,9 +582,8 @@ function createContextTree( contextDefinition, writer ) {
 }
 
 /**
- * Conversion interface that is registered for given {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}
- * and is passed as one of parameters when {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher dispatcher}
- * fires it's events.
+ * A set of conversion utils available as the third parameter of
+ * {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher upcast dispatcher}'s events.
  *
  * @interface module:engine/conversion/upcastdispatcher~UpcastConversionApi
  */
@@ -541,13 +615,74 @@ function createContextTree( contextDefinition, writer ) {
  * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:text
  * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:documentFragment
  * @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|module:engine/model/element~Element} positionOrElement Position or element of conversion.
  * @returns {Object} result Conversion result.
  * @returns {module:engine/model/range~Range} result.modelRange Model range containing results of conversion of all children of given item.
  * When no children was converted then range is collapsed.
  * @returns {module:engine/model/position~Position} result.modelCursor Position where conversion should be continued.
  */
 
+/**
+ * Safely inserts an element to the document checking {@link module:engine/model/schema~Schema schema} to find allowed parent for
+ * an element that we are going to insert starting from given position. If current parent does not allow to insert element
+ * but one of the ancestors does then split nodes to allowed parent.
+ *
+ * If schema allows to insert node in given position, nothing is split.
+ *
+ * If it was not possible to find allowed parent, `false` is returned, nothing is split.
+ *
+ * Otherwise, ancestors are split.
+ *
+ * For instance, if `<image>` is not allowed in `<paragraph>` but is allowed in `$root`:
+ *
+ *		<paragraph>foo[]bar</paragraph>
+ *
+ *		-> safe insert for `<image>` will split ->
+ *
+ *		<paragraph>foo</paragraph>[]<paragraph>bar</paragraph>
+ *
+ * Example usage:
+ *
+ *		const myElement = conversionApi.writer.createElement( 'myElement' );
+ *
+ *		if ( !conversionApi.safeInsert( myElement, data.modelCursor ) ) {
+ *			return;
+ *		}
+ *
+ * The split result is saved and {@link #updateConversionResult} should be used to update
+ * {@link module:engine/conversion/upcastdispatcher~UpcastConversionData conversion data}.
+ *
+ * @method #safeInsert
+ * @param {module:engine/model/node~Node} node Node to insert.
+ * @param {module:engine/model/position~Position} position Position on which element is going to be inserted.
+ * @returns {Boolean} Split result. If it was not possible to find allowed position `false` is returned.
+ */
+
+/**
+ * Updates the conversion result and sets proper {@link module:engine/conversion/upcastdispatcher~UpcastConversionData#modelRange} and
+ * next {@link module:engine/conversion/upcastdispatcher~UpcastConversionData#modelCursor} after the conversion.
+ * Used together with {@link #safeInsert} enables you to easily convert elements without worrying if the node was split
+ * during its children conversion.
+ *
+ * Example of a usage in a converter code:
+ *
+ *		const myElement = conversionApi.writer.createElement( 'myElement' );
+ *
+ *		if ( !conversionApi.safeInsert( myElement, data.modelCursor ) ) {
+ *			return;
+ *		}
+ *
+ *		// Children conversion may split `myElement`.
+ *		conversionApi.convertChildren( data.viewItem, myElement );
+ *
+ *		conversionApi.updateConversionResult( myElement, data );
+ *
+ * @method #updateConversionResult
+ * @param {module:engine/model/element~Element} element
+ * @param {module:engine/conversion/upcastdispatcher~UpcastConversionData} data Conversion data.
+ * @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion utilities to be used by callback.
+ */
+
 /**
  * Checks {@link module:engine/model/schema~Schema schema} to find allowed parent for element that we are going to insert
  * starting from given position. If current parent does not allow to insert element but one of the ancestors does then
@@ -563,13 +698,15 @@ function createContextTree( contextDefinition, writer ) {
  *
  *		<paragraph>foo[]bar</paragraph>
  *
- *  	-> split for `<image>` ->
+ *		-> split for `<image>` ->
  *
- *  	<paragraph>foo</paragraph>[]<paragraph>bar</paragraph>
+ *		<paragraph>foo</paragraph>[]<paragraph>bar</paragraph>
  *
  * In the sample above position between `<paragraph>` elements will be returned as `position` and the second `paragraph`
  * as `cursorParent`.
  *
+ * **Note:** This is an advanced method. For most cases {@link #safeInsert} and {@link #updateConversionResult} should be used.
+ *
  * @method #splitToAllowedParent
  * @param {module:engine/model/position~Position} position Position on which element is going to be inserted.
  * @param {module:engine/model/node~Node} node Node to insert.
@@ -596,7 +733,7 @@ function createContextTree( contextDefinition, writer ) {
  *		const myElement = conversionApi.writer.createElement( 'myElement' );
  *
  *		// Children conversion may split `myElement`.
- *		conversionApi.convertChildren( myElement, modelCursor );
+ *		conversionApi.convertChildren( data.viewItem, data.modelCursor );
  *
  *		const splitParts = conversionApi.getSplitParts( myElement );
  *		const lastSplitPart = splitParts[ splitParts.length - 1 ];
@@ -614,6 +751,8 @@ function createContextTree( contextDefinition, writer ) {
  * 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.
  *
+ * **Note:** This is an advanced method. For most cases {@link #safeInsert} and {@link #updateConversionResult} should be used.
+ *
  * @method #getSplitParts
  * @param {module:engine/model/element~Element} element
  * @returns {Array.<module:engine/model/element~Element>}
@@ -648,3 +787,18 @@ function createContextTree( contextDefinition, writer ) {
  *
  * @member {module:engine/model/writer~Writer} #writer
  */
+
+/**
+ * Conversion data.
+ *
+ * **Note:** Keep in mind that this object is shared by reference between all conversion callbacks that will be called.
+ * This means that callbacks can override values if needed, and those values will be available in other callbacks.
+ *
+ * @typedef {Object} module:engine/conversion/upcastdispatcher~UpcastConversionData
+ *
+ * @property {module:engine/view/item~Item} viewItem Converted item.
+ * @property {module:engine/model/position~Position} modelCursor Position where a converter should start changes.
+ * Change this value for the next converter to tell where the conversion should continue.
+ * @property {module:engine/model/range~Range} [modelRange] The current state of conversion result. Every change to
+ * converted element should be reflected by setting or modifying this property.
+ */

+ 78 - 100
packages/ckeditor5-engine/src/conversion/upcasthelpers.js

@@ -4,14 +4,13 @@
  */
 
 import Matcher from '../view/matcher';
-import ModelRange from '../model/range';
 import ConversionHelpers from './conversionhelpers';
 
 import { cloneDeep } from 'lodash-es';
-import ModelSelection from '../model/selection';
 import { attachLinkToDocumentation } from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
+import { isParagraphable, wrapInParagraph } from '../model/utils/autoparagraphing';
 
 /* global console */
 
@@ -61,7 +60,9 @@ export default class UpcastHelpers extends ConversionHelpers {
 	 *				name: 'p',
 	 *				classes: 'heading'
 	 * 			},
-	 * 			model: ( viewElement, modelWriter ) => {
+	 * 			model: ( viewElement, conversionApi ) => {
+	 * 				const modelWriter = conversionApi.writer;
+	 *
 	 * 				return modelWriter.createElement( 'heading', { level: viewElement.getAttribute( 'data-level' ) } );
 	 * 			}
 	 * 		} );
@@ -73,8 +74,9 @@ export default class UpcastHelpers extends ConversionHelpers {
 	 * @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 {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 {@link module:engine/conversion/upcastdispatcher~UpcastConversionApi upcast conversion API}
+	 * 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}
 	 */
@@ -137,7 +139,7 @@ export default class UpcastHelpers extends ConversionHelpers {
 	 *			},
 	 *			model: {
 	 *				key: 'fontSize',
-	 *				value: viewElement => {
+	 *				value: ( viewElement, conversionApi ) => {
 	 *					const fontSize = viewElement.getStyle( 'font-size' );
 	 *					const value = fontSize.substr( 0, fontSize.length - 2 );
 	 *
@@ -159,7 +161,8 @@ export default class UpcastHelpers extends ConversionHelpers {
 	 * @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.
+	 * the model attribute. `value` property may be set as a function that takes a view element and
+	 * {@link module:engine/conversion/upcastdispatcher~UpcastConversionApi upcast conversion API} and returns the value.
 	 * If `String` is given, the model attribute value will be set to `true`.
 	 * @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority.
 	 * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
@@ -233,7 +236,7 @@ export default class UpcastHelpers extends ConversionHelpers {
 	 *			},
 	 *			model: {
 	 *				key: 'styled'
-	 *				value: viewElement => {
+	 *				value: ( viewElement, conversionApi ) => {
 	 *					const regexp = /styled-([\S]+)/;
 	 *					const match = viewElement.getAttribute( 'class' ).match( regexp );
 	 *
@@ -265,7 +268,7 @@ export default class UpcastHelpers extends ConversionHelpers {
 	 *			},
 	 *			model: {
 	 *				key: 'lineHeight',
-	 *				value: viewElement => viewElement.getStyle( 'line-height' )
+	 *				value: ( viewElement, conversionApi ) => viewElement.getStyle( 'line-height' )
 	 *			}
 	 *		} );
 	 *
@@ -280,7 +283,8 @@ export default class UpcastHelpers extends ConversionHelpers {
 	 * 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.
+	 * the model attribute. `value` property may be set as a function that takes a view element and
+	 * {@link module:engine/conversion/upcastdispatcher~UpcastConversionApi upcast conversion API} 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}
@@ -312,7 +316,7 @@ export default class UpcastHelpers extends ConversionHelpers {
 	 *
 	 *		editor.conversion.for( 'upcast' ).elementToMarker( {
 	 *			view: 'marker-search',
-	 *			model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' )
+	 *			model: ( viewElement, conversionApi ) => 'comment:' + viewElement.getAttribute( 'data-comment-id' )
 	 *		} );
 	 *
 	 *		editor.conversion.for( 'upcast' ).elementToMarker( {
@@ -340,7 +344,7 @@ export default class UpcastHelpers extends ConversionHelpers {
 	elementToMarker( config ) {
 		/**
 		 * The {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToMarker `UpcastHelpers#elementToMarker()`}
-		 * method has been deprecated and will be removed in the near future.
+		 * method was deprecated and will be removed in the near future.
 		 * Please use {@link module:engine/conversion/upcasthelpers~UpcastHelpers#dataToMarker `UpcastHelpers#dataToMarker()`} instead.
 		 *
 		 * @error upcast-helpers-element-to-marker-deprecated
@@ -348,7 +352,7 @@ export default class UpcastHelpers extends ConversionHelpers {
 		console.warn(
 			attachLinkToDocumentation(
 				'upcast-helpers-element-to-marker-deprecated: ' +
-				'The UpcastHelpers#elementToMarker() method has been deprecated and will be removed in the near future. ' +
+				'The UpcastHelpers#elementToMarker() method was deprecated and will be removed in the near future. ' +
 				'Please use UpcastHelpers#dataToMarker() instead.'
 			)
 		);
@@ -357,7 +361,7 @@ export default class UpcastHelpers extends ConversionHelpers {
 	}
 
 	/**
-	 * View to model marker conversion helper.
+	 * View-to-model marker conversion helper.
 	 *
 	 * Converts view data created by {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToData `#markerToData()`}
 	 * back to a model marker.
@@ -368,7 +372,7 @@ export default class UpcastHelpers extends ConversionHelpers {
 	 *
 	 * The `config.view` property is equal to the marker group name to convert.
 	 *
-	 * By default, this converter creates markers with `group:name` name convention (to match the default `markerToData` conversion).
+	 * By default, this converter creates markers with the `group:name` name convention (to match the default `markerToData` conversion).
 	 *
 	 * The conversion configuration can take a function that will generate a marker name.
 	 * If such function is set as the `config.model` parameter, it is passed the `name` part from the view element or attribute and it is
@@ -377,7 +381,7 @@ export default class UpcastHelpers extends ConversionHelpers {
 	 * Basic usage:
 	 *
 	 *		// Using the default conversion.
-	 *		// In this case, all markers from `comment` group will be converted.
+	 *		// In this case, all markers from the `comment` group will be converted.
 	 *		// The conversion will look for `<comment-start>` and `<comment-end>` tags and
 	 *		// `data-comment-start-before`, `data-comment-start-after`,
 	 *		// `data-comment-end-before` and `data-comment-end-after` attributes.
@@ -395,20 +399,20 @@ export default class UpcastHelpers extends ConversionHelpers {
 	 *		<paragraph>Foo[bar</paragraph>
 	 *		<image src="abc.jpg"></image>]
 	 *
-	 * Where `[]` are boundaries of a marker that will receive `comment:commentId:uid` name.
+	 * Where `[]` are boundaries of a marker that will receive the `comment:commentId:uid` name.
 	 *
 	 * Other examples of usage:
 	 *
-	 *		// Using custom function which is the same as the default conversion:
+	 *		// Using a custom function which is the same as the default conversion:
 	 *		editor.conversion.for( 'upcast' ).dataToMarker( {
 	 *			view: 'comment',
-	 *			model: name => 'comment:' + name,
+	 *			model: ( name, conversionApi ) => 'comment:' + name,
 	 *		} );
 	 *
-	 *		// Using converter priority:
+	 *		// Using the converter priority:
 	 *		editor.conversion.for( 'upcast' ).dataToMarker( {
 	 *			view: 'comment',
-	 *			model: name => 'comment:' + name,
+	 *			model: ( name, conversionApi ) => 'comment:' + name,
 	 *			converterPriority: 'high'
 	 *		} );
 	 *
@@ -417,8 +421,9 @@ export default class UpcastHelpers extends ConversionHelpers {
 	 *
 	 * @method #dataToMarker
 	 * @param {Object} config Conversion configuration.
-	 * @param {String} config.view Marker group name to convert.
-	 * @param {Function} [config.model] Function that takes `name` part from the view element or attribute and returns the marker name.
+	 * @param {String} config.view The marker group name to convert.
+	 * @param {Function} [config.model] A function that takes the `name` part from the view element or attribute and
+	 * {@link module:engine/conversion/upcastdispatcher~UpcastConversionApi upcast conversion API} and returns the marker name.
 	 * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
 	 * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
 	 */
@@ -460,17 +465,33 @@ export function convertToModelFragment() {
  * @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 );
+	return ( evt, data, { schema, consumable, writer } ) => {
+		let position = data.modelCursor;
 
-				conversionApi.writer.insert( text, data.modelCursor );
+		// When node is already converted then do nothing.
+		if ( !consumable.test( data.viewItem ) ) {
+			return;
+		}
 
-				data.modelRange = ModelRange._createFromPositionAndShift( data.modelCursor, text.offsetSize );
-				data.modelCursor = data.modelRange.end;
+		if ( !schema.checkChild( position, '$text' ) ) {
+			if ( !isParagraphable( position, '$text', schema ) ) {
+				return;
 			}
+
+			position = wrapInParagraph( position, writer );
 		}
+
+		consumable.consume( data.viewItem );
+
+		const text = writer.createText( data.viewItem.data );
+
+		writer.insert( text, position );
+
+		data.modelRange = writer.createRange(
+			position,
+			position.getShiftedBy( text.offsetSize )
+		);
+		data.modelCursor = data.modelRange.end;
 	};
 }
 
@@ -491,7 +512,6 @@ export function convertText() {
 export function convertSelectionChange( model, mapper ) {
 	return ( evt, data ) => {
 		const viewSelection = data.newSelection;
-		const modelSelection = new ModelSelection();
 
 		const ranges = [];
 
@@ -499,7 +519,7 @@ export function convertSelectionChange( model, mapper ) {
 			ranges.push( mapper.toModelRange( viewRange ) );
 		}
 
-		modelSelection.setTo( ranges, { backward: viewSelection.isBackward } );
+		const modelSelection = model.createSelection( ranges, { backward: viewSelection.isBackward } );
 
 		if ( !modelSelection.isEqual( model.document.selection ) ) {
 			model.change( writer => {
@@ -696,7 +716,7 @@ function upcastAttributeToMarker( config ) {
 
 		function addMarkerElements( position, markerViewNames ) {
 			for ( const markerViewName of markerViewNames ) {
-				const markerName = config.model( markerViewName );
+				const markerName = config.model( markerViewName, conversionApi );
 				const element = conversionApi.writer.createElement( '$marker', { 'data-name': markerName } );
 
 				conversionApi.writer.insert( element, position );
@@ -735,80 +755,37 @@ function getViewElementNameFromConfig( viewConfig ) {
 // @param {Object} config Conversion configuration.
 // @returns {Function} View to model converter.
 function prepareToElementConverter( config ) {
-	const matcher = config.view ? new Matcher( config.view ) : null;
+	const matcher = new Matcher( config.view );
 
 	return ( evt, data, conversionApi ) => {
-		let match = {};
+		const matcherResult = matcher.match( data.viewItem );
 
-		// 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;
+		if ( !matcherResult ) {
+			return;
 		}
 
+		const 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 );
+		const modelElement = getModelElement( config.model, data.viewItem, conversionApi );
 
-		// When there is no split result it means that we can't insert element to model tree, so let's skip it.
-		if ( !splitResult ) {
+		if ( !modelElement ) {
 			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 ) );
+		if ( !conversionApi.safeInsert( modelElement, data.modelCursor ) ) {
+			return;
+		}
 
-		// 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;
-		}
+		conversionApi.convertChildren( data.viewItem, modelElement );
+		conversionApi.updateConversionResult( modelElement, data );
 	};
 }
 
@@ -817,12 +794,12 @@ function prepareToElementConverter( config ) {
 //
 // @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 ) {
+// @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi The upcast conversion API.
+function getModelElement( model, input, conversionApi ) {
 	if ( model instanceof Function ) {
-		return model( input, writer );
+		return model( input, conversionApi );
 	} else {
-		return writer.createElement( model );
+		return conversionApi.writer.createElement( model );
 	}
 }
 
@@ -900,7 +877,8 @@ function prepareToAttributeConverter( config, shallow ) {
 		}
 
 		const modelKey = config.model.key;
-		const modelValue = typeof config.model.value == 'function' ? config.model.value( data.viewItem ) : config.model.value;
+		const modelValue = typeof config.model.value == 'function' ?
+			config.model.value( data.viewItem, conversionApi ) : config.model.value;
 
 		// Do not convert if attribute building function returned falsy value.
 		if ( modelValue === null ) {
@@ -981,10 +959,10 @@ function setAttributeOn( modelRange, modelAttribute, shallow, conversionApi ) {
 function normalizeElementToMarkerConfig( config ) {
 	const oldModel = config.model;
 
-	config.model = ( viewElement, modelWriter ) => {
-		const markerName = typeof oldModel == 'string' ? oldModel : oldModel( viewElement );
+	config.model = ( viewElement, conversionApi ) => {
+		const markerName = typeof oldModel == 'string' ? oldModel : oldModel( viewElement, conversionApi );
 
-		return modelWriter.createElement( '$marker', { 'data-name': markerName } );
+		return conversionApi.writer.createElement( '$marker', { 'data-name': markerName } );
 	};
 }
 
@@ -998,11 +976,11 @@ function normalizeDataToMarkerConfig( config, type ) {
 	// Upcast <markerGroup-start> and <markerGroup-end> elements.
 	configForElements.view = config.view + '-' + type;
 
-	configForElements.model = ( viewElement, modelWriter ) => {
+	configForElements.model = ( viewElement, conversionApi ) => {
 		const viewName = viewElement.getAttribute( 'name' );
-		const markerName = config.model( viewName );
+		const markerName = config.model( viewName, conversionApi );
 
-		return modelWriter.createElement( '$marker', { 'data-name': markerName } );
+		return conversionApi.writer.createElement( '$marker', { 'data-name': markerName } );
 	};
 
 	return configForElements;

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

@@ -229,8 +229,8 @@ export function stringify( node, selectionOrPositionOrRange = null, markers = nu
 	downcastDispatcher.on( 'insert:$text', insertText() );
 	downcastDispatcher.on( 'attribute', ( evt, data, conversionApi ) => {
 		if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection || data.item.is( '$textProxy' ) ) {
-			const converter = wrap( ( modelAttributeValue, viewWriter ) => {
-				return viewWriter.createAttributeElement(
+			const converter = wrap( ( modelAttributeValue, { writer } ) => {
+				return writer.createAttributeElement(
 					'model-text-with-attributes',
 					{ [ data.attributeKey ]: stringifyAttributeValue( modelAttributeValue ) }
 				);
@@ -248,7 +248,7 @@ export function stringify( node, selectionOrPositionOrRange = null, markers = nu
 
 	downcastDispatcher.on( 'selection', convertRangeSelection() );
 	downcastDispatcher.on( 'selection', convertCollapsedSelection() );
-	downcastDispatcher.on( 'addMarker', insertUIElement( ( data, writer ) => {
+	downcastDispatcher.on( 'addMarker', insertUIElement( ( data, { writer } ) => {
 		const name = data.markerName + ':' + ( data.isOpening ? 'start' : 'end' );
 
 		return writer.createUIElement( name );
@@ -406,7 +406,7 @@ function convertToModelElement() {
 
 		conversionApi.mapper.bindElements( element, data.viewItem );
 
-		conversionApi.convertChildren( data.viewItem, ModelPosition._createAt( element, 0 ) );
+		conversionApi.convertChildren( data.viewItem, element );
 
 		data.modelRange = ModelRange._createOn( element );
 		data.modelCursor = data.modelRange.end;

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

@@ -51,9 +51,9 @@ const allowedTypes = {
  * the default `main` name will be used.
  * @param {Boolean} [options.showType=false] When set to `true`, the type of elements will be printed (`<container:p>`
  * instead of `<p>`, `<attribute:b>` instead of `<b>` and `<empty:img>` instead of `<img>`).
- * @param {Boolean} [options.showPriority=false] When set to `true`, attribute element's priority will be printed
+ * @param {Boolean} [options.showPriority=false] When set to `true`, the attribute element's priority will be printed
  * (`<span view-priority="12">`, `<b view-priority="10">`).
- * @param {Boolean} [options.showAttributeElementId=false] When set to `true`, attribute element's id will be printed
+ * @param {Boolean} [options.showAttributeElementId=false] When set to `true`, the attribute element's ID will be printed
  * (`<span id="marker:foo">`).
  * @param {Boolean} [options.renderUIElements=false] When set to `true`, the inner content of each
  * {@link module:engine/view/uielement~UIElement} will be printed.

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

@@ -1095,7 +1095,7 @@ class LiveSelection extends Selection {
 			if ( !this.isGravityOverridden && !attrs ) {
 				let node = nodeBefore;
 
-				while ( node && !attrs ) {
+				while ( node && !schema.isInline( node ) && !attrs ) {
 					node = node.previousSibling;
 					attrs = getAttrsIfCharacter( node );
 				}
@@ -1105,7 +1105,7 @@ class LiveSelection extends Selection {
 			if ( !attrs ) {
 				let node = nodeAfter;
 
-				while ( node && !attrs ) {
+				while ( node && !schema.isInline( node ) && !attrs ) {
 					node = node.nextSibling;
 					attrs = getAttrsIfCharacter( node );
 				}

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

@@ -99,7 +99,7 @@ export default class MarkerCollection {
 			 *
 			 * @error markercollection-incorrect-marker-name
 			 */
-			throw new CKEditorError( 'markercollection-incorrect-marker-name: Marker name cannot contain "," character.', this );
+			throw new CKEditorError( 'markercollection-incorrect-marker-name: Marker name cannot contain the "," character.', this );
 		}
 
 		const oldMarker = this._markers.get( markerName );

+ 15 - 8
packages/ckeditor5-engine/src/model/model.js

@@ -25,6 +25,7 @@ import deleteContent from './utils/deletecontent';
 import modifySelection from './utils/modifyselection';
 import getSelectedContent from './utils/getselectedcontent';
 import { injectSelectionPostFixer } from './utils/selection-post-fixer';
+import { autoParagraphEmptyRoots } from './utils/autoparagraphing';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 // @if CK_DEBUG_ENGINE // const { dumpTrees } = require( '../dev-utils/utils' );
@@ -100,7 +101,8 @@ export default class Model {
 		} );
 		this.schema.register( '$text', {
 			allowIn: '$block',
-			isInline: true
+			isInline: true,
+			isContent: true
 		} );
 		this.schema.register( '$clipboardHolder', {
 			allowContentOf: '$root',
@@ -121,6 +123,9 @@ export default class Model {
 
 		injectSelectionPostFixer( this );
 
+		// Post-fixer which takes care of adding empty paragraph elements to the empty roots.
+		this.document.registerPostFixer( autoParagraphEmptyRoots );
+
 		// @if CK_DEBUG_ENGINE // this.on( 'applyOperation', () => {
 		// @if CK_DEBUG_ENGINE // 	dumpTrees( this.document, this.document.version );
 		// @if CK_DEBUG_ENGINE // }, { priority: 'lowest' } );
@@ -540,7 +545,7 @@ export default class Model {
 	 *
 	 * * 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/schema~Schema#isContent content element},
 	 * * or any {@link module:engine/model/markercollection~Marker marker} which
 	 * {@link module:engine/model/markercollection~Marker#_affectsData affects data}.
 	 *
@@ -573,14 +578,16 @@ export default class Model {
 		}
 
 		for ( const item of range.getItems() ) {
-			if ( item.is( '$textProxy' ) ) {
-				if ( !ignoreWhitespaces ) {
-					return true;
-				} else if ( item.data.search( /\S/ ) !== -1 ) {
+			if ( this.schema.isContent( item ) ) {
+				if ( item.is( '$textProxy' ) ) {
+					if ( !ignoreWhitespaces ) {
+						return true;
+					} else if ( item.data.search( /\S/ ) !== -1 ) {
+						return true;
+					}
+				} else {
 					return true;
 				}
-			} else if ( this.schema.isObject( item ) ) {
-				return true;
 			}
 		}
 

+ 81 - 2
packages/ckeditor5-engine/src/model/schema.js

@@ -282,7 +282,13 @@ export default class Schema {
 	isObject( item ) {
 		const def = this.getDefinition( item );
 
-		return !!( def && def.isObject );
+		if ( !def ) {
+			return false;
+		}
+
+		// Note: Check out the implementation of #isLimit(), #isSelectable(), and #isContent()
+		// to understand why these three constitute an object.
+		return !!( def.isObject || ( def.isLimit && def.isSelectable && def.isContent ) );
 	}
 
 	/**
@@ -292,7 +298,7 @@ export default class Schema {
 	 *		schema.isInline( 'paragraph' ); // -> false
 	 *		schema.isInline( 'softBreak' ); // -> true
 	 *
-	 *		const text = writer.createText('foo' );
+	 *		const text = writer.createText( 'foo' );
 	 *		schema.isInline( text ); // -> true
 	 *
 	 * See the {@glink framework/guides/deep-dive/schema#inline-elements Inline elements} section of the Schema deep dive
@@ -306,6 +312,60 @@ export default class Schema {
 		return !!( def && def.isInline );
 	}
 
+	/**
+	 * Returns `true` if the given item is defined to be
+	 * a selectable element by the {@link module:engine/model/schema~SchemaItemDefinition}'s `isSelectable` property.
+	 *
+	 *		schema.isSelectable( 'paragraph' ); // -> false
+	 *		schema.isSelectable( 'heading1' ); // -> false
+	 *		schema.isSelectable( 'image' ); // -> true
+	 *		schema.isSelectable( 'tableCell' ); // -> true
+	 *
+	 *		const text = writer.createText( 'foo' );
+	 *		schema.isSelectable( text ); // -> false
+	 *
+	 * See the {@glink framework/guides/deep-dive/schema#selectable-elements Selectable elements} section of the Schema deep dive}
+	 * guide for more details.
+	 *
+	 * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
+	 */
+	isSelectable( item ) {
+		const def = this.getDefinition( item );
+
+		if ( !def ) {
+			return false;
+		}
+
+		return !!( def.isSelectable || def.isObject );
+	}
+
+	/**
+	 * Returns `true` if the given item is defined to be
+	 * a content by the {@link module:engine/model/schema~SchemaItemDefinition}'s `isContent` property.
+	 *
+	 *		schema.isContent( 'paragraph' ); // -> false
+	 *		schema.isContent( 'heading1' ); // -> false
+	 *		schema.isContent( 'image' ); // -> true
+	 *		schema.isContent( 'horizontalLine' ); // -> true
+	 *
+	 *		const text = writer.createText( 'foo' );
+	 *		schema.isContent( text ); // -> true
+	 *
+	 * See the {@glink framework/guides/deep-dive/schema#content-elements Content elements} section of the Schema deep dive}
+	 * guide for more details.
+	 *
+	 * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
+	 */
+	isContent( item ) {
+		const def = this.getDefinition( item );
+
+		if ( !def ) {
+			return false;
+		}
+
+		return !!( def.isContent || def.isObject );
+	}
+
 	/**
 	 * Checks whether the given node (`child`) can be a child of the given context.
 	 *
@@ -1185,6 +1245,25 @@ mix( Schema, ObservableMixin );
  *
  * Read more about the object elements in the
  * {@glink framework/guides/deep-dive/schema#object-elements Object elements} section of the Schema deep dive} guide.
+ *
+ * @property {Boolean} isSelectable
+ * `true` when an element should be selectable as a whole by the user. Examples of selectable elements: `image`, `table`, `tableCell`, etc.
+ *
+ * **Note:** An object is also a selectable element, so
+ * {@link module:engine/model/schema~Schema#isSelectable `isSelectable()`} returns `true` for object elements automatically.
+ *
+ * Read more about selectable elements in the
+ * {@glink framework/guides/deep-dive/schema#selectable-elements Selectable elements} section of the Schema deep dive} guide.
+ *
+ * @property {Boolean} isContent
+ * An item is a content when it always finds its way to editor data output regardless of the number and type of its descendants.
+ * Examples of content elements: `$text`, `image`, `table`, etc. (but not `paragraph`, `heading1` or `tableCell`).
+ *
+ * **Note:** An object is also a content element, so
+ * {@link module:engine/model/schema~Schema#isContent `isContent()`} returns `true` for object elements automatically.
+ *
+ * Read more about content elements in the
+ * {@glink framework/guides/deep-dive/schema#content-elements Content elements} section of the Schema deep dive} guide.
  */
 
 /**

+ 77 - 0
packages/ckeditor5-engine/src/model/utils/autoparagraphing.js

@@ -0,0 +1,77 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module engine/model/utils/autoparagraphing
+ */
+
+/**
+ * Fixes all empty roots.
+ *
+ * @protected
+ * @param {module:engine/model/writer~Writer} writer The model writer.
+ * @returns {Boolean} `true` if any change has been applied, `false` otherwise.
+ */
+export function autoParagraphEmptyRoots( writer ) {
+	const { schema, document } = writer.model;
+
+	for ( const rootName of document.getRootNames() ) {
+		const root = document.getRoot( rootName );
+
+		if ( root.isEmpty && !schema.checkChild( root, '$text' ) ) {
+			// If paragraph element is allowed in the root, create paragraph element.
+			if ( schema.checkChild( root, 'paragraph' ) ) {
+				writer.insertElement( 'paragraph', root );
+
+				// Other roots will get fixed in the next post-fixer round. Those will be triggered
+				// in the same batch no matter if this method was triggered by the post-fixing or not
+				// (the above insertElement call will trigger the post-fixers).
+				return true;
+			}
+		}
+	}
+
+	return false;
+}
+
+/**
+ * Checks if the given node wrapped with a paragraph would be accepted by the schema in the given position.
+ *
+ * @protected
+ * @param {module:engine/model/position~Position} position The position at which to check.
+ * @param {module:engine/model/node~Node|String} nodeOrType The child node or child type to check.
+ * @param {module:engine/model/schema~Schema} schema A schema instance used for element validation.
+ */
+export function isParagraphable( position, nodeOrType, schema ) {
+	const context = schema.createContext( position );
+
+	// When paragraph is allowed in this context...
+	if ( !schema.checkChild( context, 'paragraph' ) ) {
+		return false;
+	}
+
+	// And a node would be allowed in this paragraph...
+	if ( !schema.checkChild( context.push( 'paragraph' ), nodeOrType ) ) {
+		return false;
+	}
+
+	return true;
+}
+
+/**
+ * Inserts a new paragraph at the given position and returns a position inside that paragraph.
+ *
+ * @protected
+ * @param {module:engine/model/position~Position} position The position where a paragraph should be inserted.
+ * @param {module:engine/model/writer~Writer} writer The model writer.
+ * @returns {module:engine/model/position~Position} Position inside the created paragraph.
+ */
+export function wrapInParagraph( position, writer ) {
+	const paragraph = writer.createElement( 'paragraph' );
+
+	writer.insert( paragraph, position );
+
+	return writer.createPositionAt( paragraph, 0 );
+}

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

@@ -92,41 +92,44 @@ export default function modifySelection( model, selection, options = {} ) {
 // @param {{ walker, unit, isForward, schema }} data
 // @param {module:engine/view/treewalker~TreeWalkerValue} value
 function tryExtendingTo( data, value ) {
+	const { isForward, walker, unit, schema } = data;
+	const { type, item, nextPosition } = value;
+
 	// If found text, we can certainly put the focus in it. Let's just find a correct position
 	// based on the unit.
-	if ( value.type == 'text' ) {
+	if ( type == 'text' ) {
 		if ( data.unit === 'word' ) {
-			return getCorrectWordBreakPosition( data.walker, data.isForward );
+			return getCorrectWordBreakPosition( walker, isForward );
 		}
 
-		return getCorrectPosition( data.walker, data.unit, data.isForward );
+		return getCorrectPosition( walker, unit, isForward );
 	}
 
 	// Entering an element.
-	if ( value.type == ( data.isForward ? 'elementStart' : 'elementEnd' ) ) {
-		// If it's an object, we can select it now.
-		if ( data.schema.isObject( value.item ) ) {
-			return Position._createAt( value.item, data.isForward ? 'after' : 'before' );
+	if ( type == ( isForward ? 'elementStart' : 'elementEnd' ) ) {
+		// If it's a selectable, we can select it now.
+		if ( schema.isSelectable( item ) ) {
+			return Position._createAt( item, isForward ? 'after' : 'before' );
 		}
 
 		// If text allowed on this position, extend to this place.
-		if ( data.schema.checkChild( value.nextPosition, '$text' ) ) {
-			return value.nextPosition;
+		if ( schema.checkChild( nextPosition, '$text' ) ) {
+			return nextPosition;
 		}
 	}
 	// Leaving an element.
 	else {
 		// If leaving a limit element, stop.
-		if ( data.schema.isLimit( value.item ) ) {
+		if ( schema.isLimit( item ) ) {
 			// NOTE: Fast-forward the walker until the end.
-			data.walker.skip( () => true );
+			walker.skip( () => true );
 
 			return;
 		}
 
 		// If text allowed on this position, extend to this place.
-		if ( data.schema.checkChild( value.nextPosition, '$text' ) ) {
-			return value.nextPosition;
+		if ( schema.checkChild( nextPosition, '$text' ) ) {
+			return nextPosition;
 		}
 	}
 }

+ 12 - 13
packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js

@@ -22,7 +22,7 @@ import Position from '../position';
  * allows a `$text`.
  * * None of the selection's non-collapsed ranges crosses a {@link module:engine/model/schema~Schema#isLimit limit element}
  * boundary (a range must be rooted within one limit element).
- * * Only {@link module:engine/model/schema~Schema#isObject object elements} can be selected from the outside
+ * * Only {@link module:engine/model/schema~Schema#isSelectable selectable elements} can be selected from the outside
  * (e.g. `[<paragraph>foo</paragraph>]` is invalid). This rule applies independently to both selection ends, so this
  * selection is correct: `<paragraph>f[oo</paragraph><image></image>]`.
  *
@@ -154,8 +154,7 @@ function tryFixingCollapsedRange( range, schema ) {
 // @param {module:engine/model/schema~Schema} schema
 // @returns {module:engine/model/range~Range|null} Returns fixed range or null if range is valid.
 function tryFixingNonCollapsedRage( range, schema ) {
-	const start = range.start;
-	const end = range.end;
+	const { start, end } = range;
 
 	const isTextAllowedOnStart = schema.checkChild( start, '$text' );
 	const isTextAllowedOnEnd = schema.checkChild( end, '$text' );
@@ -176,13 +175,13 @@ function tryFixingNonCollapsedRage( range, schema ) {
 		// - [<block>foo</block>]    ->    <block>[foo]</block>
 		// - [<block>foo]</block>    ->    <block>[foo]</block>
 		// - <block>f[oo</block>]    ->    <block>f[oo]</block>
-		// - [<block>foo</block><object></object>]    ->    <block>[foo</block><object></object>]
+		// - [<block>foo</block><selectable></selectable>]    ->    <block>[foo</block><selectable></selectable>]
 		if ( checkSelectionOnNonLimitElements( start, end, schema ) ) {
-			const isStartObject = start.nodeAfter && schema.isObject( start.nodeAfter );
-			const fixedStart = isStartObject ? null : schema.getNearestSelectionRange( start, 'forward' );
+			const isStartBeforeSelectable = start.nodeAfter && schema.isSelectable( start.nodeAfter );
+			const fixedStart = isStartBeforeSelectable ? null : schema.getNearestSelectionRange( start, 'forward' );
 
-			const isEndObject = end.nodeBefore && schema.isObject( end.nodeBefore );
-			const fixedEnd = isEndObject ? null : schema.getNearestSelectionRange( end, 'backward' );
+			const isEndAfterSelectable = end.nodeBefore && schema.isSelectable( end.nodeBefore );
+			const fixedEnd = isEndAfterSelectable ? null : schema.getNearestSelectionRange( end, 'backward' );
 
 			// The schema.getNearestSelectionRange might return null - if that happens use original position.
 			const rangeStart = fixedStart ? fixedStart.start : start;
@@ -200,8 +199,8 @@ function tryFixingNonCollapsedRage( range, schema ) {
 	if ( isStartInLimit || isEndInLimit ) {
 		const bothInSameParent = ( start.nodeAfter && end.nodeBefore ) && start.nodeAfter.parent === end.nodeBefore.parent;
 
-		const expandStart = isStartInLimit && ( !bothInSameParent || !isInObject( start.nodeAfter, schema ) );
-		const expandEnd = isEndInLimit && ( !bothInSameParent || !isInObject( end.nodeBefore, schema ) );
+		const expandStart = isStartInLimit && ( !bothInSameParent || !isSelectable( start.nodeAfter, schema ) );
+		const expandEnd = isEndInLimit && ( !bothInSameParent || !isSelectable( end.nodeBefore, schema ) );
 
 		// Although we've already found limit element on start/end positions we must find the outer-most limit element.
 		// as limit elements might be nested directly inside (ie table > tableRow > tableCell).
@@ -285,11 +284,11 @@ function mergeIntersectingRanges( ranges ) {
 	return nonIntersectingRanges;
 }
 
-// Checks if node exists and if it's an object.
+// Checks if node exists and if it's a selectable.
 //
 // @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 );
+function isSelectable( node, schema ) {
+	return node && schema.isSelectable( node );
 }

+ 15 - 14
packages/ckeditor5-engine/src/view/domconverter.js

@@ -32,20 +32,21 @@ const BR_FILLER_REF = BR_FILLER( document );
  * `DomConverter` is a set of tools to do transformations between DOM nodes and view nodes. It also handles
  * {@link module:engine/view/domconverter~DomConverter#bindElements bindings} between these nodes.
  *
- * The instance of `DOMConverter` is available under {@link module:engine/view/view~View#domConverter `editor.editing.view.domConverter`}.
+ * An instance of the DOM converter is available under
+ * {@link module:engine/view/view~View#domConverter `editor.editing.view.domConverter`}.
  *
- * `DomConverter` does not check which nodes should be rendered (use {@link module:engine/view/renderer~Renderer}), does not keep a
- * state of a tree nor keeps synchronization between tree view and DOM tree (use {@link module:engine/view/document~Document}).
+ * The DOM converter does not check which nodes should be rendered (use {@link module:engine/view/renderer~Renderer}), does not keep the
+ * state of a tree nor keeps the synchronization between the tree view and the DOM tree (use {@link module:engine/view/document~Document}).
  *
- * `DomConverter` keeps DOM elements to View element bindings, so when the converter gets destroyed, the bindings are lost.
+ * The DOM converter keeps DOM elements to view element bindings, so when the converter gets destroyed, the bindings are lost.
  * Two converters will keep separate binding maps, so one tree view can be bound with two DOM trees.
  */
 export default class DomConverter {
 	/**
-	 * Creates DOM converter.
+	 * Creates a DOM converter.
 	 *
 	 * @param {module:engine/view/document~Document} document The view document instance.
-	 * @param {Object} options Object with configuration options.
+	 * @param {Object} options An object with configuration options.
 	 * @param {module:engine/view/filler~BlockFillerMode} [options.blockFillerMode='br'] The type of the block filler to use.
 	 */
 	constructor( document, options = {} ) {
@@ -56,7 +57,7 @@ export default class DomConverter {
 		this.document = document;
 
 		/**
-		 * The mode of a block filler used by DOM converter.
+		 * The mode of a block filler used by the DOM converter.
 		 *
 		 * @readonly
 		 * @member {'br'|'nbsp'} module:engine/view/domconverter~DomConverter#blockFillerMode
@@ -86,7 +87,7 @@ export default class DomConverter {
 
 		/**
 		 * Block {@link module:engine/view/filler filler} creator, which is used to create all block fillers during the
-		 * view to DOM conversion and to recognize block fillers during the DOM to view conversion.
+		 * view-to-DOM conversion and to recognize block fillers during the DOM-to-view conversion.
 		 *
 		 * @readonly
 		 * @private
@@ -95,7 +96,7 @@ export default class DomConverter {
 		this._blockFiller = this.blockFillerMode == 'br' ? BR_FILLER : NBSP_FILLER;
 
 		/**
-		 * DOM to View mapping.
+		 * The DOM-to-view mapping.
 		 *
 		 * @private
 		 * @member {WeakMap} module:engine/view/domconverter~DomConverter#_domToViewMapping
@@ -103,7 +104,7 @@ export default class DomConverter {
 		this._domToViewMapping = new WeakMap();
 
 		/**
-		 * View to DOM mapping.
+		 * The view-to-DOM mapping.
 		 *
 		 * @private
 		 * @member {WeakMap} module:engine/view/domconverter~DomConverter#_viewToDomMapping
@@ -111,7 +112,7 @@ export default class DomConverter {
 		this._viewToDomMapping = new WeakMap();
 
 		/**
-		 * Holds mapping between fake selection containers and corresponding view selections.
+		 * Holds the mapping between fake selection containers and corresponding view selections.
 		 *
 		 * @private
 		 * @member {WeakMap} module:engine/view/domconverter~DomConverter#_fakeSelectionMapping
@@ -894,15 +895,15 @@ export default class DomConverter {
 	}
 
 	/**
-	 * Checks if given selection's boundaries are at correct places.
+	 * Checks if the given selection's boundaries are at correct places.
 	 *
 	 * The following places are considered as incorrect for selection boundaries:
 	 *
-	 * * before or in the middle of the inline filler sequence,
+	 * * before or in the middle of an inline filler sequence,
 	 * * inside a DOM element which represents {@link module:engine/view/uielement~UIElement a view UI element},
 	 * * inside a DOM element which represents {@link module:engine/view/rawelement~RawElement a view raw element}.
 	 *
-	 * @param {Selection} domSelection DOM Selection object to be checked.
+	 * @param {Selection} domSelection The DOM selection object to be checked.
 	 * @returns {Boolean} `true` if the given selection is at a correct place, `false` otherwise.
 	 */
 	isDomSelectionCorrect( domSelection ) {

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

@@ -155,7 +155,7 @@ export default class DowncastWriter {
 	}
 
 	/**
-	 * Creates new {@link module:engine/view/attributeelement~AttributeElement}.
+	 * Creates a new {@link module:engine/view/attributeelement~AttributeElement}.
 	 *
 	 *		writer.createAttributeElement( 'strong' );
 	 *		writer.createAttributeElement( 'a', { href: 'foo.bar' } );
@@ -188,7 +188,7 @@ export default class DowncastWriter {
 	}
 
 	/**
-	 * Creates new {@link module:engine/view/containerelement~ContainerElement}.
+	 * Creates a new {@link module:engine/view/containerelement~ContainerElement}.
 	 *
 	 *		writer.createContainerElement( 'p' );
 	 *
@@ -210,7 +210,7 @@ export default class DowncastWriter {
 	}
 
 	/**
-	 * Creates new {@link module:engine/view/editableelement~EditableElement}.
+	 * Creates a new {@link module:engine/view/editableelement~EditableElement}.
 	 *
 	 *		writer.createEditableElement( 'div' );
 	 *		writer.createEditableElement( 'div', { id: 'foo-1234' } );
@@ -230,7 +230,7 @@ export default class DowncastWriter {
 	}
 
 	/**
-	 * Creates new {@link module:engine/view/emptyelement~EmptyElement}.
+	 * Creates a new {@link module:engine/view/emptyelement~EmptyElement}.
 	 *
 	 *		writer.createEmptyElement( 'img' );
 	 *		writer.createEmptyElement( 'img', { id: 'foo-1234' } );
@@ -244,12 +244,12 @@ export default class DowncastWriter {
 	}
 
 	/**
-	 * Creates new {@link module:engine/view/uielement~UIElement}.
+	 * Creates a new {@link module:engine/view/uielement~UIElement}.
 	 *
 	 *		writer.createUIElement( 'span' );
 	 *		writer.createUIElement( 'span', { id: 'foo-1234' } );
 	 *
-	 * Custom render function can be provided as third parameter:
+	 * A custom render function can be provided as the third parameter:
 	 *
 	 *		writer.createUIElement( 'span', null, function( domDocument ) {
 	 *			const domElement = this.toDomElement( domDocument );
@@ -263,10 +263,10 @@ export default class DowncastWriter {
 	 *
 	 * You should not use UI elements as data containers. Check out {@link #createRawElement} instead.
 	 *
-	 * @param {String} name Name of the element.
-	 * @param {Object} [attributes] Elements attributes.
-	 * @param {Function} [renderFunction] Custom render function.
-	 * @returns {module:engine/view/uielement~UIElement} Created element.
+	 * @param {String} name The name of the element.
+	 * @param {Object} [attributes] Element attributes.
+	 * @param {Function} [renderFunction] A custom render function.
+	 * @returns {module:engine/view/uielement~UIElement} The created element.
 	 */
 	createUIElement( name, attributes, renderFunction ) {
 		const uiElement = new UIElement( this.document, name, attributes );
@@ -288,18 +288,19 @@ export default class DowncastWriter {
 	 * Raw elements work as data containers ("wrappers", "sandboxes") but their children are not managed or
 	 * even recognized by the editor. This encapsulation allows integrations to maintain custom DOM structures
 	 * in the editor content without, for instance, worrying about compatibility with other editor features.
-	 * Raw elements make a perfect tool for integration with external frameworks and data sources.
+	 * Raw elements are a perfect tool for integration with external frameworks and data sources.
 	 *
-	 * Unlike {@link #createUIElement ui elements}, raw elements act like a "real" editor content (similar to
+	 * Unlike {@link #createUIElement UI elements}, raw elements act like "real" editor content (similar to
 	 * {@link module:engine/view/containerelement~ContainerElement} or {@link module:engine/view/emptyelement~EmptyElement}),
 	 * and they are considered by the editor selection.
 	 *
-	 * You should not use raw elements to render UI in the editor content. Check out {@link #createUIElement `#createUIElement()`} instead.
+	 * You should not use raw elements to render the UI in the editor content. Check out {@link #createUIElement `#createUIElement()`}
+	 * instead.
 	 *
-	 * @param {String} name Name of the element.
-	 * @param {Object} [attributes] Elements attributes.
-	 * @param {Function} [renderFunction] Custom render function.
-	 * @returns {module:engine/view/rawelement~RawElement} Created element.
+	 * @param {String} name The name of the element.
+	 * @param {Object} [attributes] Element attributes.
+	 * @param {Function} [renderFunction] A custom render function.
+	 * @returns {module:engine/view/rawelement~RawElement} The created element.
 	 */
 	createRawElement( name, attributes, renderFunction ) {
 		const rawElement = new RawElement( this.document, name, attributes );
@@ -310,12 +311,12 @@ export default class DowncastWriter {
 	}
 
 	/**
-	 * Adds or overwrite element's attribute with a specified key and value.
+	 * Adds or overwrites the element's attribute with a specified key and value.
 	 *
 	 *		writer.setAttribute( 'href', 'http://ckeditor.com', linkElement );
 	 *
-	 * @param {String} key Attribute key.
-	 * @param {String} value Attribute value.
+	 * @param {String} key The attribute key.
+	 * @param {String} value The attribute value.
 	 * @param {module:engine/view/element~Element} element
 	 */
 	setAttribute( key, value, element ) {

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

@@ -14,41 +14,41 @@ import Node from './node';
 /**
  * The raw element class.
  *
- * Raw elements work as data containers ("wrappers", "sandboxes") but their children are not managed or
+ * The raw elements work as data containers ("wrappers", "sandboxes") but their children are not managed or
  * even recognized by the editor. This encapsulation allows integrations to maintain custom DOM structures
  * in the editor content without, for instance, worrying about compatibility with other editor features.
- * Raw elements make a perfect tool for integration with external frameworks and data sources.
+ * Raw elements are a perfect tool for integration with external frameworks and data sources.
  *
- * Unlike {@link module:engine/view/uielement~UIElement ui elements}, raw elements act like a real editor
+ * Unlike {@link module:engine/view/uielement~UIElement UI elements}, raw elements act like real editor
  * content (similar to {@link module:engine/view/containerelement~ContainerElement} or
  * {@link module:engine/view/emptyelement~EmptyElement}), they are considered by the editor selection and
  * {@link module:widget/utils~toWidget they can work as widgets}.
  *
- * To create a new raw element use the
+ * To create a new raw element, use the
  * {@link module:engine/view/downcastwriter~DowncastWriter#createRawElement `downcastWriter#createRawElement()`} method.
  *
  * @extends module:engine/view/element~Element
  */
 export default class RawElement extends Element {
 	/**
-	 * Creates new instance of RawElement.
+	 * Creates a new instance of a raw element.
 	 *
-	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-rawelement-cannot-add` when `children` parameter
-	 * is passed, to inform that usage of `RawElement` is incorrect (adding child nodes to `RawElement` is forbidden).
+	 * Throws the `view-rawelement-cannot-add` {@link module:utils/ckeditorerror~CKEditorError CKEditorError} when the `children`
+	 * parameter is passed to inform that the usage of `RawElement` is incorrect (adding child nodes to `RawElement` is forbidden).
 	 *
 	 * @see module:engine/view/downcastwriter~DowncastWriter#createRawElement
 	 * @protected
 	 * @param {module:engine/view/document~Document} document The document instance to which this element belongs.
-	 * @param {String} name Node name.
-	 * @param {Object|Iterable} [attrs] Collection of attributes.
+	 * @param {String} name A node name.
+	 * @param {Object|Iterable} [attrs] The collection of attributes.
 	 * @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children]
-	 * A list of nodes to be inserted into created element.
+	 * A list of nodes to be inserted into the created element.
 	 */
 	constructor( document, name, attrs, children ) {
 		super( document, name, attrs, children );
 
 		/**
-		 * Returns `null` because filler is not needed for RawElements.
+		 * Returns `null` because filler is not needed for raw elements.
 		 *
 		 * @method #getFillerOffset
 		 * @returns {null} Always returns null.
@@ -72,15 +72,15 @@ export default class RawElement extends Element {
 	 * Assuming that the object being checked is a raw element, you can also check its
 	 * {@link module:engine/view/rawelement~RawElement#name name}:
 	 *
-	 *		rawElement.is( 'img' ); // -> true if this is a img element
+	 *		rawElement.is( 'img' ); // -> true if this is an img element
 	 *		rawElement.is( 'rawElement', 'img' ); // -> same as above
 	 *		text.is( 'img' ); -> false
 	 *
 	 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
 	 *
-	 * @param {String} type Type to check when `name` parameter is present.
+	 * @param {String} type The type to check when the `name` parameter is present.
 	 * Otherwise, it acts like the `name` parameter.
-	 * @param {String} [name] Element name.
+	 * @param {String} [name] The element name.
 	 * @returns {Boolean}
 	 */
 	is( type, name = null ) {
@@ -99,9 +99,9 @@ export default class RawElement extends Element {
 	}
 
 	/**
-	 * Overrides {@link module:engine/view/element~Element#_insertChild} method.
-	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-rawelement-cannot-add` to prevent
-	 * adding any child nodes to a `RawElement`.
+	 * Overrides the {@link module:engine/view/element~Element#_insertChild} method.
+	 * Throws the `view-rawelement-cannot-add` {@link module:utils/ckeditorerror~CKEditorError CKEditorError} to prevent
+	 * adding any child nodes to a raw element.
 	 *
 	 * @protected
 	 */
@@ -120,11 +120,11 @@ export default class RawElement extends Element {
 	}
 
 	/**
-	 * Allows rendering the children of a {@link module:engine/view/rawelement~RawElement} on the DOM level.
+	 * This allows rendering the children of a {@link module:engine/view/rawelement~RawElement} on the DOM level.
 	 * This method is called by the {@link module:engine/view/domconverter~DomConverter} with the raw DOM element
-	 * passed as an argument leaving the number and shape of the children up to the integrator.
+	 * passed as an argument, leaving the number and shape of the children up to the integrator.
 	 *
-	 * This method **must be defined** for the `RawElement` to work:
+	 * This method **must be defined** for the raw element to work:
 	 *
 	 *		const myRawElement = downcastWriter.createRawElement( 'div' );
 	 *
@@ -137,7 +137,7 @@ export default class RawElement extends Element {
 	 */
 }
 
-// Returns `null` because block filler is not needed for RawElements.
+// Returns `null` because block filler is not needed for raw elements.
 //
 // @returns {null}
 function getFillerOffset() {

+ 126 - 2
packages/ckeditor5-engine/tests/controller/datacontroller.js

@@ -155,8 +155,8 @@ describe( 'DataController', () => {
 
 			const viewFragment = new ViewDocumentFragment( viewDocument, [ parseView( 'foo' ) ] );
 
-			// Model fragment in root.
-			expect( stringify( data.toModel( viewFragment ) ) ).to.equal( '' );
+			// Model fragment in root (note that it is auto-paragraphed because $text is not allowed directly in $root).
+			expect( stringify( data.toModel( viewFragment ) ) ).to.equal( '<paragraph>foo</paragraph>' );
 
 			// Model fragment in inline root.
 			expect( stringify( data.toModel( viewFragment, [ 'inlineRoot' ] ) ) ).to.equal( 'foo' );
@@ -454,6 +454,82 @@ describe( 'DataController', () => {
 				data.get( { rootName: 'nonexistent' } );
 			}, /datacontroller-get-non-existent-root:/ );
 		} );
+
+		it( 'should allow to provide additional options for retrieving data - insert conversion', () => {
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+
+			data.downcastDispatcher.on( 'insert:paragraph', ( evt, data, conversionApi ) => {
+				conversionApi.consumable.consume( data.item, 'insert' );
+
+				const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
+				const viewElement = conversionApi.writer.createContainerElement( 'p', {
+					attribute: conversionApi.options.attributeValue
+				} );
+
+				conversionApi.mapper.bindElements( data.item, viewElement );
+				conversionApi.writer.insert( viewPosition, viewElement );
+			}, { priority: 'high' } );
+
+			setData( model, '<paragraph>foo</paragraph>' );
+
+			expect( data.get( { attributeValue: 'foo' } ) ).to.equal( '<p attribute="foo">foo</p>' );
+			expect( data.get( { attributeValue: 'bar' } ) ).to.equal( '<p attribute="bar">foo</p>' );
+		} );
+
+		it( 'should allow to provide additional options for retrieving data - attribute conversion', () => {
+			schema.register( 'paragraph', { inheritAllFrom: '$block', allowAttributes: [ 'foo' ] } );
+			downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
+
+			data.downcastDispatcher.on( 'attribute:foo', ( evt, data, conversionApi ) => {
+				if ( data.attributeNewValue === conversionApi.options.skipAttribute ) {
+					return;
+				}
+
+				const viewRange = conversionApi.mapper.toViewRange( data.range );
+				const viewElement = conversionApi.writer.createAttributeElement( data.attributeNewValue );
+
+				conversionApi.writer.wrap( viewRange, viewElement );
+			} );
+
+			setData( model, '<paragraph>f<$text foo="a">o</$text>ob<$text foo="b">a</$text>r</paragraph>' );
+
+			expect( data.get() ).to.equal( '<p>f<a>o</a>ob<b>a</b>r</p>' );
+			expect( data.get( { skipAttribute: 'a' } ) ).to.equal( '<p>foob<b>a</b>r</p>' );
+			expect( data.get( { skipAttribute: 'b' } ) ).to.equal( '<p>f<a>o</a>obar</p>' );
+		} );
+
+		it( 'should allow to provide additional options for retrieving data - addMarker conversion', () => {
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+			downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
+
+			data.downcastDispatcher.on( 'addMarker', ( evt, data, conversionApi ) => {
+				if ( conversionApi.options.skipMarker ) {
+					return;
+				}
+
+				const viewElement = conversionApi.writer.createAttributeElement( 'marker' );
+				const viewRange = conversionApi.mapper.toViewRange( data.markerRange );
+
+				conversionApi.writer.wrap( viewRange, viewElement );
+			} );
+
+			setData( model, '<paragraph>foo</paragraph>' );
+
+			const root = model.document.getRoot();
+
+			model.change( writer => {
+				const start = writer.createPositionFromPath( root, [ 0, 1 ] );
+				const end = writer.createPositionFromPath( root, [ 0, 2 ] );
+
+				writer.addMarker( 'marker', {
+					range: writer.createRange( start, end ),
+					usingOperation: false
+				} );
+			} );
+
+			expect( data.get( { skipMarker: false } ) ).to.equal( '<p>f<marker>o</marker>o</p>' );
+			expect( data.get( { skipMarker: true } ) ).to.equal( '<p>foo</p>' );
+		} );
 	} );
 
 	describe( 'stringify()', () => {
@@ -478,6 +554,24 @@ describe( 'DataController', () => {
 
 			expect( data.stringify( modelDocumentFragment ) ).to.equal( '<p>foo</p><p>bar</p>' );
 		} );
+
+		it( 'should allow to provide additional options to the conversion process', () => {
+			const spy = sinon.spy();
+
+			data.downcastDispatcher.on( 'insert:paragraph', ( evt, data, conversionApi ) => {
+				spy( conversionApi.options );
+			}, { priority: 'high' } );
+
+			const modelDocumentFragment = parseModel( '<paragraph>foo</paragraph><paragraph>bar</paragraph>', schema );
+
+			const options = { foo: 'bar' };
+
+			data.stringify( modelDocumentFragment );
+			expect( spy.lastCall.args[ 0 ] ).to.not.equal( options );
+
+			data.stringify( modelDocumentFragment, options );
+			expect( spy.lastCall.args[ 0 ] ).to.equal( options );
+		} );
 	} );
 
 	describe( 'toView()', () => {
@@ -590,6 +684,36 @@ describe( 'DataController', () => {
 			expect( mappedViewRange.end.nodeBefore ).to.equal( firstViewElement );
 			expect( mappedViewRange.end.nodeAfter ).to.equal( viewDocumentFragment.getChild( 1 ) );
 		} );
+
+		it( 'should allow to provide additional options to the conversion process', () => {
+			const root = model.document.getRoot();
+			const spy = sinon.spy();
+
+			data.downcastDispatcher.on( 'insert:paragraph', ( evt, data, conversionApi ) => {
+				spy( conversionApi.options );
+			}, { priority: 'high' } );
+
+			data.downcastDispatcher.on( 'addMarker:marker', ( evt, data, conversionApi ) => {
+				spy( conversionApi.options );
+			}, { priority: 'high' } );
+
+			setData( model, '<paragraph>foo</paragraph>' );
+
+			model.change( writer => {
+				writer.addMarker( 'marker', {
+					range: model.createRange( model.createPositionFromPath( root, [ 0, 1 ] ) ),
+					usingOperation: false
+				} );
+			} );
+
+			const options = { foo: 'bar' };
+
+			data.toView( root, options );
+
+			sinon.assert.calledTwice( spy );
+			expect( spy.firstCall.args[ 0 ] ).to.equal( options );
+			expect( spy.lastCall.args[ 0 ] ).to.equal( options );
+		} );
 	} );
 
 	describe( 'destroy()', () => {

+ 40 - 64
packages/ckeditor5-engine/tests/conversion/downcasthelpers.js

@@ -104,7 +104,7 @@ describe( 'DowncastHelpers', () => {
 		it( 'config.view is a function', () => {
 			downcastHelpers.elementToElement( {
 				model: 'heading',
-				view: ( modelElement, viewWriter ) => viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
+				view: ( modelElement, { writer } ) => writer.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
 			} );
 
 			model.change( writer => {
@@ -225,8 +225,8 @@ describe( 'DowncastHelpers', () => {
 		it( 'config.view is a function', () => {
 			downcastHelpers.attributeToElement( {
 				model: 'bold',
-				view: ( modelAttributeValue, viewWriter ) => {
-					return viewWriter.createAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } );
+				view: ( modelAttributeValue, { writer } ) => {
+					return writer.createAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } );
 				}
 			} );
 
@@ -243,15 +243,15 @@ describe( 'DowncastHelpers', () => {
 					key: 'color',
 					name: '$text'
 				},
-				view: ( modelAttributeValue, viewWriter ) => {
-					return viewWriter.createAttributeElement( 'span', { style: 'color:' + modelAttributeValue } );
+				view: ( modelAttributeValue, { writer } ) => {
+					return writer.createAttributeElement( 'span', { style: 'color:' + modelAttributeValue } );
 				}
 			} );
 
 			downcastHelpers.elementToElement( {
 				model: 'smiley',
-				view: ( modelElement, viewWriter ) => {
-					return viewWriter.createEmptyElement( 'img', {
+				view: ( modelElement, { writer } ) => {
+					return writer.createEmptyElement( 'img', {
 						src: 'smile.jpg',
 						class: 'smiley'
 					} );
@@ -283,7 +283,7 @@ describe( 'DowncastHelpers', () => {
 
 			downcastHelpers.attributeToElement( {
 				model: 'bold',
-				view: ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' )
+				view: ( modelAttributeValue, { writer } ) => writer.createAttributeElement( 'b' )
 			} );
 
 			model.change( writer => {
@@ -304,9 +304,9 @@ describe( 'DowncastHelpers', () => {
 
 			downcastHelpers.attributeToElement( {
 				model: 'style',
-				view: ( modelAttributeValue, viewWriter ) => {
+				view: ( modelAttributeValue, { writer } ) => {
 					if ( modelAttributeValue == 'bold' ) {
-						return viewWriter.createAttributeElement( 'b' );
+						return writer.createAttributeElement( 'b' );
 					}
 				}
 			} );
@@ -333,8 +333,8 @@ describe( 'DowncastHelpers', () => {
 
 			downcastHelpers.attributeToElement( {
 				model: 'link',
-				view: ( modelAttributeValue, viewWriter ) => {
-					return viewWriter.createAttributeElement( 'a', { href: modelAttributeValue } );
+				view: ( modelAttributeValue, { writer } ) => {
+					return writer.createAttributeElement( 'a', { href: modelAttributeValue } );
 				}
 			} );
 
@@ -357,7 +357,7 @@ describe( 'DowncastHelpers', () => {
 
 			downcastHelpers.attributeToElement( {
 				model: 'bold',
-				view: ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' )
+				view: ( modelAttributeValue, { writer } ) => writer.createAttributeElement( 'b' )
 			} );
 
 			model.change( writer => {
@@ -378,11 +378,11 @@ describe( 'DowncastHelpers', () => {
 
 			downcastHelpers.attributeToElement( {
 				model: 'bold',
-				view: ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' )
+				view: ( modelAttributeValue, { writer } ) => writer.createAttributeElement( 'b' )
 			} );
 			downcastHelpers.attributeToElement( {
 				model: 'bold',
-				view: ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'strong' ),
+				view: ( modelAttributeValue, { writer } ) => writer.createAttributeElement( 'strong' ),
 				converterPriority: 'high'
 			} );
 
@@ -423,7 +423,7 @@ describe( 'DowncastHelpers', () => {
 			downcastHelpers.elementToElement( { model: 'image', view: 'img' } );
 			downcastHelpers.elementToElement( {
 				model: 'paragraph',
-				view: ( modelItem, viewWriter ) => viewWriter.createContainerElement( 'p' )
+				view: ( modelItem, { writer } ) => writer.createContainerElement( 'p' )
 			} );
 
 			downcastHelpers.attributeToAttribute( {
@@ -605,7 +605,12 @@ describe( 'DowncastHelpers', () => {
 		it( 'config.view is a function', () => {
 			downcastHelpers.attributeToAttribute( {
 				model: 'styled',
-				view: attributeValue => ( { key: 'class', value: 'styled-' + attributeValue } )
+				view: ( attributeValue, conversionApi ) => {
+					// To ensure conversion API is provided.
+					expect( conversionApi.writer ).to.instanceof( DowncastWriter );
+
+					return { key: 'class', value: 'styled-' + attributeValue };
+				}
 			} );
 
 			model.change( writer => {
@@ -677,41 +682,6 @@ describe( 'DowncastHelpers', () => {
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
 		} );
 
-		it( 'should convert insert/change/remove with attribute generating function as a parameter', () => {
-			downcastHelpers.elementToElement( { model: 'div', view: 'div' } );
-			downcastHelpers.attributeToAttribute( {
-				model: 'theme',
-				view: ( value, data ) => {
-					if ( data.item instanceof ModelElement && data.item.childCount > 0 ) {
-						value += ' fix-content';
-					}
-
-					return { key: 'class', value };
-				}
-			} );
-
-			const modelParagraph = new ModelElement( 'paragraph', { theme: 'nice' }, new ModelText( 'foobar' ) );
-			const modelDiv = new ModelElement( 'div', { theme: 'nice' } );
-
-			model.change( writer => {
-				writer.insert( [ modelParagraph, modelDiv ], modelRootStart );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><p class="nice fix-content">foobar</p><div class="nice"></div></div>' );
-
-			model.change( writer => {
-				writer.setAttribute( 'theme', 'awesome', modelParagraph );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><p class="awesome fix-content">foobar</p><div class="nice"></div></div>' );
-
-			model.change( writer => {
-				writer.removeAttribute( 'theme', modelParagraph );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p><div class="nice"></div></div>' );
-		} );
-
 		it( 'should be possible to override setAttribute', () => {
 			downcastHelpers.attributeToAttribute( {
 				model: 'class',
@@ -807,8 +777,8 @@ describe( 'DowncastHelpers', () => {
 		it( 'config.view is a function', () => {
 			downcastHelpers.markerToElement( {
 				model: 'search',
-				view: ( data, viewWriter ) => {
-					return viewWriter.createUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } );
+				view: ( data, { writer } ) => {
+					return writer.createUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } );
 				}
 			} );
 
@@ -838,7 +808,7 @@ describe( 'DowncastHelpers', () => {
 			it( 'should insert and remove ui element', () => {
 				downcastHelpers.markerToElement( {
 					model: 'marker',
-					view: ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } )
+					view: ( data, { writer } ) => writer.createUIElement( 'span', { 'class': 'marker' } )
 				} );
 
 				model.change( writer => {
@@ -859,7 +829,7 @@ describe( 'DowncastHelpers', () => {
 
 				downcastHelpers.markerToElement( {
 					model: 'marker',
-					view: ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } )
+					view: ( data, { writer } ) => writer.createUIElement( 'span', { 'class': 'marker' } )
 				} );
 
 				controller.downcastDispatcher.on( 'addMarker:marker', ( evt, data, conversionApi ) => {
@@ -911,7 +881,7 @@ describe( 'DowncastHelpers', () => {
 			it( 'should insert and remove ui element - element as a creator', () => {
 				downcastHelpers.markerToElement( {
 					model: 'marker',
-					view: ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } )
+					view: ( data, { writer } ) => writer.createUIElement( 'span', { 'class': 'marker' } )
 				} );
 
 				model.change( writer => {
@@ -931,7 +901,7 @@ describe( 'DowncastHelpers', () => {
 			it( 'should insert and remove ui element - function as a creator', () => {
 				downcastHelpers.markerToElement( {
 					model: 'marker',
-					view: ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': data.markerName } )
+					view: ( data, { writer } ) => writer.createUIElement( 'span', { 'class': data.markerName } )
 				} );
 
 				model.change( writer => {
@@ -951,12 +921,12 @@ describe( 'DowncastHelpers', () => {
 			it( 'should insert and remove different opening and ending element', () => {
 				downcastHelpers.markerToElement( {
 					model: 'marker',
-					view: ( data, viewWriter ) => {
+					view: ( data, { writer } ) => {
 						if ( data.isOpening ) {
-							return viewWriter.createUIElement( 'span', { 'class': data.markerName, 'data-start': true } );
+							return writer.createUIElement( 'span', { 'class': data.markerName, 'data-start': true } );
 						}
 
-						return viewWriter.createUIElement( 'span', { 'class': data.markerName, 'data-end': true } );
+						return writer.createUIElement( 'span', { 'class': data.markerName, 'data-end': true } );
 					}
 				} );
 
@@ -980,7 +950,7 @@ describe( 'DowncastHelpers', () => {
 
 				downcastHelpers.markerToElement( {
 					model: 'marker',
-					view: ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } )
+					view: ( data, { writer } ) => writer.createUIElement( 'span', { 'class': 'marker' } )
 				} );
 				controller.downcastDispatcher.on( 'addMarker:marker', ( evt, data, conversionApi ) => {
 					conversionApi.consumable.consume( data.item, 'addMarker:marker' );
@@ -1316,9 +1286,12 @@ describe( 'DowncastHelpers', () => {
 
 			downcastHelpers.markerToData( {
 				model: 'group',
-				view: markerName => {
+				view: ( markerName, conversionApi ) => {
 					const namePart = markerName.split( ':' )[ 1 ];
 
+					// To ensure conversion API is provided.
+					expect( conversionApi.writer ).to.instanceof( DowncastWriter );
+
 					return {
 						group: 'g',
 						name: namePart + '_' + customData[ namePart ]
@@ -1510,9 +1483,12 @@ describe( 'DowncastHelpers', () => {
 		it( 'config.view is a function', () => {
 			downcastHelpers.markerToHighlight( {
 				model: 'comment',
-				view: data => {
+				view: ( data, conversionApi ) => {
 					const commentType = data.markerName.split( ':' )[ 1 ];
 
+					// To ensure conversion API is provided.
+					expect( conversionApi.writer ).to.instanceof( DowncastWriter );
+
 					return {
 						classes: [ 'comment', 'comment-' + commentType ]
 					};

+ 279 - 5
packages/ckeditor5-engine/tests/conversion/upcastdispatcher.js

@@ -275,7 +275,7 @@ describe( 'UpcastDispatcher', () => {
 				const modelElement = writer.createElement( data.viewItem.name );
 				writer.insert( modelElement, data.modelCursor );
 
-				const result = conversionApi.convertChildren( data.viewItem, writer.createPositionAt( modelElement, 0 ) );
+				const result = conversionApi.convertChildren( data.viewItem, modelElement );
 
 				data.modelRange = writer.createRange(
 					writer.createPositionBefore( modelElement ),
@@ -485,12 +485,39 @@ describe( 'UpcastDispatcher', () => {
 		} );
 
 		describe( 'convertChildren()', () => {
-			it( 'should fire conversion for all children of passed element and return conversion results ' +
-				'wrapped in document fragment', () => {
+			it( 'should fire conversion for all children of passed view element and return conversion results ' +
+				'wrapped in document fragment (using modelCursor)', () => {
 				dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
 					spy();
 
-					const result = conversionApi.convertChildren( data.viewItem, ModelPosition._createAt( rootMock, 0 ) );
+					const modelCursor = ModelPosition._createAt( rootMock, 0 );
+					const result = conversionApi.convertChildren( data.viewItem, modelCursor );
+
+					expect( result.modelRange ).to.be.instanceof( ModelRange );
+					expect( result.modelRange.start.path ).to.deep.equal( [ 0 ] );
+					expect( result.modelRange.end.path ).to.deep.equal( [ 7 ] );
+					expect( Array.from( result.modelRange.getItems() ) ).to.length( 2 );
+					expect( Array.from( result.modelRange.getItems() )[ 0 ] ).to.equal( modelP );
+					expect( Array.from( result.modelRange.getItems() )[ 1 ] ).to.instanceof( ModelTextProxy );
+					expect( Array.from( result.modelRange.getItems() )[ 1 ].data ).to.equal( 'foobar' );
+
+					expect( result.modelCursor ).instanceof( ModelPosition );
+					expect( result.modelCursor.path ).to.deep.equal( [ 7 ] );
+				} );
+
+				model.change( writer => dispatcher.convert( new ViewDocumentFragment( viewDocument, [ viewP, viewText ] ), writer ) );
+
+				expect( spy.calledOnce ).to.be.true;
+				expect( spyP.calledOnce ).to.be.true;
+				expect( spyText.calledOnce ).to.be.true;
+			} );
+
+			it( 'should fire conversion for all children of passed view element and return conversion results ' +
+				'wrapped in document fragment (using model element)', () => {
+				dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
+					spy();
+
+					const result = conversionApi.convertChildren( data.viewItem, rootMock );
 
 					expect( result.modelRange ).to.be.instanceof( ModelRange );
 					expect( result.modelRange.start.path ).to.deep.equal( [ 0 ] );
@@ -578,6 +605,43 @@ describe( 'UpcastDispatcher', () => {
 				sinon.assert.calledOnce( spy );
 			} );
 
+			it(
+				'should auto-paragraph an element if it is not allowed at the insertion position' +
+				'but would be inserted if auto-paragraphed',
+				() => {
+					const spy = sinon.spy();
+
+					model.schema.register( 'section', {
+						allowIn: '$root'
+					} );
+					model.schema.register( 'span', {
+						allowIn: 'paragraph'
+					} );
+					model.schema.extend( 'paragraph', {
+						allowIn: 'section'
+					} );
+
+					dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
+						const section = conversionApi.writer.createElement( 'section' );
+						const position = ModelPosition._createAt( section, 0 );
+
+						const span = conversionApi.writer.createElement( 'span' );
+
+						const result = conversionApi.splitToAllowedParent( span, position );
+
+						expect( section.getChild( 0 ).name ).to.equal( 'paragraph' );
+						expect( result ).to.deep.equal( {
+							position: ModelPosition._createAt( section.getChild( 0 ), 0 )
+						} );
+
+						spy();
+					} );
+
+					model.change( writer => dispatcher.convert( new ViewDocumentFragment( viewDocument ), writer ) );
+					sinon.assert.calledOnce( spy );
+				}
+			);
+
 			it( 'should return null if element is not allowed in position and any of ancestors', () => {
 				const spy = sinon.spy();
 
@@ -680,7 +744,7 @@ describe( 'UpcastDispatcher', () => {
 					const modelElement = conversionApi.writer.createElement( 'paragraph' );
 
 					conversionApi.writer.insert( modelElement, data.modelCursor );
-					conversionApi.convertChildren( data.viewItem, conversionApi.writer.createPositionAt( modelElement, 0 ) );
+					conversionApi.convertChildren( data.viewItem, modelElement );
 
 					const parts = conversionApi.getSplitParts( modelElement );
 
@@ -711,5 +775,215 @@ describe( 'UpcastDispatcher', () => {
 				expect( spy.called ).to.be.true;
 			} );
 		} );
+
+		describe( 'safeInsert()', () => {
+			beforeEach( () => {
+				model.schema.register( 'paragraph', {
+					allowIn: '$root'
+				} );
+			} );
+
+			it( 'should return true when element was inserted on given position', done => {
+				model.schema.register( 'span', {
+					allowIn: 'paragraph'
+				} );
+
+				dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
+					const span = conversionApi.writer.createElement( 'span' );
+					const position = conversionApi.writer.createPositionAt( modelP, 0 );
+
+					const wasInserted = conversionApi.safeInsert( span, position );
+
+					expect( wasInserted ).to.be.true;
+
+					expect( rootMock.getNodeByPath( [ 0, 0 ] ) ).to.equal( span );
+
+					done();
+				} );
+
+				model.change( writer => dispatcher.convert( new ViewDocumentFragment( viewDocument ), writer ) );
+			} );
+
+			it( 'should return true on split to allowed ancestor if element is allowed in one of the ancestors', done => {
+				model.schema.register( 'section', {
+					allowIn: '$root'
+				} );
+				model.schema.register( 'span', {
+					allowIn: 'paragraph'
+				} );
+				model.schema.extend( 'paragraph', {
+					allowIn: 'section'
+				} );
+
+				// Insert "section > paragraph > span".
+				model.change( writer => {
+					const section = writer.createElement( 'section' );
+					const paragraph = writer.createElement( 'paragraph' );
+					const span = writer.createElement( 'span' );
+
+					writer.insert( section, writer.createPositionAt( rootMock, 0 ) );
+					writer.insert( paragraph, writer.createPositionAt( section, 0 ) );
+					writer.insert( span, writer.createPositionAt( paragraph, 0 ) );
+				} );
+
+				dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
+					// Insert "paragraph" in "section > paragraph > span".
+					const span = rootMock.getNodeByPath( [ 0, 0, 0 ] );
+					const position = conversionApi.writer.createPositionAt( span, 0 );
+
+					const paragraph2 = conversionApi.writer.createElement( 'paragraph' );
+					const wasInserted = conversionApi.safeInsert( paragraph2, position );
+
+					expect( wasInserted ).to.be.true;
+
+					const section = rootMock.getNodeByPath( [ 0 ] );
+
+					// The "paragraph" should be split to 2 and 1 inserted paragraph.
+					expect( section.childCount ).to.equal( 3 );
+					expect( section.getChild( 1 ) ).to.equal( paragraph2 );
+					done();
+				} );
+
+				model.change( writer => dispatcher.convert( new ViewDocumentFragment( viewDocument ), writer ) );
+			} );
+
+			it( 'should return false if element is not allowed in position and any of ancestors', done => {
+				model.schema.register( 'span' );
+
+				dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
+					const paragraph = conversionApi.writer.createElement( 'paragraph' );
+					const span = conversionApi.writer.createElement( 'span' );
+					const position = conversionApi.writer.createPositionAt( paragraph, 0 );
+
+					const wasInserted = conversionApi.safeInsert( span, position );
+
+					expect( wasInserted ).to.be.false;
+					done();
+				} );
+
+				model.change( writer => dispatcher.convert( new ViewDocumentFragment( viewDocument ), writer ) );
+			} );
+
+			it( 'should return false if element is not allowed in position and any of ancestors but is allowed in context tree', done => {
+				model.schema.register( 'div', {
+					allowIn: '$root'
+				} );
+
+				dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
+					const code = conversionApi.writer.createElement( 'div' );
+					const wasInserted = conversionApi.safeInsert( code, data.modelCursor );
+
+					expect( wasInserted ).to.be.false;
+					done();
+				} );
+
+				model.change( writer => dispatcher.convert( new ViewDocumentFragment( viewDocument ), writer, [ '$root', 'paragraph' ] ) );
+			} );
+		} );
+
+		describe( 'updateConversionResult()', () => {
+			beforeEach( () => {
+				model.schema.register( 'paragraph', {
+					allowIn: '$root'
+				} );
+			} );
+
+			it( 'should update the modelCursor and modelRange in data when element was inserted on given position', done => {
+				model.schema.register( 'span', {
+					allowIn: 'paragraph'
+				} );
+
+				dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
+					const span = conversionApi.writer.createElement( 'span' );
+					const position = conversionApi.writer.createPositionAt( modelP, 0 );
+
+					conversionApi.safeInsert( span, position );
+
+					conversionApi.updateConversionResult( span, data );
+
+					const expectedPosition = conversionApi.writer.createPositionAfter( span );
+					expect( data.modelCursor.isEqual( expectedPosition ) ).to.be.true;
+
+					const expectedRange = conversionApi.writer.createRangeOn( span );
+					expect( data.modelRange.isEqual( expectedRange ) ).to.be.true;
+
+					done();
+				} );
+
+				model.change( writer => dispatcher.convert( new ViewDocumentFragment( viewDocument ), writer ) );
+			} );
+
+			it( 'should update the modelCursor and modelRange in data when split to allowed ancestor occurred', done => {
+				model.schema.register( 'section', {
+					allowIn: '$root'
+				} );
+				model.schema.register( 'span', {
+					allowIn: 'paragraph'
+				} );
+				model.schema.extend( 'paragraph', {
+					allowIn: 'section'
+				} );
+
+				// Insert "section > paragraph > span".
+				model.change( writer => {
+					const section = writer.createElement( 'section' );
+					const paragraph = writer.createElement( 'paragraph' );
+					const span = writer.createElement( 'span' );
+
+					writer.insert( section, writer.createPositionAt( rootMock, 0 ) );
+					writer.insert( paragraph, writer.createPositionAt( section, 0 ) );
+					writer.insert( span, writer.createPositionAt( paragraph, 0 ) );
+				} );
+
+				dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
+					// Insert "paragraph" in "section > paragraph > span".
+					const span = rootMock.getNodeByPath( [ 0, 0, 0 ] );
+					const position = conversionApi.writer.createPositionAt( span, 0 );
+
+					const paragraph2 = conversionApi.writer.createElement( 'paragraph' );
+					conversionApi.safeInsert( paragraph2, position );
+
+					conversionApi.updateConversionResult( paragraph2, data );
+
+					const expectedPosition = conversionApi.writer.createPositionAt( rootMock.getNodeByPath( [ 0, 2, 0 ] ), 0 );
+					expect( data.modelCursor.isEqual( expectedPosition ) ).to.be.true;
+
+					const expectedRange = conversionApi.writer.createRangeOn( paragraph2 );
+					expect( data.modelRange.isEqual( expectedRange ) ).to.be.true;
+
+					done();
+				} );
+
+				model.change( writer => dispatcher.convert( new ViewDocumentFragment( viewDocument ), writer ) );
+			} );
+
+			it( 'should not update the modelRange if it was already set on data (complex converter case - e.g. list)', done => {
+				model.schema.register( 'span', {
+					allowIn: 'paragraph'
+				} );
+
+				dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
+					const span = conversionApi.writer.createElement( 'span' );
+					const position = conversionApi.writer.createPositionAt( modelP, 0 );
+
+					conversionApi.safeInsert( span, position );
+
+					const start = conversionApi.writer.createPositionAt( span, 0 );
+					const end = conversionApi.writer.createPositionAt( span, 1 );
+					data.modelRange = conversionApi.writer.createRange( start, end );
+					conversionApi.updateConversionResult( span, data );
+
+					const expectedRange = conversionApi.writer.createRange( start, end );
+					expect( data.modelRange.isEqual( expectedRange ) ).to.be.true;
+
+					// Model cursor will be equal to range end - no split occurred.
+					expect( data.modelCursor.isEqual( end ) ).to.be.true;
+
+					done();
+				} );
+
+				model.change( writer => dispatcher.convert( new ViewDocumentFragment( viewDocument ), writer ) );
+			} );
+		} );
 	} );
 } );

+ 54 - 9
packages/ckeditor5-engine/tests/conversion/upcasthelpers.js

@@ -29,6 +29,7 @@ import Mapper from '../../src/conversion/mapper';
 import ViewSelection from '../../src/view/selection';
 import ViewRange from '../../src/view/range';
 import { StylesProcessor } from '../../src/view/stylesmap';
+import Writer from '../../src/model/writer';
 
 /* globals console */
 
@@ -108,8 +109,8 @@ describe( 'UpcastHelpers', () => {
 					name: 'p',
 					classes: 'heading'
 				},
-				model: ( viewElement, modelWriter ) => {
-					return modelWriter.createElement( 'heading', { level: viewElement.getAttribute( 'data-level' ) } );
+				model: ( viewElement, { writer } ) => {
+					return writer.createElement( 'heading', { level: viewElement.getAttribute( 'data-level' ) } );
 				}
 			} );
 
@@ -122,7 +123,8 @@ describe( 'UpcastHelpers', () => {
 
 		it( 'config.view is not set - should fire conversion for every element', () => {
 			upcastHelpers.elementToElement( {
-				model: 'paragraph'
+				model: 'paragraph',
+				view: /.+/
 			} );
 
 			expectResult( new ViewContainerElement( viewDocument, 'p' ), '<paragraph></paragraph>' );
@@ -249,10 +251,13 @@ describe( 'UpcastHelpers', () => {
 				},
 				model: {
 					key: 'fontSize',
-					value: viewElement => {
+					value: ( viewElement, conversionApi ) => {
 						const fontSize = viewElement.getStyle( 'font-size' );
 						const value = fontSize.substr( 0, fontSize.length - 2 );
 
+						// To ensure conversion API is provided.
+						expect( conversionApi.writer ).to.instanceof( Writer );
+
 						if ( value <= 10 ) {
 							return 'small';
 						} else if ( value > 12 ) {
@@ -503,10 +508,13 @@ describe( 'UpcastHelpers', () => {
 				},
 				model: {
 					key: 'styled',
-					value: viewElement => {
+					value: ( viewElement, conversionApi ) => {
 						const regexp = /styled-([\S]+)/;
 						const match = viewElement.getAttribute( 'class' ).match( regexp );
 
+						// To ensure conversion API is provided.
+						expect( conversionApi.writer ).to.instanceof( Writer );
+
 						return match[ 1 ];
 					}
 				}
@@ -659,7 +667,12 @@ describe( 'UpcastHelpers', () => {
 		it( 'config.model is a function', () => {
 			upcastHelpers.elementToMarker( {
 				view: 'comment',
-				model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' )
+				model: ( viewElement, conversionApi ) => {
+					// To ensure conversion API is provided.
+					expect( conversionApi.writer ).to.instanceof( Writer );
+
+					return 'comment:' + viewElement.getAttribute( 'data-comment-id' );
+				}
 			} );
 
 			const frag = new ViewDocumentFragment( viewDocument, [
@@ -836,7 +849,14 @@ describe( 'UpcastHelpers', () => {
 		} );
 
 		it( 'conversion callback, mixed, multiple markers, name', () => {
-			upcastHelpers.dataToMarker( { view: 'g', model: name => 'group:' + name.split( '_' )[ 0 ] } );
+			upcastHelpers.dataToMarker( {
+				view: 'g',
+				model: ( name, conversionApi ) => {
+					// To ensure conversion API is provided.
+					expect( conversionApi.writer ).to.instanceof( Writer );
+
+					return 'group:' + name.split( '_' )[ 0 ];
+				} } );
 
 			expectResult(
 				viewParse(
@@ -948,7 +968,7 @@ describe( 'upcast-converters', () => {
 
 		it( 'should not convert text if it is wrong with schema', () => {
 			schema.addChildCheck( ( ctx, childDef ) => {
-				if ( childDef.name == '$text' && ctx.endsWith( '$root' ) ) {
+				if ( ( childDef.name == '$text' || childDef.name == 'paragraph' ) && ctx.endsWith( '$root' ) ) {
 					return false;
 				}
 			} );
@@ -968,6 +988,31 @@ describe( 'upcast-converters', () => {
 			expect( conversionResult.getChild( 0 ).data ).to.equal( 'foobar' );
 		} );
 
+		it( 'should auto-paragraph a text if it is not allowed at the insertion position but would be inserted if auto-paragraphed', () => {
+			schema.addChildCheck( ( ctx, childDef ) => {
+				if ( childDef.name == '$text' && ctx.endsWith( '$root' ) ) {
+					return false;
+				}
+			} );
+
+			const viewText = new ViewText( viewDocument, 'foobar' );
+			dispatcher.on( 'text', convertText() );
+			let conversionResult = model.change( writer => dispatcher.convert( viewText, writer, context ) );
+
+			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
+			expect( conversionResult.childCount ).to.equal( 1 );
+			expect( conversionResult.getChild( 0 ).name ).to.equal( 'paragraph' );
+			expect( conversionResult.getNodeByPath( [ 0, 0 ] ) ).to.be.instanceof( ModelText );
+			expect( conversionResult.getNodeByPath( [ 0, 0 ] ).data ).to.equal( 'foobar' );
+
+			conversionResult = model.change( writer => dispatcher.convert( viewText, writer, [ '$block' ] ) );
+
+			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
+			expect( conversionResult.childCount ).to.equal( 1 );
+			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
+			expect( conversionResult.getChild( 0 ).data ).to.equal( 'foobar' );
+		} );
+
 		it( 'should support unicode', () => {
 			const viewText = new ViewText( viewDocument, 'நிலைக்கு' );
 
@@ -1014,7 +1059,7 @@ describe( 'upcast-converters', () => {
 					const paragraph = conversionApi.writer.createElement( 'paragraph' );
 
 					conversionApi.writer.insert( paragraph, data.modelCursor );
-					conversionApi.convertChildren( data.viewItem, ModelPosition._createAt( paragraph, 0 ) );
+					conversionApi.convertChildren( data.viewItem, paragraph );
 
 					data.modelRange = ModelRange._createOn( paragraph );
 					data.modelCursor = data.modelRange.end;

+ 26 - 0
packages/ckeditor5-engine/tests/model/documentselection.js

@@ -1218,6 +1218,32 @@ describe( 'DocumentSelection', () => {
 				expect( emptyP.getAttribute( fooStoreAttrKey ) ).to.equal( 'bar' );
 			} );
 		} );
+
+		// #7459
+		describe( 'ignores inline elements while reading surrounding attributes', () => {
+			beforeEach( () => {
+				model.schema.register( 'softBreak', {
+					allowWhere: '$text',
+					isInline: true
+				} );
+			} );
+
+			it( 'should not inherit attributes from a node before an inline element', () => {
+				setData( model, '<p><$text bold="true">Foo Bar.</$text><softBreak></softBreak>[]</p>' );
+
+				expect( selection.hasAttribute( 'bold' ) ).to.equal( false );
+			} );
+
+			it( 'should not inherit attributes from a node after an inline element (override gravity)', () => {
+				setData( model, '<p>[]<softBreak></softBreak><$text bold="true">Foo Bar.</$text></p>' );
+
+				const overrideGravityUid = selection._overrideGravity();
+
+				expect( selection.hasAttribute( 'bold' ) ).to.equal( false );
+
+				selection._restoreGravity( overrideGravityUid );
+			} );
+		} );
 	} );
 
 	describe( '_overrideGravity()', () => {

+ 25 - 3
packages/ckeditor5-engine/tests/model/model.js

@@ -43,6 +43,7 @@ describe( 'Model', () => {
 
 		it( 'registers $text to the schema', () => {
 			expect( schema.isRegistered( '$text' ) ).to.be.true;
+			expect( schema.isContent( '$text' ) ).to.be.true;
 			expect( schema.checkChild( [ '$block' ], '$text' ) ).to.be.true;
 		} );
 
@@ -535,6 +536,10 @@ describe( 'Model', () => {
 			schema.register( 'image', {
 				isObject: true
 			} );
+			schema.register( 'content', {
+				inheritAllFrom: '$block',
+				isContent: true
+			} );
 			schema.extend( 'image', { allowIn: 'div' } );
 			schema.register( 'listItem', {
 				inheritAllFrom: '$block'
@@ -544,15 +549,19 @@ describe( 'Model', () => {
 				model,
 
 				'<div>' +
-				'<paragraph></paragraph>' +
+					'<paragraph></paragraph>' +
 				'</div>' +
 				'<paragraph>foo</paragraph>' +
 				'<div>' +
-				'<image></image>' +
+					'<image></image>' +
 				'</div>' +
 				'<listItem></listItem>' +
 				'<listItem></listItem>' +
-				'<listItem></listItem>'
+				'<listItem></listItem>' +
+				'<content>foo</content>' +
+				'<div>' +
+					'<content></content>' +
+				'</div>'
 			);
 
 			root = model.document.getRoot();
@@ -761,6 +770,19 @@ describe( 'Model', () => {
 			expect( model.hasContent( pEmpty, { ignoreMarkers: true } ) ).to.be.true;
 			expect( model.hasContent( pEmpty, { ignoreMarkers: true, ignoreWhitespaces: true } ) ).to.be.false;
 		} );
+
+		it( 'should return true for an item registered as a content (isContent=true, isObject=false) in the schema', () => {
+			const contentElement = root.getChild( 6 );
+
+			expect( model.hasContent( contentElement ) ).to.be.true;
+		} );
+
+		it( 'should return true if a range contains an item registered as a content (isContent=true, isObject=false) in the schema', () => {
+			// [<div><content></content></div>]
+			const range = new ModelRange( ModelPosition._createAt( root, 6 ), ModelPosition._createAt( root, 7 ) );
+
+			expect( model.hasContent( range ) ).to.be.true;
+		} );
 	} );
 
 	describe( 'createPositionFromPath()', () => {

+ 113 - 0
packages/ckeditor5-engine/tests/model/schema.js

@@ -401,6 +401,16 @@ describe( 'Schema', () => {
 			expect( schema.isObject( 'foo' ) ).to.be.true;
 		} );
 
+		it( 'returns true if an item is a limit, selectable, and a content at once (but not explicitely an object)', () => {
+			schema.register( 'foo', {
+				isLimit: true,
+				isSelectable: true,
+				isContent: true
+			} );
+
+			expect( schema.isObject( 'foo' ) ).to.be.true;
+		} );
+
 		it( 'returns false if an item was registered as a limit (because not all limits are objects)', () => {
 			schema.register( 'foo', {
 				isLimit: true
@@ -409,6 +419,39 @@ describe( 'Schema', () => {
 			expect( schema.isObject( 'foo' ) ).to.be.false;
 		} );
 
+		it( 'returns false if an item is a limit and a selectable but not a content ' +
+			'(because an object must always find its way into data regardless of its children)',
+		() => {
+			schema.register( 'foo', {
+				isLimit: true,
+				isSelectable: true
+			} );
+
+			expect( schema.isObject( 'foo' ) ).to.be.false;
+		} );
+
+		it( 'returns false if an item is a limit and content but not a selectable ' +
+			'(because the user must always be able to select an object)',
+		() => {
+			schema.register( 'foo', {
+				isLimit: true,
+				isContent: true
+			} );
+
+			expect( schema.isObject( 'foo' ) ).to.be.false;
+		} );
+
+		it( 'returns false if an item is a selectable and a content but not a limit ' +
+			'(because an object should never be split or crossed by the selection)',
+		() => {
+			schema.register( 'foo', {
+				isSelectable: true,
+				isContent: true
+			} );
+
+			expect( schema.isObject( 'foo' ) ).to.be.false;
+		} );
+
 		it( 'returns false if an item was not registered as an object', () => {
 			schema.register( 'foo' );
 
@@ -462,6 +505,76 @@ describe( 'Schema', () => {
 		} );
 	} );
 
+	describe( 'isSelectable()', () => {
+		it( 'should return true if an item was registered as a selectable', () => {
+			schema.register( 'foo', {
+				isSelectable: true
+			} );
+
+			expect( schema.isSelectable( 'foo' ) ).to.be.true;
+		} );
+
+		it( 'should return true if an item was registered as an object (because all objects are selectables)', () => {
+			schema.register( 'foo', {
+				isObject: true
+			} );
+
+			expect( schema.isSelectable( 'foo' ) ).to.be.true;
+		} );
+
+		it( 'should return false if an item was not registered as an object or selectable', () => {
+			schema.register( 'foo' );
+
+			expect( schema.isSelectable( 'foo' ) ).to.be.false;
+		} );
+
+		it( 'should return false if an item was not registered at all', () => {
+			expect( schema.isSelectable( 'foo' ) ).to.be.false;
+		} );
+
+		it( 'uses getDefinition()\'s item to definition normalization', () => {
+			const stub = sinon.stub( schema, 'getDefinition' ).returns( { isSelectable: true } );
+
+			expect( schema.isSelectable( 'foo' ) ).to.be.true;
+			expect( stub.calledOnce ).to.be.true;
+		} );
+	} );
+
+	describe( 'isContent()', () => {
+		it( 'should return true if an item was registered as a content', () => {
+			schema.register( 'foo', {
+				isContent: true
+			} );
+
+			expect( schema.isContent( 'foo' ) ).to.be.true;
+		} );
+
+		it( 'should return true if an item was registered as an object (because all objects are content)', () => {
+			schema.register( 'foo', {
+				isObject: true
+			} );
+
+			expect( schema.isContent( 'foo' ) ).to.be.true;
+		} );
+
+		it( 'should return false if an item was not registered as an object or a content', () => {
+			schema.register( 'foo' );
+
+			expect( schema.isContent( 'foo' ) ).to.be.false;
+		} );
+
+		it( 'should return false if an item was not registered at all', () => {
+			expect( schema.isContent( 'foo' ) ).to.be.false;
+		} );
+
+		it( 'uses getDefinition()\'s item to definition normalization', () => {
+			const stub = sinon.stub( schema, 'getDefinition' ).returns( { isContent: true } );
+
+			expect( schema.isContent( 'foo' ) ).to.be.true;
+			expect( stub.calledOnce ).to.be.true;
+		} );
+	} );
+
 	describe( 'checkChild()', () => {
 		beforeEach( () => {
 			schema.register( '$root' );

+ 1 - 1
packages/ckeditor5-engine/tests/model/selection.js

@@ -973,7 +973,7 @@ describe( 'Selection', () => {
 
 			model.schema.register( 'table', { isBlock: true, isLimit: true, isObject: true, allowIn: '$root' } );
 			model.schema.register( 'tableRow', { allowIn: 'table', isLimit: true } );
-			model.schema.register( 'tableCell', { allowIn: 'tableRow', isObject: true } );
+			model.schema.register( 'tableCell', { allowIn: 'tableRow', isLimit: true, isSelectable: true } );
 
 			model.schema.extend( 'p', { allowIn: 'tableCell' } );
 		} );

+ 8 - 3
packages/ckeditor5-engine/tests/model/utils/deletecontent.js

@@ -941,10 +941,15 @@ describe( 'DataController utils', () => {
 					{ rootName: 'bodyRoot' }
 				);
 
-				deleteContent( model, doc.selection, { doNotAutoparagraph: true } );
+				// This must be tested inside a change block to check results before the post-fixers get triggered.
+				model.change( () => {
+					deleteContent( model, doc.selection, { doNotAutoparagraph: true } );
 
-				expect( getData( model, { rootName: 'bodyRoot' } ) )
-					.to.equal( '[]' );
+					expect( getData( model, { rootName: 'bodyRoot' } ) ).to.equal( '[]' );
+				} );
+
+				// Note that auto-paragraphing post-fixer injected a paragraph into the empty root.
+				expect( getData( model, { rootName: 'bodyRoot' } ) ).to.equal( '<paragraph>[]</paragraph>' );
 			} );
 		} );
 

+ 70 - 0
packages/ckeditor5-engine/tests/model/utils/modifyselection.js

@@ -917,6 +917,76 @@ describe( 'DataController utils', () => {
 			);
 		} );
 
+		describe( 'selectable handling', () => {
+			beforeEach( () => {
+				model.schema.register( 'sel', {
+					isSelectable: true
+				} );
+				model.schema.extend( 'sel', { allowIn: '$root' } );
+				model.schema.extend( '$text', { allowIn: 'sel' } );
+
+				model.schema.register( 'inlineSel', {
+					allowIn: 'p',
+					isObject: true
+				} );
+				model.schema.extend( '$text', { allowIn: 'inlineSel' } );
+			} );
+
+			test(
+				'extends over next selectable element when at the end of an element',
+				'<p>foo[]</p><sel>bar</sel>',
+				'<p>foo[</p><sel>bar</sel>]'
+			);
+
+			test(
+				'extends over previous selectable element when at the beginning of an element ',
+				'<sel>bar</sel><p>[]foo</p>',
+				'[<sel>bar</sel><p>]foo</p>',
+				{ direction: 'backward' }
+			);
+
+			test(
+				'extends over selectable elements - forward',
+				'[<sel></sel>]<sel></sel>',
+				'[<sel></sel><sel></sel>]'
+			);
+
+			it( 'extends over selectable elements - backward', () => {
+				setData( model, '<sel></sel>[<sel></sel>]', { lastRangeBackward: true } );
+
+				modifySelection( model, doc.selection, { direction: 'backward' } );
+
+				expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '[<sel></sel><sel></sel>]' );
+				expect( doc.selection.isBackward ).to.true;
+			} );
+
+			test(
+				'extends over inline selectables - forward',
+				'<p>foo[]<inlineSel>bar</inlineSel></p>',
+				'<p>foo[<inlineSel>bar</inlineSel>]</p>'
+			);
+
+			test(
+				'extends over inline selectables - backward',
+				'<p><inlineSel>bar</inlineSel>[]foo</p>',
+				'<p>[<inlineSel>bar</inlineSel>]foo</p>',
+				{ direction: 'backward' }
+			);
+
+			test(
+				'extends over empty inline selectables - forward',
+				'<p>foo[]<inlineSel></inlineSel></p>',
+				'<p>foo[<inlineSel></inlineSel>]</p>'
+			);
+
+			test(
+				'extends over empty inline selectables - backward',
+				'<p><inlineSel></inlineSel>[]foo</p>',
+				'<p>[<inlineSel></inlineSel>]foo</p>',
+				{ direction: 'backward' }
+			);
+		} );
+
 		describe( 'limits handling', () => {
 			beforeEach( () => {
 				model.schema.register( 'inlineLimit', {

+ 30 - 4
packages/ckeditor5-engine/tests/model/utils/selection-post-fixer.js

@@ -41,7 +41,8 @@ describe( 'Selection post-fixer', () => {
 			model.schema.register( 'tableCell', {
 				allowIn: 'tableRow',
 				allowAttributes: [ 'colspan', 'rowspan' ],
-				isObject: true
+				isLimit: true,
+				isSelectable: true
 			} );
 
 			model.schema.extend( '$block', { allowIn: 'tableCell' } );
@@ -74,7 +75,8 @@ describe( 'Selection post-fixer', () => {
 		it( 'should not crash if there is no correct position for model selection', () => {
 			setModelData( model, '' );
 
-			expect( getModelData( model ) ).to.equal( '[]' );
+			// Note that auto-paragraphing post-fixer injected a paragraph into the empty root.
+			expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
 		} );
 
 		it( 'should react to structure changes', () => {
@@ -1205,6 +1207,30 @@ describe( 'Selection post-fixer', () => {
 
 				expect( getModelData( model ) ).to.equal( '<div>[<div></div>]</div>' );
 			} );
+
+			it( 'should not fix #5 (selection starts before a selectable, which is not an object)', () => {
+				model.schema.register( 'div', { allowIn: '$root' } );
+				model.schema.register( 'selectable', { isSelectable: true, allowIn: 'div' } );
+				model.schema.extend( '$text', { allowIn: 'selectable' } );
+
+				setModelData( model,
+					'<div>[<selectable>foo]</selectable></div>'
+				);
+
+				expect( getModelData( model ) ).to.equal( '<div>[<selectable>foo]</selectable></div>' );
+			} );
+
+			it( 'should not fix #6 (selection ends after before a selectable, which is not an object)', () => {
+				model.schema.register( 'div', { allowIn: '$root' } );
+				model.schema.register( 'selectable', { isSelectable: true, allowIn: 'div' } );
+				model.schema.extend( '$text', { allowIn: 'selectable' } );
+
+				setModelData( model,
+					'<div><selectable>[foo</selectable>]</div>'
+				);
+
+				expect( getModelData( model ) ).to.equal( '<div><selectable>[foo</selectable>]</div>' );
+			} );
 		} );
 
 		describe( 'non-collapsed selection - inline widget scenarios', () => {
@@ -1274,7 +1300,7 @@ describe( 'Selection post-fixer', () => {
 					'<paragraph>foo</paragraph>' +
 					'<table>' +
 						'<tableRow>' +
-							'[<tableCell><paragraph>aaa</paragraph></tableCell>]' +
+							'<tableCell><paragraph>[]aaa</paragraph></tableCell>' +
 							'<tableCell><paragraph>bbb</paragraph></tableCell>' +
 						'</tableRow>' +
 					'</table>' +
@@ -1296,7 +1322,7 @@ describe( 'Selection post-fixer', () => {
 					'<paragraph>foo</paragraph>' +
 					'<table>' +
 						'<tableRow>' +
-							'[<tableCell><paragraph>aaa</paragraph></tableCell>]' +
+							'<tableCell><paragraph>[]aaa</paragraph></tableCell>' +
 							'<tableCell><paragraph>bbb</paragraph></tableCell>' +
 						'</tableRow>' +
 					'</table>' +

+ 1 - 0
packages/ckeditor5-enter/package.json

@@ -18,6 +18,7 @@
     "@ckeditor/ckeditor5-basic-styles": "^21.0.0",
     "@ckeditor/ckeditor5-editor-classic": "^21.0.0",
     "@ckeditor/ckeditor5-heading": "^21.0.0",
+    "@ckeditor/ckeditor5-link": "^21.0.0",
     "@ckeditor/ckeditor5-paragraph": "^21.0.0",
     "@ckeditor/ckeditor5-typing": "^21.0.0",
     "@ckeditor/ckeditor5-undo": "^21.0.0"

+ 1 - 1
packages/ckeditor5-enter/src/shiftenter.js

@@ -51,7 +51,7 @@ export default class ShiftEnter extends Plugin {
 		conversion.for( 'downcast' )
 			.elementToElement( {
 				model: 'softBreak',
-				view: ( modelElement, viewWriter ) => viewWriter.createEmptyElement( 'br' )
+				view: ( modelElement, { writer } ) => writer.createEmptyElement( 'br' )
 			} );
 
 		view.addObserver( EnterObserver );

+ 22 - 3
packages/ckeditor5-enter/tests/shiftenter-integration.js

@@ -6,8 +6,10 @@
 /* global document */
 
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
-
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import LinkEditing from '@ckeditor/ckeditor5-link/src/linkediting';
+import Delete from '@ckeditor/ckeditor5-typing/src/delete';
+import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting';
 import ShiftEnter from '../src/shiftenter';
 
 import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
@@ -24,7 +26,7 @@ describe( 'ShiftEnter integration', () => {
 
 		document.body.appendChild( div );
 
-		return ClassicEditor.create( div, { plugins: [ Paragraph, ShiftEnter ] } )
+		return ClassicEditor.create( div, { plugins: [ Paragraph, ShiftEnter, LinkEditing, Delete, BoldEditing ] } )
 			.then( newEditor => {
 				editor = newEditor;
 
@@ -46,9 +48,26 @@ describe( 'ShiftEnter integration', () => {
 	it( 'BLOCK_FILLER should be inserted after <br> in the paragraph', () => {
 		setModelData( model, '<paragraph>[]</paragraph>' );
 
-		editor.commands.get( 'shiftEnter' ).execute();
+		editor.execute( 'shiftEnter' );
 
 		expect( editor.getData( { trim: 'none' } ) ).to.equal( '<p><br>&nbsp;</p>' );
 		expect( editor.ui.view.editable.element.innerHTML ).to.equal( '<p><br><br data-cke-filler="true"></p>' );
 	} );
+
+	it( 'should not inherit text attributes before the "softBreak" element', () => {
+		setModelData( model,
+			'<paragraph>' +
+				'<$text linkHref="foo" bold="true">Bolded link</$text>' +
+				'<softBreak></softBreak>' +
+				'F[]' +
+			'</paragraph>'
+		);
+
+		editor.execute( 'delete' );
+
+		const selection = model.document.selection;
+
+		expect( selection.hasAttribute( 'linkHref' ) ).to.equal( false );
+		expect( selection.hasAttribute( 'bold' ) ).to.equal( false );
+	} );
 } );

+ 4 - 4
packages/ckeditor5-font/lang/translations/ko.po

@@ -38,7 +38,7 @@ msgstr "매우 큰"
 
 msgctxt "Tooltip for the font family dropdown."
 msgid "Font Family"
-msgstr "글꼴"
+msgstr "글꼴 집합"
 
 msgctxt "Dropdown option label for the default font family."
 msgid "Default"
@@ -46,12 +46,12 @@ msgstr "기본"
 
 msgctxt "Label of a button that allows selecting a font color."
 msgid "Font Color"
-msgstr "글자 색"
+msgstr "글자 색"
 
 msgctxt "Label of a button that allows selecting a font background color."
 msgid "Font Background Color"
-msgstr "글자 배경색"
+msgstr "글자 배경 "
 
 msgctxt "Title of a color picker section containing the colors currently used in the document."
 msgid "Document colors"
-msgstr "문서 색들"
+msgstr "문서 색들"

+ 1 - 1
packages/ckeditor5-font/lang/translations/zh.po

@@ -54,4 +54,4 @@ msgstr "前景顏色"
 
 msgctxt "Title of a color picker section containing the colors currently used in the document."
 msgid "Document colors"
-msgstr ""
+msgstr "文件顏色"

+ 1 - 1
packages/ckeditor5-font/src/fontfamily/fontfamilyediting.js

@@ -92,7 +92,7 @@ export default class FontFamilyEditing extends Plugin {
 
 		editor.conversion.for( 'downcast' ).attributeToElement( {
 			model: FONT_FAMILY,
-			view: ( attributeValue, writer ) => {
+			view: ( attributeValue, { writer } ) => {
 				return writer.createAttributeElement( 'span', { style: 'font-family:' + attributeValue }, { priority: 7 } );
 			}
 		} );

+ 1 - 1
packages/ckeditor5-font/src/fontsize/fontsizeediting.js

@@ -117,7 +117,7 @@ export default class FontSizeEditing extends Plugin {
 
 		editor.conversion.for( 'downcast' ).attributeToElement( {
 			model: FONT_SIZE,
-			view: ( attributeValue, writer ) => {
+			view: ( attributeValue, { writer } ) => {
 				if ( !attributeValue ) {
 					return;
 				}

+ 1 - 1
packages/ckeditor5-font/src/utils.js

@@ -82,7 +82,7 @@ export function renderUpcastAttribute( styleAttr ) {
  * @param {String} styleAttr
  */
 export function renderDowncastElement( styleAttr ) {
-	return ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'span', {
+	return ( modelAttributeValue, { writer } ) => writer.createAttributeElement( 'span', {
 		style: `${ styleAttr }:${ modelAttributeValue }`
 	}, { priority: 7 } );
 }

+ 1 - 1
packages/ckeditor5-font/tests/utils.js

@@ -60,7 +60,7 @@ describe( 'utils', () => {
 			const fake = testUtils.sinon.fake();
 			const fakeViewWriter = { createAttributeElement: fake };
 
-			downcastViewConverterFn( 'blue', fakeViewWriter );
+			downcastViewConverterFn( 'blue', { writer: fakeViewWriter } );
 
 			sinon.assert.calledWithExactly( fake, 'span', { style: 'color:blue' }, { priority: 7 } );
 		} );

+ 7 - 7
packages/ckeditor5-heading/lang/translations/ko.po

@@ -30,31 +30,31 @@ msgstr "제목 선택"
 
 msgctxt "Dropdown option label for the heading level 1 format."
 msgid "Heading 1"
-msgstr "제목1"
+msgstr "제목 1"
 
 msgctxt "Dropdown option label for the heading level 2 format."
 msgid "Heading 2"
-msgstr "제목2"
+msgstr "제목 2"
 
 msgctxt "Dropdown option label for the heading level 3 format."
 msgid "Heading 3"
-msgstr "제목3"
+msgstr "제목 3"
 
 msgctxt "Dropdown option label for the heading level 4 format."
 msgid "Heading 4"
-msgstr "제목4"
+msgstr "제목 4"
 
 msgctxt "Dropdown option label for the heading level 5 format."
 msgid "Heading 5"
-msgstr "제목5"
+msgstr "제목 5"
 
 msgctxt "Dropdown option label for the heading level 6 format."
 msgid "Heading 6"
-msgstr "제목6"
+msgstr "제목 6"
 
 msgctxt "A default value of the placeholder for the content title."
 msgid "Type your title"
-msgstr "제목 입력"
+msgstr "제목 입력해주세요"
 
 msgctxt "A default value of the placeholder for the content body."
 msgid "Type or paste your content here."

+ 2 - 2
packages/ckeditor5-heading/lang/translations/zh.po

@@ -54,8 +54,8 @@ msgstr "標題 6"
 
 msgctxt "A default value of the placeholder for the content title."
 msgid "Type your title"
-msgstr ""
+msgstr "輸入你的標題"
 
 msgctxt "A default value of the placeholder for the content body."
 msgid "Type or paste your content here."
-msgstr ""
+msgstr "在此輸入或貼上你的內容。"

+ 2 - 3
packages/ckeditor5-heading/src/title.js

@@ -445,10 +445,9 @@ function dataViewModelH1Insertion( evt, data, conversionApi ) {
 	modelWriter.append( titleContent, title );
 	modelWriter.insert( title, modelCursor );
 
-	conversionApi.convertChildren( viewItem, modelWriter.createPositionAt( titleContent, 0 ) );
+	conversionApi.convertChildren( viewItem, titleContent );
 
-	data.modelRange = modelWriter.createRangeOn( title );
-	data.modelCursor = modelWriter.createPositionAt( data.modelRange.end );
+	conversionApi.updateConversionResult( title, data );
 }
 
 // Maps position from the beginning of the model `title` element to the beginning of the view `h1` element.

+ 53 - 0
packages/ckeditor5-highlight/lang/translations/ko.po

@@ -0,0 +1,53 @@
+# Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+#
+#                                     !!! IMPORTANT !!!
+#
+#         Before you edit this file, please keep in mind that contributing to the project
+#                translations is possible ONLY via the Transifex online service.
+#
+#         To submit your translations, visit https://www.transifex.com/ckeditor/ckeditor5.
+#
+#                   To learn more, check out the official contributor's guide:
+#     https://ckeditor.com/docs/ckeditor5/latest/framework/guides/contributing/contributing.html
+#
+msgid ""
+msgstr ""
+"Language-Team: Korean (https://www.transifex.com/ckeditor/teams/11143/ko/)\n"
+"Language: ko\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+msgctxt "Toolbar button tooltip for applying yellow marker (text highlight)."
+msgid "Yellow marker"
+msgstr "노란색 마커"
+
+msgctxt "Toolbar button tooltip for applying green marker (text highlight)."
+msgid "Green marker"
+msgstr "초록색 마커"
+
+msgctxt "Toolbar button tooltip for applying pink marker (text highlight)."
+msgid "Pink marker"
+msgstr "분홍색 마커"
+
+msgctxt "Toolbar button tooltip for applying blue marker (text highlight)."
+msgid "Blue marker"
+msgstr "파란색 마커"
+
+msgctxt "Toolbar button tooltip for applying red pen (text color)."
+msgid "Red pen"
+msgstr "빨간색 펜"
+
+msgctxt "Toolbar button tooltip for applying green pen (text color)."
+msgid "Green pen"
+msgstr "초록색 펜"
+
+msgctxt "Toolbar button tooltip for removing text highlight."
+msgid "Remove highlight"
+msgstr "강조 제거"
+
+msgctxt "Toolbar button tooltip for the text highlight feature."
+msgid "Highlight"
+msgstr "강조"
+
+msgctxt "Label used by assistive technologies describing the highlight feature toolbar."
+msgid "Text highlight toolbar"
+msgstr "글자 강조 툴바"

+ 1 - 1
packages/ckeditor5-highlight/lang/translations/zh.po

@@ -50,4 +50,4 @@ msgstr "高亮"
 
 msgctxt "Label used by assistive technologies describing the highlight feature toolbar."
 msgid "Text highlight toolbar"
-msgstr ""
+msgstr "高亮"

+ 21 - 0
packages/ckeditor5-horizontal-line/lang/translations/zh.po

@@ -0,0 +1,21 @@
+# Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+#
+#                                     !!! IMPORTANT !!!
+#
+#         Before you edit this file, please keep in mind that contributing to the project
+#                translations is possible ONLY via the Transifex online service.
+#
+#         To submit your translations, visit https://www.transifex.com/ckeditor/ckeditor5.
+#
+#                   To learn more, check out the official contributor's guide:
+#     https://ckeditor.com/docs/ckeditor5/latest/framework/guides/contributing/contributing.html
+#
+msgid ""
+msgstr ""
+"Language-Team: Chinese (Taiwan) (https://www.transifex.com/ckeditor/teams/11143/zh_TW/)\n"
+"Language: zh_TW\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+msgctxt "Horizontal line"
+msgid "Horizontal line"
+msgstr "水平線"

+ 9 - 9
packages/ckeditor5-horizontal-line/src/horizontallineediting.js

@@ -42,24 +42,24 @@ export default class HorizontalLineEditing extends Plugin {
 
 		conversion.for( 'dataDowncast' ).elementToElement( {
 			model: 'horizontalLine',
-			view: ( modelElement, viewWriter ) => {
-				return viewWriter.createEmptyElement( 'hr' );
+			view: ( modelElement, { writer } ) => {
+				return writer.createEmptyElement( 'hr' );
 			}
 		} );
 
 		conversion.for( 'editingDowncast' ).elementToElement( {
 			model: 'horizontalLine',
-			view: ( modelElement, viewWriter ) => {
+			view: ( modelElement, { writer } ) => {
 				const label = t( 'Horizontal line' );
-				const viewWrapper = viewWriter.createContainerElement( 'div' );
-				const viewHrElement = viewWriter.createEmptyElement( 'hr' );
+				const viewWrapper = writer.createContainerElement( 'div' );
+				const viewHrElement = writer.createEmptyElement( 'hr' );
 
-				viewWriter.addClass( 'ck-horizontal-line', viewWrapper );
-				viewWriter.setCustomProperty( 'hr', true, viewWrapper );
+				writer.addClass( 'ck-horizontal-line', viewWrapper );
+				writer.setCustomProperty( 'hr', true, viewWrapper );
 
-				viewWriter.insert( viewWriter.createPositionAt( viewWrapper, 0 ), viewHrElement );
+				writer.insert( writer.createPositionAt( viewWrapper, 0 ), viewHrElement );
 
-				return toHorizontalLineWidget( viewWrapper, viewWriter, label );
+				return toHorizontalLineWidget( viewWrapper, writer, label );
 			}
 		} );
 

+ 1 - 1
packages/ckeditor5-horizontal-line/tests/horizontallinecommand.js

@@ -84,7 +84,7 @@ describe( 'HorizontalLineCommand', () => {
 		it( 'should be true when the selection is inside block element inside isLimit element which allows horizontal line', () => {
 			model.schema.register( 'table', { allowWhere: '$block', isLimit: true, isObject: true, isBlock: true } );
 			model.schema.register( 'tableRow', { allowIn: 'table', isLimit: true } );
-			model.schema.register( 'tableCell', { allowIn: 'tableRow', isLimit: true } );
+			model.schema.register( 'tableCell', { allowIn: 'tableRow', isLimit: true, isSelectable: true } );
 			model.schema.extend( '$block', { allowIn: 'tableCell' } );
 			editor.conversion.for( 'downcast' ).elementToElement( { model: 'table', view: 'table' } );
 			editor.conversion.for( 'downcast' ).elementToElement( { model: 'tableRow', view: 'tableRow' } );

+ 2 - 2
packages/ckeditor5-image/docs/_snippets/features/image-caption.html

@@ -1,12 +1,12 @@
 <div id="snippet-image-caption">
-	<p>Image with caption:</p>
+	<p>Image with a caption:</p>
 
 	<figure class="image">
 		<img src="%BASE_PATH%/assets/img/fields.jpg" alt="Autumn fields">
 		<figcaption>Autumn fields by Aleksander Nowodziński</figcaption>
 	</figure>
 
-	<p>Image without caption:</p>
+	<p>Image without a caption:</p>
 
 	<figure class="image">
 		<img src="%BASE_PATH%/assets/img/fields.jpg" alt="Autumn fields">

+ 2 - 2
packages/ckeditor5-image/docs/_snippets/features/image-link.html

@@ -1,8 +1,8 @@
 <div id="snippet-image-link">
-	<h3>Linked image</h3>
+	<p>Linked image - use the contextual toolbar to edit image link properties:</p>
 
 	<figure class="image">
-		<a href="https://cksource.com">
+		<a href="https://cksource.com/">
 			<img src="%BASE_PATH%/assets/img/fields.jpg" alt="Autumn fields">
 		</a>
 	</figure>

+ 1 - 1
packages/ckeditor5-image/docs/_snippets/features/image-resize-buttons-dropdown.html

@@ -1,5 +1,5 @@
 <div id="snippet-image-resize-buttons-dropdown">
-	<h3>Resize me using the dropdown!</h3>
+	<p>Click me and resize using the contextual toolbar!</p>
 
 	<figure class="image">
 		<img src="%BASE_PATH%/assets/img/fields.jpg" alt="Autumn fields">

+ 1 - 1
packages/ckeditor5-image/docs/_snippets/features/image-resize-buttons.html

@@ -1,5 +1,5 @@
 <div id="snippet-image-resize-buttons">
-	<h3>Resize me using image toolbar buttons!</h3>
+	<p>Click me and resize using the image toolbar buttons!</p>
 
 	<figure class="image">
 		<img src="%BASE_PATH%/assets/img/fields.jpg" alt="Autumn fields">

+ 2 - 2
packages/ckeditor5-image/docs/_snippets/features/image-resize-px.html

@@ -1,12 +1,12 @@
 <div id="snippet-image-resize-px">
-	<h3>Resize me!</h3>
+	<p>Resize me using pixel values!</p>
 
 	<figure class="image">
 		<img src="%BASE_PATH%/assets/img/fields.jpg" alt="Autumn fields">
 		<figcaption>Autumn fields by Aleksander Nowodziński</figcaption>
 	</figure>
 
-	<h3>Resized image (width: 400px):</h3>
+	<p>Resized image (width: 400px):</p>
 
 	<figure class="image image_resized" style="width:400px;">
 		<img src="%BASE_PATH%/assets/img/fields.jpg" alt="Autumn fields">

+ 2 - 2
packages/ckeditor5-image/docs/_snippets/features/image-resize.html

@@ -1,12 +1,12 @@
 <div id="snippet-image-resize">
-	<h3>Resize me!</h3>
+	<p>Resize me by using handles!</p>
 
 	<figure class="image">
 		<img src="%BASE_PATH%/assets/img/fields.jpg" alt="Autumn fields">
 		<figcaption>Autumn fields by Aleksander Nowodziński</figcaption>
 	</figure>
 
-	<h3>Resized image (width: 75%):</h3>
+	<p>Resized image (width: 75%):</p>
 
 	<figure class="image image_resized" style="width:75%;">
 		<img src="%BASE_PATH%/assets/img/fields.jpg" alt="Autumn fields">

+ 2 - 2
packages/ckeditor5-image/docs/_snippets/features/image-style-presentational.html

@@ -5,11 +5,11 @@
 		<img src="%BASE_PATH%/assets/img/fields.jpg" alt="Autumn fields">
 	</figure>
 
-	<p>This is a right-aligned image:</p>
+	<p>This is a right-aligned image, resized to 50% width:</p>
 
 	<figure class="image image-style-align-right" style="width: 50%;">
 		<img src="%BASE_PATH%/assets/img/fields.jpg" alt="Autumn fields">
 	</figure>
 
-	<p>Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here.</p>
+	<p>Yet another sample paragraph and lots of text here, again. Quite a lot of text, just like before. And this huge amount of text forms a whole long paragraph. This whole lot of text is here to help you see the image's alignment in context. It serves no other purpose, though. But we still find it useful and plain like it. And it is even longer than the previous one. Go on an use the image style buttons to make the most out of this pretty text block and the image alignment demo.</p>
 </div>

+ 1 - 1
packages/ckeditor5-image/docs/_snippets/features/image-style.html

@@ -11,5 +11,5 @@
 		<img src="%BASE_PATH%/assets/img/fields.jpg" alt="Autumn fields">
 	</figure>
 
-	<p>Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here. Lots of text here.</p>
+	<p>Lots of text here. Quite a lot of text, indeed. In fact, there is plenty of text beside this image and it forms a whole long paragraph. This whole lot of text is here to help you see the image's alignment in context. It serves no other purpose, though. But we still find it useful and plain like it.</p>
 </div>

+ 1 - 1
packages/ckeditor5-image/docs/_snippets/features/image-toolbar.html

@@ -1,5 +1,5 @@
 <div id="snippet-image-toolbar">
-	<p>This is <a href="https://ckeditor.com">CKEditor&nbsp;5</a>.</p>
+	<p>This is <a href="https://ckeditor.com">CKEditor&nbsp;5 WYSIWYG editor</a>.</p>
 
 	<figure class="image">
 		<img src="%BASE_PATH%/assets/img/fields.jpg" alt="Autumn fields">

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