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

Merge branch 'master' into release

Marek Lewandowski 5 лет назад
Родитель
Сommit
5474d1f962
100 измененных файлов с 1834 добавлено и 919 удалено
  1. 1 1
      .travis.yml
  2. 21 21
      CHANGELOG.md
  3. 9 0
      docs/assets/snippet-styles.css
  4. 183 159
      docs/builds/guides/integration/content-styles.md
  5. 24 24
      package.json
  6. 1 1
      packages/ckeditor-cloud-services-core/package.json
  7. 2 2
      packages/ckeditor5-adapter-ckfinder/lang/translations/ug.po
  8. 1 1
      packages/ckeditor5-adapter-ckfinder/package.json
  9. 1 1
      packages/ckeditor5-alignment/package.json
  10. 1 1
      packages/ckeditor5-autoformat/docs/features/autoformat.md
  11. 1 1
      packages/ckeditor5-autoformat/package.json
  12. 14 27
      packages/ckeditor5-autoformat/src/autoformat.js
  13. 82 91
      packages/ckeditor5-autoformat/src/blockautoformatediting.js
  14. 165 175
      packages/ckeditor5-autoformat/src/inlineautoformatediting.js
  15. 146 0
      packages/ckeditor5-autoformat/tests/autoformat.js
  16. 28 17
      packages/ckeditor5-autoformat/tests/blockautoformatediting.js
  17. 35 25
      packages/ckeditor5-autoformat/tests/inlineautoformatediting.js
  18. 1 1
      packages/ckeditor5-autoformat/tests/manual/autoformat.html
  19. 2 2
      packages/ckeditor5-autosave/package.json
  20. 2 2
      packages/ckeditor5-basic-styles/lang/translations/ug.po
  21. 1 1
      packages/ckeditor5-basic-styles/package.json
  22. 2 2
      packages/ckeditor5-block-quote/lang/translations/ug.po
  23. 1 1
      packages/ckeditor5-block-quote/package.json
  24. 0 0
      packages/ckeditor5-build-balloon-block/build/ckeditor.js
  25. 0 0
      packages/ckeditor5-build-balloon-block/build/ckeditor.js.map
  26. 8 8
      packages/ckeditor5-build-balloon-block/package.json
  27. 0 0
      packages/ckeditor5-build-balloon/build/ckeditor.js
  28. 0 0
      packages/ckeditor5-build-balloon/build/ckeditor.js.map
  29. 8 8
      packages/ckeditor5-build-balloon/package.json
  30. 0 0
      packages/ckeditor5-build-classic/build/ckeditor.js
  31. 0 0
      packages/ckeditor5-build-classic/build/ckeditor.js.map
  32. 8 8
      packages/ckeditor5-build-classic/package.json
  33. 0 0
      packages/ckeditor5-build-decoupled-document/build/ckeditor.js
  34. 0 0
      packages/ckeditor5-build-decoupled-document/build/ckeditor.js.map
  35. 8 8
      packages/ckeditor5-build-decoupled-document/package.json
  36. 1 1
      packages/ckeditor5-build-decoupled-document/tests/ckeditor.js
  37. 0 0
      packages/ckeditor5-build-inline/build/ckeditor.js
  38. 0 0
      packages/ckeditor5-build-inline/build/ckeditor.js.map
  39. 8 8
      packages/ckeditor5-build-inline/package.json
  40. 1 1
      packages/ckeditor5-ckfinder/package.json
  41. 1 1
      packages/ckeditor5-clipboard/package.json
  42. 1 1
      packages/ckeditor5-cloud-services/package.json
  43. 1 1
      packages/ckeditor5-code-block/lang/translations/de.po
  44. 2 2
      packages/ckeditor5-code-block/package.json
  45. 2 2
      packages/ckeditor5-core/lang/translations/ug.po
  46. 2 2
      packages/ckeditor5-core/package.json
  47. 6 1
      packages/ckeditor5-core/tests/_utils-tests/articlepluginset.js
  48. 15 5
      packages/ckeditor5-core/tests/_utils/memory.js
  49. 1 1
      packages/ckeditor5-core/tests/_utils/utils.js
  50. 1 1
      packages/ckeditor5-easy-image/package.json
  51. 2 2
      packages/ckeditor5-editor-balloon/package.json
  52. 2 2
      packages/ckeditor5-editor-balloon/tests/manual/memory.md
  53. 2 2
      packages/ckeditor5-editor-classic/package.json
  54. 2 2
      packages/ckeditor5-editor-classic/tests/manual/memory.md
  55. 2 2
      packages/ckeditor5-editor-decoupled/package.json
  56. 2 2
      packages/ckeditor5-editor-decoupled/tests/manual/memory.md
  57. 2 2
      packages/ckeditor5-editor-inline/package.json
  58. 2 2
      packages/ckeditor5-editor-inline/tests/manual/memory.md
  59. 8 8
      packages/ckeditor5-engine/docs/framework/guides/deep-dive/schema.md
  60. 2 2
      packages/ckeditor5-engine/package.json
  61. 1 1
      packages/ckeditor5-engine/src/model/element.js
  62. 9 6
      packages/ckeditor5-engine/src/model/model.js
  63. 2 0
      packages/ckeditor5-engine/src/model/operation/transform.js
  64. 39 39
      packages/ckeditor5-engine/src/model/schema.js
  65. 297 47
      packages/ckeditor5-engine/src/model/utils/deletecontent.js
  66. 1 1
      packages/ckeditor5-engine/src/model/utils/getselectedcontent.js
  67. 8 1
      packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js
  68. 12 0
      packages/ckeditor5-engine/src/model/writer.js
  69. 1 0
      packages/ckeditor5-engine/src/view/renderer.js
  70. 45 11
      packages/ckeditor5-engine/tests/manual/highlight.html
  71. 3 2
      packages/ckeditor5-engine/tests/manual/highlight.js
  72. 6 1
      packages/ckeditor5-engine/tests/manual/highlight.md
  73. 1 1
      packages/ckeditor5-engine/tests/model/differ.js
  74. 13 0
      packages/ckeditor5-engine/tests/model/model.js
  75. 2 3
      packages/ckeditor5-engine/tests/model/operation/transform.js
  76. 302 97
      packages/ckeditor5-engine/tests/model/utils/deletecontent.js
  77. 29 0
      packages/ckeditor5-engine/tests/model/utils/selection-post-fixer.js
  78. 35 0
      packages/ckeditor5-engine/tests/model/writer.js
  79. 100 1
      packages/ckeditor5-engine/tests/view/renderer.js
  80. 1 1
      packages/ckeditor5-enter/package.json
  81. 1 1
      packages/ckeditor5-essentials/package.json
  82. 1 1
      packages/ckeditor5-font/package.json
  83. 1 1
      packages/ckeditor5-font/src/fontfamily.js
  84. 1 1
      packages/ckeditor5-font/src/fontfamily/utils.js
  85. 6 4
      packages/ckeditor5-font/tests/fontfamily/fontfamilyediting.js
  86. 50 28
      packages/ckeditor5-font/tests/fontfamily/fontfamilyui.js
  87. 2 2
      packages/ckeditor5-font/tests/fontfamily/utils.js
  88. 12 2
      packages/ckeditor5-font/tests/integration.js
  89. 2 1
      packages/ckeditor5-font/tests/manual/font-family.md
  90. 2 2
      packages/ckeditor5-heading/lang/translations/ug.po
  91. 1 1
      packages/ckeditor5-heading/package.json
  92. 1 1
      packages/ckeditor5-highlight/package.json
  93. 1 1
      packages/ckeditor5-horizontal-line/package.json
  94. 4 4
      packages/ckeditor5-horizontal-line/theme/horizontalline.css
  95. 2 2
      packages/ckeditor5-image/lang/translations/ug.po
  96. 1 1
      packages/ckeditor5-image/package.json
  97. 1 5
      packages/ckeditor5-image/src/image/converters.js
  98. 13 1
      packages/ckeditor5-image/src/image/utils.js
  99. 1 1
      packages/ckeditor5-image/src/imagestyle/utils.js
  100. 0 6
      packages/ckeditor5-image/tests/image.js

+ 1 - 1
.travis.yml

@@ -8,7 +8,7 @@ services:
 cache:
   yarn: true
 node_js:
-- "10"
+- "12"
 cache:
 - node_modules
 before_install:

+ 21 - 21
CHANGELOG.md

@@ -32,35 +32,35 @@ Other releases:
 
 We are happy to announce the release of CKEditor 5 v19.1.0.
 
-This release further refines the table plugin, brings a helper for convenient typing in tight places before or after widgets and brings a major change in our code infrastructure. Most notable enhancements are:
+This release further refines the table feature, brings a helper for convenient typing in tight places before or after widgets (such as images or tables) and brings a major change in our code infrastructure. Most notable enhancements are:
 
-* Pasting within a table, with different table selection — which marks an end of our [Table selection stage III](https://github.com/ckeditor/ckeditor5/issues/6297) task.
-* New [widget feature, allowing to type before or after a widget](https://github.com/ckeditor/ckeditor5/issues/6689) in case there's no space around it.
+* Pasting a table into a selected table fragment — which marks the end of the [Table selection stage III](https://github.com/ckeditor/ckeditor5/issues/6297) task.
+* A new [widget feature that allows typing before or after a widget](https://github.com/ckeditor/ckeditor5/issues/6689) when there is no space around it.
 * [Project migration to a monorepo architecture](https://github.com/ckeditor/ckeditor5/issues/6466).
 
-But we did not stop there, as the release comes with several bug fixes too:
+But we did not stop there, as the release comes with several bug fixes, too:
 
 * [Entities handling in code blocks](https://github.com/ckeditor/ckeditor5/issues/5901).
 * [Potential editor crash when removing a column](https://github.com/ckeditor/ckeditor5/issues/6789).
-* [Editor crash when inserting a table row/column with another widget selected in a cell](https://github.com/ckeditor/ckeditor5/issues/6607).
+* [Editor crash when inserting a table row or column with another widget selected in the cell](https://github.com/ckeditor/ckeditor5/issues/6607).
 
-Blog post coming soon...
+Read more in the blog post: https://ckeditor.com/blog/CKEditor-5-v19.1.1-with-table-enhancements-typing-around-widgets-and-print-to-PDF-feature/
 
 ### Collaboration features
 
-The CKEditor 5 Collaboration features changelog can be found here: https://ckeditor.com/collaboration/changelog.
+The CKEditor 5 collaboration features changelog can be found here: https://ckeditor.com/collaboration/changelog.
 
 ### MINOR BREAKING CHANGES [ℹ️](https://ckeditor.com/docs/ckeditor5/latest/framework/guides/support/versioning-policy.html#major-and-minor-breaking-changes)
 
 * **[media-embed](https://www.npmjs.com/package/@ckeditor/ckeditor5-media-embed)**: The `MediaEmbedUI#form` property was removed from the API.
-* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: The `cropTable()` utility method was removed. Use the `cropTableToDimensions()` instead.
-* **[theme-lark](https://www.npmjs.com/package/@ckeditor/ckeditor5-theme-lark)**: A new `--ck-color-focus-border-coordinates` CSS custom property has been added and the existing `--ck-color-focus-border` property now uses it internally. If your integration overrides the latter, we recommend you update the former to avoid compatibility issues with various editor UI features.
+* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: The `cropTable()` utility method was removed. Use the [`cropTableToDimensions()`](https://ckeditor.com/docs/ckeditor5/latest/api/module_table_tableselection_croptable.html#static-function-cropTableToDimensions) method instead.
+* **[theme-lark](https://www.npmjs.com/package/@ckeditor/ckeditor5-theme-lark)**: A new custom `--ck-color-focus-border-coordinates` CSS property was added and the existing `--ck-color-focus-border` property now uses it internally. If your integration overrides the latter, we recommend you update the former to avoid compatibility issues with various editor UI features.
 
 ### Features
 
-* **[paragraph](https://www.npmjs.com/package/@ckeditor/ckeditor5-paragraph)**: Implemented the `InsertParagraphCommand` registered as `'insertParagraph'` in the editor. Closes [#6823](https://github.com/ckeditor/ckeditor5/issues/6823), [#7229](https://github.com/ckeditor/ckeditor5/issues/7229). ([commit](https://github.com/ckeditor/ckeditor5/commit/126701895d2bff8fb0ded7b4f4bf5e26d36ba7d7))
+* **[paragraph](https://www.npmjs.com/package/@ckeditor/ckeditor5-paragraph)**: Implemented the [`InsertParagraphCommand`](https://ckeditor.com/docs/ckeditor5/latest/api/module_paragraph_insertparagraphcommand-InsertParagraphCommand.html) registered as `'insertParagraph'` in the editor. Closes [#6823](https://github.com/ckeditor/ckeditor5/issues/6823), [#7229](https://github.com/ckeditor/ckeditor5/issues/7229). ([commit](https://github.com/ckeditor/ckeditor5/commit/126701895d2bff8fb0ded7b4f4bf5e26d36ba7d7))
 * **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Introduced support for pasting tables into a selected table fragment. Closes [#6120](https://github.com/ckeditor/ckeditor5/issues/6120). ([commit](https://github.com/ckeditor/ckeditor5/commit/1b426397f9e2d6762681abdef5e99e6e101e25fa))
-* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Introduced table cells selection using keyboard. Closes [#6115](https://github.com/ckeditor/ckeditor5/issues/6115), [#3203](https://github.com/ckeditor/ckeditor5/issues/3203). ([commit](https://github.com/ckeditor/ckeditor5/commit/b567de402d1438790c3e7314d5b7ed330b308d9d))
+* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Introduced table cell selection using keyboard. Closes [#6115](https://github.com/ckeditor/ckeditor5/issues/6115), [#3203](https://github.com/ckeditor/ckeditor5/issues/3203). ([commit](https://github.com/ckeditor/ckeditor5/commit/b567de402d1438790c3e7314d5b7ed330b308d9d))
 * **[theme-lark](https://www.npmjs.com/package/@ckeditor/ckeditor5-theme-lark)**: Brought styles for the feature allowing users to type in tight spots around block widgets (see [#407](https://github.com/ckeditor/ckeditor5/issues/407)). ([commit](https://github.com/ckeditor/ckeditor5/commit/dbf24a29ac64f52bceb2efc106b50c736c16f1c3))
 * **[widget](https://www.npmjs.com/package/@ckeditor/ckeditor5-widget)**: Brought the feature allowing users to type in tight spots around block widgets where web browsers do not allow the caret to be placed (see [#407](https://github.com/ckeditor/ckeditor5/issues/407)). Closes [#6740](https://github.com/ckeditor/ckeditor5/issues/6740), [#6688](https://github.com/ckeditor/ckeditor5/issues/6688), [#6689](https://github.com/ckeditor/ckeditor5/issues/6689), [#6695](https://github.com/ckeditor/ckeditor5/issues/6695). ([commit](https://github.com/ckeditor/ckeditor5/commit/dbf24a29ac64f52bceb2efc106b50c736c16f1c3))
 
@@ -68,27 +68,27 @@ The CKEditor 5 Collaboration features changelog can be found here: https://ckedi
 
 * **[cloud-services](https://www.npmjs.com/package/@ckeditor/ckeditor5-cloud-services)**: A `Token` instance will be destroyed by the `CloudServices` context plugin. Closes [#7248](https://github.com/ckeditor/ckeditor5/issues/7248). ([commit](https://github.com/ckeditor/ckeditor5/commit/6b60cb630b72105577696b6ccc291c17cf230c40))
 * **[code-block](https://www.npmjs.com/package/@ckeditor/ckeditor5-code-block)**: Fixed conversion of some entities (like ` `, `&`) in a code block. Closes [#5901](https://github.com/ckeditor/ckeditor5/issues/5901). ([commit](https://github.com/ckeditor/ckeditor5/commit/ad227917a6b85edbc41dca314d9d4caec97b56f5))
-* **[media-embed](https://www.npmjs.com/package/@ckeditor/ckeditor5-media-embed)**: Made it possible to use the `mediaEmbed` button more than once (in more than one toolbar. Closes [#6333](https://github.com/ckeditor/ckeditor5/issues/6333). ([commit](https://github.com/ckeditor/ckeditor5/commit/3011e37768225dfe928f3e3321753fc04ca58ff2))
+* **[media-embed](https://www.npmjs.com/package/@ckeditor/ckeditor5-media-embed)**: Made it possible to use the `mediaEmbed` button more than once (in more than one toolbar). Closes [#6333](https://github.com/ckeditor/ckeditor5/issues/6333). ([commit](https://github.com/ckeditor/ckeditor5/commit/3011e37768225dfe928f3e3321753fc04ca58ff2))
 * **[media-mebed](https://www.npmjs.com/package/@ckeditor/ckeditor5-media-mebed)**: The media widget conversion will no longer discard widget internals (drag or resize handlers, buttons to insert paragraphs, etc.) injected by other features when converting the URL (see [#407](https://github.com/ckeditor/ckeditor5/issues/407)). ([commit](https://github.com/ckeditor/ckeditor5/commit/dbf24a29ac64f52bceb2efc106b50c736c16f1c3))
-* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Setting the column as a header will now properly split col-spanned cells. Closes [#6658](https://github.com/ckeditor/ckeditor5/issues/6658). ([commit](https://github.com/ckeditor/ckeditor5/commit/9531af43623b6e15aff27872a83ac1dd22ea8654))
-* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: The table properties balloon should always be visible if the table is bigger than a visible viewport. Closes [#6190](https://github.com/ckeditor/ckeditor5/issues/6190). ([commit](https://github.com/ckeditor/ckeditor5/commit/75d6912a3e667ef075f4283ec2d45de05d4da8b6))
-* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: When the state is restored or the user enters color value manually, the color input will now properly match color label (if any is available). Closes [#6791](https://github.com/ckeditor/ckeditor5/issues/6791). ([commit](https://github.com/ckeditor/ckeditor5/commit/f18f4fd31e16a11b32dd433d3f40fd0933e2bf26))
+* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Setting the column as a header will now properly split column-spanned cells. Closes [#6658](https://github.com/ckeditor/ckeditor5/issues/6658). ([commit](https://github.com/ckeditor/ckeditor5/commit/9531af43623b6e15aff27872a83ac1dd22ea8654))
+* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: The table properties balloon should always be visible if a table is bigger than the visible viewport. Closes [#6190](https://github.com/ckeditor/ckeditor5/issues/6190). ([commit](https://github.com/ckeditor/ckeditor5/commit/75d6912a3e667ef075f4283ec2d45de05d4da8b6))
+* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: When the state is restored or the user enters a color value manually, the color input will now properly match the color label (if any is available). Closes [#6791](https://github.com/ckeditor/ckeditor5/issues/6791). ([commit](https://github.com/ckeditor/ckeditor5/commit/f18f4fd31e16a11b32dd433d3f40fd0933e2bf26))
 * **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: The editor will not crash when removing columns next to row-spanned cells. Closes [#6789](https://github.com/ckeditor/ckeditor5/issues/6789). ([commit](https://github.com/ckeditor/ckeditor5/commit/84e3310c33c770489777906bc36fd037b5afc86b))
 * **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: The table properties button should not be enabled if all the property commands are disabled. Closes [#6679](https://github.com/ckeditor/ckeditor5/issues/6679). ([commit](https://github.com/ckeditor/ckeditor5/commit/056e06e1e552a609aaad4108e51272cf4a2644c0))
 * **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Table heading rows should be properly updated after removing rows as a side effect of merging cells. Closes [#6667](https://github.com/ckeditor/ckeditor5/issues/6667). ([commit](https://github.com/ckeditor/ckeditor5/commit/72f6491b8dfd72f897904fbfad54310a0d2ef9b8))
-* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Empty table rows are properly handled during conversion and layout post-fixing. Closes [#3274](https://github.com/ckeditor/ckeditor5/issues/3274). ([commit](https://github.com/ckeditor/ckeditor5/commit/fb5fe8b8950cf11700d691bd4369b8bb8aa12cf2))
+* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Empty table rows are properly handled during the conversion and layout post-fixing. Closes [#3274](https://github.com/ckeditor/ckeditor5/issues/3274). ([commit](https://github.com/ckeditor/ckeditor5/commit/fb5fe8b8950cf11700d691bd4369b8bb8aa12cf2))
 * **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: <kbd>Shift</kbd>+click will now use an anchor cell if there is any. Closes [#6453](https://github.com/ckeditor/ckeditor5/issues/6453). ([commit](https://github.com/ckeditor/ckeditor5/commit/d799b9d148f2e8a10784e0cf5fd7ea3a69b93bd1))
-* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Fixed insert table row/column commands when a widget is selected inside of a table cell. Closes [#6607](https://github.com/ckeditor/ckeditor5/issues/6607). ([commit](https://github.com/ckeditor/ckeditor5/commit/3d85aca751f45be923210edfe598780eccacd0dc))
-* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Table keyboard navigation should not alter the native <kbd>Shift</kbd>+Arrow behavior inside of a table cell. Closes [#6641](https://github.com/ckeditor/ckeditor5/issues/6641). ([commit](https://github.com/ckeditor/ckeditor5/commit/88543374bc1cac78e6bbc917759aa6a512cfad47))
+* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Fixed insert table row/column commands when a widget is selected inside a table cell. Closes [#6607](https://github.com/ckeditor/ckeditor5/issues/6607). ([commit](https://github.com/ckeditor/ckeditor5/commit/3d85aca751f45be923210edfe598780eccacd0dc))
+* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Table keyboard navigation should not alter the native <kbd>Shift</kbd>+Arrow behavior inside a table cell. Closes [#6641](https://github.com/ckeditor/ckeditor5/issues/6641). ([commit](https://github.com/ckeditor/ckeditor5/commit/88543374bc1cac78e6bbc917759aa6a512cfad47))
 * **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Merging cells no longer wraps the text in a `<span>` element rather than paragraph in a certain scenario. Closes [#6260](https://github.com/ckeditor/ckeditor5/issues/6260). ([commit](https://github.com/ckeditor/ckeditor5/commit/fbec6b2af7a8a45c189388b537ed48d223b9f18a))
-* **[widget](https://www.npmjs.com/package/@ckeditor/ckeditor5-widget)**: The widget toolbar should always be visible even if the widget is bigger than a visible viewport (see [#6190](https://github.com/ckeditor/ckeditor5/issues/6190)). ([commit](https://github.com/ckeditor/ckeditor5/commit/75d6912a3e667ef075f4283ec2d45de05d4da8b6))
+* **[widget](https://www.npmjs.com/package/@ckeditor/ckeditor5-widget)**: The widget toolbar should always be visible even if the widget is bigger than the visible viewport (see [#6190](https://github.com/ckeditor/ckeditor5/issues/6190)). ([commit](https://github.com/ckeditor/ckeditor5/commit/75d6912a3e667ef075f4283ec2d45de05d4da8b6))
 
 ### Other changes
 
-* **[mention](https://www.npmjs.com/package/@ckeditor/ckeditor5-mention)**: Renamed the `MentionAttribute._uid` to a `MentionAttribute.uid` as it needs to be used by integrators when implementing custom converters. Closes [#6587](https://github.com/ckeditor/ckeditor5/issues/6587). ([commit](https://github.com/ckeditor/ckeditor5/commit/94a6952a6a07146e5ac6daad8e836262d2381664))
+* **[mention](https://www.npmjs.com/package/@ckeditor/ckeditor5-mention)**: Renamed `MentionAttribute._uid` to a `MentionAttribute.uid` as it needs to be used by integrators when implementing custom converters. Closes [#6587](https://github.com/ckeditor/ckeditor5/issues/6587). ([commit](https://github.com/ckeditor/ckeditor5/commit/94a6952a6a07146e5ac6daad8e836262d2381664))
 * **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Adding a new row in the table copies the structure of the selected row. Closes [#6549](https://github.com/ckeditor/ckeditor5/issues/6549). ([commit](https://github.com/ckeditor/ckeditor5/commit/9f2091158ed8bfaba5ddf91f89308023a345351c))
 * **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Display a human readable color value in the color input field. Closes [#6241](https://github.com/ckeditor/ckeditor5/issues/6241). ([commit](https://github.com/ckeditor/ckeditor5/commit/af7928f1febebeef1f4b0243169dd01415531c1d))
-* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Changed insert row above/below buttons order in the table dropdown. Closes [#6702](https://github.com/ckeditor/ckeditor5/issues/6702). ([commit](https://github.com/ckeditor/ckeditor5/commit/a78bca8806064ca7acdd969222bb11b853ca4f0c))
+* **[table](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)**: Changed the insert row above/below buttons order in the table dropdown. Closes [#6702](https://github.com/ckeditor/ckeditor5/issues/6702). ([commit](https://github.com/ckeditor/ckeditor5/commit/a78bca8806064ca7acdd969222bb11b853ca4f0c))
 
 ### Released packages
 

+ 9 - 0
docs/assets/snippet-styles.css

@@ -196,6 +196,15 @@ It breaks CKEditor 5 (see how <p><code>[]</code></p> looks). */
 	margin-left: 2.5rem;
 }
 
+/* Umberto stylesheet is centering `<img>` inside a `<p>`, let's overwrite it with higher CSS Specificity.
+ * See: https://github.com/ckeditor/ckeditor5/issues/2080.
+ */
+.live-snippet p img {
+	display: initial;
+	margin: initial;
+	box-sizing: initial;
+}
+
 .live-snippet .image img {
 	margin: 0 auto;
 }

+ 183 - 159
docs/builds/guides/integration/content-styles.md

@@ -48,166 +48,22 @@ Below there is a full list of content styles used by the editor features. You ca
 
 ```css
 /*
- * CKEditor 5 (v17.0.0) content styles.
- * Generated on Fri, 13 Mar 2020 13:27:10 GMT.
+ * CKEditor 5 (v19.1.1) content styles.
+ * Generated on Fri, 19 Jun 2020 01:26:44 GMT.
  * For more information, check out https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/content-styles.html
  */
 
 :root {
-	--ck-highlight-marker-blue: #72cdfd;
-	--ck-highlight-marker-green: #63f963;
-	--ck-highlight-marker-pink: #fc7999;
-	--ck-highlight-marker-yellow: #fdfd77;
-	--ck-highlight-pen-green: #118800;
-	--ck-highlight-pen-red: #e91313;
+	--ck-highlight-marker-blue: hsl(201, 97%, 72%);
+	--ck-highlight-marker-green: hsl(120, 93%, 68%);
+	--ck-highlight-marker-pink: hsl(345, 96%, 73%);
+	--ck-highlight-marker-yellow: hsl(60, 97%, 73%);
+	--ck-highlight-pen-green: hsl(112, 100%, 27%);
+	--ck-highlight-pen-red: hsl(0, 85%, 49%);
 	--ck-image-style-spacing: 1.5em;
 	--ck-todo-list-checkmark-size: 16px;
 }
 
-/* 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-basic-styles/theme/code.css */
-.ck-content code {
-	background-color: hsla(0, 0%, 78%, 0.3);
-	padding: .15em;
-	border-radius: 2px;
-}
-/* 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/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;
-	clear: both;
-	padding: 5px 0;
-	display: flex;
-	align-items: center;
-	justify-content: center;
-}
-/* ckeditor5-page-break/theme/pagebreak.css */
-.ck-content .page-break::after {
-	content: '';
-	position: absolute;
-	border-bottom: 2px dashed hsl(0, 0%, 77%);
-	width: 100%;
-}
-/* ckeditor5-page-break/theme/pagebreak.css */
-.ck-content .page-break__label {
-	position: relative;
-	z-index: 1;
-	padding: .3em .6em;
-	display: block;
-	text-transform: uppercase;
-	border: 1px solid hsl(0, 0%, 77%);
-	border-radius: 2px;
-	font-family: Helvetica, Arial, Tahoma, Verdana, Sans-Serif;
-	font-size: 0.75em;
-	font-weight: bold;
-	color: hsl(0, 0%, 20%);
-	background: #fff;
-	box-shadow: 2px 2px 1px hsla(0, 0%, 0%, 0.15);
-	-webkit-user-select: none;
-	-moz-user-select: none;
-	-ms-user-select: none;
-	user-select: none;
-}
-/* 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-media-embed/theme/mediaembed.css */
-.ck-content .media {
-	clear: both;
-	margin: 1em 0;
-	display: block;
-	min-width: 15em;
-}
-/* ckeditor5-table/theme/table.css */
-.ck-content .table {
-	margin: 1em auto;
-	display: table;
-}
-/* ckeditor5-table/theme/table.css */
-.ck-content .table table {
-	border-collapse: collapse;
-	border-spacing: 0;
-	width: 100%;
-	height: 100%;
-	border: 1px double hsl(0, 0%, 70%);
-}
-/* ckeditor5-table/theme/table.css */
-.ck-content .table table td,
-.ck-content .table table th {
-	min-width: 2em;
-	padding: .4em;
-	border-color: hsl(0, 0%, 75%);
-}
-/* ckeditor5-table/theme/table.css */
-.ck-content .table table th {
-	font-weight: bold;
-	background: hsla(0, 0%, 0%, 5%);
-}
 /* ckeditor5-list/theme/todolist.css */
 .ck-content .todo-list {
 	list-style: none;
@@ -275,6 +131,95 @@ 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-basic-styles/theme/code.css */
+.ck-content code {
+	background-color: hsla(0, 0%, 78%, 0.3);
+	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;
+	display: table;
+}
+/* ckeditor5-table/theme/table.css */
+.ck-content .table table {
+	border-collapse: collapse;
+	border-spacing: 0;
+	width: 100%;
+	height: 100%;
+	border: 1px double hsl(0, 0%, 70%);
+}
+/* ckeditor5-table/theme/table.css */
+.ck-content .table table td,
+.ck-content .table table th {
+	min-width: 2em;
+	padding: .4em;
+	border: 1px solid hsl(0, 0%, 75%);
+}
+/* ckeditor5-table/theme/table.css */
+.ck-content .table table th {
+	font-weight: bold;
+	background: hsla(0, 0%, 0%, 5%);
+}
+/* ckeditor5-table/theme/table.css */
+.ck-content[dir="rtl"] .table th {
+	text-align: right;
+}
+/* ckeditor5-table/theme/table.css */
+.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;
@@ -312,17 +257,89 @@ Below there is a full list of content styles used by the editor features. You ca
 	color: var(--ck-highlight-pen-green);
 	background-color: transparent;
 }
-/* ckeditor5-horizontal-line/theme/horizontalline.css */
-.ck-content hr {
-	border-width: 1px 0 0;
-	border-style: solid;
-	border-color: hsl(0, 0%, 37%);
-	margin: 0;
+/* 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;
+	clear: both;
+	padding: 5px 0;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+/* ckeditor5-page-break/theme/pagebreak.css */
+.ck-content .page-break::after {
+	content: '';
+	position: absolute;
+	border-bottom: 2px dashed hsl(0, 0%, 77%);
+	width: 100%;
+}
+/* ckeditor5-page-break/theme/pagebreak.css */
+.ck-content .page-break__label {
+	position: relative;
+	z-index: 1;
+	padding: .3em .6em;
+	display: block;
+	text-transform: uppercase;
+	border: 1px solid hsl(0, 0%, 77%);
+	border-radius: 2px;
+	font-family: Helvetica, Arial, Tahoma, Verdana, Sans-Serif;
+	font-size: 0.75em;
+	font-weight: bold;
+	color: hsl(0, 0%, 20%);
+	background: hsl(0, 0%, 100%);
+	box-shadow: 2px 2px 1px hsla(0, 0%, 0%, 0.15);
+	-webkit-user-select: none;
+	-moz-user-select: none;
+	-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-code-block/theme/codeblock.css */
 .ck-content pre {
 	padding: 1em;
-	color: #353535;
+	color: hsl(0, 0%, 20.8%);
 	background: hsla(0, 0%, 78%, 0.3);
 	border: 1px solid hsl(0, 0%, 77%);
 	border-radius: 2px;
@@ -339,6 +356,13 @@ 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;
+}
 @media print {
 	/* ckeditor5-page-break/theme/pagebreak.css */
 	.ck-content .page-break {

+ 24 - 24
package.json

@@ -82,38 +82,38 @@
     "@ckeditor/ckeditor5-dev-tests": "^20.0.1",
     "@ckeditor/ckeditor5-dev-utils": "^20.0.0",
     "@ckeditor/ckeditor5-dev-webpack-plugin": "^20.0.0",
-    "@ckeditor/ckeditor5-inspector": "^2.0.0",
-    "@ckeditor/ckeditor5-react": "^1.1.3",
+    "@ckeditor/ckeditor5-inspector": "^2.1.0",
+    "@ckeditor/ckeditor5-react": "^2.1.0",
     "@ckeditor/ckeditor5-real-time-collaboration": "^19.0.2",
     "@ckeditor/ckeditor5-track-changes": "^19.1.0",
-    "@wiris/mathtype-ckeditor5": "^7.19.0",
+    "@wiris/mathtype-ckeditor5": "^7.20.0",
     "babel-standalone": "^6.26.0",
     "coveralls": "^3.1.0",
-    "css-loader": "^1.0.0",
-    "eslint": "^5.5.0",
-    "eslint-config-ckeditor5": "^2.0.0",
-    "glob": "^7.1.2",
-    "http-server": "^0.12.1",
-    "husky": "^1.3.1",
-    "lint-staged": "^7.0.0",
-    "mini-css-extract-plugin": "^0.4.0",
+    "css-loader": "^3.5.3",
+    "eslint": "^7.1.0",
+    "eslint-config-ckeditor5": "^2.1.0",
+    "glob": "^7.1.6",
+    "http-server": "^0.12.3",
+    "husky": "^4.2.5",
+    "lint-staged": "^10.2.6",
+    "mini-css-extract-plugin": "^0.9.0",
     "minimatch": "^3.0.4",
     "nyc": "^15.0.1",
     "postcss-loader": "^3.0.0",
-    "progress-bar-webpack-plugin": "^1.12.1",
-    "raw-loader": "^3.1.0",
-    "react": "^16.9.0",
-    "react-dom": "^16.9.0",
-    "style-loader": "^1.0.0",
-    "stylelint": "^11.1.1",
-    "stylelint-config-ckeditor5": "^1.0.0",
-    "svgo": "^1.1.0",
-    "terser-webpack-plugin": "^2.2.1",
+    "progress-bar-webpack-plugin": "^2.1.0",
+    "raw-loader": "^4.0.1",
+    "react": "^16.13.1",
+    "react-dom": "^16.13.1",
+    "style-loader": "^1.2.1",
+    "stylelint": "^13.5.0",
+    "stylelint-config-ckeditor5": "^1.0.3",
+    "svgo": "^1.3.2",
+    "terser-webpack-plugin": "^3.0.2",
     "umberto": "^1.3.2",
-    "webpack": "^4.39.1"
+    "webpack": "^4.43.0"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",
@@ -146,7 +146,6 @@
     "postchangelog": "node ./scripts/release/update-utils-version.js",
     "release:bump-version": "node ./scripts/release/bump-versions.js",
     "release:publish": "node ./scripts/release/publish.js",
-    "release:stable-branches": "bash ./scripts/update-stable-branches.sh",
     "switch-to-dev-dev": "sh ./scripts/switch-to-dev-dev.sh",
     "clean-up-svg-icons": "sh ./scripts/clean-up-svg-icons.sh"
   },
@@ -163,7 +162,8 @@
     "packages/*/node_modules/**",
     "packages/*/build/**",
     "packages/*/src/lib/**",
-    "coverage/**"
+    "coverage/**",
+    "external/**"
   ],
   "workspaces": {
     "packages": [

+ 1 - 1
packages/ckeditor-cloud-services-core/package.json

@@ -10,7 +10,7 @@
     "@ckeditor/ckeditor5-utils": "^19.0.1"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

+ 2 - 2
packages/ckeditor5-adapter-ckfinder/lang/translations/ug.po

@@ -12,9 +12,9 @@
 #
 msgid ""
 msgstr ""
-"Language-Team: Uighur (https://www.transifex.com/ckeditor/teams/11143/ug/)\n"
+"Language-Team: Uyghur (https://www.transifex.com/ckeditor/teams/11143/ug/)\n"
 "Language: ug\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
 msgctxt "A generic error message displayed on upload failure. The file name is concatenated to this text."
 msgid "Cannot upload file:"

+ 1 - 1
packages/ckeditor5-adapter-ckfinder/package.json

@@ -26,7 +26,7 @@
     "@ckeditor/ckeditor5-undo": "^19.0.1"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

+ 1 - 1
packages/ckeditor5-alignment/package.json

@@ -26,7 +26,7 @@
     "@ckeditor/ckeditor5-utils": "^19.0.1"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

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

@@ -81,7 +81,7 @@ ClassicEditor
 
 ## Creating custom autoformatters
 
-The {@link module:autoformat/autoformat~Autoformat} feature bases on {@link module:autoformat/blockautoformatediting~BlockAutoformatEditing} and {@link module:autoformat/inlineautoformatediting~InlineAutoformatEditing} tools to create the autoformatters mentioned above.
+The {@link module:autoformat/autoformat~Autoformat} feature bases on {@link module:autoformat/blockautoformatediting~blockAutoformatEditing} and {@link module:autoformat/inlineautoformatediting~inlineAutoformatEditing} tools to create the autoformatters mentioned above.
 
 You can use these tools to create your own autoformatters. Check the [`Autoformat` feature's code](https://github.com/ckeditor/ckeditor5/blob/master/packages/ckeditor5-autoformat/src/autoformat.js) as an example.
 

+ 1 - 1
packages/ckeditor5-autoformat/package.json

@@ -26,7 +26,7 @@
     "@ckeditor/ckeditor5-undo": "^19.0.1"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

+ 14 - 27
packages/ckeditor5-autoformat/src/autoformat.js

@@ -7,8 +7,8 @@
  * @module autoformat/autoformat
  */
 
-import BlockAutoformatEditing from './blockautoformatediting';
-import InlineAutoformatEditing from './inlineautoformatediting';
+import blockAutoformatEditing from './blockautoformatediting';
+import inlineAutoformatEditing from './inlineautoformatediting';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 
 /**
@@ -51,13 +51,11 @@ export default class Autoformat extends Plugin {
 		const commands = this.editor.commands;
 
 		if ( commands.get( 'bulletedList' ) ) {
-			// eslint-disable-next-line no-new
-			new BlockAutoformatEditing( this.editor, /^[*-]\s$/, 'bulletedList' );
+			blockAutoformatEditing( this.editor, this, /^[*-]\s$/, 'bulletedList' );
 		}
 
 		if ( commands.get( 'numberedList' ) ) {
-			// eslint-disable-next-line no-new
-			new BlockAutoformatEditing( this.editor, /^1[.|)]\s$/, 'numberedList' );
+			blockAutoformatEditing( this.editor, this, /^1[.|)]\s$/, 'numberedList' );
 		}
 	}
 
@@ -80,39 +78,31 @@ export default class Autoformat extends Plugin {
 		const commands = this.editor.commands;
 
 		if ( commands.get( 'bold' ) ) {
-			/* eslint-disable no-new */
 			const boldCallback = getCallbackFunctionForInlineAutoformat( this.editor, 'bold' );
 
-			new InlineAutoformatEditing( this.editor, /(\*\*)([^*]+)(\*\*)$/g, boldCallback );
-			new InlineAutoformatEditing( this.editor, /(__)([^_]+)(__)$/g, boldCallback );
-			/* eslint-enable no-new */
+			inlineAutoformatEditing( this.editor, this, /(\*\*)([^*]+)(\*\*)$/g, boldCallback );
+			inlineAutoformatEditing( this.editor, this, /(__)([^_]+)(__)$/g, boldCallback );
 		}
 
 		if ( commands.get( 'italic' ) ) {
-			/* eslint-disable no-new */
 			const italicCallback = getCallbackFunctionForInlineAutoformat( this.editor, 'italic' );
 
 			// The italic autoformatter cannot be triggered by the bold markers, so we need to check the
 			// text before the pattern (e.g. `(?:^|[^\*])`).
-			new InlineAutoformatEditing( this.editor, /(?:^|[^*])(\*)([^*_]+)(\*)$/g, italicCallback );
-			new InlineAutoformatEditing( this.editor, /(?:^|[^_])(_)([^_]+)(_)$/g, italicCallback );
-			/* eslint-enable no-new */
+			inlineAutoformatEditing( this.editor, this, /(?:^|[^*])(\*)([^*_]+)(\*)$/g, italicCallback );
+			inlineAutoformatEditing( this.editor, this, /(?:^|[^_])(_)([^_]+)(_)$/g, italicCallback );
 		}
 
 		if ( commands.get( 'code' ) ) {
-			/* eslint-disable no-new */
 			const codeCallback = getCallbackFunctionForInlineAutoformat( this.editor, 'code' );
 
-			new InlineAutoformatEditing( this.editor, /(`)([^`]+)(`)$/g, codeCallback );
-			/* eslint-enable no-new */
+			inlineAutoformatEditing( this.editor, this, /(`)([^`]+)(`)$/g, codeCallback );
 		}
 
 		if ( commands.get( 'strikethrough' ) ) {
-			/* eslint-disable no-new */
 			const strikethroughCallback = getCallbackFunctionForInlineAutoformat( this.editor, 'strikethrough' );
 
-			new InlineAutoformatEditing( this.editor, /(~~)([^~]+)(~~)$/g, strikethroughCallback );
-			/* eslint-enable no-new */
+			inlineAutoformatEditing( this.editor, this, /(~~)([^~]+)(~~)$/g, strikethroughCallback );
 		}
 	}
 
@@ -137,8 +127,7 @@ export default class Autoformat extends Plugin {
 					const level = commandValue[ 7 ];
 					const pattern = new RegExp( `^(#{${ level }})\\s$` );
 
-					// eslint-disable-next-line no-new
-					new BlockAutoformatEditing( this.editor, pattern, () => {
+					blockAutoformatEditing( this.editor, this, pattern, () => {
 						if ( !command.isEnabled ) {
 							return false;
 						}
@@ -159,8 +148,7 @@ export default class Autoformat extends Plugin {
 	 */
 	_addBlockQuoteAutoformats() {
 		if ( this.editor.commands.get( 'blockQuote' ) ) {
-			// eslint-disable-next-line no-new
-			new BlockAutoformatEditing( this.editor, /^>\s$/, 'blockQuote' );
+			blockAutoformatEditing( this.editor, this, /^>\s$/, 'blockQuote' );
 		}
 	}
 
@@ -174,13 +162,12 @@ export default class Autoformat extends Plugin {
 	 */
 	_addCodeBlockAutoformats() {
 		if ( this.editor.commands.get( 'codeBlock' ) ) {
-			// eslint-disable-next-line no-new
-			new BlockAutoformatEditing( this.editor, /^```$/, 'codeBlock' );
+			blockAutoformatEditing( this.editor, this, /^```$/, 'codeBlock' );
 		}
 	}
 }
 
-// Helper function for getting `InlineAutoformatEditing` callbacks that checks if command is enabled.
+// Helper function for getting `inlineAutoformatEditing` callbacks that checks if command is enabled.
 //
 // @param {module:core/editor/editor~Editor} editor
 // @param {String} attributeKey

+ 82 - 91
packages/ckeditor5-autoformat/src/blockautoformatediting.js

@@ -2,11 +2,6 @@
  * @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 autoformat/blockautoformatediting
- */
-
 import LiveRange from '@ckeditor/ckeditor5-engine/src/model/liverange';
 
 /**
@@ -16,108 +11,104 @@ import LiveRange from '@ckeditor/ckeditor5-engine/src/model/liverange';
  * The autoformatting operation is integrated with the undo manager,
  * so the autoformatting step can be undone if the user's intention was not to format the text.
  *
- * See the constructors documentation to learn how to create custom inline autoformatters. You can also use
+ * See the {@link module:autoformat/blockautoformatediting~blockAutoformatEditing `blockAutoformatEditing`} documentation
+ * to learn how to create custom block autoformatters. You can also use
  * the {@link module:autoformat/autoformat~Autoformat} feature which enables a set of default autoformatters
  * (lists, headings, bold and italic).
+ *
+ * @module autoformat/blockautoformatediting
  */
-export default class BlockAutoformatEditing {
-	/**
-	 * @inheritDoc
-	 */
-	static get pluginName() {
-		return 'BlockAutoformatEditing';
+
+/**
+ * Creates a listener triggered on {@link module:engine/model/document~Document#event:change:data `change:data`} event in the document.
+ * Calls the callback when inserted text matches the regular expression or the command name
+ * if provided instead of the callback.
+ *
+ * Examples of usage:
+ *
+ * To convert a paragraph to heading 1 when `- ` is typed, using just the command name:
+ *
+ *		blockAutoformatEditing( editor, plugin, /^\- $/, 'heading1' );
+ *
+ * To convert a paragraph to heading 1 when `- ` is typed, using just the callback:
+ *
+ *		blockAutoformatEditing( editor, plugin, /^\- $/, ( context ) => {
+ *			const { match } = context;
+ *			const headingLevel = match[ 1 ].length;
+ *
+ *			editor.execute( 'heading', {
+ *				formatId: `heading${ headingLevel }`
+ *			} );
+ * 		} );
+ *
+ * @param {module:core/editor/editor~Editor} editor The editor instance.
+ * @param {module:autoformat/autoformat~Autoformat} plugin The autoformat plugin instance.
+ * @param {RegExp} pattern The regular expression to execute on just inserted text.
+ * @param {Function|String} callbackOrCommand The callback to execute or the command to run when the text is matched.
+ * In case of providing the callback, it receives the following parameter:
+ * * {Object} match RegExp.exec() result of matching the pattern to inserted text.
+ */
+export default function blockAutoformatEditing( editor, plugin, pattern, callbackOrCommand ) {
+	let callback;
+	let command = null;
+
+	if ( typeof callbackOrCommand == 'function' ) {
+		callback = callbackOrCommand;
+	} else {
+		// We assume that the actual command name was provided.
+		command = editor.commands.get( callbackOrCommand );
+
+		callback = () => {
+			editor.execute( callbackOrCommand );
+		};
 	}
 
-	/**
-	 * Creates a listener triggered on `change` event in the document.
-	 * Calls the callback when inserted text matches the regular expression or the command name
-	 * if provided instead of the callback.
-	 *
-	 * Examples of usage:
-	 *
-	 * To convert a paragraph to heading 1 when `- ` is typed, using just the command name:
-	 *
-	 *		new BlockAutoformatEditing( editor, /^\- $/, 'heading1' );
-	 *
-	 * To convert a paragraph to heading 1 when `- ` is typed, using just the callback:
-	 *
-	 *		new BlockAutoformatEditing( editor, /^\- $/, ( context ) => {
-	 *			const { match } = context;
-	 *			const headingLevel = match[ 1 ].length;
-	 *
-	 *			editor.execute( 'heading', {
-	 *				formatId: `heading${ headingLevel }`
-	 *			} );
-	 * 		} );
-	 *
-	 * @param {module:core/editor/editor~Editor} editor The editor instance.
-	 * @param {RegExp} pattern The regular expression to execute on just inserted text.
-	 * @param {Function|String} callbackOrCommand The callback to execute or the command to run when the text is matched.
-	 * In case of providing the callback, it receives the following parameter:
-	 * * {Object} match RegExp.exec() result of matching the pattern to inserted text.
-	 */
-	constructor( editor, pattern, callbackOrCommand ) {
-		let callback;
-		let command = null;
-
-		if ( typeof callbackOrCommand == 'function' ) {
-			callback = callbackOrCommand;
-		} else {
-			// We assume that the actual command name was provided.
-			command = editor.commands.get( callbackOrCommand );
-
-			callback = () => {
-				editor.execute( callbackOrCommand );
-			};
+	editor.model.document.on( 'change:data', ( evt, batch ) => {
+		if ( command && !command.isEnabled || !plugin.isEnabled ) {
+			return;
 		}
 
-		editor.model.document.on( 'change', ( evt, batch ) => {
-			if ( command && !command.isEnabled ) {
-				return;
-			}
-
-			if ( batch.type == 'transparent' ) {
-				return;
-			}
+		if ( batch.type == 'transparent' ) {
+			return;
+		}
 
-			const changes = Array.from( editor.model.document.differ.getChanges() );
-			const entry = changes[ 0 ];
+		const changes = Array.from( editor.model.document.differ.getChanges() );
+		const entry = changes[ 0 ];
 
-			// Typing is represented by only a single change.
-			if ( changes.length != 1 || entry.type !== 'insert' || entry.name != '$text' || entry.length != 1 ) {
-				return;
-			}
+		// Typing is represented by only a single change.
+		if ( changes.length != 1 || entry.type !== 'insert' || entry.name != '$text' || entry.length != 1 ) {
+			return;
+		}
 
-			const blockToFormat = entry.position.parent;
+		const blockToFormat = entry.position.parent;
 
-			// Block formatting should trigger only if the entire content of a paragraph is a single text node... (see ckeditor5#5671).
-			if ( !blockToFormat.is( 'paragraph' ) || blockToFormat.childCount !== 1 ) {
-				return;
-			}
+		// Block formatting should trigger only if the entire content of a paragraph is a single text node... (see ckeditor5#5671).
+		if ( !blockToFormat.is( 'paragraph' ) || blockToFormat.childCount !== 1 ) {
+			return;
+		}
 
-			const match = pattern.exec( blockToFormat.getChild( 0 ).data );
+		const match = pattern.exec( blockToFormat.getChild( 0 ).data );
 
-			// ...and this text node's data match the pattern.
-			if ( !match ) {
-				return;
-			}
+		// ...and this text node's data match the pattern.
+		if ( !match ) {
+			return;
+		}
 
-			// Use enqueueChange to create new batch to separate typing batch from the auto-format changes.
-			editor.model.enqueueChange( writer => {
-				// Matched range.
-				const start = writer.createPositionAt( blockToFormat, 0 );
-				const end = writer.createPositionAt( blockToFormat, match[ 0 ].length );
-				const range = new LiveRange( start, end );
+		// Use enqueueChange to create new batch to separate typing batch from the auto-format changes.
+		editor.model.enqueueChange( writer => {
+			// Matched range.
+			const start = writer.createPositionAt( blockToFormat, 0 );
+			const end = writer.createPositionAt( blockToFormat, match[ 0 ].length );
+			const range = new LiveRange( start, end );
 
-				const wasChanged = callback( { match } );
+			const wasChanged = callback( { match } );
 
-				// Remove matched text.
-				if ( wasChanged !== false ) {
-					writer.remove( range );
-				}
+			// Remove matched text.
+			if ( wasChanged !== false ) {
+				writer.remove( range );
+			}
 
-				range.detach();
-			} );
+			range.detach();
 		} );
-	}
+	} );
 }

+ 165 - 175
packages/ckeditor5-autoformat/src/inlineautoformatediting.js

@@ -3,12 +3,6 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-/**
- * @module autoformat/inlineautoformatediting
- */
-
-import getLastTextLine from '@ckeditor/ckeditor5-typing/src/utils/getlasttextline';
-
 /**
  * The inline autoformatting engine. It allows to format various inline patterns. For example,
  * it can be configured to make "foo" bold when typed `**foo**` (the `**` markers will be removed).
@@ -16,199 +10,171 @@ import getLastTextLine from '@ckeditor/ckeditor5-typing/src/utils/getlasttextlin
  * The autoformatting operation is integrated with the undo manager,
  * so the autoformatting step can be undone if the user's intention was not to format the text.
  *
- * See the constructors documentation to learn how to create custom inline autoformatters. You can also use
+ * See the {@link module:autoformat/inlineautoformatediting~inlineAutoformatEditing `inlineAutoformatEditing`} documentation
+ * to learn how to create custom inline autoformatters. You can also use
  * the {@link module:autoformat/autoformat~Autoformat} feature which enables a set of default autoformatters
  * (lists, headings, bold and italic).
+ *
+ * @module autoformat/inlineautoformatediting
  */
-export default class InlineAutoformatEditing {
-	/**
-	 * @inheritDoc
-	 */
-	static get pluginName() {
-		return 'InlineAutoformatEditing';
-	}
 
-	/**
-	 * Enables autoformatting mechanism for a given {@link module:core/editor/editor~Editor}.
-	 *
-	 * It formats the matched text by applying the given model attribute or by running the provided formatting callback.
-	 * On every change applied to the model the autoformatting engine checks the text on the left of the selection
-	 * and executes the provided action if the text matches given criteria (regular expression or callback).
-	 *
-	 * @param {module:core/editor/editor~Editor} editor The editor instance.
-	 * @param {Function|RegExp} testRegexpOrCallback The regular expression or callback to execute on text.
-	 * Provided regular expression *must* have three capture groups. The first and the third capture group
-	 * should match opening and closing delimiters. The second capture group should match the text to format.
-	 *
-	 *		// Matches the `**bold text**` pattern.
-	 *		// There are three capturing groups:
-	 *		// - The first to match the starting `**` delimiter.
-	 *		// - The second to match the text to format.
-	 *		// - The third to match the ending `**` delimiter.
-	 *		new InlineAutoformatEditing( editor, /(\*\*)([^\*]+?)(\*\*)$/g, 'bold' );
-	 *
-	 * When a function is provided instead of the regular expression, it will be executed with the text to match as a parameter.
-	 * The function should return proper "ranges" to delete and format.
-	 *
-	 *		{
-	 *			remove: [
-	 *				[ 0, 1 ],	// Remove the first letter from the given text.
-	 *				[ 5, 6 ]	// Remove the 6th letter from the given text.
-	 *			],
-	 *			format: [
-	 *				[ 1, 5 ]	// Format all letters from 2nd to 5th.
-	 *			]
-	 *		}
-	 *
-	 * @param {Function|String} attributeOrCallback The name of attribute to apply on matching text or a callback for manual
-	 * formatting. If callback is passed it should return `false` if changes should not be applied (e.g. if a command is disabled).
-	 *
-	 *		// Use attribute name:
-	 *		new InlineAutoformatEditing( editor, /(\*\*)([^\*]+?)(\*\*)$/g, 'bold' );
-	 *
-	 *		// Use formatting callback:
-	 *		new InlineAutoformatEditing( editor, /(\*\*)([^\*]+?)(\*\*)$/g, ( writer, rangesToFormat ) => {
-	 *			const command = editor.commands.get( 'bold' );
-	 *
-	 *			if ( !command.isEnabled ) {
-	 *				return false;
-	 *			}
-	 *
-	 *			const validRanges = editor.model.schema.getValidRanges( rangesToFormat, 'bold' );
-	 *
-	 *			for ( let range of validRanges ) {
-	 *				writer.setAttribute( 'bold', true, range );
-	 *			}
-	 *		} );
-	 */
-	constructor( editor, testRegexpOrCallback, attributeOrCallback ) {
-		let regExp;
-		let attributeKey;
-		let testCallback;
-		let formatCallback;
-
-		if ( testRegexpOrCallback instanceof RegExp ) {
-			regExp = testRegexpOrCallback;
-		} else {
-			testCallback = testRegexpOrCallback;
-		}
+/**
+ * Enables autoformatting mechanism for a given {@link module:core/editor/editor~Editor}.
+ *
+ * It formats the matched text by applying the given model attribute or by running the provided formatting callback.
+ * On every {@link module:engine/model/document~Document#event:change:data data change} in the model document
+ * the autoformatting engine checks the text on the left of the selection
+ * and executes the provided action if the text matches given criteria (regular expression or callback).
+ *
+ * @param {module:core/editor/editor~Editor} editor The editor instance.
+ * @param {module:autoformat/autoformat~Autoformat} plugin The autoformat plugin instance.
+ * @param {Function|RegExp} testRegexpOrCallback The regular expression or callback to execute on text.
+ * Provided regular expression *must* have three capture groups. The first and the third capture group
+ * should match opening and closing delimiters. The second capture group should match the text to format.
+ *
+ *		// Matches the `**bold text**` pattern.
+ *		// There are three capturing groups:
+ *		// - The first to match the starting `**` delimiter.
+ *		// - The second to match the text to format.
+ *		// - The third to match the ending `**` delimiter.
+ *		inlineAutoformatEditing( editor, plugin, /(\*\*)([^\*]+?)(\*\*)$/g, formatCallback );
+ *
+ * When a function is provided instead of the regular expression, it will be executed with the text to match as a parameter.
+ * The function should return proper "ranges" to delete and format.
+ *
+ *		{
+ *			remove: [
+ *				[ 0, 1 ],	// Remove the first letter from the given text.
+ *				[ 5, 6 ]	// Remove the 6th letter from the given text.
+ *			],
+ *			format: [
+ *				[ 1, 5 ]	// Format all letters from 2nd to 5th.
+ *			]
+ *		}
+ *
+ * @param {Function} formatCallback A callback to apply actual formatting.
+ * It should return `false` if changes should not be applied (e.g. if a command is disabled).
+ *
+ *		inlineAutoformatEditing( editor, plugin, /(\*\*)([^\*]+?)(\*\*)$/g, ( writer, rangesToFormat ) => {
+ *			const command = editor.commands.get( 'bold' );
+ *
+ *			if ( !command.isEnabled ) {
+ *				return false;
+ *			}
+ *
+ *			const validRanges = editor.model.schema.getValidRanges( rangesToFormat, 'bold' );
+ *
+ *			for ( let range of validRanges ) {
+ *				writer.setAttribute( 'bold', true, range );
+ *			}
+ *		} );
+ */
+export default function inlineAutoformatEditing( editor, plugin, testRegexpOrCallback, formatCallback ) {
+	let regExp;
+	let testCallback;
+
+	if ( testRegexpOrCallback instanceof RegExp ) {
+		regExp = testRegexpOrCallback;
+	} else {
+		testCallback = testRegexpOrCallback;
+	}
 
-		if ( typeof attributeOrCallback == 'string' ) {
-			attributeKey = attributeOrCallback;
-		} else {
-			formatCallback = attributeOrCallback;
-		}
+	// A test callback run on changed text.
+	testCallback = testCallback || ( text => {
+		let result;
+		const remove = [];
+		const format = [];
 
-		// A test callback run on changed text.
-		testCallback = testCallback || ( text => {
-			let result;
-			const remove = [];
-			const format = [];
-
-			while ( ( result = regExp.exec( text ) ) !== null ) {
-				// There should be full match and 3 capture groups.
-				if ( result && result.length < 4 ) {
-					break;
-				}
-
-				let {
-					index,
-					'1': leftDel,
-					'2': content,
-					'3': rightDel
-				} = result;
-
-				// Real matched string - there might be some non-capturing groups so we need to recalculate starting index.
-				const found = leftDel + content + rightDel;
-				index += result[ 0 ].length - found.length;
-
-				// Start and End offsets of delimiters to remove.
-				const delStart = [
-					index,
-					index + leftDel.length
-				];
-				const delEnd = [
-					index + leftDel.length + content.length,
-					index + leftDel.length + content.length + rightDel.length
-				];
-
-				remove.push( delStart );
-				remove.push( delEnd );
-
-				format.push( [ index + leftDel.length, index + leftDel.length + content.length ] );
+		while ( ( result = regExp.exec( text ) ) !== null ) {
+			// There should be full match and 3 capture groups.
+			if ( result && result.length < 4 ) {
+				break;
 			}
 
-			return {
-				remove,
-				format
-			};
-		} );
+			let {
+				index,
+				'1': leftDel,
+				'2': content,
+				'3': rightDel
+			} = result;
+
+			// Real matched string - there might be some non-capturing groups so we need to recalculate starting index.
+			const found = leftDel + content + rightDel;
+			index += result[ 0 ].length - found.length;
+
+			// Start and End offsets of delimiters to remove.
+			const delStart = [
+				index,
+				index + leftDel.length
+			];
+			const delEnd = [
+				index + leftDel.length + content.length,
+				index + leftDel.length + content.length + rightDel.length
+			];
+
+			remove.push( delStart );
+			remove.push( delEnd );
+
+			format.push( [ index + leftDel.length, index + leftDel.length + content.length ] );
+		}
 
-		// A format callback run on matched text.
-		formatCallback = formatCallback || ( ( writer, rangesToFormat ) => {
-			const validRanges = editor.model.schema.getValidRanges( rangesToFormat, attributeKey );
+		return {
+			remove,
+			format
+		};
+	} );
 
-			for ( const range of validRanges ) {
-				writer.setAttribute( attributeKey, true, range );
-			}
+	editor.model.document.on( 'change:data', ( evt, batch ) => {
+		if ( batch.type == 'transparent' || !plugin.isEnabled ) {
+			return;
+		}
 
-			// After applying attribute to the text, remove given attribute from the selection.
-			// This way user is able to type a text without attribute used by auto formatter.
-			writer.removeSelectionAttribute( attributeKey );
-		} );
+		const model = editor.model;
+		const selection = model.document.selection;
 
-		editor.model.document.on( 'change', ( evt, batch ) => {
-			if ( batch.type == 'transparent' ) {
-				return;
-			}
+		// Do nothing if selection is not collapsed.
+		if ( !selection.isCollapsed ) {
+			return;
+		}
 
-			const model = editor.model;
-			const selection = model.document.selection;
+		const changes = Array.from( model.document.differ.getChanges() );
+		const entry = changes[ 0 ];
 
-			// Do nothing if selection is not collapsed.
-			if ( !selection.isCollapsed ) {
-				return;
-			}
+		// Typing is represented by only a single change.
+		if ( changes.length != 1 || entry.type !== 'insert' || entry.name != '$text' || entry.length != 1 ) {
+			return;
+		}
 
-			const changes = Array.from( model.document.differ.getChanges() );
-			const entry = changes[ 0 ];
+		const focus = selection.focus;
+		const block = focus.parent;
+		const { text, range } = getTextAfterCode( model.createRange( model.createPositionAt( block, 0 ), focus ), model );
+		const testOutput = testCallback( text );
+		const rangesToFormat = testOutputToRanges( range.start, testOutput.format, model );
+		const rangesToRemove = testOutputToRanges( range.start, testOutput.remove, model );
 
-			// Typing is represented by only a single change.
-			if ( changes.length != 1 || entry.type !== 'insert' || entry.name != '$text' || entry.length != 1 ) {
-				return;
-			}
+		if ( !( rangesToFormat.length && rangesToRemove.length ) ) {
+			return;
+		}
 
-			const focus = selection.focus;
-			const block = focus.parent;
-			const { text, range } = getLastTextLine( model.createRange( model.createPositionAt( block, 0 ), focus ), model );
-			const testOutput = testCallback( text );
-			const rangesToFormat = testOutputToRanges( range.start, testOutput.format, model );
-			const rangesToRemove = testOutputToRanges( range.start, testOutput.remove, model );
+		// Use enqueueChange to create new batch to separate typing batch from the auto-format changes.
+		model.enqueueChange( writer => {
+			// Apply format.
+			const hasChanged = formatCallback( writer, rangesToFormat );
 
-			if ( !( rangesToFormat.length && rangesToRemove.length ) ) {
+			// Strict check on `false` to have backward compatibility (when callbacks were returning `undefined`).
+			if ( hasChanged === false ) {
 				return;
 			}
 
-			// Use enqueueChange to create new batch to separate typing batch from the auto-format changes.
-			model.enqueueChange( writer => {
-				// Apply format.
-				const hasChanged = formatCallback( writer, rangesToFormat );
-
-				// Strict check on `false` to have backward compatibility (when callbacks were returning `undefined`).
-				if ( hasChanged === false ) {
-					return;
-				}
-
-				// Remove delimiters - use reversed order to not mix the offsets while removing.
-				for ( const range of rangesToRemove.reverse() ) {
-					writer.remove( range );
-				}
-			} );
+			// Remove delimiters - use reversed order to not mix the offsets while removing.
+			for ( const range of rangesToRemove.reverse() ) {
+				writer.remove( range );
+			}
 		} );
-	}
+	} );
 }
 
-// Converts output of the test function provided to the InlineAutoformatEditing and converts it to the model ranges
+// Converts output of the test function provided to the inlineAutoformatEditing and converts it to the model ranges
 // inside provided block.
 //
 // @private
@@ -222,3 +188,27 @@ function testOutputToRanges( start, arrays, model ) {
 			return model.createRange( start.getShiftedBy( array[ 0 ] ), start.getShiftedBy( array[ 1 ] ) );
 		} );
 }
+
+// Returns the last text line after the last code element from the given range.
+// It is similar to {@link module:typing/utils/getlasttextline.getLastTextLine `getLastTextLine()`},
+// but it ignores any text before the last `code`.
+//
+// @param {module:engine/model/range~Range} range
+// @param {module:engine/model/model~Model} model
+// @returns {module:typing/utils/getlasttextline~LastTextLineData}
+function getTextAfterCode( range, model ) {
+	let start = range.start;
+
+	const text = Array.from( range.getItems() ).reduce( ( rangeText, node ) => {
+		// Trim text to a last occurrence of an inline element and update range start.
+		if ( !( node.is( 'text' ) || node.is( 'textProxy' ) ) || node.getAttribute( 'code' ) ) {
+			start = model.createPositionAfter( node );
+
+			return '';
+		}
+
+		return rangeText + node.data;
+	}, '' );
+
+	return { text, range: model.createRange( start, range.end ) };
+}

+ 146 - 0
packages/ckeditor5-autoformat/tests/autoformat.js

@@ -410,6 +410,152 @@ describe( 'Autoformat', () => {
 			expect( getData( model ) ).to.equal( '<paragraph>**foobar**[]</paragraph>' );
 		} );
 
+		it( 'should not format if the plugin is disabled', () => {
+			editor.plugins.get( 'Autoformat' ).forceDisabled( 'Test' );
+
+			setData( model, '<paragraph>**foobar*[]</paragraph>' );
+
+			model.change( writer => {
+				writer.insertText( '*', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).to.equal( '<paragraph>**foobar**[]</paragraph>' );
+		} );
+
+		describe( 'with code element', () => {
+			describe( 'should not format', () => {
+				it( '* inside', () => {
+					setData( model, '<paragraph><$text code="true">fo*obar[]</$text></paragraph>' );
+
+					model.change( writer => {
+						writer.insertText( '*', { code: true }, doc.selection.getFirstPosition() );
+					} );
+
+					expect( getData( model ) ).to
+						.equal( '<paragraph><$text code="true">fo*obar*[]</$text></paragraph>' );
+				} );
+
+				it( '__ inside', () => {
+					setData( model, '<paragraph><$text code="true">fo__obar_[]</$text></paragraph>' );
+
+					model.change( writer => {
+						writer.insertText( '_', { code: true }, doc.selection.getFirstPosition() );
+					} );
+
+					expect( getData( model ) ).to
+						.equal( '<paragraph><$text code="true">fo__obar__[]</$text></paragraph>' );
+				} );
+
+				it( '~~ inside', () => {
+					setData( model, '<paragraph><$text code="true">fo~~obar~[]</$text></paragraph>' );
+
+					model.change( writer => {
+						writer.insertText( '~', { code: true }, doc.selection.getFirstPosition() );
+					} );
+
+					expect( getData( model ) ).to
+						.equal( '<paragraph><$text code="true">fo~~obar~~[]</$text></paragraph>' );
+				} );
+
+				it( '` inside', () => {
+					setData( model, '<paragraph><$text code="true">fo`obar[]</$text></paragraph>' );
+
+					model.change( writer => {
+						writer.insertText( '`', { code: true }, doc.selection.getFirstPosition() );
+					} );
+
+					expect( getData( model ) ).to
+						.equal( '<paragraph><$text code="true">fo`obar`[]</$text></paragraph>' );
+				} );
+			} );
+
+			describe( 'should not format', () => {
+				it( '* across', () => {
+					setData( model, '<paragraph><$text code="true">fo*o</$text>bar[]</paragraph>' );
+
+					model.change( writer => {
+						writer.insertText( '*', doc.selection.getFirstPosition() );
+					} );
+
+					expect( getData( model ) ).to
+						.equal( '<paragraph><$text code="true">fo*o</$text>bar*[]</paragraph>' );
+				} );
+				it( '__ across', () => {
+					setData( model, '<paragraph><$text code="true">fo__o</$text>bar_[]</paragraph>' );
+
+					model.change( writer => {
+						writer.insertText( '_', doc.selection.getFirstPosition() );
+					} );
+
+					expect( getData( model ) ).to
+						.equal( '<paragraph><$text code="true">fo__o</$text>bar__[]</paragraph>' );
+				} );
+				it( '~~ across', () => {
+					setData( model, '<paragraph><$text code="true">fo~~o</$text>bar~[]</paragraph>' );
+
+					model.change( writer => {
+						writer.insertText( '~', doc.selection.getFirstPosition() );
+					} );
+
+					expect( getData( model ) ).to
+						.equal( '<paragraph><$text code="true">fo~~o</$text>bar~~[]</paragraph>' );
+				} );
+				it( '` across', () => {
+					setData( model, '<paragraph><$text code="true">fo`o</$text>bar[]</paragraph>' );
+
+					model.change( writer => {
+						writer.insertText( '`', doc.selection.getFirstPosition() );
+					} );
+
+					expect( getData( model ) ).to
+						.equal( '<paragraph><$text code="true">fo`o</$text>bar`[]</paragraph>' );
+				} );
+			} );
+
+			describe( 'should format', () => {
+				it( '* after', () => {
+					setData( model, '<paragraph><$text code="true">fo*o</$text>b*ar[]</paragraph>' );
+
+					model.change( writer => {
+						writer.insertText( '*', doc.selection.getFirstPosition() );
+					} );
+
+					expect( getData( model ) ).to
+						.equal( '<paragraph><$text code="true">fo*o</$text>b<$text italic="true">ar</$text>[]</paragraph>' );
+				} );
+				it( '__ after', () => {
+					setData( model, '<paragraph><$text code="true">fo__o</$text>b__ar_[]</paragraph>' );
+
+					model.change( writer => {
+						writer.insertText( '_', doc.selection.getFirstPosition() );
+					} );
+
+					expect( getData( model ) ).to
+						.equal( '<paragraph><$text code="true">fo__o</$text>b<$text bold="true">ar</$text>[]</paragraph>' );
+				} );
+				it( '~~ after', () => {
+					setData( model, '<paragraph><$text code="true">fo~~o</$text>b~~ar~[]</paragraph>' );
+
+					model.change( writer => {
+						writer.insertText( '~', doc.selection.getFirstPosition() );
+					} );
+
+					expect( getData( model ) ).to
+						.equal( '<paragraph><$text code="true">fo~~o</$text>b<$text strikethrough="true">ar</$text>[]</paragraph>' );
+				} );
+				it( '` after', () => {
+					setData( model, '<paragraph><$text code="true">fo`o</$text>b`ar[]</paragraph>' );
+
+					model.change( writer => {
+						writer.insertText( '`', doc.selection.getFirstPosition() );
+					} );
+
+					expect( getData( model ) ).to
+						.equal( '<paragraph><$text code="true">fo`o</$text>b<$text code="true">ar</$text>[]</paragraph>' );
+				} );
+			} );
+		} );
+
 		it( 'should work with <softBreak>s in paragraph', () => {
 			setData( model, '<paragraph>foo<softBreak></softBreak>**barbaz*[]</paragraph>' );
 			model.change( writer => {

+ 28 - 17
packages/ckeditor5-autoformat/tests/blockautoformatediting.js

@@ -4,7 +4,7 @@
  */
 
 import Autoformat from '../src/autoformat';
-import BlockAutoformatEditing from '../src/blockautoformatediting';
+import blockAutoformatEditing from '../src/blockautoformatediting';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import Enter from '@ckeditor/ckeditor5-enter/src/enter';
@@ -12,8 +12,8 @@ import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import Command from '@ckeditor/ckeditor5-core/src/command';
 
-describe( 'BlockAutoformatEditing', () => {
-	let editor, model, doc;
+describe( 'blockAutoformatEditing', () => {
+	let editor, model, doc, plugin;
 
 	testUtils.createSinonSandbox();
 
@@ -26,13 +26,10 @@ describe( 'BlockAutoformatEditing', () => {
 				editor = newEditor;
 				model = editor.model;
 				doc = model.document;
+				plugin = editor.plugins.get( 'Autoformat' );
 			} );
 	} );
 
-	it( 'should have pluginName', () => {
-		expect( BlockAutoformatEditing.pluginName ).to.equal( 'BlockAutoformatEditing' );
-	} );
-
 	describe( 'command name', () => {
 		it( 'should run a command when the pattern is matched', () => {
 			const spy = testUtils.sinon.spy();
@@ -40,7 +37,7 @@ describe( 'BlockAutoformatEditing', () => {
 
 			editor.commands.add( 'testCommand', testCommand );
 
-			new BlockAutoformatEditing( editor, /^[*]\s$/, 'testCommand' ); // eslint-disable-line no-new
+			blockAutoformatEditing( editor, plugin, /^[*]\s$/, 'testCommand' );
 
 			setData( model, '<paragraph>*[]</paragraph>' );
 
@@ -57,7 +54,7 @@ describe( 'BlockAutoformatEditing', () => {
 
 			editor.commands.add( 'testCommand', testCommand );
 
-			new BlockAutoformatEditing( editor, /^[*]\s$/, 'testCommand' ); // eslint-disable-line no-new
+			blockAutoformatEditing( editor, plugin, /^[*]\s$/, 'testCommand' );
 
 			setData( model, '<paragraph>*[]</paragraph>' );
 
@@ -79,7 +76,7 @@ describe( 'BlockAutoformatEditing', () => {
 
 			editor.commands.add( 'testCommand', testCommand );
 
-			new BlockAutoformatEditing( editor, /^[*]\s$/, 'testCommand' ); // eslint-disable-line no-new
+			blockAutoformatEditing( editor, plugin, /^[*]\s$/, 'testCommand' );
 
 			setData( model, '<paragraph>*[]</paragraph>' );
 
@@ -94,7 +91,7 @@ describe( 'BlockAutoformatEditing', () => {
 	describe( 'callback', () => {
 		it( 'should run callback when the pattern is matched', () => {
 			const spy = testUtils.sinon.spy();
-			new BlockAutoformatEditing( editor, /^[*]\s$/, spy ); // eslint-disable-line no-new
+			blockAutoformatEditing( editor, plugin, /^[*]\s$/, spy );
 
 			setData( model, '<paragraph>*[]</paragraph>' );
 			model.change( writer => {
@@ -104,9 +101,23 @@ describe( 'BlockAutoformatEditing', () => {
 			sinon.assert.calledOnce( spy );
 		} );
 
+		it( 'should not call the callback when the pattern is matched but the plugin is disabled', () => {
+			const callbackSpy = testUtils.sinon.spy().named( 'callback' );
+			blockAutoformatEditing( editor, plugin, /^[*]\s$/, callbackSpy );
+
+			plugin.isEnabled = false;
+
+			setData( model, '<paragraph>*[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
+			} );
+
+			sinon.assert.notCalled( callbackSpy );
+		} );
+
 		it( 'should ignore other delta operations', () => {
 			const spy = testUtils.sinon.spy();
-			new BlockAutoformatEditing( editor, /^[*]\s/, spy ); // eslint-disable-line no-new
+			blockAutoformatEditing( editor, plugin, /^[*]\s/, spy );
 
 			setData( model, '<paragraph>*[]</paragraph>' );
 			model.change( writer => {
@@ -118,7 +129,7 @@ describe( 'BlockAutoformatEditing', () => {
 
 		it( 'should stop if there is no text to run matching on', () => {
 			const spy = testUtils.sinon.spy();
-			new BlockAutoformatEditing( editor, /^[*]\s/, spy ); // eslint-disable-line no-new
+			blockAutoformatEditing( editor, plugin, /^[*]\s/, spy );
 
 			setData( model, '<paragraph>[]</paragraph>' );
 			model.change( writer => {
@@ -146,7 +157,7 @@ describe( 'BlockAutoformatEditing', () => {
 				} );
 
 			const spy = testUtils.sinon.spy();
-			new BlockAutoformatEditing( editor, /^[*]\s/, spy ); // eslint-disable-line no-new
+			blockAutoformatEditing( editor, plugin, /^[*]\s/, spy );
 
 			setData( model, '<paragraph>*<softBreak></softBreak>[]</paragraph>' );
 			model.change( writer => {
@@ -174,7 +185,7 @@ describe( 'BlockAutoformatEditing', () => {
 				} );
 
 			const spy = testUtils.sinon.spy();
-			new BlockAutoformatEditing( editor, /^[*]\s/, spy ); // eslint-disable-line no-new
+			blockAutoformatEditing( editor, plugin, /^[*]\s/, spy );
 
 			setData( model, '<paragraph>* <softBreak></softBreak>[]</paragraph>' );
 
@@ -186,7 +197,7 @@ describe( 'BlockAutoformatEditing', () => {
 		} );
 
 		it( 'should stop if callback returned false', () => {
-			new BlockAutoformatEditing( editor, /^[*]\s$/, () => false ); // eslint-disable-line no-new
+			blockAutoformatEditing( editor, plugin, /^[*]\s$/, () => false );
 
 			setData( model, '<paragraph>*[]</paragraph>' );
 			model.change( writer => {
@@ -199,7 +210,7 @@ describe( 'BlockAutoformatEditing', () => {
 
 	it( 'should ignore transparent batches', () => {
 		const spy = testUtils.sinon.spy();
-		new BlockAutoformatEditing( editor, /^[*]\s$/, spy ); // eslint-disable-line no-new
+		blockAutoformatEditing( editor, plugin, /^[*]\s$/, spy );
 
 		setData( model, '<paragraph>*[]</paragraph>' );
 		model.enqueueChange( 'transparent', writer => {

+ 35 - 25
packages/ckeditor5-autoformat/tests/inlineautoformatediting.js

@@ -4,19 +4,21 @@
  */
 
 import Autoformat from '../src/autoformat';
-import InlineAutoformatEditing from '../src/inlineautoformatediting';
+import inlineAutoformatEditing from '../src/inlineautoformatediting';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import Enter from '@ckeditor/ckeditor5-enter/src/enter';
 import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 
-describe( 'InlineAutoformatEditing', () => {
-	let editor, model, doc;
+describe( 'inlineAutoformatEditing', () => {
+	let editor, model, doc, plugin, formatSpy;
 
 	testUtils.createSinonSandbox();
 
 	beforeEach( () => {
+		formatSpy = testUtils.sinon.spy().named( 'formatCallback' );
+
 		return VirtualTestEditor
 			.create( {
 				plugins: [ Enter, Paragraph, Autoformat ]
@@ -25,59 +27,55 @@ describe( 'InlineAutoformatEditing', () => {
 				editor = newEditor;
 				model = editor.model;
 				doc = model.document;
+				plugin = editor.plugins.get( 'Autoformat' );
 
 				model.schema.extend( '$text', { allowAttributes: 'testAttribute' } );
 			} );
 	} );
 
-	it( 'should have pluginName', () => {
-		expect( InlineAutoformatEditing.pluginName ).to.equal( 'InlineAutoformatEditing' );
-	} );
-
-	describe( 'attribute', () => {
-		it( 'should stop early if there are less than 3 capture groups', () => {
-			new InlineAutoformatEditing( editor, /(\*)(.+?)\*/g, 'testAttribute' ); // eslint-disable-line no-new
+	describe( 'regExp', () => {
+		it( 'should not call the formatCallback if there are less than 3 capture groups', () => {
+			inlineAutoformatEditing( editor, plugin, /(\*)(.+?)\*/g, formatSpy );
 
 			setData( model, '<paragraph>*foobar[]</paragraph>' );
 			model.change( writer => {
 				writer.insertText( '*', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( model ) ).to.equal( '<paragraph>*foobar*[]</paragraph>' );
+			sinon.assert.notCalled( formatSpy );
 		} );
 
-		it( 'should apply an attribute when the pattern is matched', () => {
-			new InlineAutoformatEditing( editor, /(\*)(.+?)(\*)/g, 'testAttribute' ); // eslint-disable-line no-new
+		it( 'should call the formatCallback when the pattern is matched', () => {
+			inlineAutoformatEditing( editor, plugin, /(\*)(.+?)(\*)/g, formatSpy );
 
 			setData( model, '<paragraph>*foobar[]</paragraph>' );
 			model.change( writer => {
 				writer.insertText( '*', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( model ) ).to.equal( '<paragraph><$text testAttribute="true">foobar</$text>[]</paragraph>' );
+			sinon.assert.calledOnce( formatSpy );
 		} );
 
-		it( 'should stop early if selection is not collapsed', () => {
-			new InlineAutoformatEditing( editor, /(\*)(.+?)\*/g, 'testAttribute' ); // eslint-disable-line no-new
+		it( 'should not call the formatCallback if the selection is not collapsed', () => {
+			inlineAutoformatEditing( editor, plugin, /(\*)(.+?)\*/g, formatSpy );
 
 			setData( model, '<paragraph>*foob[ar]</paragraph>' );
 			model.change( writer => {
 				writer.insertText( '*', doc.selection.getFirstPosition() );
 			} );
 
-			expect( getData( model ) ).to.equal( '<paragraph>*foob*[ar]</paragraph>' );
+			sinon.assert.notCalled( formatSpy );
 		} );
 	} );
 
 	describe( 'callback', () => {
 		it( 'should stop when there are no format ranges returned from testCallback', () => {
-			const formatSpy = testUtils.sinon.spy();
 			const testStub = testUtils.sinon.stub().returns( {
 				format: [ [] ],
 				remove: []
 			} );
 
-			new InlineAutoformatEditing( editor, testStub, formatSpy ); // eslint-disable-line no-new
+			inlineAutoformatEditing( editor, plugin, testStub, formatSpy );
 
 			setData( model, '<paragraph>*[]</paragraph>' );
 			model.change( writer => {
@@ -88,13 +86,12 @@ describe( 'InlineAutoformatEditing', () => {
 		} );
 
 		it( 'should stop when there are no remove ranges returned from testCallback', () => {
-			const formatSpy = testUtils.sinon.spy();
 			const testStub = testUtils.sinon.stub().returns( {
 				format: [],
 				remove: [ [] ]
 			} );
 
-			new InlineAutoformatEditing( editor, testStub, formatSpy ); // eslint-disable-line no-new
+			inlineAutoformatEditing( editor, plugin, testStub, formatSpy );
 
 			setData( model, '<paragraph>*[]</paragraph>' );
 			model.change( writer => {
@@ -105,13 +102,12 @@ describe( 'InlineAutoformatEditing', () => {
 		} );
 
 		it( 'should stop early when there is no text', () => {
-			const formatSpy = testUtils.sinon.spy();
 			const testStub = testUtils.sinon.stub().returns( {
 				format: [],
 				remove: [ [] ]
 			} );
 
-			new InlineAutoformatEditing( editor, testStub, formatSpy ); // eslint-disable-line no-new
+			inlineAutoformatEditing( editor, plugin, testStub, formatSpy );
 
 			setData( model, '<paragraph>[]</paragraph>' );
 			model.change( writer => {
@@ -121,6 +117,19 @@ describe( 'InlineAutoformatEditing', () => {
 			sinon.assert.notCalled( formatSpy );
 		} );
 
+		it( 'should not run the formatCallback when the pattern is matched but the plugin is disabled', () => {
+			inlineAutoformatEditing( editor, plugin, /(\*)(.+?)(\*)/g, formatSpy );
+
+			plugin.isEnabled = false;
+
+			setData( model, '<paragraph>*foobar[]</paragraph>' );
+			model.change( writer => {
+				writer.insertText( '*', doc.selection.getFirstPosition() );
+			} );
+
+			sinon.assert.notCalled( formatSpy );
+		} );
+
 		it( 'should not autoformat if callback returned false', () => {
 			setData( model, '<paragraph>Foobar[]</paragraph>' );
 
@@ -133,7 +142,7 @@ describe( 'InlineAutoformatEditing', () => {
 
 			const formatCallback = () => false;
 
-			new InlineAutoformatEditing( editor, testCallback, formatCallback ); // eslint-disable-line no-new
+			inlineAutoformatEditing( editor, plugin, testCallback, formatCallback );
 
 			model.change( writer => {
 				writer.insertText( ' ', doc.selection.getFirstPosition() );
@@ -144,13 +153,14 @@ describe( 'InlineAutoformatEditing', () => {
 	} );
 
 	it( 'should ignore transparent batches', () => {
-		new InlineAutoformatEditing( editor, /(\*)(.+?)(\*)/g, 'testAttribute' ); // eslint-disable-line no-new
+		inlineAutoformatEditing( editor, plugin, /(\*)(.+?)(\*)/g, formatSpy );
 
 		setData( model, '<paragraph>*foobar[]</paragraph>' );
 		model.enqueueChange( 'transparent', writer => {
 			writer.insertText( '*', doc.selection.getFirstPosition() );
 		} );
 
+		sinon.assert.notCalled( formatSpy );
 		expect( getData( model ) ).to.equal( '<paragraph>*foobar*[]</paragraph>' );
 	} );
 } );

+ 1 - 1
packages/ckeditor5-autoformat/tests/manual/autoformat.html

@@ -1,3 +1,3 @@
 <div id="editor">
-	<p>This is the editor instance.</p>
+	<p>This is the editor instance, with a <code>code_element*2;</code> inside.</p>
 </div>

+ 2 - 2
packages/ckeditor5-autosave/package.json

@@ -12,14 +12,14 @@
   "dependencies": {
     "@ckeditor/ckeditor5-core": "^19.0.1",
     "@ckeditor/ckeditor5-utils": "^19.0.1",
-    "lodash-es": "^4.17.10"
+    "lodash-es": "^4.17.15"
   },
   "devDependencies": {
     "@ckeditor/ckeditor5-editor-classic": "^19.0.1",
     "@ckeditor/ckeditor5-paragraph": "^19.1.0"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

+ 2 - 2
packages/ckeditor5-basic-styles/lang/translations/ug.po

@@ -12,9 +12,9 @@
 #
 msgid ""
 msgstr ""
-"Language-Team: Uighur (https://www.transifex.com/ckeditor/teams/11143/ug/)\n"
+"Language-Team: Uyghur (https://www.transifex.com/ckeditor/teams/11143/ug/)\n"
 "Language: ug\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
 msgctxt "Toolbar button tooltip for the Bold feature."
 msgid "Bold"

+ 1 - 1
packages/ckeditor5-basic-styles/package.json

@@ -22,7 +22,7 @@
     "@ckeditor/ckeditor5-utils": "^19.0.1"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

+ 2 - 2
packages/ckeditor5-block-quote/lang/translations/ug.po

@@ -12,9 +12,9 @@
 #
 msgid ""
 msgstr ""
-"Language-Team: Uighur (https://www.transifex.com/ckeditor/teams/11143/ug/)\n"
+"Language-Team: Uyghur (https://www.transifex.com/ckeditor/teams/11143/ug/)\n"
 "Language: ug\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
 msgctxt "Toolbar button tooltip for the Block quote feature."
 msgid "Block quote"

+ 1 - 1
packages/ckeditor5-block-quote/package.json

@@ -27,7 +27,7 @@
     "@ckeditor/ckeditor5-typing": "^19.0.1"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
packages/ckeditor5-build-balloon-block/build/ckeditor.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
packages/ckeditor5-build-balloon-block/build/ckeditor.js.map


+ 8 - 8
packages/ckeditor5-build-balloon-block/package.json

@@ -31,8 +31,8 @@
     "@ckeditor/ckeditor5-block-quote": "^19.0.1",
     "@ckeditor/ckeditor5-ckfinder": "^19.0.1",
     "@ckeditor/ckeditor5-core": "^19.0.1",
-    "@ckeditor/ckeditor5-dev-utils": "^13.0.0",
-    "@ckeditor/ckeditor5-dev-webpack-plugin": "^9.0.0",
+    "@ckeditor/ckeditor5-dev-utils": "^20.0.0",
+    "@ckeditor/ckeditor5-dev-webpack-plugin": "^20.0.0",
     "@ckeditor/ckeditor5-easy-image": "^19.0.1",
     "@ckeditor/ckeditor5-editor-balloon": "^19.0.1",
     "@ckeditor/ckeditor5-essentials": "^19.0.1",
@@ -49,14 +49,14 @@
     "@ckeditor/ckeditor5-theme-lark": "^19.1.0",
     "@ckeditor/ckeditor5-ui": "^19.0.1",
     "postcss-loader": "^3.0.0",
-    "raw-loader": "^3.1.0",
-    "style-loader": "^1.0.0",
-    "terser-webpack-plugin": "^2.2.1",
-    "webpack": "^4.39.1",
-    "webpack-cli": "^3.3.6"
+    "raw-loader": "^4.0.1",
+    "style-loader": "^1.2.1",
+    "terser-webpack-plugin": "^3.0.2",
+    "webpack": "^4.43.0",
+    "webpack-cli": "^3.3.11"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
packages/ckeditor5-build-balloon/build/ckeditor.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
packages/ckeditor5-build-balloon/build/ckeditor.js.map


+ 8 - 8
packages/ckeditor5-build-balloon/package.json

@@ -31,8 +31,8 @@
     "@ckeditor/ckeditor5-block-quote": "^19.0.1",
     "@ckeditor/ckeditor5-ckfinder": "^19.0.1",
     "@ckeditor/ckeditor5-core": "^19.0.1",
-    "@ckeditor/ckeditor5-dev-utils": "^13.0.0",
-    "@ckeditor/ckeditor5-dev-webpack-plugin": "^9.0.0",
+    "@ckeditor/ckeditor5-dev-utils": "^20.0.0",
+    "@ckeditor/ckeditor5-dev-webpack-plugin": "^20.0.0",
     "@ckeditor/ckeditor5-easy-image": "^19.0.1",
     "@ckeditor/ckeditor5-editor-balloon": "^19.0.1",
     "@ckeditor/ckeditor5-essentials": "^19.0.1",
@@ -48,14 +48,14 @@
     "@ckeditor/ckeditor5-typing": "^19.0.1",
     "@ckeditor/ckeditor5-theme-lark": "^19.1.0",
     "postcss-loader": "^3.0.0",
-    "raw-loader": "^3.1.0",
-    "style-loader": "^1.0.0",
-    "terser-webpack-plugin": "^2.2.1",
-    "webpack": "^4.39.1",
-    "webpack-cli": "^3.3.6"
+    "raw-loader": "^4.0.1",
+    "style-loader": "^1.2.1",
+    "terser-webpack-plugin": "^3.0.2",
+    "webpack": "^4.43.0",
+    "webpack-cli": "^3.3.11"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
packages/ckeditor5-build-classic/build/ckeditor.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
packages/ckeditor5-build-classic/build/ckeditor.js.map


+ 8 - 8
packages/ckeditor5-build-classic/package.json

@@ -31,8 +31,8 @@
     "@ckeditor/ckeditor5-block-quote": "^19.0.1",
     "@ckeditor/ckeditor5-ckfinder": "^19.0.1",
     "@ckeditor/ckeditor5-core": "^19.0.1",
-    "@ckeditor/ckeditor5-dev-utils": "^13.0.0",
-    "@ckeditor/ckeditor5-dev-webpack-plugin": "^9.0.0",
+    "@ckeditor/ckeditor5-dev-utils": "^20.0.0",
+    "@ckeditor/ckeditor5-dev-webpack-plugin": "^20.0.0",
     "@ckeditor/ckeditor5-easy-image": "^19.0.1",
     "@ckeditor/ckeditor5-editor-classic": "^19.0.1",
     "@ckeditor/ckeditor5-essentials": "^19.0.1",
@@ -48,14 +48,14 @@
     "@ckeditor/ckeditor5-typing": "^19.0.1",
     "@ckeditor/ckeditor5-theme-lark": "^19.1.0",
     "postcss-loader": "^3.0.0",
-    "raw-loader": "^3.1.0",
-    "style-loader": "^1.0.0",
-    "terser-webpack-plugin": "^2.2.1",
-    "webpack": "^4.39.1",
-    "webpack-cli": "^3.3.6"
+    "raw-loader": "^4.0.1",
+    "style-loader": "^1.2.1",
+    "terser-webpack-plugin": "^3.0.2",
+    "webpack": "^4.43.0",
+    "webpack-cli": "^3.3.11"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
packages/ckeditor5-build-decoupled-document/build/ckeditor.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
packages/ckeditor5-build-decoupled-document/build/ckeditor.js.map


+ 8 - 8
packages/ckeditor5-build-decoupled-document/package.json

@@ -32,8 +32,8 @@
     "@ckeditor/ckeditor5-block-quote": "^19.0.1",
     "@ckeditor/ckeditor5-ckfinder": "^19.0.1",
     "@ckeditor/ckeditor5-core": "^19.0.1",
-    "@ckeditor/ckeditor5-dev-utils": "^13.0.0",
-    "@ckeditor/ckeditor5-dev-webpack-plugin": "^9.0.0",
+    "@ckeditor/ckeditor5-dev-utils": "^20.0.0",
+    "@ckeditor/ckeditor5-dev-webpack-plugin": "^20.0.0",
     "@ckeditor/ckeditor5-easy-image": "^19.0.1",
     "@ckeditor/ckeditor5-editor-decoupled": "^19.0.1",
     "@ckeditor/ckeditor5-essentials": "^19.0.1",
@@ -51,14 +51,14 @@
     "@ckeditor/ckeditor5-typing": "^19.0.1",
     "@ckeditor/ckeditor5-theme-lark": "^19.1.0",
     "postcss-loader": "^3.0.0",
-    "raw-loader": "^3.1.0",
-    "style-loader": "^1.0.0",
-    "terser-webpack-plugin": "^2.2.1",
-    "webpack": "^4.39.1",
-    "webpack-cli": "^3.3.6"
+    "raw-loader": "^4.0.1",
+    "style-loader": "^1.2.1",
+    "terser-webpack-plugin": "^3.0.2",
+    "webpack": "^4.43.0",
+    "webpack-cli": "^3.3.11"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

+ 1 - 1
packages/ckeditor5-build-decoupled-document/tests/ckeditor.js

@@ -200,7 +200,7 @@ describe( 'DecoupledEditor build', () => {
 
 				editor.setData( data );
 				expect( editor.getData() ).to.equal( data );
-				expect( editor.model.document.selection.getAttribute( 'fontFamily' ) ).to.equal( 'Georgia' );
+				expect( editor.model.document.selection.getAttribute( 'fontFamily' ) ).to.equal( 'Georgia, serif' );
 			} );
 
 			it( 'font background color works', () => {

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
packages/ckeditor5-build-inline/build/ckeditor.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
packages/ckeditor5-build-inline/build/ckeditor.js.map


+ 8 - 8
packages/ckeditor5-build-inline/package.json

@@ -31,8 +31,8 @@
     "@ckeditor/ckeditor5-block-quote": "^19.0.1",
     "@ckeditor/ckeditor5-ckfinder": "^19.0.1",
     "@ckeditor/ckeditor5-core": "^19.0.1",
-    "@ckeditor/ckeditor5-dev-utils": "^13.0.0",
-    "@ckeditor/ckeditor5-dev-webpack-plugin": "^9.0.0",
+    "@ckeditor/ckeditor5-dev-utils": "^20.0.0",
+    "@ckeditor/ckeditor5-dev-webpack-plugin": "^20.0.0",
     "@ckeditor/ckeditor5-easy-image": "^19.0.1",
     "@ckeditor/ckeditor5-editor-inline": "^19.0.1",
     "@ckeditor/ckeditor5-essentials": "^19.0.1",
@@ -48,14 +48,14 @@
     "@ckeditor/ckeditor5-typing": "^19.0.1",
     "@ckeditor/ckeditor5-theme-lark": "^19.1.0",
     "postcss-loader": "^3.0.0",
-    "raw-loader": "^3.1.0",
-    "style-loader": "^1.0.0",
-    "terser-webpack-plugin": "^2.2.1",
-    "webpack": "^4.39.1",
-    "webpack-cli": "^3.3.6"
+    "raw-loader": "^4.0.1",
+    "style-loader": "^1.2.1",
+    "terser-webpack-plugin": "^3.0.2",
+    "webpack": "^4.43.0",
+    "webpack-cli": "^3.3.11"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

+ 1 - 1
packages/ckeditor5-ckfinder/package.json

@@ -24,7 +24,7 @@
     "@ckeditor/ckeditor5-utils": "^19.0.1"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

+ 1 - 1
packages/ckeditor5-clipboard/package.json

@@ -22,7 +22,7 @@
     "@ckeditor/ckeditor5-paragraph": "^19.1.0"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

+ 1 - 1
packages/ckeditor5-cloud-services/package.json

@@ -16,7 +16,7 @@
     "@ckeditor/ckeditor5-editor-classic": "^19.0.1"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

+ 1 - 1
packages/ckeditor5-code-block/lang/translations/de.po

@@ -18,7 +18,7 @@ msgstr ""
 
 msgctxt "A label of the button that allows inserting a new code block into the editor content."
 msgid "Insert code block"
-msgstr "Block einfügen"
+msgstr "Code-Block einfügen"
 
 msgctxt "A language of the code block in the editor content when no specific programming language is associated with it."
 msgid "Plain text"

+ 2 - 2
packages/ckeditor5-code-block/package.json

@@ -14,7 +14,7 @@
     "@ckeditor/ckeditor5-enter": "^19.0.1",
     "@ckeditor/ckeditor5-ui": "^19.0.1",
     "@ckeditor/ckeditor5-utils": "^19.0.1",
-    "lodash-es": "^4.17.10"
+    "lodash-es": "^4.17.15"
   },
   "devDependencies": {
     "@ckeditor/ckeditor5-alignment": "^19.0.1",
@@ -28,7 +28,7 @@
     "@ckeditor/ckeditor5-undo": "^19.0.1"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

+ 2 - 2
packages/ckeditor5-core/lang/translations/ug.po

@@ -12,9 +12,9 @@
 #
 msgid ""
 msgstr ""
-"Language-Team: Uighur (https://www.transifex.com/ckeditor/teams/11143/ug/)\n"
+"Language-Team: Uyghur (https://www.transifex.com/ckeditor/teams/11143/ug/)\n"
 "Language: ug\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
 msgctxt "Label for the Save button."
 msgid "Save"

+ 2 - 2
packages/ckeditor5-core/package.json

@@ -23,7 +23,7 @@
   "dependencies": {
     "@ckeditor/ckeditor5-engine": "^19.0.1",
     "@ckeditor/ckeditor5-utils": "^19.0.1",
-    "lodash-es": "^4.17.10"
+    "lodash-es": "^4.17.15"
   },
   "devDependencies": {
     "@ckeditor/ckeditor5-autoformat": "^19.0.1",
@@ -42,7 +42,7 @@
     "@ckeditor/ckeditor5-ui": "^19.0.1"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

+ 6 - 1
packages/ckeditor5-core/tests/_utils-tests/articlepluginset.js

@@ -33,7 +33,12 @@ describe( 'ArticlePluginSet', () => {
 		editorElement = document.createElement( 'div' );
 		document.body.appendChild( editorElement );
 
-		editor = await ClassicTestEditor.create( editorElement, { plugins: [ ArticlePluginSet ] } );
+		editor = await ClassicTestEditor.create( editorElement, {
+			plugins: [ ArticlePluginSet ],
+			image: {
+				toolbar: [ 'imageStyle:full', 'imageStyle:side' ]
+			}
+		} );
 	} );
 
 	afterEach( async () => {

+ 15 - 5
packages/ckeditor5-core/tests/_utils/memory.js

@@ -31,8 +31,8 @@ const GARBAGE_COLLECTOR_TIMEOUT = 500;
  */
 export function describeMemoryUsage( callback ) {
 	// Skip all memory tests due to https://github.com/ckeditor/ckeditor5/issues/1731.
-	describe.skip( 'memory usage', () => {
-		skipIfNoGarbageCollector();
+	describe( 'memory usage', () => {
+		skipIfIncompatibleEnvironment();
 
 		beforeEach( createEditorElement );
 
@@ -141,16 +141,26 @@ function collectMemoryStats() {
 	} );
 }
 
-// Will skip test suite if not in compatible browser.
+// Will skip test suite if tests are run inside incompatible environment:
+// - No window.gc (only Google Chrome).
+// - Chrome on Windows (tests heavily break).
+//
 // Currently on Google Chrome supports this method and must be run with proper flags:
 //
 // 		google-chrome -js-flags="--expose-gc"
 //
-function skipIfNoGarbageCollector() {
+function skipIfIncompatibleEnvironment() {
 	// eslint-disable-next-line mocha/no-top-level-hooks
 	before( function() {
-		if ( !window.gc ) {
+		if ( !window.gc || isWindows() ) {
 			this.skip();
 		}
 	} );
 }
+
+// The windows environment does not cooperate with this tests.
+function isWindows() {
+	const userAgent = window.navigator.userAgent.toLowerCase();
+
+	return userAgent.indexOf( 'windows' ) > -1;
+}

+ 1 - 1
packages/ckeditor5-core/tests/_utils/utils.js

@@ -93,7 +93,7 @@ const utils = {
 		let isValid = true;
 
 		for ( const property in mixin ) {
-			if ( mixin.hasOwnProperty( property ) ) {
+			if ( Object.prototype.hasOwnProperty.call( mixin, property ) ) {
 				if ( targetClass.prototype[ property ] !== mixin[ property ] ) {
 					isValid = false;
 				}

+ 1 - 1
packages/ckeditor5-easy-image/package.json

@@ -23,7 +23,7 @@
     "@ckeditor/ckeditor5-utils": "^19.0.1"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

+ 2 - 2
packages/ckeditor5-editor-balloon/package.json

@@ -13,7 +13,7 @@
     "@ckeditor/ckeditor5-engine": "^19.0.1",
     "@ckeditor/ckeditor5-ui": "^19.0.1",
     "@ckeditor/ckeditor5-utils": "^19.0.1",
-    "lodash-es": "^4.17.10"
+    "lodash-es": "^4.17.15"
   },
   "devDependencies": {
     "@ckeditor/ckeditor5-basic-styles": "^19.0.1",
@@ -24,7 +24,7 @@
     "@ckeditor/ckeditor5-undo": "^19.0.1"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

+ 2 - 2
packages/ckeditor5-editor-balloon/tests/manual/memory.md

@@ -2,7 +2,7 @@
 2. Go to Memory tab.
 3. Select "Heap snapshot" profiling type.
 4. Click "Collect Garbage" (trash icon) and "Clear all profiles" (don't go icon).
-5. Record heap snapshot ("Take heap snapshot" a record icon).
+5. Take heap snapshot (record icon).
 6. Repeat multiple times:
     - click "Init editor",
     - wait to editor show up ,
@@ -23,7 +23,7 @@
         --disable-extensions \
         --disable-plugins \
         --incognito \
-        http://localhost:8125 
+        http://localhost:8125
     ```
 
     The above will run Chrome without extensions or plugins in incognito mode and open manual tests page.

+ 2 - 2
packages/ckeditor5-editor-classic/package.json

@@ -13,7 +13,7 @@
     "@ckeditor/ckeditor5-engine": "^19.0.1",
     "@ckeditor/ckeditor5-ui": "^19.0.1",
     "@ckeditor/ckeditor5-utils": "^19.0.1",
-    "lodash-es": "^4.17.10"
+    "lodash-es": "^4.17.15"
   },
   "devDependencies": {
     "@ckeditor/ckeditor5-basic-styles": "^19.0.1",
@@ -24,7 +24,7 @@
     "@ckeditor/ckeditor5-undo": "^19.0.1"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

+ 2 - 2
packages/ckeditor5-editor-classic/tests/manual/memory.md

@@ -2,7 +2,7 @@
 2. Go to Memory tab.
 3. Select "Heap snapshot" profiling type.
 4. Click "Collect Garbage" (trash icon) and "Clear all profiles" (don't go icon).
-5. Record heap snapshot ("Take heap snapshot" a record icon).
+5. Take heap snapshot (record icon).
 6. Repeat multiple times:
     - click "Init editor",
     - wait to editor show up ,
@@ -23,7 +23,7 @@
         --disable-extensions \
         --disable-plugins \
         --incognito \
-        http://localhost:8125 
+        http://localhost:8125
     ```
 
     The above will run Chrome without extensions or plugins in incognito mode and open manual tests page.

+ 2 - 2
packages/ckeditor5-editor-decoupled/package.json

@@ -13,7 +13,7 @@
     "@ckeditor/ckeditor5-engine": "^19.0.1",
     "@ckeditor/ckeditor5-ui": "^19.0.1",
     "@ckeditor/ckeditor5-utils": "^19.0.1",
-    "lodash-es": "^4.17.10"
+    "lodash-es": "^4.17.15"
   },
   "devDependencies": {
     "@ckeditor/ckeditor5-basic-styles": "^19.0.1",
@@ -24,7 +24,7 @@
     "@ckeditor/ckeditor5-undo": "^19.0.1"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

+ 2 - 2
packages/ckeditor5-editor-decoupled/tests/manual/memory.md

@@ -2,7 +2,7 @@
 2. Go to Memory tab.
 3. Select "Heap snapshot" profiling type.
 4. Click "Collect Garbage" (trash icon) and "Clear all profiles" (don't go icon).
-5. Record heap snapshot ("Take heap snapshot" a record icon).
+5. Take heap snapshot (record icon).
 6. Repeat multiple times:
     - click "Init editor",
     - wait to editor show up ,
@@ -23,7 +23,7 @@
         --disable-extensions \
         --disable-plugins \
         --incognito \
-        http://localhost:8125 
+        http://localhost:8125
     ```
 
     The above will run Chrome without extensions or plugins in incognito mode and open manual tests page.

+ 2 - 2
packages/ckeditor5-editor-inline/package.json

@@ -13,7 +13,7 @@
     "@ckeditor/ckeditor5-engine": "^19.0.1",
     "@ckeditor/ckeditor5-ui": "^19.0.1",
     "@ckeditor/ckeditor5-utils": "^19.0.1",
-    "lodash-es": "^4.17.10"
+    "lodash-es": "^4.17.15"
   },
   "devDependencies": {
     "@ckeditor/ckeditor5-basic-styles": "^19.0.1",
@@ -24,7 +24,7 @@
     "@ckeditor/ckeditor5-undo": "^19.0.1"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

+ 2 - 2
packages/ckeditor5-editor-inline/tests/manual/memory.md

@@ -2,7 +2,7 @@
 2. Go to Memory tab.
 3. Select "Heap snapshot" profiling type.
 4. Click "Collect Garbage" (trash icon) and "Clear all profiles" (don't go icon).
-5. Record heap snapshot ("Take heap snapshot" a record icon).
+5. Take heap snapshot (record icon).
 6. Repeat multiple times:
     - click "Init editor",
     - wait to editor show up ,
@@ -23,7 +23,7 @@
         --disable-extensions \
         --disable-plugins \
         --incognito \
-        http://localhost:8125 
+        http://localhost:8125
     ```
 
     The above will run Chrome without extensions or plugins in incognito mode and open manual tests page.

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

@@ -46,7 +46,7 @@ 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 the engine.
 
 ### Limit elements
 
@@ -55,9 +55,9 @@ Consider a feature like an image caption. The caption text area should construct
 * A selection that starts inside should not end outside.
 * Pressing <kbd>Backspace</kbd> or <kbd>Delete</kbd> should not delete the area. Pressing <kbd>Enter</kbd> should not split the area.
 
-It should also act as a boundary for external actions. This is mostly enforced by a selection post-fixer that ensures that a selection that starts outside, should not end inside. That means that most actions will either apply to the "outside" of such an element or to a content inside it.
+It should also act as a boundary for external actions. This is mostly enforced by a selection post-fixer that ensures that a selection that starts outside, should not end inside. It means that most actions will either apply to the "outside" of such an element or to the content inside it.
 
-Taken these characteristics, the image caption should be defined as limit element by using the {@link module:engine/model/schema~SchemaItemDefinition#isLimit `isLimit`} property.
+Taken these characteristics, the image caption should be defined as a limit element by using the {@link module:engine/model/schema~SchemaItemDefinition#isLimit `isLimit`} property.
 
 ```js
 schema.register( 'myCaption', {
@@ -73,9 +73,9 @@ The engine and various features then check it via {@link module:engine/model/sch
 
 ### Object elements
 
-For the image caption as in the example above it does not make much sense to select the caption box, then copy or drag it somewhere else.
+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 does not make much 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. {@link module:engine/model/schema~SchemaItemDefinition#isObject `isObject`} should be used to mark such behavior.
+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.
 
 ```js
 schema.register( 'myImage', {
@@ -95,7 +95,7 @@ The {@link module:engine/model/schema~Schema#isObject `Schema#isObject()`} can l
 
 Generally speaking, content is usually made out of blocks like paragraphs, list items, images, headings, etc. All these elements should be marked as blocks by using {@link module:engine/model/schema~SchemaItemDefinition#isBlock `isBlock`}.
 
-It is important to remember that a block should not allow another block inside. Container elements like `<blockQuote>` which can contain other block elements should not be marked as blocks.
+It is important to remember that a block should not allow another block inside. Container elements like `<blockQuote>`, which can contain other block elements, should not be marked as blocks.
 
 <info-box>
 	There is also the `$block` generic item which has `isBlock` set to `true`. Most block type items will inherit from `$block` (through `inheritAllFrom`).
@@ -103,11 +103,11 @@ It is important to remember that a block should not allow another block inside.
 
 ### Inline elements
 
-In the editor, all HTML formatting elements such as `<strong>` or `<code>` are represented by text attributes. Therefore, inline model elements are not to be used for this scenarios.
+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.
 
-The support for inline elements in CKEditor 5 is so far limited to self-contained elements. This is &mdash; all elements marked with `isInline` should also be marked with `isObject`.
+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`.
 
 ## Generic items
 

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

@@ -22,7 +22,7 @@
   ],
   "dependencies": {
     "@ckeditor/ckeditor5-utils": "^19.0.1",
-    "lodash-es": "^4.17.10"
+    "lodash-es": "^4.17.15"
   },
   "devDependencies": {
     "@ckeditor/ckeditor5-basic-styles": "^19.0.1",
@@ -42,7 +42,7 @@
     "@ckeditor/ckeditor5-widget": "^19.1.0"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

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

@@ -232,7 +232,7 @@ export default class Element extends Node {
 
 	/**
 	 * Creates a copy of this element and returns it. Created element has the same name and attributes as the original element.
-	 * If clone is deep, the original element's children are also cloned. If not, then empty element is removed.
+	 * If clone is deep, the original element's children are also cloned. If not, then empty element is returned.
 	 *
 	 * @protected
 	 * @param {Boolean} [deep=false] If set to `true` clones element and all its children recursively. When set to `false`,

+ 9 - 6
packages/ckeditor5-engine/src/model/model.js

@@ -551,24 +551,27 @@ export default class Model {
 	 * @param {module:engine/model/range~Range|module:engine/model/element~Element} rangeOrElement Range or element to check.
 	 * @param {Object} [options]
 	 * @param {Boolean} [options.ignoreWhitespaces] Whether text node with whitespaces only should be considered empty.
+	 * @param {Boolean} [options.ignoreMarkers] Whether markers should be ignored.
 	 * @returns {Boolean}
 	 */
-	hasContent( rangeOrElement, options ) {
+	hasContent( rangeOrElement, options = {} ) {
 		const range = rangeOrElement instanceof ModelElement ? ModelRange._createIn( rangeOrElement ) : rangeOrElement;
 
 		if ( range.isCollapsed ) {
 			return false;
 		}
 
+		const { ignoreWhitespaces = false, ignoreMarkers = false } = options;
+
 		// Check if there are any markers which affects data in this given range.
-		for ( const intersectingMarker of this.markers.getMarkersIntersectingRange( range ) ) {
-			if ( intersectingMarker.affectsData ) {
-				return true;
+		if ( !ignoreMarkers ) {
+			for ( const intersectingMarker of this.markers.getMarkersIntersectingRange( range ) ) {
+				if ( intersectingMarker.affectsData ) {
+					return true;
+				}
 			}
 		}
 
-		const { ignoreWhitespaces = false } = options || {};
-
 		for ( const item of range.getItems() ) {
 			if ( item.is( 'textProxy' ) ) {
 				if ( !ignoreWhitespaces ) {

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

@@ -96,6 +96,7 @@ function noUpdateTransformation( a ) {
 export function transform( a, b, context = {} ) {
 	const transformationFunction = getTransformation( a.constructor, b.constructor );
 
+	/* eslint-disable no-useless-catch */
 	try {
 		a = a.clone();
 
@@ -112,6 +113,7 @@ export function transform( a, b, context = {} ) {
 
 		throw e;
 	}
+	/* eslint-enable no-useless-catch */
 }
 
 /**

+ 39 - 39
packages/ckeditor5-engine/src/model/schema.js

@@ -26,9 +26,9 @@ import TreeWalker from './treewalker';
  *
  * Read more about the schema in:
  *
- * * {@glink framework/guides/architecture/editing-engine#schema "Schema"} section of the
- * {@glink framework/guides/architecture/editing-engine Introduction to the "Editing engine architecture"}.
- * * {@glink framework/guides/deep-dive/schema "Schema" deep dive} guide.
+ * * {@glink framework/guides/architecture/editing-engine#schema Schema} section of the
+ * {@glink framework/guides/architecture/editing-engine Introduction to the Editing engine architecture}.
+ * * {@glink framework/guides/deep-dive/schema Schema deep dive} guide.
  *
  * @mixes module:utils/observablemixin~ObservableMixin
  */
@@ -171,7 +171,7 @@ export default class Schema {
 	}
 
 	/**
-	 * Returns a definition of the given item or `undefined` if item is not registered.
+	 * Returns a definition of the given item or `undefined` if an item is not registered.
 	 *
 	 * This method should normally be used for reflection purposes (e.g. defining a clone of a certain element,
 	 * checking a list of all block elements, etc).
@@ -212,7 +212,7 @@ export default class Schema {
 
 	/**
 	 * Returns `true` if the given item is defined to be
-	 * a block by {@link module:engine/model/schema~SchemaItemDefinition}'s `isBlock` property.
+	 * a block by the {@link module:engine/model/schema~SchemaItemDefinition}'s `isBlock` property.
 	 *
 	 *		schema.isBlock( 'paragraph' ); // -> true
 	 *		schema.isBlock( '$root' ); // -> false
@@ -220,7 +220,7 @@ export default class Schema {
 	 *		const paragraphElement = writer.createElement( 'paragraph' );
 	 *		schema.isBlock( paragraphElement ); // -> true
 	 *
-	 * See the {@glink framework/guides/deep-dive/schema#block-elements Block elements} section of the "Schema" deep dive}
+	 * See the {@glink framework/guides/deep-dive/schema#block-elements Block elements} section of the Schema deep dive}
 	 * guide for more details.
 	 *
 	 * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
@@ -234,18 +234,18 @@ export default class Schema {
 	/**
 	 * Returns `true` if the given item should be treated as a limit element.
 	 *
-	 * It considers the item to be a limit element if its
+	 * It considers an item to be a limit element if its
 	 * {@link module:engine/model/schema~SchemaItemDefinition}'s
 	 * {@link module:engine/model/schema~SchemaItemDefinition#isLimit `isLimit`} or
 	 * {@link module:engine/model/schema~SchemaItemDefinition#isObject `isObject`} property
-	 * were set to `true`.
+	 * was set to `true`.
 	 *
 	 *		schema.isLimit( 'paragraph' ); // -> false
 	 *		schema.isLimit( '$root' ); // -> true
 	 *		schema.isLimit( editor.model.document.getRoot() ); // -> true
 	 *		schema.isLimit( 'image' ); // -> true
 	 *
-	 * See the {@glink framework/guides/deep-dive/schema#limit-elements Limit elements} section of the "Schema" deep dive}
+	 * See the {@glink framework/guides/deep-dive/schema#limit-elements Limit elements} section of the Schema deep dive}
 	 * guide for more details.
 	 *
 	 * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
@@ -261,12 +261,12 @@ export default class Schema {
 	}
 
 	/**
-	 * Returns `true` if the given item is should be treated as an object element.
+	 * Returns `true` if the given item should be treated as an object element.
 	 *
-	 * It considers the item to be an object element if its
+	 * It considers an item to be an object element if its
 	 * {@link module:engine/model/schema~SchemaItemDefinition}'s
 	 * {@link module:engine/model/schema~SchemaItemDefinition#isObject `isObject`} property
-	 * were set to `true`.
+	 * was set to `true`.
 	 *
 	 *		schema.isObject( 'paragraph' ); // -> false
 	 *		schema.isObject( 'image' ); // -> true
@@ -274,7 +274,7 @@ export default class Schema {
 	 *		const imageElement = writer.createElement( 'image' );
 	 *		schema.isObject( imageElement ); // -> true
 	 *
-	 * See the {@glink framework/guides/deep-dive/schema#object-elements Object elements} section of the "Schema" deep dive}
+	 * See the {@glink framework/guides/deep-dive/schema#object-elements Object elements} section of the Schema deep dive}
 	 * guide for more details.
 	 *
 	 * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
@@ -287,7 +287,7 @@ export default class Schema {
 
 	/**
 	 * Returns `true` if the given item is defined to be
-	 * an inline element by {@link module:engine/model/schema~SchemaItemDefinition}'s `isInline` property.
+	 * an inline element by the {@link module:engine/model/schema~SchemaItemDefinition}'s `isInline` property.
 	 *
 	 *		schema.isInline( 'paragraph' ); // -> false
 	 *		schema.isInline( 'softBreak' ); // -> true
@@ -295,7 +295,7 @@ export default class Schema {
 	 *		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}
+	 * See the {@glink framework/guides/deep-dive/schema#inline-elements Inline elements} section of the Schema deep dive}
 	 * guide for more details.
 	 *
 	 * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
@@ -737,13 +737,13 @@ export default class Schema {
 	}
 
 	/**
-	 * Tries to find position ancestors that allows to insert given node.
+	 * Tries to find position ancestors that allow to insert a given node.
 	 * It starts searching from the given position and goes node by node to the top of the model tree
-	 * as long as {@link module:engine/model/schema~Schema#isLimit limit element},
-	 * {@link module:engine/model/schema~Schema#isObject object element} or top-most ancestor won't be reached.
+	 * as long as a {@link module:engine/model/schema~Schema#isLimit limit element}, an
+	 * {@link module:engine/model/schema~Schema#isObject object element} or a topmost ancestor is not reached.
 	 *
-	 * @param {module:engine/model/position~Position} position Position from searching will start.
-	 * @param {module:engine/model/node~Node|String} node Node for which allowed parent should be found or its name.
+	 * @param {module:engine/model/position~Position} position The position that the search will start from.
+	 * @param {module:engine/model/node~Node|String} node The node for which an allowed parent should be found or its name.
 	 * @returns {module:engine/model/element~Element|null} element Allowed parent or null if nothing was found.
 	 */
 	findAllowedParent( position, node ) {
@@ -873,7 +873,7 @@ export default class Schema {
 	 * This is a helper function for {@link ~Schema#getValidRanges}.
 	 *
 	 * @private
-	 * @param {module:engine/model/range~Range} range Range to process.
+	 * @param {module:engine/model/range~Range} range The range to process.
 	 * @param {String} attribute The name of the attribute to check.
 	 * @returns {Iterable.<module:engine/model/range~Range>} Ranges in which the attribute is allowed.
 	 */
@@ -907,7 +907,7 @@ mix( Schema, ObservableMixin );
 
 /**
  * Event fired when the {@link #checkChild} method is called. It allows plugging in
- * additional behavior – e.g. implementing rules which cannot be defined using the declarative
+ * additional behavior, for example implementing rules which cannot be defined using the declarative
  * {@link module:engine/model/schema~SchemaItemDefinition} interface.
  *
  * **Note:** The {@link #addChildCheck} method is a more handy way to register callbacks. Internally,
@@ -916,7 +916,7 @@ mix( Schema, ObservableMixin );
  *
  * The {@link #checkChild} method fires an event because it is
  * {@link module:utils/observablemixin~ObservableMixin#decorate decorated} with it. Thanks to that you can
- * use this event in a various way, but the most important use case is overriding standard behaviour of the
+ * use this event in various ways, but the most important use case is overriding standard behavior of the
  * `checkChild()` method. Let's see a typical listener template:
  *
  *		schema.on( 'checkChild', ( evt, args ) => {
@@ -927,12 +927,12 @@ mix( Schema, ObservableMixin );
  * The listener is added with a `high` priority to be executed before the default method is really called. The `args` callback
  * parameter contains arguments passed to `checkChild( context, child )`. However, the `context` parameter is already
  * normalized to a {@link module:engine/model/schema~SchemaContext} instance and `child` to a
- * {@link module:engine/model/schema~SchemaCompiledItemDefinition} instance, so you don't have to worry about
+ * {@link module:engine/model/schema~SchemaCompiledItemDefinition} instance, so you do not have to worry about
  * the various ways how `context` and `child` may be passed to `checkChild()`.
  *
  * **Note:** `childDefinition` may be `undefined` if `checkChild()` was called with a non-registered element.
  *
- * So, in order to implement a rule "disallow `heading1` in `blockQuote`" you can add such a listener:
+ * So, in order to implement a rule "disallow `heading1` in `blockQuote`", you can add such a listener:
  *
  *		schema.on( 'checkChild', ( evt, args ) => {
  *			const context = args[ 0 ];
@@ -946,8 +946,8 @@ mix( Schema, ObservableMixin );
  *			}
  *		}, { priority: 'high' } );
  *
- * Allowing elements in specific contexts will be a far less common use case, because it's normally handled by
- * `allowIn` rule from {@link module:engine/model/schema~SchemaItemDefinition} but if you have a complex scenario
+ * Allowing elements in specific contexts will be a far less common use case, because it is normally handled by the
+ * `allowIn` rule from {@link module:engine/model/schema~SchemaItemDefinition}. But if you have a complex scenario
  * where `listItem` should be allowed only in element `foo` which must be in element `bar`, then this would be the way:
  *
  *		schema.on( 'checkChild', ( evt, args ) => {
@@ -968,16 +968,16 @@ mix( Schema, ObservableMixin );
 
 /**
  * Event fired when the {@link #checkAttribute} method is called. It allows plugging in
- * additional behavior – e.g. implementing rules which cannot be defined using the declarative
+ * additional behavior, for example implementing rules which cannot be defined using the declarative
  * {@link module:engine/model/schema~SchemaItemDefinition} interface.
  *
  * **Note:** The {@link #addAttributeCheck} method is a more handy way to register callbacks. Internally,
  * it registers a listener to this event but comes with a simpler API and it is the recommended choice
  * in most of the cases.
  *
- * The {@link #checkAttribute} method fires an event because it's
+ * The {@link #checkAttribute} method fires an event because it is
  * {@link module:utils/observablemixin~ObservableMixin#decorate decorated} with it. Thanks to that you can
- * use this event in a various way, but the most important use case is overriding standard behaviour of the
+ * use this event in various ways, but the most important use case is overriding the standard behavior of the
  * `checkAttribute()` method. Let's see a typical listener template:
  *
  *		schema.on( 'checkAttribute', ( evt, args ) => {
@@ -987,10 +987,10 @@ mix( Schema, ObservableMixin );
  *
  * The listener is added with a `high` priority to be executed before the default method is really called. The `args` callback
  * parameter contains arguments passed to `checkAttribute( context, attributeName )`. However, the `context` parameter is already
- * normalized to a {@link module:engine/model/schema~SchemaContext} instance, so you don't have to worry about
+ * normalized to a {@link module:engine/model/schema~SchemaContext} instance, so you do not have to worry about
  * the various ways how `context` may be passed to `checkAttribute()`.
  *
- * So, in order to implement a rule "disallow `bold` in a text which is in a `heading1` you can add such a listener:
+ * So, in order to implement a rule "disallow `bold` in a text which is in a `heading1`, you can add such a listener:
  *
  *		schema.on( 'checkAttribute', ( evt, args ) => {
  *			const context = args[ 0 ];
@@ -1004,8 +1004,8 @@ mix( Schema, ObservableMixin );
  *			}
  *		}, { priority: 'high' } );
  *
- * Allowing attributes in specific contexts will be a far less common use case, because it's normally handled by
- * `allowAttributes` rule from {@link module:engine/model/schema~SchemaItemDefinition} but if you have a complex scenario
+ * Allowing attributes in specific contexts will be a far less common use case, because it is normally handled by the
+ * `allowAttributes` rule from {@link module:engine/model/schema~SchemaItemDefinition}. But if you have a complex scenario
  * where `bold` should be allowed only in element `foo` which must be in element `bar`, then this would be the way:
  *
  *		schema.on( 'checkAttribute', ( evt, args ) => {
@@ -1056,7 +1056,7 @@ mix( Schema, ObservableMixin );
  * {@link module:engine/model/schema~Schema#isLimit `isLimit()`} returns `true` for object elements automatically.
  *
  * Read more about the meaning of these types in the
- * {@glink framework/guides/deep-dive/schema#defining-additional-semantics Dedicated section of the "Schema" deep dive} guide.
+ * {@glink framework/guides/deep-dive/schema#defining-additional-semantics dedicated section of the Schema deep dive} guide.
  *
  * # Generic items
  *
@@ -1159,14 +1159,14 @@ mix( Schema, ObservableMixin );
  * Most block type items will inherit from `$block` (through `inheritAllFrom`).
  *
  * Read more about the block elements in the
- * {@glink framework/guides/deep-dive/schema#block-elements Block elements} section of the "Schema" deep dive} guide.
+ * {@glink framework/guides/deep-dive/schema#block-elements Block elements} section of the Schema deep dive} guide.
  *
  * @property {Boolean} isInline
  * Whether an item is "text-like" and should be treated as an inline node. Examples of inline elements:
  * `$text`, `softBreak` (`<br>`), etc.
  *
  * Read more about the inline elements in the
- * {@glink framework/guides/deep-dive/schema#inline-elements Inline elements} section of the "Schema" deep dive} guide.
+ * {@glink framework/guides/deep-dive/schema#inline-elements Inline elements} section of the Schema deep dive} guide.
  *
  * @property {Boolean} isLimit
  * It can be understood as whether this element should not be split by <kbd>Enter</kbd>.
@@ -1174,7 +1174,7 @@ mix( Schema, ObservableMixin );
  * a limit element are limited to its content.
  *
  * Read more about the limit elements in the
- * {@glink framework/guides/deep-dive/schema#limit-elements Limit elements} section of the "Schema" deep dive} guide.
+ * {@glink framework/guides/deep-dive/schema#limit-elements Limit elements} section of the Schema deep dive} guide.
  *
  * @property {Boolean} isObject
  * Whether an item is "self-contained" and should be treated as a whole. Examples of object elements:
@@ -1184,7 +1184,7 @@ mix( Schema, ObservableMixin );
  * {@link module:engine/model/schema~Schema#isLimit `isLimit()`} returns `true` for object elements automatically.
  *
  * 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.
+ * {@glink framework/guides/deep-dive/schema#object-elements Object elements} section of the Schema deep dive} guide.
  */
 
 /**

+ 297 - 47
packages/ckeditor5-engine/src/model/utils/deletecontent.js

@@ -80,8 +80,8 @@ export default function deleteContent( model, selection, options = {} ) {
 			return;
 		}
 
-		const startPos = selRange.start;
-		const endPos = LivePosition.fromPosition( selRange.end, 'toNext' );
+		// Get the live positions for the range adjusted to span only blocks selected from the user perspective.
+		const [ startPosition, endPosition ] = getLivePositionsForSelectedBlocks( selRange );
 
 		// 2. Remove the content if there is any.
 		if ( !selRange.start.isTouching( selRange.end ) ) {
@@ -97,7 +97,7 @@ export default function deleteContent( model, selection, options = {} ) {
 		// as it's hard to imagine what should actually be the default behavior. Usually, specific features will
 		// want to override that behavior anyway.
 		if ( !options.leaveUnmerged ) {
-			mergeBranches( writer, startPos, endPos );
+			mergeBranches( writer, startPosition, endPosition );
 
 			// TMP this will be replaced with a postfixer.
 			// We need to check and strip disallowed attributes in all nested nodes because after merge
@@ -105,81 +105,331 @@ export default function deleteContent( model, selection, options = {} ) {
 			//
 			// e.g. bold is disallowed for <H1>
 			// <h1>Fo{o</h1><p>b}a<b>r</b><p> -> <h1>Fo{}a<b>r</b><h1> -> <h1>Fo{}ar<h1>.
-			schema.removeDisallowedAttributes( startPos.parent.getChildren(), writer );
+			schema.removeDisallowedAttributes( startPosition.parent.getChildren(), writer );
 		}
 
-		collapseSelectionAt( writer, selection, startPos );
+		collapseSelectionAt( writer, selection, startPosition );
 
 		// 4. Add a paragraph to set selection in it.
 		// Check if a text is allowed in the new container. If not, try to create a new paragraph (if it's allowed here).
 		// If autoparagraphing is off, we assume that you know what you do so we leave the selection wherever it was.
-		if ( !options.doNotAutoparagraph && shouldAutoparagraph( schema, startPos ) ) {
-			insertParagraph( writer, startPos, selection );
+		if ( !options.doNotAutoparagraph && shouldAutoparagraph( schema, startPosition ) ) {
+			insertParagraph( writer, startPosition, selection );
 		}
 
-		endPos.detach();
+		startPosition.detach();
+		endPosition.detach();
 	} );
 }
 
+// Returns the live positions for the range adjusted to span only blocks selected from the user perspective. Example:
+//
+//     <heading1>[foo</heading1>
+//     <paragraph>bar</paragraph>
+//     <heading1>]abc</heading1>  <-- this block is not considered as selected
+//
+// This is the same behavior as in Selection#getSelectedBlocks() "special case".
+function getLivePositionsForSelectedBlocks( range ) {
+	const model = range.root.document.model;
+
+	const startPosition = range.start;
+	let endPosition = range.end;
+
+	// If the end of selection is at the start position of last block in the selection, then
+	// shrink it to not include that trailing block. Note that this should happen only for not empty selection.
+	if ( model.hasContent( range, { ignoreMarkers: true } ) ) {
+		const endBlock = getParentBlock( endPosition );
+
+		if ( endBlock && endPosition.isTouching( model.createPositionAt( endBlock, 0 ) ) ) {
+			// Create forward selection as a probe to find a valid position after excluding last block from the range.
+			const selection = model.createSelection( range );
+
+			// Modify the forward selection in backward direction to shrink it and remove first position of following block from it.
+			// This is how modifySelection works and here we are making use of it.
+			model.modifySelection( selection, { direction: 'backward' } );
+
+			endPosition = selection.getLastPosition();
+		}
+	}
+
+	return [
+		LivePosition.fromPosition( startPosition, 'toPrevious' ),
+		LivePosition.fromPosition( endPosition, 'toNext' )
+	];
+}
+
+// Finds the lowest element in position's ancestors which is a block.
+// Returns null if a limit element is encountered before reaching a block element.
+function getParentBlock( position ) {
+	const element = position.parent;
+	const schema = element.root.document.model.schema;
+	const ancestors = element.getAncestors( { parentFirst: true, includeSelf: true } );
+
+	for ( const element of ancestors ) {
+		if ( schema.isLimit( element ) ) {
+			return null;
+		}
+
+		if ( schema.isBlock( element ) ) {
+			return element;
+		}
+	}
+}
+
 // This function is a result of reaching the Ballmer's peak for just the right amount of time.
 // Even I had troubles documenting it after a while and after reading it again I couldn't believe that it really works.
-function mergeBranches( writer, startPos, endPos ) {
-	const startParent = startPos.parent;
-	const endParent = endPos.parent;
+function mergeBranches( writer, startPosition, endPosition ) {
+	const model = writer.model;
 
-	// If both positions ended up in the same parent, then there's nothing more to merge:
-	// <$root><p>x[]</p><p>{}y</p></$root> => <$root><p>xy</p>[]{}</$root>
-	if ( startParent == endParent ) {
+	// Verify if there is a need and possibility to merge.
+	if ( !checkShouldMerge( writer.model.schema, startPosition, endPosition ) ) {
 		return;
 	}
 
-	// If one of the positions is a limit element, then there's nothing to merge because we don't want to cross the limit boundaries.
-	if ( writer.model.schema.isLimit( startParent ) || writer.model.schema.isLimit( endParent ) ) {
-		return;
+	// If the start element on the common ancestor level is empty, and the end element on the same level is not empty
+	// then merge those to the right element so that it's properties are preserved (name, attributes).
+	// Because of OT merging is used instead of removing elements.
+	//
+	// Merge left:
+	//     <heading1>foo[</heading1>    ->  <heading1>foo[]bar</heading1>
+	//     <paragraph>]bar</paragraph>  ->               --^
+	//
+	// Merge right:
+	//     <heading1>[</heading1>       ->
+	//     <paragraph>]bar</paragraph>  ->  <paragraph>[]bar</paragraph>
+	//
+	// Merge left:
+	//     <blockQuote>                     ->  <blockQuote>
+	//         <heading1>foo[</heading1>    ->      <heading1>foo[]bar</heading1>
+	//         <paragraph>]bar</paragraph>  ->                   --^
+	//     </blockQuote>                    ->  </blockQuote>
+	//
+	// Merge right:
+	//     <blockQuote>                     ->  <blockQuote>
+	//         <heading1>[</heading1>       ->
+	//         <paragraph>]bar</paragraph>  ->      <paragraph>[]bar</paragraph>
+	//     </blockQuote>                    ->  </blockQuote>
+
+	// Merging should not go deeper than common ancestor.
+	const [ startAncestor, endAncestor ] = getAncestorsJustBelowCommonAncestor( startPosition, endPosition );
+
+	if ( !model.hasContent( startAncestor, { ignoreMarkers: true } ) && model.hasContent( endAncestor, { ignoreMarkers: true } ) ) {
+		mergeBranchesRight( writer, startPosition, endPosition, startAncestor.parent );
+	} else {
+		mergeBranchesLeft( writer, startPosition, endPosition, startAncestor.parent );
 	}
+}
 
-	// Check if operations we'll need to do won't need to cross object or limit boundaries.
-	// E.g., we can't merge endParent into startParent in this case:
-	// <limit><startParent>x[]</startParent></limit><endParent>{}</endParent>
-	if ( !checkCanBeMerged( startPos, endPos, writer.model.schema ) ) {
+// Merging blocks to the left (properties of the left block are preserved).
+// Simple example:
+//     <heading1>foo[</heading1>    ->  <heading1>foo[bar</heading1>]
+//     <paragraph>]bar</paragraph>  ->              --^
+//
+// Nested example:
+//     <blockQuote>                     ->  <blockQuote>
+//         <heading1>foo[</heading1>    ->      <heading1>foo[bar</heading1>
+//     </blockQuote>                    ->  </blockQuote>]    ^
+//     <blockBlock>                     ->                    |
+//         <paragraph>]bar</paragraph>  ->                 ---
+//     </blockBlock>                    ->
+//
+function mergeBranchesLeft( writer, startPosition, endPosition, commonAncestor ) {
+	const startElement = startPosition.parent;
+	const endElement = endPosition.parent;
+
+	// Merging reached the common ancestor element, stop here.
+	if ( startElement == commonAncestor || endElement == commonAncestor ) {
 		return;
 	}
 
-	// Remember next positions to merge. For example:
-	// <a><b>x[]</b></a><c><d>{}y</d></c>
-	// will become:
-	// <a><b>xy</b>[]</a><c>{}</c>
-	startPos = writer.createPositionAfter( startParent );
-	endPos = writer.createPositionBefore( endParent );
+	// Remember next positions to merge in next recursive step (also used as modification points pointers).
+	startPosition = writer.createPositionAfter( startElement );
+	endPosition = writer.createPositionBefore( endElement );
 
-	if ( !endPos.isEqual( startPos ) ) {
-		// In this case, before we merge, we need to move `endParent` to the `startPos`:
-		// <a><b>x[]</b></a><c><d>{}y</d></c>
-		// becomes:
-		// <a><b>x</b>[]<d>y</d></a><c>{}</c>
-		writer.insert( endParent, startPos );
+	// Move endElement just after startElement if they aren't siblings.
+	if ( !endPosition.isEqual( startPosition ) ) {
+		//
+		//     <blockQuote>                     ->  <blockQuote>
+		//         <heading1>foo[</heading1>    ->      <heading1>foo</heading1>[<paragraph>bar</paragraph>
+		//     </blockQuote>                    ->  </blockQuote>                ^
+		//     <blockBlock>                     ->  <blockBlock>                 |
+		//         <paragraph>]bar</paragraph>  ->      ]                     ---
+		//     </blockBlock>                    ->  </blockBlock>
+		//
+		writer.insert( endElement, startPosition );
 	}
 
-	// Merge two siblings:
-	// <a>x</a>[]<b>y</b> -> <a>xy</a> (the usual case)
-	// <a><b>x</b>[]<d>y</d></a><c></c> -> <a><b>xy</b>[]</a><c></c> (this is the "move parent" case shown above)
-	writer.merge( startPos );
+	// Merge two siblings (nodes on sides of startPosition):
+	//
+	//     <blockQuote>                                             ->  <blockQuote>
+	//         <heading1>foo</heading1>[<paragraph>bar</paragraph>  ->      <heading1>foo[bar</heading1>
+	//     </blockQuote>                                            ->  </blockQuote>
+	//     <blockBlock>                                             ->  <blockBlock>
+	//         ]                                                    ->      ]
+	//     </blockBlock>                                            ->  </blockBlock>
+	//
+	// Or in simple case (without moving elements in above if):
+	//     <heading1>foo</heading1>[<paragraph>bar</paragraph>]  ->  <heading1>foo[bar</heading1>]
+	//
+	writer.merge( startPosition );
 
 	// Remove empty end ancestors:
-	// <a>fo[o</a><b><a><c>bar]</c></a></b>
-	// becomes:
-	// <a>fo[]</a><b><a>{}</a></b>
-	// So we can remove <a> and <b>.
-	while ( endPos.parent.isEmpty ) {
-		const parentToRemove = endPos.parent;
+	//
+	//     <blockQuote>                      ->  <blockQuote>
+	//         <heading1>foo[bar</heading1>  ->      <heading1>foo[bar</heading1>
+	//     </blockQuote>                     ->  </blockQuote>
+	//     <blockBlock>                      ->
+	//         ]                             ->  ]
+	//     </blockBlock>                     ->
+	//
+	while ( endPosition.parent.isEmpty ) {
+		const parentToRemove = endPosition.parent;
+
+		endPosition = writer.createPositionBefore( parentToRemove );
 
-		endPos = writer.createPositionBefore( parentToRemove );
+		writer.remove( parentToRemove );
+	}
+
+	// Verify if there is a need and possibility to merge next level.
+	if ( !checkShouldMerge( writer.model.schema, startPosition, endPosition ) ) {
+		return;
+	}
+
+	// Continue merging next level (blockQuote with blockBlock in the examples above if it would not be empty and got removed).
+	mergeBranchesLeft( writer, startPosition, endPosition, commonAncestor );
+}
+
+// Merging blocks to the right (properties of the right block are preserved).
+// Simple example:
+//     <heading1>foo[</heading1>    ->            --v
+//     <paragraph>]bar</paragraph>  ->  [<paragraph>foo]bar</paragraph>
+//
+// Nested example:
+//     <blockQuote>                     ->
+//         <heading1>foo[</heading1>    ->              ---
+//     </blockQuote>                    ->                 |
+//     <blockBlock>                     ->  [<blockBlock>  v
+//         <paragraph>]bar</paragraph>  ->      <paragraph>foo]bar</paragraph>
+//     </blockBlock>                    ->  </blockBlock>
+//
+function mergeBranchesRight( writer, startPosition, endPosition, commonAncestor ) {
+	const startElement = startPosition.parent;
+	const endElement = endPosition.parent;
+
+	// Merging reached the common ancestor element, stop here.
+	if ( startElement == commonAncestor || endElement == commonAncestor ) {
+		return;
+	}
+
+	// Remember next positions to merge in next recursive step (also used as modification points pointers).
+	startPosition = writer.createPositionAfter( startElement );
+	endPosition = writer.createPositionBefore( endElement );
+
+	// Move startElement just before endElement if they aren't siblings.
+	if ( !endPosition.isEqual( startPosition ) ) {
+		//
+		//     <blockQuote>                     ->  <blockQuote>
+		//         <heading1>foo[</heading1>    ->      [                   ---
+		//     </blockQuote>                    ->  </blockQuote>              |
+		//     <blockBlock>                     ->  <blockBlock>               v
+		//         <paragraph>]bar</paragraph>  ->      <heading1>foo</heading1>]<paragraph>bar</paragraph>
+		//     </blockBlock>                    ->  </blockBlock>
+		//
+		writer.insert( startElement, endPosition );
+	}
+
+	// Remove empty end ancestors:
+	//
+	//     <blockQuote>                                             ->
+	//         [                                                    ->  [
+	//     </blockQuote>                                            ->
+	//     <blockBlock>                                             ->  <blockBlock>
+	//         <heading1>foo</heading1>]<paragraph>bar</paragraph>  ->      <heading1>foo</heading1>]<paragraph>bar</paragraph>
+	//     </blockBlock>                                            ->  </blockBlock>
+	//
+	while ( startPosition.parent.isEmpty ) {
+		const parentToRemove = startPosition.parent;
+
+		startPosition = writer.createPositionBefore( parentToRemove );
 
 		writer.remove( parentToRemove );
 	}
 
-	// Continue merging next level.
-	mergeBranches( writer, startPos, endPos );
+	// Update endPosition after inserting and removing elements.
+	endPosition = writer.createPositionBefore( endElement );
+
+	// Merge right two siblings (nodes on sides of endPosition):
+	//                                                              ->
+	//     [                                                        ->  [
+	//                                                              ->
+	//     <blockBlock>                                             ->  <blockBlock>
+	//         <heading1>foo</heading1>]<paragraph>bar</paragraph>  ->      <paragraph>foo]bar</paragraph>
+	//     </blockBlock>                                            ->  </blockBlock>
+	//
+	// Or in simple case (without moving elements in above if):
+	//     [<heading1>foo</heading1>]<paragraph>bar</paragraph>  ->  [<heading1>foo]bar</heading1>
+	//
+	mergeRight( writer, endPosition );
+
+	// Verify if there is a need and possibility to merge next level.
+	if ( !checkShouldMerge( writer.model.schema, startPosition, endPosition ) ) {
+		return;
+	}
+
+	// Continue merging next level (blockQuote with blockBlock in the examples above if it would not be empty and got removed).
+	mergeBranchesRight( writer, startPosition, endPosition, commonAncestor );
+}
+
+// There is no right merge operation so we need to simulate it.
+function mergeRight( writer, position ) {
+	const startElement = position.nodeBefore;
+	const endElement = position.nodeAfter;
+
+	if ( startElement.name != endElement.name ) {
+		writer.rename( startElement, endElement.name );
+	}
+
+	writer.clearAttributes( startElement );
+	writer.setAttributes( Object.fromEntries( endElement.getAttributes() ), startElement );
+
+	writer.merge( position );
+}
+
+// Verifies if merging is needed and possible. It's not needed if both positions are in the same element
+// and it's not possible if some element is a limit or the range crosses a limit element.
+function checkShouldMerge( schema, startPosition, endPosition ) {
+	const startElement = startPosition.parent;
+	const endElement = endPosition.parent;
+
+	// If both positions ended up in the same parent, then there's nothing more to merge:
+	// <$root><p>x[</p><p>]y</p></$root> => <$root><p>xy</p>[]</$root>
+	if ( startElement == endElement ) {
+		return false;
+	}
+
+	// If one of the positions is a limit element, then there's nothing to merge because we don't want to cross the limit boundaries.
+	if ( schema.isLimit( startElement ) || schema.isLimit( endElement ) ) {
+		return false;
+	}
+
+	// Check if operations we'll need to do won't need to cross object or limit boundaries.
+	// E.g., we can't merge endElement into startElement in this case:
+	// <limit><startElement>x[</startElement></limit><endElement>]</endElement>
+	return isCrossingLimitElement( startPosition, endPosition, schema );
+}
+
+// Returns the elements that are the ancestors of the provided positions that are direct children of the common ancestor.
+function getAncestorsJustBelowCommonAncestor( positionA, positionB ) {
+	const ancestorsA = positionA.getAncestors();
+	const ancestorsB = positionB.getAncestors();
+
+	let i = 0;
+
+	while ( ancestorsA[ i ] && ancestorsA[ i ] == ancestorsB[ i ] ) {
+		i++;
+	}
+
+	return [ ancestorsA[ i ], ancestorsB[ i ] ];
 }
 
 function shouldAutoparagraph( schema, position ) {
@@ -195,7 +445,7 @@ function shouldAutoparagraph( schema, position ) {
 // E.g. in <bQ><p>x[]</p></bQ><widget><caption>{}</caption></widget>
 // we'll check <p>, <bQ>, <widget> and <caption>.
 // Usually, widget and caption are marked as objects/limits in the schema, so in this case merging will be blocked.
-function checkCanBeMerged( leftPos, rightPos, schema ) {
+function isCrossingLimitElement( leftPos, rightPos, schema ) {
 	const rangeToCheck = new Range( leftPos, rightPos );
 
 	for ( const value of rangeToCheck.getWalker() ) {

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

@@ -73,7 +73,7 @@ export default function getSelectedContent( model, selection ) {
 			if ( item.is( 'textProxy' ) ) {
 				writer.appendText( item.data, item.getAttributes(), frag );
 			} else {
-				writer.append( item._clone( true ), frag );
+				writer.append( writer.cloneElement( item, true ), frag );
 			}
 		}
 

+ 8 - 1
packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js

@@ -82,7 +82,14 @@ function selectionPostFixer( writer, model ) {
 		// Those ranges might overlap but will be corrected later.
 		const correctedRange = tryFixingRange( modelRange, schema );
 
-		if ( correctedRange ) {
+		// "Selection fixing" algorithms sometimes get lost. In consequence, it may happen
+		// that a new range is returned but, in fact, it has the same positions as the original
+		// range anyway. If this range is not discarded, a new selection will be set and that,
+		// for instance, would destroy the selection attributes. Let's make sure that the post-fixer
+		// actually worked first before setting a new selection.
+		//
+		// https://github.com/ckeditor/ckeditor5/issues/6693
+		if ( correctedRange && !correctedRange.isEqual( modelRange ) ) {
 			ranges.push( correctedRange );
 			wasFixed = true;
 		} else {

+ 12 - 0
packages/ckeditor5-engine/src/model/writer.js

@@ -116,6 +116,18 @@ export default class Writer {
 		return new DocumentFragment();
 	}
 
+	/**
+	 * Creates a copy of the element and returns it. Created element has the same name and attributes as the original element.
+	 * If clone is deep, the original element's children are also cloned. If not, then empty element is returned.
+	 *
+	 * @param {module:engine/model/element~Element} element The element to clone.
+	 * @param {Boolean} [deep=true] If set to `true` clones element and all its children recursively. When set to `false`,
+	 * element will be cloned without any child.
+	 */
+	cloneElement( element, deep = true ) {
+		return element._clone( deep );
+	}
+
 	/**
 	 * Inserts item on given position.
 	 *

+ 1 - 0
packages/ckeditor5-engine/src/view/renderer.js

@@ -930,6 +930,7 @@ function addInlineFiller( domDocument, domParentOrArray, offset ) {
 function areSimilar( node1, node2 ) {
 	return isNode( node1 ) && isNode( node2 ) &&
 		!isText( node1 ) && !isText( node2 ) &&
+		node1.nodeType !== Node.COMMENT_NODE && node2.nodeType !== Node.COMMENT_NODE &&
 		node1.tagName.toLowerCase() === node2.tagName.toLowerCase();
 }
 

+ 45 - 11
packages/ckeditor5-engine/tests/manual/highlight.html

@@ -1,37 +1,61 @@
 <style>
+	:root {
+		--ck-manual-test-highlight-yellow: hsla(60, 100%, 50%, 0.5);
+		--ck-manual-test-highlight-red: hsla(0, 100%, 50%, 0.5);
+		--ck-manual-test-highlight-blue: hsla(240, 100%, 50%, 0.5);
+	}
+
 	span.highlight-yellow {
-		background-color: rgba( 255, 255, 0, 0.5 );
+		background-color: var(--ck-manual-test-highlight-yellow);
 	}
 
 	span.highlight-blue {
-		background-color: rgba( 0, 0, 255, 0.5 );
+		background-color: var(--ck-manual-test-highlight-blue);
 	}
 
 	span.highlight-red {
-		background-color: rgba( 255, 0, 0, 0.5 );
+		background-color: var(--ck-manual-test-highlight-red);
 	}
 
 	.ck-widget.fancy-widget {
-		background-color: black;
-		color: white;
+		background-color: hsl(0, 0%, 0%);
+		color: hsl(0, 0%, 100%);
 		text-align: center;
 		height: 40px;
 		line-height: 40px;
 	}
 
 	.ck-widget.fancy-widget.highlight-yellow {
-		background-color: yellow;
-		color: black;
+		background-color: var(--ck-manual-test-highlight-yellow);
+		color: hsl(0, 0%, 0%);
 	}
 
 	.ck-widget.fancy-widget.highlight-blue {
-		background-color: blue;
-		color: black;
+		background-color: var(--ck-manual-test-highlight-blue);
+		color: hsl(0, 0%, 0%);
 	}
 
 	.ck-widget.fancy-widget.highlight-red {
-		background-color: red;
-		color: black;
+		background-color: var(--ck-manual-test-highlight-red);
+		color: hsl(0, 0%, 0%);
+	}
+
+	.table td.highlight-yellow,
+	.table th.highlight-yellow {
+		/* !important to override selected editable styles. */
+		background-color: var(--ck-manual-test-highlight-yellow) !important;
+	}
+
+	.table td.highlight-blue,
+	.table th.highlight-blue {
+		/* !important to override selected editable styles. */
+		background-color: var(--ck-manual-test-highlight-blue) !important;
+	}
+
+	.table td.highlight-red,
+	.table th.highlight-red {
+		/* !important to override selected editable styles. */
+		background-color: var(--ck-manual-test-highlight-red) !important;
 	}
 
 	button {
@@ -43,6 +67,16 @@
 	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas at diam quis justo imperdiet posuere non eu tortor. Mauris vel magna eu sem hendrerit varius. Ut imperdiet velit ut ante interdum convallis. Vestibulum vitae lacinia mi. </p>
 	<figure></figure>
 	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas at diam quis justo imperdiet posuere non eu tortor. Mauris vel magna eu sem hendrerit varius. Ut imperdiet velit ut ante interdum convallis. Vestibulum vitae lacinia mi. </p>
+	<table>
+		<tr>
+			<td>foo</td>
+			<td>bar</td>
+		</tr>
+		<tr>
+			<td>baz</td>
+			<td>qux</td>
+		</tr>
+	</table>
 </div>
 <h2>Markers</h2>
 <button id="add-marker-yellow">Set yellow highlight</button><button id="remove-marker-yellow">Remove yellow highlight</button>

+ 3 - 2
packages/ckeditor5-engine/tests/manual/highlight.js

@@ -16,6 +16,7 @@ import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
 import List from '@ckeditor/ckeditor5-list/src/list';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import Table from '@ckeditor/ckeditor5-table/src/table';
 import Widget from '@ckeditor/ckeditor5-widget/src/widget';
 import { toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
 
@@ -53,8 +54,8 @@ class FancyWidget extends Plugin {
 }
 
 ClassicEditor.create( global.document.querySelector( '#editor' ), {
-	plugins: [ Enter, Typing, Paragraph, Undo, Heading, Bold, Italic, List, FancyWidget ],
-	toolbar: [ 'heading', '|', 'undo', 'redo', 'bold', 'italic', 'numberedList', 'bulletedList' ]
+	plugins: [ Enter, Typing, Paragraph, Undo, Heading, Bold, Italic, List, Table, FancyWidget ],
+	toolbar: [ 'heading', '|', 'undo', 'redo', 'bold', 'italic', 'numberedList', 'bulletedList', 'insertTable' ]
 } )
 	.then( editor => {
 		window.editor = editor;

+ 6 - 1
packages/ckeditor5-engine/tests/manual/highlight.md

@@ -10,5 +10,10 @@ applied.
 1. Red highlight should be visible on the whole content, including the widget.
 1. Play with highlights, add and remove them, check if they are applied correctly.
 
-NOTE: Yellow highlight should be visible over red and blue one. Red highlight should be visible over blue one. 
+### Highlighting table cells
+
+1. You can select the entire table cell and apply a highlight to it.
+1. The highlight should change the background of an **entire cell** (not only its content).
+
+NOTE: Yellow highlight should be visible over red and blue one. Red highlight should be visible over blue one.
 

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

@@ -2035,7 +2035,7 @@ describe( 'Differ', () => {
 
 		for ( let i = 0; i < expected.length; i++ ) {
 			for ( const key in expected[ i ] ) {
-				if ( expected[ i ].hasOwnProperty( key ) ) {
+				if ( Object.prototype.hasOwnProperty.call( expected[ i ], key ) ) {
 					if ( key == 'position' || key == 'range' ) {
 						expect( changes[ i ][ key ].isEqual( expected[ i ][ key ] ), `item ${ i }, key "${ key }"` ).to.be.true;
 					} else {

+ 13 - 0
packages/ckeditor5-engine/tests/model/model.js

@@ -659,6 +659,8 @@ describe( 'Model', () => {
 
 			expect( model.hasContent( pEmpty ) ).to.be.false;
 			expect( model.hasContent( pEmpty, { ignoreWhitespaces: true } ) ).to.be.false;
+			expect( model.hasContent( pEmpty, { ignoreMarkers: true } ) ).to.be.false;
+			expect( model.hasContent( pEmpty, { ignoreMarkers: true, ignoreWhitespaces: true } ) ).to.be.false;
 		} );
 
 		it( 'should return false for empty element with marker (usingOperation=true, affectsData=false)', () => {
@@ -672,6 +674,8 @@ describe( 'Model', () => {
 
 			expect( model.hasContent( pEmpty ) ).to.be.false;
 			expect( model.hasContent( pEmpty, { ignoreWhitespaces: true } ) ).to.be.false;
+			expect( model.hasContent( pEmpty, { ignoreMarkers: true } ) ).to.be.false;
+			expect( model.hasContent( pEmpty, { ignoreMarkers: true, ignoreWhitespaces: true } ) ).to.be.false;
 		} );
 
 		it( 'should return false (ignoreWhitespaces) for empty text with marker (usingOperation=false, affectsData=false)', () => {
@@ -688,6 +692,7 @@ describe( 'Model', () => {
 			} );
 
 			expect( model.hasContent( pEmpty, { ignoreWhitespaces: true } ) ).to.be.false;
+			expect( model.hasContent( pEmpty, { ignoreWhitespaces: true, ignoreMarkers: true } ) ).to.be.false;
 		} );
 
 		it( 'should return true for empty text with marker (usingOperation=false, affectsData=false)', () => {
@@ -704,6 +709,8 @@ describe( 'Model', () => {
 			} );
 
 			expect( model.hasContent( pEmpty ) ).to.be.true;
+			expect( model.hasContent( pEmpty, { ignoreMarkers: true } ) ).to.be.true;
+			expect( model.hasContent( pEmpty, { ignoreMarkers: true, ignoreWhitespaces: true } ) ).to.be.false;
 		} );
 
 		it( 'should return false for empty element with marker (usingOperation=false, affectsData=true)', () => {
@@ -717,6 +724,8 @@ describe( 'Model', () => {
 
 			expect( model.hasContent( pEmpty ) ).to.be.false;
 			expect( model.hasContent( pEmpty, { ignoreWhitespaces: true } ) ).to.be.false;
+			expect( model.hasContent( pEmpty, { ignoreMarkers: true } ) ).to.be.false;
+			expect( model.hasContent( pEmpty, { ignoreMarkers: true, ignoreWhitespaces: true } ) ).to.be.false;
 		} );
 
 		it( 'should return false for empty element with marker (usingOperation=true, affectsData=true)', () => {
@@ -730,6 +739,8 @@ describe( 'Model', () => {
 
 			expect( model.hasContent( pEmpty ) ).to.be.false;
 			expect( model.hasContent( pEmpty, { ignoreWhitespaces: true } ) ).to.be.false;
+			expect( model.hasContent( pEmpty, { ignoreMarkers: true } ) ).to.be.false;
+			expect( model.hasContent( pEmpty, { ignoreMarkers: true, ignoreWhitespaces: true } ) ).to.be.false;
 		} );
 
 		it( 'should return true (ignoreWhitespaces) for empty text with marker (usingOperation=false, affectsData=true)', () => {
@@ -747,6 +758,8 @@ describe( 'Model', () => {
 
 			expect( model.hasContent( pEmpty ) ).to.be.true;
 			expect( model.hasContent( pEmpty, { ignoreWhitespaces: true } ) ).to.be.true;
+			expect( model.hasContent( pEmpty, { ignoreMarkers: true } ) ).to.be.true;
+			expect( model.hasContent( pEmpty, { ignoreMarkers: true, ignoreWhitespaces: true } ) ).to.be.false;
 		} );
 	} );
 

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

@@ -39,11 +39,10 @@ describe( 'transform', () => {
 
 	function expectOperation( op, params ) {
 		for ( const i in params ) {
-			if ( params.hasOwnProperty( i ) ) {
+			if ( Object.prototype.hasOwnProperty.call( params, i ) ) {
 				if ( i == 'type' ) {
 					expect( op, 'type' ).to.be.instanceof( params[ i ] );
-				}
-				else if ( params[ i ] instanceof Array ) {
+				} else if ( params[ i ] instanceof Array ) {
 					expect( op[ i ].length, i ).to.equal( params[ i ].length );
 
 					for ( let j = 0; j < params[ i ].length; j++ ) {

+ 302 - 97
packages/ckeditor5-engine/tests/model/utils/deletecontent.js

@@ -216,11 +216,13 @@ describe( 'DataController utils', () => {
 					allowIn: 'pparent',
 					allowAttributes: 'align'
 				} );
-				schema.register( 'heading1', { inheritAllFrom: '$block' } );
+				schema.register( 'heading1', { inheritAllFrom: '$block', allowIn: 'pparent' } );
 				schema.register( 'image', { inheritAllFrom: '$text' } );
 				schema.register( 'pchild', { allowIn: 'paragraph' } );
 				schema.register( 'pparent', { allowIn: '$root' } );
-				schema.extend( '$text', { allowIn: [ 'pchild', 'pparent' ] } );
+				schema.register( 'hchild', { allowIn: 'heading1' } );
+				schema.register( 'widget', { isObject: true, allowWhere: '$text', isInline: true } );
+				schema.extend( '$text', { allowIn: [ 'pchild', 'pparent', 'hchild' ] } );
 			} );
 
 			test(
@@ -235,6 +237,30 @@ describe( 'DataController utils', () => {
 				'<paragraph>x</paragraph><paragraph>fo[]ar</paragraph><paragraph>y</paragraph>'
 			);
 
+			test(
+				'merges first element into the second element (it would become empty but second element would not) (same name)',
+				'<paragraph>x</paragraph><paragraph>[foo</paragraph><paragraph>b]ar</paragraph><paragraph>y</paragraph>',
+				'<paragraph>x</paragraph><paragraph>[]ar</paragraph><paragraph>y</paragraph>'
+			);
+
+			test(
+				'do not remove end block if selection ends at start position of it',
+				'<paragraph>x</paragraph><paragraph>[foo</paragraph><paragraph>]bar</paragraph><paragraph>y</paragraph>',
+				'<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>bar</paragraph><paragraph>y</paragraph>'
+			);
+
+			test(
+				'removes empty element (merges it into second element)',
+				'<paragraph>x</paragraph><paragraph>[</paragraph><paragraph>]bar</paragraph><paragraph>y</paragraph>',
+				'<paragraph>x</paragraph><paragraph>[]bar</paragraph><paragraph>y</paragraph>'
+			);
+
+			test(
+				'treats inline widget elements as content so parent element is not considered as empty after merging (same name)',
+				'<paragraph>x</paragraph><paragraph><widget></widget>[foo</paragraph><paragraph>b]ar</paragraph><paragraph>y</paragraph>',
+				'<paragraph>x</paragraph><paragraph><widget></widget>[]ar</paragraph><paragraph>y</paragraph>'
+			);
+
 			test(
 				'does not merge second element into the first one (same name, !option.merge)',
 				'<paragraph>x</paragraph><paragraph>fo[o</paragraph><paragraph>b]ar</paragraph><paragraph>y</paragraph>',
@@ -242,12 +268,25 @@ describe( 'DataController utils', () => {
 				{ leaveUnmerged: true }
 			);
 
+			test(
+				'does not remove first empty element when it\'s empty but second element is not empty (same name, !option.merge)',
+				'<paragraph>x</paragraph><paragraph>[foo</paragraph><paragraph>b]ar</paragraph><paragraph>y</paragraph>',
+				'<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>ar</paragraph><paragraph>y</paragraph>',
+				{ leaveUnmerged: true }
+			);
+
 			test(
 				'merges second element into the first one (different name)',
 				'<paragraph>x</paragraph><heading1>fo[o</heading1><paragraph>b]ar</paragraph><paragraph>y</paragraph>',
 				'<paragraph>x</paragraph><heading1>fo[]ar</heading1><paragraph>y</paragraph>'
 			);
 
+			test(
+				'removes first element when it\'s empty but second element is not empty (different name)',
+				'<paragraph>x</paragraph><heading1>[foo</heading1><paragraph>b]ar</paragraph><paragraph>y</paragraph>',
+				'<paragraph>x</paragraph><paragraph>[]ar</paragraph><paragraph>y</paragraph>'
+			);
+
 			// Note: in all these cases we ignore the direction of merge.
 			// If https://github.com/ckeditor/ckeditor5-engine/issues/470 was fixed we could differently treat
 			// forward and backward delete.
@@ -270,9 +309,9 @@ describe( 'DataController utils', () => {
 			);
 
 			test(
-				'merges second element to an empty first element',
+				'removes empty first element',
 				'<paragraph>x</paragraph><heading1>[</heading1><paragraph>fo]o</paragraph><paragraph>y</paragraph>',
-				'<paragraph>x</paragraph><heading1>[]o</heading1><paragraph>y</paragraph>'
+				'<paragraph>x</paragraph><paragraph>[]o</paragraph><paragraph>y</paragraph>'
 			);
 
 			test(
@@ -283,8 +322,20 @@ describe( 'DataController utils', () => {
 
 			test(
 				'leaves just one element when all selected',
+				'<paragraph>[x</paragraph><paragraph>foo</paragraph><paragraph>y]bar</paragraph>',
+				'<paragraph>[]bar</paragraph>'
+			);
+
+			test(
+				'leaves just one (last) element when all selected (first block would become empty) (different name)',
 				'<heading1>[x</heading1><paragraph>foo</paragraph><paragraph>y]bar</paragraph>',
-				'<heading1>[]bar</heading1>'
+				'<paragraph>[]bar</paragraph>'
+			);
+
+			test(
+				'leaves just one (first) element when all selected (first block would not become empty) (different name)',
+				'<heading1>foo[x</heading1><paragraph>bar</paragraph><paragraph>y]</paragraph>',
+				'<heading1>foo[]</heading1>'
 			);
 
 			it( 'uses merge operation even if merged element is empty', () => {
@@ -317,6 +368,36 @@ describe( 'DataController utils', () => {
 				expect( mergeSpy.called ).to.be.true;
 			} );
 
+			it( 'uses "merge" operation (from OT) if first element is empty (because of content delete) and last is not', () => {
+				let mergeSpy;
+
+				setData( model, '<paragraph>[abcd</paragraph><paragraph>ef]gh</paragraph>' );
+
+				model.change( writer => {
+					mergeSpy = sinon.spy( writer, 'merge' );
+					deleteContent( model, doc.selection );
+				} );
+
+				expect( getData( model ) ).to.equal( '<paragraph>[]gh</paragraph>' );
+
+				expect( mergeSpy.called ).to.be.true;
+			} );
+
+			it( 'uses merge operation if first element is empty and last is not', () => {
+				let mergeSpy;
+
+				setData( model, '<paragraph>[</paragraph><paragraph>ef]gh</paragraph>' );
+
+				model.change( writer => {
+					mergeSpy = sinon.spy( writer, 'merge' );
+					deleteContent( model, doc.selection );
+				} );
+
+				expect( getData( model ) ).to.equal( '<paragraph>[]gh</paragraph>' );
+
+				expect( mergeSpy.called ).to.be.true;
+			} );
+
 			it( 'does not try to move the second block if not needed', () => {
 				let mergeSpy, moveSpy;
 
@@ -344,47 +425,41 @@ describe( 'DataController utils', () => {
 					'<paragraph>x<pchild>fo[]ar</pchild>y</paragraph>'
 				);
 
-				it( 'merges elements when deep nested (3rd level)', () => {
-					const root = doc.getRoot();
-
-					// We need to use the raw API due to https://github.com/ckeditor/ckeditor5-engine/issues/905.
-					// <pparent>x<paragraph>x<pchild>fo[o</pchild></paragraph></pparent>
-					// <pparent><paragraph><pchild>b]ar</pchild>y</paragraph>y</pparent>
-
-					root._appendChild(
-						new Element( 'pparent', null, [
-							'x',
-							new Element( 'paragraph', null, [
-								'x',
-								new Element( 'pchild', null, 'foo' )
-							] )
-						] )
-					);
+				test(
+					'merges block element to the right (with nested element)',
+					'<paragraph><pchild>[foo</pchild></paragraph><paragraph><pchild>b]ar</pchild></paragraph>',
+					'<paragraph><pchild>[]ar</pchild></paragraph>'
+				);
 
-					root._appendChild(
-						new Element( 'pparent', null, [
-							new Element( 'paragraph', null, [
-								new Element( 'pchild', null, 'bar' ),
-								'y'
-							] ),
-							'y'
-						] )
-					);
+				test(
+					'does not remove block element with nested element and object',
+					'<paragraph><pchild><widget></widget>[foo</pchild></paragraph><paragraph><pchild>b]ar</pchild></paragraph>',
+					'<paragraph><pchild><widget></widget>[]ar</pchild></paragraph>'
+				);
 
-					const range = new Range(
-						new Position( doc.getRoot(), [ 0, 1, 1, 2 ] ), // fo[o
-						new Position( doc.getRoot(), [ 1, 0, 0, 1 ] ) // b]ar
-					);
+				test(
+					'merges nested elements',
+					'<heading1><hchild>x[foo</hchild></heading1><paragraph><pchild>b]ar</pchild></paragraph>',
+					'<heading1><hchild>x[]ar</hchild></heading1>'
+				);
 
-					model.change( writer => {
-						writer.setSelection( range );
-					} );
+				test(
+					'merges nested elements on multiple levels',
+					'<heading1><hchild>x[foo</hchild></heading1><paragraph><pchild>b]ar</pchild>abc</paragraph>',
+					'<heading1><hchild>x[]ar</hchild>abc</heading1>'
+				);
 
-					deleteContent( model, doc.selection );
+				test(
+					'merges nested elements to the right if left side element would become empty',
+					'<heading1><hchild>[foo</hchild></heading1><paragraph><pchild>b]ar</pchild></paragraph>',
+					'<paragraph><pchild>[]ar</pchild></paragraph>'
+				);
 
-					expect( getData( model ) )
-						.to.equal( '<pparent>x<paragraph>x<pchild>fo[]ar</pchild>y</paragraph>y</pparent>' );
-				} );
+				test(
+					'merges to the left if first element contains object (considers it as a content of that element)',
+					'<heading1><hchild><widget></widget>[foo</hchild></heading1><paragraph><pchild>b]ar</pchild></paragraph>',
+					'<heading1><hchild><widget></widget>[]ar</hchild></heading1>'
+				);
 
 				test(
 					'merges elements when left end deep nested',
@@ -392,90 +467,167 @@ describe( 'DataController utils', () => {
 					'<paragraph>x<pchild>fo[]ary</pchild></paragraph><paragraph>x</paragraph>'
 				);
 
+				test(
+					'merges nested elements to the right (on multiple levels) if left side element would become empty',
+					'<heading1><hchild>[foo</hchild></heading1><paragraph><pchild>b]ar</pchild>abc</paragraph>',
+					'<paragraph><pchild>[]ar</pchild>abc</paragraph>'
+				);
+
+				test(
+					'merges to the right element when left end deep nested and will be empty',
+					'<paragraph><pchild>[foo</pchild></paragraph><paragraph>b]ar</paragraph><paragraph>x</paragraph>',
+					'<paragraph>[]ar</paragraph><paragraph>x</paragraph>'
+				);
+
 				test(
 					'merges elements when right end deep nested',
 					'<paragraph>x</paragraph><paragraph>fo[o</paragraph><paragraph><pchild>b]ar</pchild>x</paragraph>',
 					'<paragraph>x</paragraph><paragraph>fo[]ar</paragraph><paragraph>x</paragraph>'
 				);
 
-				it( 'merges elements when left end deep nested (3rd level)', () => {
-					const root = doc.getRoot();
+				test(
+					'removes element when right end deep nested but left end would be empty',
+					'<paragraph>x</paragraph><paragraph>[foo</paragraph><paragraph><pchild>b]ar</pchild></paragraph>',
+					'<paragraph>x</paragraph><paragraph><pchild>[]ar</pchild></paragraph>'
+				);
 
-					// We need to use the raw API due to https://github.com/ckeditor/ckeditor5-engine/issues/905.
-					// <pparent>x<paragraph>foo<pchild>ba[r</pchild></paragraph></pparent><paragraph>b]om</paragraph>
+				test(
+					'merges elements when right end deep nested (in an empty container)',
+					'<paragraph>fo[o</paragraph><paragraph><pchild>bar]</pchild></paragraph>',
+					'<paragraph>fo[]</paragraph>'
+				);
 
-					root._appendChild(
-						new Element( 'pparent', null, [
-							'x',
-							new Element( 'paragraph', null, [
-								'foo',
-								new Element( 'pchild', null, 'bar' )
-							] )
-						] )
-					);
+				test(
+					'removes elements when left end deep nested (in an empty container)',
+					'<paragraph><pchild>[foo</pchild></paragraph><paragraph>b]ar</paragraph><paragraph>x</paragraph>',
+					'<paragraph>[]ar</paragraph><paragraph>x</paragraph>'
+				);
 
-					root._appendChild(
-						new Element( 'paragraph', null, 'bom' )
+				describe( 'with 3rd level of nesting', () => {
+					test(
+						'merges elements when deep nested (same name)',
+						'<pparent>x<paragraph>x<pchild>fo[o</pchild></paragraph></pparent>' +
+						'<pparent><paragraph><pchild>b]ar</pchild>y</paragraph>y</pparent>',
+						'<pparent>x<paragraph>x<pchild>fo[]ar</pchild>y</paragraph>y</pparent>'
 					);
 
-					const range = new Range(
-						new Position( doc.getRoot(), [ 0, 1, 3, 2 ] ), // ba[r
-						new Position( doc.getRoot(), [ 1, 1 ] ) // b]om
+					test(
+						'removes elements when deep nested (same name)',
+						'<pparent><paragraph><pchild>[foo</pchild></paragraph></pparent>' +
+						'<pparent><paragraph><pchild>b]ar</pchild>y</paragraph>y</pparent>',
+						'<pparent><paragraph><pchild>[]ar</pchild>y</paragraph>y</pparent>'
 					);
 
-					model.change( writer => {
-						writer.setSelection( range );
-					} );
+					test(
+						'removes elements up to common ancestor when deep nested (same name)',
+						'<pparent>' +
+							'<paragraph><pchild>[foo</pchild></paragraph>' +
+							'<paragraph><pchild>b]ar</pchild>y</paragraph>y' +
+						'</pparent>',
+						'<pparent><paragraph><pchild>[]ar</pchild>y</paragraph>y</pparent>'
+					);
 
-					deleteContent( model, doc.selection );
+					test(
+						'merges elements when deep nested (different name)',
+						'<pparent>x<heading1>x<hchild>fo[o</hchild></heading1></pparent>' +
+						'<pparent><paragraph><pchild>b]ar</pchild>y</paragraph>y</pparent>',
+						'<pparent>x<heading1>x<hchild>fo[]ar</hchild>y</heading1>y</pparent>'
+					);
 
-					expect( getData( model ) )
-						.to.equal( '<pparent>x<paragraph>foo<pchild>ba[]om</pchild></paragraph></pparent>' );
-				} );
+					test(
+						'removes elements when deep nested (different name)',
+						'<pparent><heading1><hchild>[foo</hchild></heading1></pparent>' +
+						'<pparent><paragraph><pchild>b]ar</pchild>y</paragraph>y</pparent>',
+						'<pparent><paragraph><pchild>[]ar</pchild>y</paragraph>y</pparent>'
+					);
 
-				test(
-					'merges elements when right end deep nested (in an empty container)',
-					'<paragraph>fo[o</paragraph><paragraph><pchild>bar]</pchild></paragraph>',
-					'<paragraph>fo[]</paragraph>'
-				);
+					test(
+						'merges elements up to common ancestor when deep nested (different names)',
+						'<pparent>' +
+							'<heading1><hchild>fo[o</hchild></heading1>' +
+							'<paragraph><pchild>b]ar</pchild></paragraph>' +
+						'</pparent>',
+						'<pparent><heading1><hchild>fo[]ar</hchild></heading1></pparent>'
+					);
 
-				test(
-					'merges elements when left end deep nested (in an empty container)',
-					'<paragraph><pchild>[foo</pchild></paragraph><paragraph>b]ar</paragraph><paragraph>x</paragraph>',
-					'<paragraph><pchild>[]ar</pchild></paragraph><paragraph>x</paragraph>'
-				);
+					test(
+						'removes elements up to common ancestor when deep nested (different names)',
+						'<pparent>' +
+							'<heading1><hchild>[foo</hchild></heading1>' +
+							'<paragraph><pchild>b]ar</pchild>y</paragraph>y' +
+						'</pparent>',
+						'<pparent><paragraph><pchild>[]ar</pchild>y</paragraph>y</pparent>'
+					);
+				} );
 
-				it( 'merges elements when right end deep nested (3rd level)', () => {
-					const root = doc.getRoot();
+				describe( 'with 3rd level of nesting o the left end', () => {
+					test(
+						'merges elements',
+						'<pparent>x<paragraph>foo<pchild>ba[r</pchild></paragraph></pparent>' +
+						'<paragraph>b]om</paragraph>',
+						'<pparent>x<paragraph>foo<pchild>ba[]om</pchild></paragraph></pparent>'
+					);
 
-					// We need to use the raw API due to https://github.com/ckeditor/ckeditor5-engine/issues/905.
-					// <paragraph>fo[o</paragraph><pparent><paragraph><pchild>bar]</pchild></paragraph></pparent>
+					test(
+						'merges elements (different names)',
+						'<pparent>x<heading1>foo<hchild>ba[r</hchild></heading1></pparent>' +
+						'<paragraph>b]om</paragraph>',
+						'<pparent>x<heading1>foo<hchild>ba[]om</hchild></heading1></pparent>'
+					);
 
-					root._appendChild(
-						new Element( 'paragraph', null, 'foo' )
+					test(
+						'removes elements',
+						'<pparent><paragraph><pchild>[bar</pchild></paragraph></pparent>' +
+						'<paragraph>b]om</paragraph>',
+						'<paragraph>[]om</paragraph>'
 					);
 
-					root._appendChild(
-						new Element( 'pparent', null, [
-							new Element( 'paragraph', null, [
-								new Element( 'pchild', null, 'bar' )
-							] )
-						] )
+					test(
+						'removes elements up to common ancestor (different names)',
+						'<pparent>' +
+							'<heading1><hchild>[foo</hchild></heading1>' +
+							'<paragraph>b]ar</paragraph>y' +
+						'</pparent>',
+						'<pparent><paragraph>[]ar</paragraph>y</pparent>'
 					);
+				} );
 
-					const range = new Range(
-						new Position( doc.getRoot(), [ 0, 2 ] ), // f[oo
-						new Position( doc.getRoot(), [ 1, 0, 0, 3 ] ) // bar]
+				describe( 'with 3rd level of nesting o the right end', () => {
+					test(
+						'merges elements',
+						'<paragraph>b[om</paragraph>' +
+						'<pparent><paragraph><pchild>ba]r</pchild></paragraph></pparent>',
+						'<paragraph>b[]r</paragraph>'
 					);
 
-					model.change( writer => {
-						writer.setSelection( range );
-					} );
+					test(
+						'merges elements (different names)',
+						'<paragraph>bo[m</paragraph>' +
+						'<pparent><heading1><hchild>b]ar</hchild></heading1></pparent>',
+						'<paragraph>bo[]ar</paragraph>'
+					);
+					test(
+						'merges elements (different names, reversed)',
+						'<heading1>bo[m</heading1>' +
+						'<pparent><paragraph><pchild>b]ar</pchild></paragraph></pparent>',
+						'<heading1>bo[]ar</heading1>'
+					);
 
-					deleteContent( model, doc.selection );
+					test(
+						'removes elements',
+						'<paragraph>[bom</paragraph>' +
+						'<pparent><paragraph><pchild>b]ar</pchild></paragraph></pparent>',
+						'<pparent><paragraph><pchild>[]ar</pchild></paragraph></pparent>'
+					);
 
-					expect( getData( model ) )
-						.to.equal( '<paragraph>fo[]</paragraph>' );
+					test(
+						'removes elements up to common ancestor (different names)',
+						'<pparent>' +
+							'<heading1>[bar</heading1>y' +
+							'<paragraph><pchild>f]oo</pchild></paragraph>' +
+						'</pparent>',
+						'<pparent><paragraph><pchild>[]oo</pchild></paragraph></pparent>'
+					);
 				} );
 			} );
 
@@ -509,6 +661,59 @@ describe( 'DataController utils', () => {
 				);
 			} );
 
+			describe( 'with markers', () => {
+				it( 'should merge left if the first element is not empty', () => {
+					setData( model, '<heading1>foo[</heading1><paragraph>]bar</paragraph>' );
+
+					model.enqueueChange( 'transparent', writer => {
+						const root = doc.getRoot( );
+						const range = writer.createRange(
+							writer.createPositionFromPath( root, [ 0, 3 ] ),
+							writer.createPositionFromPath( root, [ 1, 0 ] )
+						);
+						writer.addMarker( 'comment1', { range, usingOperation: true, affectsData: true } );
+					} );
+
+					deleteContent( model, doc.selection );
+
+					expect( getData( model ) ).to.equal( '<heading1>foo[]bar</heading1>' );
+				} );
+
+				it( 'should merge right if the first element is empty', () => {
+					setData( model, '<heading1>[</heading1><paragraph>]bar</paragraph>' );
+
+					model.enqueueChange( 'transparent', writer => {
+						const root = doc.getRoot( );
+						const range = writer.createRange(
+							writer.createPositionFromPath( root, [ 0, 0 ] ),
+							writer.createPositionFromPath( root, [ 1, 0 ] )
+						);
+						writer.addMarker( 'comment1', { range, usingOperation: true, affectsData: true } );
+					} );
+
+					deleteContent( model, doc.selection );
+
+					expect( getData( model ) ).to.equal( '<paragraph>[]bar</paragraph>' );
+				} );
+
+				it( 'should merge left if the last element is empty', () => {
+					setData( model, '<heading1>foo[</heading1><paragraph>]</paragraph>' );
+
+					model.enqueueChange( 'transparent', writer => {
+						const root = doc.getRoot( );
+						const range = writer.createRange(
+							writer.createPositionFromPath( root, [ 0, 3 ] ),
+							writer.createPositionFromPath( root, [ 1, 0 ] )
+						);
+						writer.addMarker( 'comment1', { range, usingOperation: true, affectsData: true } );
+					} );
+
+					deleteContent( model, doc.selection );
+
+					expect( getData( model ) ).to.equal( '<heading1>foo[]</heading1>' );
+				} );
+			} );
+
 			describe( 'filtering out', () => {
 				beforeEach( () => {
 					const schema = model.schema;

+ 29 - 0
packages/ckeditor5-engine/tests/model/utils/selection-post-fixer.js

@@ -835,6 +835,35 @@ describe( 'Selection post-fixer', () => {
 					'</table>]'
 				);
 			} );
+
+			it( 'should not reset the selection if the final range is the same as the initial one', () => {
+				setModelData( model,
+					'<table>' +
+						'<tableRow>' +
+							'<tableCell>[<image></image>]</tableCell>' +
+						'</tableRow>' +
+					'</table>'
+				);
+
+				// Setting a selection attribute will trigger the post-fixer. However, because this
+				// action does not affect ranges, the post-fixer should not set a new selection and,
+				// in consequence, should not clear the selection attribute (like it normally would when
+				// a new selection is set).
+				// https://github.com/ckeditor/ckeditor5/issues/6693
+				model.change( writer => {
+					writer.setSelectionAttribute( 'foo', 'bar' );
+				} );
+
+				assertEqualMarkup( getModelData( model ),
+					'<table>' +
+						'<tableRow>' +
+							'<tableCell>[<image></image>]</tableCell>' +
+						'</tableRow>' +
+					'</table>'
+				);
+
+				expect( model.document.selection.hasAttribute( 'foo' ) ).to.be.true;
+			} );
 		} );
 
 		describe( 'non-collapsed selection - image scenarios', () => {

+ 35 - 0
packages/ckeditor5-engine/tests/model/writer.js

@@ -86,6 +86,35 @@ describe( 'Writer', () => {
 		} );
 	} );
 
+	describe( 'cloneElement()', () => {
+		it( 'should make deep copy of element', () => {
+			const element = createElement( 'foo', { 'abc': '123' } );
+
+			insertElement( createElement( 'bar', { 'xyz': '789' } ), element );
+
+			const clonedElement = cloneElement( element );
+
+			expect( clonedElement ).to.not.equal( element );
+			expect( clonedElement.getChild( 0 ) ).to.not.equal( element.getChild( 0 ) );
+			expect( clonedElement.toJSON() ).to.deep.equal( element.toJSON() );
+		} );
+
+		it( 'should make shallow copy of element', () => {
+			const element = createElement( 'foo', { 'abc': '123' } );
+
+			insertElement( createElement( 'bar', { 'xyz': '789' } ), element );
+
+			const elementJson = element.toJSON();
+			delete elementJson.children;
+
+			const clonedElement = cloneElement( element, false );
+
+			expect( clonedElement ).to.not.equal( element );
+			expect( clonedElement.childCount ).to.equal( 0 );
+			expect( clonedElement.toJSON() ).to.deep.equal( elementJson );
+		} );
+	} );
+
 	describe( 'insert()', () => {
 		it( 'should insert node at given position', () => {
 			const parent = createDocumentFragment();
@@ -2902,6 +2931,12 @@ describe( 'Writer', () => {
 		} );
 	}
 
+	function cloneElement( element, deep ) {
+		return model.change( writer => {
+			return writer.cloneElement( element, deep );
+		} );
+	}
+
 	function insert( item, itemOrPosition, offset ) {
 		model.enqueueChange( batch, writer => {
 			writer.insert( item, itemOrPosition, offset );

+ 100 - 1
packages/ckeditor5-engine/tests/view/renderer.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-/* globals document, window, NodeFilter, MutationObserver */
+/* globals document, window, NodeFilter, MutationObserver, HTMLImageElement */
 
 import View from '../../src/view/view';
 import ViewElement from '../../src/view/element';
@@ -305,6 +305,33 @@ describe( 'Renderer', () => {
 			expect( domRoot.childNodes[ 0 ] ).to.equal( domImg );
 		} );
 
+		it( 'should remove any comment from the DOM element', () => {
+			// This comment should be cleared during render.
+			const domComment = document.createComment( 'some comment from the user-land' );
+			domRoot.appendChild( domComment );
+
+			renderer.markToSync( 'children', viewRoot );
+			renderer.render();
+
+			expect( domRoot.childNodes.length ).to.equal( 0 );
+		} );
+
+		// https://github.com/ckeditor/ckeditor5/issues/5734
+		it( 'should remove the comment and add a child element', () => {
+			const viewImg = new ViewElement( viewDocument, 'img' );
+			viewRoot._appendChild( viewImg );
+
+			// This comment should be cleared during render.
+			const domComment = document.createComment( 'some comment from the user-land' );
+			domRoot.appendChild( domComment );
+
+			renderer.markToSync( 'children', viewRoot );
+			renderer.render();
+
+			expect( domRoot.childNodes.length ).to.equal( 1 );
+			expect( domRoot.childNodes[ 0 ] ).to.be.an.instanceof( HTMLImageElement );
+		} );
+
 		it( 'should change element if it is different', () => {
 			const viewImg = new ViewElement( viewDocument, 'img' );
 			viewRoot._appendChild( viewImg );
@@ -2706,6 +2733,78 @@ describe( 'Renderer', () => {
 				expect( mappings.get( domQULB ) ).to.equal( viewQ.getChild( 0 ).getChild( 0 ).getChild( 1 ) );
 			} );
 
+			// https://github.com/ckeditor/ckeditor5/issues/5734
+			it( 'should not rerender DOM when view replaced with the same structure without a comment', () => {
+				const domContent = '' +
+					'<h2>He<i>ading 1</i></h2>' +
+					'<p>Ph <strong>Bold</strong>' +
+						'<a href="https://ckeditor.com"><strong>Lin<i>k</i></strong></a>' +
+					'</p>' +
+					'<!-- A comment to be ignored -->' +
+					'<blockquote><ul><li>Quoted <strong>item 1</strong></li></ul></blockquote>';
+				const content = '' +
+					'<container:h2>He' +
+						'<attribute:i>ading 1</attribute:i>' +
+					'</container:h2>' +
+					'<container:p>Ph ' +
+						'<attribute:strong>Bold</attribute:strong>' +
+						'<attribute:a href="https://ckeditor.com">' +
+							'<attribute:strong>Lin<attribute:i>k</attribute:i></attribute:strong>' +
+						'</attribute:a>' +
+					'</container:p>' +
+					'<container:blockquote>' +
+						'<container:ul>' +
+							'<container:li>Quoted <attribute:strong>item 1</attribute:strong></container:li>' +
+						'</container:ul>' +
+					'</container:blockquote>';
+
+				domRoot.innerHTML = domContent;
+				viewRoot._appendChild( parse( content ) );
+
+				const viewH = viewRoot.getChild( 0 );
+				const viewP = viewRoot.getChild( 1 );
+				const viewQ = viewRoot.getChild( 2 );
+
+				const domH = domRoot.childNodes[ 0 ];
+				const domHI = domH.childNodes[ 1 ];
+				const domP = domRoot.childNodes[ 1 ];
+				const domPT = domP.childNodes[ 0 ];
+				const domPABI = domP.childNodes[ 2 ].childNodes[ 0 ].childNodes[ 1 ];
+				const domC = domRoot.childNodes[ 2 ];
+				const domQ = domRoot.childNodes[ 3 ];
+				const domQULB = domQ.childNodes[ 0 ].childNodes[ 0 ].childNodes[ 1 ];
+
+				renderer.markToSync( 'children', viewRoot );
+				renderer.render();
+
+				// Assert the comment is no longer in the content.
+				expect( domRoot.contains( domC ), 'domRoot should not contain the comment' ).to.be.false;
+
+				// Assert content, without the comment.
+				expect( domRoot.innerHTML ).to.equal( '<h2>He<i>ading 1</i></h2><p>Ph <strong>Bold</strong>' +
+					'<a href="https://ckeditor.com"><strong>Lin<i>k</i></strong></a></p><blockquote><ul><li>' +
+					'Quoted <strong>item 1</strong></li></ul></blockquote>' );
+
+				// Assert if other DOM elements did not change.
+				expect( domRoot.childNodes[ 0 ] ).to.equal( domH );
+				expect( domH.childNodes[ 1 ] ).to.equal( domHI );
+				expect( domRoot.childNodes[ 1 ] ).to.equal( domP );
+				expect( domP.childNodes[ 0 ] ).to.equal( domPT );
+				expect( domP.childNodes[ 2 ].childNodes[ 0 ].childNodes[ 1 ] ).to.equal( domPABI );
+				// Note the shifted index of domQ, from 3 to 2.
+				expect( domRoot.childNodes[ 2 ] ).to.equal( domQ );
+				expect( domQ.childNodes[ 0 ].childNodes[ 0 ].childNodes[ 1 ] ).to.equal( domQULB );
+
+				// Assert mappings.
+				const mappings = renderer.domConverter._domToViewMapping;
+				expect( mappings.get( domH ) ).to.equal( viewH );
+				expect( mappings.get( domHI ) ).to.equal( viewH.getChild( 1 ) );
+				expect( mappings.get( domP ) ).to.equal( viewP );
+				expect( mappings.get( domPABI ) ).to.equal( viewP.getChild( 2 ).getChild( 0 ).getChild( 1 ) );
+				expect( mappings.get( domQ ) ).to.equal( viewQ );
+				expect( mappings.get( domQULB ) ).to.equal( viewQ.getChild( 0 ).getChild( 0 ).getChild( 1 ) );
+			} );
+
 			it( 'should not rerender DOM when view replaced with the same structure without first node', () => {
 				const content = '' +
 					'<container:h2>He' +

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

@@ -23,7 +23,7 @@
     "@ckeditor/ckeditor5-undo": "^19.0.1"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

+ 1 - 1
packages/ckeditor5-essentials/package.json

@@ -22,7 +22,7 @@
     "@ckeditor/ckeditor5-paragraph": "^19.1.0"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

+ 1 - 1
packages/ckeditor5-font/package.json

@@ -21,7 +21,7 @@
     "@ckeditor/ckeditor5-paragraph": "^19.1.0"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

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

@@ -120,7 +120,7 @@ export default class FontFamily extends Plugin {
  *
  * You can preserve pasted font family values by switching the `supportAllValues` option to `true`:
  *
- *		const fontSizeConfig = {
+ *		const fontFamilyConfig = {
  *			supportAllValues: true
  *		};
  *

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

@@ -66,7 +66,7 @@ function generateFontPreset( fontDefinition ) {
 
 	return {
 		title: firstFontName,
-		model: firstFontName,
+		model: cssFontNames,
 		view: {
 			name: 'span',
 			styles: {

+ 6 - 4
packages/ckeditor5-font/tests/fontfamily/fontfamilyediting.js

@@ -104,9 +104,9 @@ describe( 'FontFamilyEditing', () => {
 				} );
 
 				it( 'should convert defined fontFamily attribute values', () => {
-					setModelData( doc, '<paragraph>f<$text fontFamily="Arial">o</$text>o</paragraph>' );
+					setModelData( doc, '<paragraph>f<$text fontFamily="Arial, Helvetica, sans-serif">o</$text>o</paragraph>' );
 
-					expect( editor.getData() ).to.equal( '<p>f<span style="font-family:Arial;">o</span>o</p>' );
+					expect( editor.getData() ).to.equal( '<p>f<span style="font-family:Arial, Helvetica, sans-serif;">o</span>o</p>' );
 				} );
 			} );
 
@@ -174,10 +174,12 @@ describe( 'FontFamilyEditing', () => {
 		} );
 
 		it( 'should convert fontFamily attribute to configured complex preset', () => {
-			setModelData( doc, '<paragraph>f<$text fontFamily="Lucida Sans Unicode">o</$text>o</paragraph>' );
+			const fontFamily = '\'Lucida Sans Unicode\', \'Lucida Grande\', sans-serif';
+
+			setModelData( doc, `<paragraph>f<$text fontFamily="${ fontFamily }">o</$text>o</paragraph>` );
 
 			expect( editor.getData() )
-				.to.equal( '<p>f<span style="font-family:\'Lucida Sans Unicode\', \'Lucida Grande\', sans-serif;">o</span>o</p>' );
+				.to.equal( `<p>f<span style="font-family:${ fontFamily };">o</span>o</p>` );
 		} );
 
 		it( 'should convert fontFamily attribute from user defined settings', () => {

+ 50 - 28
packages/ckeditor5-font/tests/fontfamily/fontfamilyui.js

@@ -10,9 +10,11 @@ import FontFamilyUI from '../../src/fontfamily/fontfamilyui';
 
 import fontFamilyIcon from '../../theme/icons/font-family.svg';
 
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import { add as addTranslations, _clear as clearTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service';
+import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 describe( 'FontFamilyUI', () => {
 	let editor, command, element;
@@ -97,47 +99,67 @@ describe( 'FontFamilyUI', () => {
 			expect( listView.items.map( item => item.children.first.isOn ) )
 				.to.deep.equal( [ true, false, false, false, false, false, false, false, false ] );
 
-			command.value = 'Arial';
+			command.value = 'Arial, Helvetica, sans-serif';
 
 			// The second item is 'Arial' font family.
 			expect( listView.items.map( item => item.children.first.isOn ) )
 				.to.deep.equal( [ false, true, false, false, false, false, false, false, false ] );
 		} );
 
-		it( 'should activate current option in dropdown for full font family definitions', () => {
-			const element = document.createElement( 'div' );
-			document.body.appendChild( element );
+		describe( 'with supportAllValues=true', () => {
+			let editor, element, command, dropdown;
 
-			return ClassicTestEditor
-				.create( element, {
-					plugins: [ FontFamilyEditing, FontFamilyUI ],
-					fontSize: {
-						supportAllValues: true
-					}
-				} )
-				.then( editor => {
-					const command = editor.commands.get( 'fontFamily' );
-					const dropdown = editor.ui.componentFactory.create( 'fontFamily' );
+			beforeEach( async () => {
+				element = document.createElement( 'div' );
+				document.body.appendChild( element );
+
+				editor = await ClassicTestEditor
+					.create( element, {
+						plugins: [ Paragraph, FontFamilyEditing, FontFamilyUI ],
+						fontSize: {
+							supportAllValues: true
+						}
+					} );
+
+				command = editor.commands.get( 'fontFamily' );
+				dropdown = editor.ui.componentFactory.create( 'fontFamily' );
+			} );
+
+			afterEach( async () => {
+				await editor.destroy();
+				element.remove();
+			} );
 
-					const listView = dropdown.listView;
+			it( 'should activate the current option in the dropdown for full font family definitions', () => {
+				const listView = dropdown.listView;
+
+				command.value = undefined;
+
+				// The first item is 'default' font family.
+				expect( listView.items.map( item => item.children.first.isOn ) )
+					.to.deep.equal( [ true, false, false, false, false, false, false, false, false ] );
+
+				command.value = '\'Courier New\', Courier, monospace';
 
-					command.value = undefined;
+				// The third item is 'Courier New' font family.
+				expect( listView.items.map( item => item.children.first.isOn ) )
+					.to.deep.equal( [ false, false, true, false, false, false, false, false, false ] );
+			} );
+
+			it( 'should apply the complete font-family value (list of font-families)', () => {
+				const listView = dropdown.listView;
+				const fontFamilyArialButton = listView.items.get( 1 ).children.first;
 
-					// The first item is 'default' font family.
-					expect( listView.items.map( item => item.children.first.isOn ) )
-						.to.deep.equal( [ true, false, false, false, false, false, false, false, false ] );
+				setModelData( editor.model, '<paragraph>f[oo]</paragraph>' );
 
-					command.value = '\'Courier New\', Courier, monospace';
+				fontFamilyArialButton.fire( 'execute' );
 
-					// The third item is 'Courier New' font family.
-					expect( listView.items.map( item => item.children.first.isOn ) )
-						.to.deep.equal( [ false, false, true, false, false, false, false, false, false ] );
+				expect( getModelData( editor.model ) ).to.equal(
+					'<paragraph>f[<$text fontFamily="Arial, Helvetica, sans-serif">oo</$text>]</paragraph>'
+				);
 
-					return editor.destroy();
-				} )
-				.then( () => {
-					element.remove();
-				} );
+				expect( editor.getData() ).to.equal( '<p>f<span style="font-family:Arial, Helvetica, sans-serif;">oo</span></p>' );
+			} );
 		} );
 
 		describe( 'model to command binding', () => {

+ 2 - 2
packages/ckeditor5-font/tests/fontfamily/utils.js

@@ -62,7 +62,7 @@ describe( 'FontFamily utils', () => {
 					},
 					{
 						title: 'Comic Sans MS',
-						model: 'Comic Sans MS',
+						model: '\'Comic Sans MS\', sans-serif',
 						view: {
 							name: 'span',
 							styles: {
@@ -73,7 +73,7 @@ describe( 'FontFamily utils', () => {
 					},
 					{
 						title: 'Lucida Console',
-						model: 'Lucida Console',
+						model: '\'Lucida Console\', \'Courier New\', Courier, monospace',
 						view: {
 							name: 'span',
 							styles: {

+ 12 - 2
packages/ckeditor5-font/tests/integration.js

@@ -19,7 +19,10 @@ describe( 'Integration test Font', () => {
 
 		return ClassicTestEditor
 			.create( element, {
-				plugins: [ Font, ArticlePluginSet ]
+				plugins: [ Font, ArticlePluginSet ],
+				image: {
+					toolbar: [ 'imageStyle:full', 'imageStyle:side' ]
+				}
 			} )
 			.then( newEditor => {
 				editor = newEditor;
@@ -37,7 +40,8 @@ describe( 'Integration test Font', () => {
 		it( 'should render one span element for all types of font features', () => {
 			setModelData( model,
 				'<paragraph>' +
-					'<$text fontColor="#123456" fontBackgroundColor="rgb(10,20,30)" fontSize="big" fontFamily="Arial">foo</$text>' +
+					'<$text fontColor="#123456" fontBackgroundColor="rgb(10,20,30)" fontSize="big" ' +
+						'fontFamily="Arial, Helvetica, sans-serif">foo</$text>' +
 				'</paragraph>'
 			);
 
@@ -65,6 +69,9 @@ describe( 'Integration test Font', () => {
 					fontSize: {
 						options: [ 10, 12, 14 ],
 						supportAllValues: true
+					},
+					image: {
+						toolbar: [ 'imageStyle:full', 'imageStyle:side' ]
 					}
 				} )
 				.then( editor => {
@@ -128,6 +135,9 @@ describe( 'Integration test Font', () => {
 					fontSize: {
 						options: [ 10, 12, 14 ],
 						supportAllValues: true
+					},
+					image: {
+						toolbar: [ 'imageStyle:full', 'imageStyle:side' ]
 					}
 				} )
 				.then( editor => {

+ 2 - 1
packages/ckeditor5-font/tests/manual/font-family.md

@@ -14,7 +14,8 @@ Try to:
 ### Converters mode
 
 The "Restricted value matching" option means that all font-family values that aren't defined in the plugin's configuration will be removed (e.g. when pasted from Google Docs).
-This behaviour can be disabled by selecting the "Disabled value matching" option.
+
+This behaviour can be disabled by selecting the "Disabled value matching" option, which sets ["`supportAllValues: true`"](https://ckeditor.com/docs/ckeditor5/latest/api/module_font_fontfamily-FontFamilyConfig.html#member-supportAllValues) in the font family configuration.
 
 The `Docs-Roboto, Arial` font-family is not specified in the plugin's configuration and should be restored to default font when the "Restricted value matching" option is selected.
 

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

@@ -12,9 +12,9 @@
 #
 msgid ""
 msgstr ""
-"Language-Team: Uighur (https://www.transifex.com/ckeditor/teams/11143/ug/)\n"
+"Language-Team: Uyghur (https://www.transifex.com/ckeditor/teams/11143/ug/)\n"
 "Language: ug\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
 msgctxt "Dropdown option label for the paragraph format."
 msgid "Paragraph"

+ 1 - 1
packages/ckeditor5-heading/package.json

@@ -29,7 +29,7 @@
     "@ckeditor/ckeditor5-upload": "^19.0.1"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

+ 1 - 1
packages/ckeditor5-highlight/package.json

@@ -26,7 +26,7 @@
     "@ckeditor/ckeditor5-utils": "^19.0.1"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

+ 1 - 1
packages/ckeditor5-horizontal-line/package.json

@@ -24,7 +24,7 @@
     "@ckeditor/ckeditor5-utils": "^19.0.1"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

+ 4 - 4
packages/ckeditor5-horizontal-line/theme/horizontalline.css

@@ -10,8 +10,8 @@
 }
 
 .ck-content hr {
-	border-width: 1px 0 0;
-	border-style: solid;
-	border-color: hsl(0, 0%, 37%);
-	margin: 0;
+	margin: 15px 0;
+	height: 4px;
+	background: hsl(0, 0%, 87%);
+	border: 0;
 }

+ 2 - 2
packages/ckeditor5-image/lang/translations/ug.po

@@ -12,9 +12,9 @@
 #
 msgid ""
 msgstr ""
-"Language-Team: Uighur (https://www.transifex.com/ckeditor/teams/11143/ug/)\n"
+"Language-Team: Uyghur (https://www.transifex.com/ckeditor/teams/11143/ug/)\n"
 "Language: ug\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
 msgctxt "Label for the image widget."
 msgid "image widget"

+ 1 - 1
packages/ckeditor5-image/package.json

@@ -35,7 +35,7 @@
     "@ckeditor/ckeditor5-undo": "^19.0.1"
   },
   "engines": {
-    "node": ">=8.0.0",
+    "node": ">=12.0.0",
     "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",

+ 1 - 5
packages/ckeditor5-image/src/image/converters.js

@@ -125,10 +125,6 @@ export function modelToViewAttributeConverter( attributeKey ) {
 		const figure = conversionApi.mapper.toViewElement( data.item );
 		const img = getViewImgFromWidget( figure );
 
-		if ( data.attributeNewValue !== null ) {
-			viewWriter.setAttribute( data.attributeKey, data.attributeNewValue, img );
-		} else {
-			viewWriter.removeAttribute( data.attributeKey, img );
-		}
+		viewWriter.setAttribute( data.attributeKey, data.attributeNewValue || '', img );
 	}
 }

+ 13 - 1
packages/ckeditor5-image/src/image/utils.js

@@ -113,11 +113,23 @@ export function isImageAllowed( model ) {
  * Assuming that image is always a first child of a widget (ie. `figureView.getChild( 0 )`) is unsafe as other features might
  * inject their own elements to the widget.
  *
+ * The `<img>` can be wrapped to other elements, e.g. `<a>`. Nested check required.
+ *
  * @param {module:engine/view/element~Element} figureView
  * @returns {module:engine/view/element~Element}
  */
 export function getViewImgFromWidget( figureView ) {
-	return Array.from( figureView.getChildren() ).find( viewChild => viewChild.is( 'img' ) );
+	const figureChildren = [];
+
+	for ( const figureChild of figureView.getChildren() ) {
+		figureChildren.push( figureChild );
+
+		if ( figureChild.is( 'element' ) ) {
+			figureChildren.push( ...figureChild.getChildren() );
+		}
+	}
+
+	return figureChildren.find( viewChild => viewChild.is( 'img' ) );
 }
 
 // Checks if image is allowed by schema in optimal insertion parent.

+ 1 - 1
packages/ckeditor5-image/src/imagestyle/utils.js

@@ -135,7 +135,7 @@ function _normalizeStyle( style ) {
 		const extendedStyle = Object.assign( {}, style );
 
 		for ( const prop in defaultStyle ) {
-			if ( !style.hasOwnProperty( prop ) ) {
+			if ( !Object.prototype.hasOwnProperty.call( style, prop ) ) {
 				extendedStyle[ prop ] = defaultStyle[ prop ];
 			}
 		}

+ 0 - 6
packages/ckeditor5-image/tests/image.js

@@ -64,7 +64,6 @@ describe( 'Image', () => {
 			expect( getViewData( view ) ).to.equal(
 				'[<figure class="' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'ck-widget_selected image" contenteditable="false"' +
 				'>' +
 					'<img alt="alt text" src="/assets/sample.png"></img>' +
@@ -82,7 +81,6 @@ describe( 'Image', () => {
 			expect( getViewData( view ) ).to.equal(
 				'[<figure class="' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'ck-widget_selected image" contenteditable="false"' +
 				'>' +
 					'<img alt="" src="/assets/sample.png"></img>' +
@@ -103,7 +101,6 @@ describe( 'Image', () => {
 			expect( getViewData( view ) ).to.equal(
 				'[<figure class="' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'ck-widget_selected image" contenteditable="false"' +
 				'>' +
 					'<img alt="alt text" src="/assets/sample.png"></img>' +
@@ -111,7 +108,6 @@ describe( 'Image', () => {
 				'</figure>]' +
 				'<figure class="' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'image" contenteditable="false"' +
 				'>' +
 					'<img alt="alt text" src="/assets/sample.png"></img>' +
@@ -127,7 +123,6 @@ describe( 'Image', () => {
 			expect( getViewData( view ) ).to.equal(
 				'<figure class="' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'image" contenteditable="false"' +
 				'>' +
 					'<img alt="alt text" src="/assets/sample.png"></img>' +
@@ -135,7 +130,6 @@ describe( 'Image', () => {
 				'</figure>' +
 				'[<figure class="' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'ck-widget_selected image" contenteditable="false"' +
 				'>' +
 					'<img alt="alt text" src="/assets/sample.png"></img>' +

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