diff --git a/docs/builds/guides/migration/migration-from-ckeditor-4.md b/docs/builds/guides/migration/migration-from-ckeditor-4.md
index d043595feca..71243ea3b3b 100644
--- a/docs/builds/guides/migration/migration-from-ckeditor-4.md
+++ b/docs/builds/guides/migration/migration-from-ckeditor-4.md
@@ -95,7 +95,7 @@ Note: The number of options was reduced on purpose. We understood that configuri
Extending the list of HTML tags or attributes that CKEditor should support can be achieved via the {@link features/general-html-support General HTML Support feature}. The GHS allows adding HTML markup not covered by official CKEditor 5 features into the editor's content. Such elements can be loaded, pasted, or output. It does not, however, provide a dedicated UI for the extended HTML markup.
Having full-fledged HTML support can be achieved by writing a plugin that (ideally) provides also means to control (insert, edit, delete) such markup. For more information on how to create plugins check the {@link framework/guides/creating-simple-plugin Creating a simple plugin} article. Looking at the source code of CKEditor 5 plugins may also give you a lot of inspiration.
-
Note that only content that is explicitly converted between the model and the view by the editor plugins will be preserved in CKEditor 5. Check the {@link framework/guides/deep-dive/conversion-introduction conversion tutorials} to learn how to extend the conversion rules.
+
Note that only content that is explicitly converted between the model and the view by the editor plugins will be preserved in CKEditor 5. Check the {@link framework/guides/deep-dive/conversion/intro conversion tutorials} to learn how to extend the conversion rules.
diff --git a/docs/builds/guides/migration/migration-to-26.md b/docs/builds/guides/migration/migration-to-26.md
index 917b35c82d3..d2a4cc50de0 100644
--- a/docs/builds/guides/migration/migration-to-26.md
+++ b/docs/builds/guides/migration/migration-to-26.md
@@ -216,7 +216,7 @@ Command name changes (before → after):
* `forwardDelete` → `deleteForward`
* `todoListCheck` → `checkTodoList`
-The `TodoListCheckCommand` module was moved to {@link module:list/checktodolistcommand~CheckTodoListCommand `CheckTodoListCommand`}.
+The `TodoListCheckCommand` module was moved to {@link module:list/todolist/checktodolistcommand~CheckTodoListCommand `CheckTodoListCommand`}.
The `ImageInsertCommand` module was moved to {@link module:image/image/insertimagecommand~InsertImageCommand `InsertImageCommand`}.
diff --git a/docs/umberto.json b/docs/umberto.json
index b00ca6d7f5b..71402916b11 100644
--- a/docs/umberto.json
+++ b/docs/umberto.json
@@ -64,7 +64,12 @@
"framework/guides/ui/external-ui.html": "framework/guides/deep-dive/ui/external-ui.html",
"framework/guides/ui/theme-customization.html": "framework/guides/deep-dive/ui/theme-customization.html",
"framework/guides/creating-simple-plugin.html": "framework/guides/plugins/creating-simple-plugin.html",
- "examples/builds/custom-build.html": "examples/builds-custom/full-featured-editor.html"
+ "examples/builds/custom-build.html": "examples/builds-custom/full-featured-editor.html",
+ "framework/guides/deep-dive/conversion/conversion-introduction.html": "framework/guides/deep-dive/conversion/intro.html",
+ "framework/guides/deep-dive/conversion/conversion-extending-output.html": "framework/guides/deep-dive/conversion/intro.html",
+ "framework/guides/deep-dive/conversion/conversion-preserving-custom-content.html": "framework/guides/deep-dive/conversion/intro.html",
+ "framework/guides/deep-dive/conversion/custom-element-conversion.html": "framework/guides/deep-dive/conversion/intro.html",
+ "framework/guides/deep-dive/conversion/element-reconversion.html": "framework/guides/deep-dive/conversion/intro.html"
},
"scripts": {
"snippet-adapter": "../scripts/docs/snippetadapter",
@@ -201,7 +206,15 @@
"name": "Conversion",
"id": "framework-deep-dive-conversion",
"slug": "conversion",
- "order": 100
+ "order": 100,
+ "categories": [
+ {
+ "name": "Conversion helpers",
+ "id": "framework-deep-dive-conversion-helpers",
+ "slug": "helpers",
+ "order": 100
+ }
+ ]
},
{
"name": "User interface",
diff --git a/package.json b/package.json
index 9b5c88e627c..2b5cc8ab7cf 100644
--- a/package.json
+++ b/package.json
@@ -88,7 +88,7 @@
"@ckeditor/ckeditor5-dev-webpack-plugin": "^28.0.1",
"@ckeditor/ckeditor5-export-pdf": ">=1.0.0",
"@ckeditor/ckeditor5-export-word": ">=1.0.0",
- "@ckeditor/ckeditor5-inspector": "^2.2.2",
+ "@ckeditor/ckeditor5-inspector": "^3.0.0",
"@ckeditor/ckeditor5-pagination": ">=1.0.0",
"@ckeditor/ckeditor5-react": "^3.0.0",
"@ckeditor/ckeditor5-real-time-collaboration": ">=28.0.0",
diff --git a/packages/ckeditor5-alignment/tests/alignmentediting.js b/packages/ckeditor5-alignment/tests/alignmentediting.js
index 2e41c96b0fc..86980eb44de 100644
--- a/packages/ckeditor5-alignment/tests/alignmentediting.js
+++ b/packages/ckeditor5-alignment/tests/alignmentediting.js
@@ -6,7 +6,7 @@
import AlignmentEditing from '../src/alignmentediting';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import ImageCaptionEditing from '@ckeditor/ckeditor5-image/src/imagecaption/imagecaptionediting';
-import ListEditing from '@ckeditor/ckeditor5-list/src/listediting';
+import ListEditing from '@ckeditor/ckeditor5-list/src/list/listediting';
import HeadingEditing from '@ckeditor/ckeditor5-heading/src/headingediting';
import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
diff --git a/packages/ckeditor5-autoformat/tests/autoformat.js b/packages/ckeditor5-autoformat/tests/autoformat.js
index 68bf34c38ac..103aa63a226 100644
--- a/packages/ckeditor5-autoformat/tests/autoformat.js
+++ b/packages/ckeditor5-autoformat/tests/autoformat.js
@@ -6,8 +6,8 @@
import Autoformat from '../src/autoformat';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
-import ListEditing from '@ckeditor/ckeditor5-list/src/listediting';
-import TodoListEditing from '@ckeditor/ckeditor5-list/src/todolistediting';
+import ListEditing from '@ckeditor/ckeditor5-list/src/list/listediting';
+import TodoListEditing from '@ckeditor/ckeditor5-list/src/todolist/todolistediting';
import HeadingEditing from '@ckeditor/ckeditor5-heading/src/headingediting';
import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting';
import StrikethroughEditing from '@ckeditor/ckeditor5-basic-styles/src/strikethrough/strikethroughediting';
diff --git a/packages/ckeditor5-autoformat/tests/undointegration.js b/packages/ckeditor5-autoformat/tests/undointegration.js
index c9dc537c39b..317f5e18482 100644
--- a/packages/ckeditor5-autoformat/tests/undointegration.js
+++ b/packages/ckeditor5-autoformat/tests/undointegration.js
@@ -6,7 +6,7 @@
import Autoformat from '../src/autoformat';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
-import ListEditing from '@ckeditor/ckeditor5-list/src/listediting';
+import ListEditing from '@ckeditor/ckeditor5-list/src/list/listediting';
import HeadingEditing from '@ckeditor/ckeditor5-heading/src/headingediting';
import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting';
import CodeEditing from '@ckeditor/ckeditor5-basic-styles/src/code/codeediting';
diff --git a/packages/ckeditor5-block-quote/tests/blockquoteediting.js b/packages/ckeditor5-block-quote/tests/blockquoteediting.js
index f398399f310..dc10f4f8179 100644
--- a/packages/ckeditor5-block-quote/tests/blockquoteediting.js
+++ b/packages/ckeditor5-block-quote/tests/blockquoteediting.js
@@ -5,7 +5,7 @@
import BlockQuoteEditing from '../src/blockquoteediting';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
-import ListEditing from '@ckeditor/ckeditor5-list/src/listediting';
+import ListEditing from '@ckeditor/ckeditor5-list/src/list/listediting';
import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting';
import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
diff --git a/packages/ckeditor5-clipboard/tests/pasteplaintext.js b/packages/ckeditor5-clipboard/tests/pasteplaintext.js
index cd909c8b832..3074ba43442 100644
--- a/packages/ckeditor5-clipboard/tests/pasteplaintext.js
+++ b/packages/ckeditor5-clipboard/tests/pasteplaintext.js
@@ -43,7 +43,7 @@ describe( 'PastePlainText', () => {
isInline: true
} );
- editor.conversion.for( 'upcast' ).elementToElement( {
+ editor.conversion.elementToElement( {
model: 'softBreak',
view: 'br'
} );
diff --git a/packages/ckeditor5-code-block/src/converters.js b/packages/ckeditor5-code-block/src/converters.js
index 85422995eda..235b3849c76 100644
--- a/packages/ckeditor5-code-block/src/converters.js
+++ b/packages/ckeditor5-code-block/src/converters.js
@@ -69,12 +69,12 @@ export function modelToViewCodeBlockInsertion( model, languageDefs, useLabels =
preAttributes.spellcheck = 'false';
}
- const pre = writer.createContainerElement( 'pre', preAttributes );
const code = writer.createContainerElement( 'code', {
class: languagesToClasses[ codeBlockLanguage ] || null
} );
- writer.insert( writer.createPositionAt( pre, 0 ), code );
+ const pre = writer.createContainerElement( 'pre', preAttributes, code );
+
writer.insert( targetViewPosition, pre );
mapper.bindElements( data.item, code );
};
diff --git a/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-bold.html b/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-bold.html
new file mode 100644
index 00000000000..397850711ef
--- /dev/null
+++ b/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-bold.html
@@ -0,0 +1,5 @@
+
+
+
diff --git a/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-upcast-element.js b/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-upcast-element.js
new file mode 100644
index 00000000000..91b76e042d6
--- /dev/null
+++ b/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-upcast-element.js
@@ -0,0 +1,33 @@
+/**
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals DecoupledEditor, MiniCKEditorInspector, Essentials, console, document */
+
+function Example( editor ) {
+ editor.model.schema.register( 'example', {
+ inheritAllFrom: '$block'
+ } );
+
+ editor.conversion.elementToElement( {
+ view: {
+ name: 'div',
+ classes: [ 'example' ]
+ },
+ model: 'example'
+ } );
+}
+
+DecoupledEditor.create( document.querySelector( '#mini-inspector-upcast-element' ), {
+ plugins: [ Essentials, Example ]
+} )
+ .then( editor => {
+ MiniCKEditorInspector.attach(
+ editor,
+ document.querySelector( '#mini-inspector-upcast-element-container' )
+ );
+ } )
+ .catch( err => {
+ console.error( err.stack );
+ } );
diff --git a/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector.html b/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector.html
new file mode 100644
index 00000000000..94f04b6e070
--- /dev/null
+++ b/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector.html
@@ -0,0 +1,77 @@
+
diff --git a/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector.js b/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector.js
new file mode 100644
index 00000000000..2c938a82cb7
--- /dev/null
+++ b/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector.js
@@ -0,0 +1,16 @@
+/**
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals window */
+
+import DecoupledEditor from '@ckeditor/ckeditor5-build-decoupled-document/src/ckeditor';
+import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import MiniCKEditorInspector from '@ckeditor/ckeditor5-inspector/build/miniinspector.js';
+
+window.DecoupledEditor = DecoupledEditor;
+window.Essentials = Essentials;
+window.Paragraph = Paragraph;
+window.MiniCKEditorInspector = MiniCKEditorInspector;
diff --git a/packages/ckeditor5-engine/docs/assets/img/downcast-basic.svg b/packages/ckeditor5-engine/docs/assets/img/downcast-basic.svg
new file mode 100644
index 00000000000..62b0262da61
--- /dev/null
+++ b/packages/ckeditor5-engine/docs/assets/img/downcast-basic.svg
@@ -0,0 +1,39 @@
+
diff --git a/packages/ckeditor5-engine/docs/assets/img/downcast-pipelines.svg b/packages/ckeditor5-engine/docs/assets/img/downcast-pipelines.svg
new file mode 100644
index 00000000000..0342d1776e6
--- /dev/null
+++ b/packages/ckeditor5-engine/docs/assets/img/downcast-pipelines.svg
@@ -0,0 +1,77 @@
+
diff --git a/packages/ckeditor5-engine/docs/assets/img/upcast-basic.svg b/packages/ckeditor5-engine/docs/assets/img/upcast-basic.svg
new file mode 100644
index 00000000000..3677d7bea0b
--- /dev/null
+++ b/packages/ckeditor5-engine/docs/assets/img/upcast-basic.svg
@@ -0,0 +1,39 @@
+
diff --git a/packages/ckeditor5-engine/docs/assets/img/upcast-pipeline.svg b/packages/ckeditor5-engine/docs/assets/img/upcast-pipeline.svg
new file mode 100644
index 00000000000..d68ea8023fc
--- /dev/null
+++ b/packages/ckeditor5-engine/docs/assets/img/upcast-pipeline.svg
@@ -0,0 +1,85 @@
+
diff --git a/packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion-extending-output.md b/packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion-extending-output.md
deleted file mode 100644
index f4475665f96..00000000000
--- a/packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion-extending-output.md
+++ /dev/null
@@ -1,356 +0,0 @@
----
-category: framework-deep-dive-conversion
-menu-title: Extending editor output
-order: 20
----
-
-{@snippet framework/build-extending-content-source}
-
-# Extending the editor output
-
-This guide focuses on customization of the one–way {@link framework/guides/architecture/editing-engine#editing-pipeline "downcast"} pipeline of CKEditor 5. This pipeline transforms the data from the model to the editing view and the output data. The following examples do not customize the model and do not process the (input) data — you can picture them as post–processors (filters) applied to the output only.
-
-If you want to learn how to load some extra content (element, attributes, classes) into the rich-text editor, check out the {@link framework/guides/deep-dive/conversion-preserving-custom-content next guide} of this section.
-
-## Before starting
-
-### Code architecture
-
-It is recommended for the code that customizes the editor data and editing pipelines to be delivered as {@link framework/guides/architecture/core-editor-architecture#plugins plugins} and all examples in this guide follow this convention.
-
-Also for the sake of simplicity all examples use the same {@link module:editor-classic/classiceditor~ClassicEditor `ClassicEditor`}, but keep in mind that code snippets will work with other editors, too.
-
-Finally, none of the converters covered in this guide requires to import any modules from CKEditor 5 Framework, hence, you can write them without rebuilding the editor. In other words, such converters can easily be added to existing {@link builds/guides/overview CKEditor 5 builds}.
-
-### Granular converters
-
-You can create separate converters for the data and editing (downcast) pipelines. The former (`dataDowncast`) will customize the data in the editor output (e.g. when {@link builds/guides/integration/saving-data#manually-retrieving-the-data obtaining the editor data}). The latter (`editingDowncast`) will only work for the content of the editor when editing.
-
-If you do not want to complicate your conversion, you can just add a single (`downcast`) converter which will apply both to the data and the editing view. We did that in all the examples to keep them simple but keep in mind you have several options:
-
-```js
-// Adds a conversion dispatcher for the editing downcast pipeline only.
-editor.conversion.for( 'editingDowncast' ).add( dispatcher => {
- // ...
-} );
-
-// Adds a conversion dispatcher for the data downcast pipeline only.
-editor.conversion.for( 'dataDowncast' ).add( dispatcher => {
- // ...
-} );
-
-// Adds a conversion dispatcher for both the data and the editing downcast pipelines.
-editor.conversion.for( 'downcast' ).add( dispatcher => {
- // ...
-} );
-```
-
-### CKEditor 5 inspector
-
-The {@link framework/guides/development-tools#ckeditor-5-inspector CKEditor 5 inspector} is an invaluable help when working with the model and view structures. It allows browsing their structure and checking selection positions like in typical browser developer tools. Make sure to enable the inspector when playing with CKEditor 5.
-
-## Adding a CSS class to inline elements
-
-In this example all links (`...`) get the `.my-green-link` CSS class. This includes all links in the editor output (`editor.getData()`) and all links in the edited content (existing and future ones).
-
-
-Note that the same behavior can be obtained with {@link features/link#custom-link-attributes-decorators link decorators}:
-
-```js
-ClassicEditor
- .create( ..., {
- // ...
- link: {
- decorators: {
- addGreenLink: {
- mode: 'automatic',
- classes: 'my-green-link'
- }
- }
- }
- } )
-```
-
-
-{@snippet framework/extending-content-add-link-class}
-
-A custom CSS class is added to all links by a custom converter plugged into the downcast pipeline, following the default converters brought by the {@link features/link link} feature:
-
-```js
-// This plugin brings customization to the downcast pipeline of the editor.
-function AddClassToAllLinks( editor ) {
- // Both the data and the editing pipelines are affected by this conversion.
- editor.conversion.for( 'downcast' ).add( dispatcher => {
- // Links are represented in the model as a "linkHref" attribute.
- // Use the "low" listener priority to apply the changes after the link feature.
- dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => {
- const viewWriter = conversionApi.writer;
- const viewSelection = viewWriter.document.selection;
-
- // Adding a new CSS class is done by wrapping all link ranges and selection
- // in a new attribute element with a class.
- const viewElement = viewWriter.createAttributeElement( 'a', {
- class: 'my-green-link'
- }, {
- priority: 5
- } );
-
- if ( data.item.is( 'selection' ) ) {
- viewWriter.wrap( viewSelection.getFirstRange(), viewElement );
- } else {
- viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
- }
- }, { priority: 'low' } );
- } );
-}
-```
-
-Activate the plugin in the editor:
-
-```js
-ClassicEditor
- .create( ..., {
- extraPlugins: [ AddClassToAllLinks ],
- } )
- .then( editor => {
- // ...
- } )
- .catch( err => {
- console.error( err.stack );
- } );
-```
-
-Add some CSS styles for `.my-green-link` to see the customization in action:
-
-```css
-.my-green-link {
- color: #209a25;
- border: 1px solid #209a25;
- border-radius: 2px;
- padding: 0 3px;
- box-shadow: 1px 1px 0 0 #209a25;
-}
-```
-
-## Adding an HTML attribute to certain inline elements
-
-In this example all the links (`...`) that do not have "ckeditor.com" in their `href="..."` get the `target="_blank"` attribute. This includes all links in the editor output (`editor.getData()`) and all links in the edited content (existing and future ones).
-
-
-Note that similar behavior can be obtained with {@link module:link/link~LinkConfig#addTargetToExternalLinks link decorators}:
-
-```js
-ClassicEditor
- .create( ..., {
- // ...
- link: {
- addTargetToExternalLinks: true
- }
- } )
-```
-
-{@snippet framework/extending-content-add-external-link-target}
-
-The `target` attribute is added to all "external" links by a custom converter plugged into the downcast pipeline, following the default converters brought by the {@link features/link link} feature:
-
-```js
-// This plugin brings customization to the downcast pipeline of the editor.
-function AddTargetToExternalLinks( editor ) {
- // Both the data and the editing pipelines are affected by this conversion.
- editor.conversion.for( 'downcast' ).add( dispatcher => {
- // Links are represented in the model as a "linkHref" attribute.
- // Use the "low" listener priority to apply the changes after the link feature.
- dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => {
- const viewWriter = conversionApi.writer;
- const viewSelection = viewWriter.document.selection;
-
- // Adding a new CSS class is done by wrapping all link ranges and selection
- // in a new attribute element with the "target" attribute.
- const viewElement = viewWriter.createAttributeElement( 'a', {
- target: '_blank'
- }, {
- priority: 5
- } );
-
- if ( data.attributeNewValue.match( /ckeditor\.com/ ) ) {
- viewWriter.unwrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
- } else {
- if ( data.item.is( 'selection' ) ) {
- viewWriter.wrap( viewSelection.getFirstRange(), viewElement );
- } else {
- viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
- }
- }
- }, { priority: 'low' } );
- } );
-}
-```
-
-Activate the plugin in the editor:
-
-```js
-ClassicEditor
- .create( ..., {
- extraPlugins: [ AddTargetToExternalLinks ],
- } )
- .then( editor => {
- // ...
- } )
- .catch( err => {
- console.error( err.stack );
- } );
-```
-
-Add some CSS styles for links with `target="_blank"` to mark them with with the "⧉" symbol:
-
-```css
-a[target="_blank"]::after {
- content: '\29C9';
-}
-```
-
-## Adding a CSS class to certain inline elements
-
-In this example all links (`...`) that do not have `https://` in their `href="..."` attribute get the `.unsafe-link` CSS class. This includes all links in the editor output (`editor.getData()`) and all links in the edited content (existing and future ones).
-
-
-Note that the same behavior can be obtained with {@link features/link#custom-link-attributes-decorators link decorators}:
-
-```js
-ClassicEditor
- .create( ..., {
- // ...
- link: {
- decorators: {
- markUnsafeLink: {
- mode: 'automatic',
- callback: url => /^(http:)?\/\//.test( url ),
- classes: 'unsafe-link'
- }
- }
- }
- } )
-```
-
-
-{@snippet framework/extending-content-add-unsafe-link-class}
-
-The `.unsafe-link` CSS class is added to all "unsafe" links by a custom converter plugged into the downcast pipeline, following the default converters brought by the {@link features/link link} feature:
-
-```js
-// This plugin brings customization to the downcast pipeline of the editor.
-function AddClassToUnsafeLinks( editor ) {
- // Both the data and the editing pipelines are affected by this conversion.
- editor.conversion.for( 'downcast' ).add( dispatcher => {
- // Links are represented in the model as a "linkHref" attribute.
- // Use the "low" listener priority to apply the changes after the link feature.
- dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => {
- const viewWriter = conversionApi.writer;
- const viewSelection = viewWriter.document.selection;
-
- // Adding a new CSS class is done by wrapping all link ranges and selection
- // in a new attribute element with the "target" attribute.
- const viewElement = viewWriter.createAttributeElement( 'a', {
- class: 'unsafe-link'
- }, {
- priority: 5
- } );
-
- if ( data.attributeNewValue.match( /http:\/\// ) ) {
- if ( data.item.is( 'selection' ) ) {
- viewWriter.wrap( viewSelection.getFirstRange(), viewElement );
- } else {
- viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
- }
- } else {
- viewWriter.unwrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
- }
- }, { priority: 'low' } );
- } );
-}
-```
-
-Activate the plugin in the editor:
-
-```js
-ClassicEditor
- .create( ..., {
- extraPlugins: [ AddClassToUnsafeLinks ],
- } )
- .then( editor => {
- // ...
- } )
- .catch( err => {
- console.error( err.stack );
- } );
-```
-
-Add some CSS styles for "unsafe" links to make them visible:
-
-```css
-.unsafe-link {
- padding: 0 2px;
- outline: 2px dashed red;
- background: #ffff00;
-}
-```
-
-## Adding a CSS class to block elements
-
-In this example all second–level headings (`
...
`) get the `.my-heading` CSS class. This includes all the heading elements in the editor output (`editor.getData()`) and in the edited content (existing and future ones).
-
-{@snippet framework/extending-content-add-heading-class}
-
-A custom CSS class is added to all `
...
` elements by a custom converter plugged into the downcast pipeline, following the default converters brought by the {@link features/headings headings} feature:
-
-
- The `heading1` element in the model corresponds to `
...
` in the output HTML because in the default {@link features/headings#configuring-heading-levels headings feature configuration} `
...
` is reserved for the top–most heading of the webpage.
-
-
-```js
-// This plugin brings customization to the downcast pipeline of the editor.
-function AddClassToAllHeading1( editor ) {
- // Both the data and the editing pipelines are affected by this conversion.
- editor.conversion.for( 'downcast' ).add( dispatcher => {
- // Headings are represented in the model as a "heading1" element.
- // Use the "low" listener priority to apply the changes after the headings feature.
- dispatcher.on( 'insert:heading1', ( evt, data, conversionApi ) => {
- const viewWriter = conversionApi.writer;
-
- viewWriter.addClass( 'my-heading', conversionApi.mapper.toViewElement( data.item ) );
- }, { priority: 'low' } );
- } );
-}
-```
-
-Activate the plugin in the editor:
-
-```js
-ClassicEditor
- .create( ..., {
- extraPlugins: [ AddClassToAllHeading1 ],
- } )
- .then( editor => {
- // ...
- } )
- .catch( err => {
- console.error( err.stack );
- } );
-```
-
-Add some CSS styles for `.my-heading` to see the customization in action:
-
-```css
-.my-heading {
- font-family: Georgia, Times, Times New Roman, serif;
- border-left: 6px solid #fd0000;
- padding-left: .8em;
- padding: .1em .8em;
-}
-```
-
-## What's next?
-
-If you would like to read more about how to make CKEditor 5 accept more content, refer to the {@link framework/guides/deep-dive/conversion-preserving-custom-content Preserving custom content} guide.
-
-If you want to learn how to create complex view structures or how to move from {@link module:engine/conversion/conversion~Conversion two-way} or {@link module:engine/conversion/conversion~Conversion#for one-way} converters to event-based ones, refer to the {@link framework/guides/deep-dive/custom-element-conversion Custom element conversion} guide.
diff --git a/packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion-introduction.md b/packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion-introduction.md
deleted file mode 100644
index d6a6ba1777e..00000000000
--- a/packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion-introduction.md
+++ /dev/null
@@ -1,129 +0,0 @@
----
-category: framework-deep-dive-conversion
-menu-title: Advanced concepts
-order: 10
-
-# IMPORTANT:
-# This guide is meant to become "Introduction to conversion" later on, hence the file name.
-# For now, due to lack of content, it is called "advanced concepts".
----
-
-# Advanced conversion concepts — attributes
-
-This guide extends the {@link framework/guides/architecture/editing-engine Introduction to CKEditor 5 editing engine architecture guide}, which we highly recommend reading first. Also, the {@link framework/guides/tutorials/implementing-a-block-widget#defining-converters Implementing a block widget} and {@link framework/guides/tutorials/implementing-an-inline-widget#defining-converters Implementing an inline widget} tutorials explain the basics of conversion with examples, hence reading them is recommended as well.
-
-In this guide we will dive deeper into some of the conversion concepts.
-
-## Inline and block content
-
-Generally speaking, there are two main types of content in the editor view and data output: inline and block.
-
-The inline content means elements like ``, `` or ``. Unlike `
`) loaded into the editor content will preserve their attributes. All the DOM attributes will be stored in the editor model as corresponding attributes.
-
-{@snippet framework/extending-content-allow-div-attributes}
-
-All attributes are allowed on `
` elements thanks to custom "upcast" and "downcast" converters that copy each attribute one by one.
-
-Allowing every possible attribute on a `
` element in the model is done by adding an {@link module:engine/model/schema~Schema#addAttributeCheck addAttributeCheck()} callback.
-
-
- Allowing every attribute on `
` elements might introduce security issues — including XSS attacks. The production code should use only application-related attributes and/or properly encode the data.
-
-
-Adding "upcast" and "downcast" converters for the `
` element is enough for these cases where its attributes do not change. If the attributes in the model are modified, however, these `elementToElement()` converters will not be called as the `
` is already converted. To overcome this, a lower-level API is used.
-
-Instead of using predefined converters, the {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event-attribute `attribute`} event listener is registered for the "downcast" dispatcher.
-
-```js
-function ConvertDivAttributes( editor ) {
- // Allow
elements in the model.
- editor.model.schema.register( 'div', {
- allowWhere: '$block',
- allowContentOf: '$root'
- } );
-
- // Allow
elements in the model to have all attributes.
- editor.model.schema.addAttributeCheck( context => {
- if ( context.endsWith( 'div' ) ) {
- return true;
- }
- } );
-
- // The view-to-model converter converting a view
with all its attributes to the model.
- editor.conversion.for( 'upcast' ).elementToElement( {
- view: 'div',
- model: ( viewElement, { writer: modelWriter } ) => {
- return modelWriter.createElement( 'div', viewElement.getAttributes() );
- }
- } );
-
- // The model-to-view converter for the
element (attributes are converted separately).
- editor.conversion.for( 'downcast' ).elementToElement( {
- model: 'div',
- view: 'div'
- } );
-
- // The model-to-view converter for
attributes.
- // Note that a lower-level, event-based API is used here.
- editor.conversion.for( 'downcast' ).add( dispatcher => {
- dispatcher.on( 'attribute', ( evt, data, conversionApi ) => {
- // Convert
attributes only.
- if ( data.item.name != 'div' ) {
- return;
- }
-
- const viewWriter = conversionApi.writer;
- const viewDiv = conversionApi.mapper.toViewElement( data.item );
-
- // In the model-to-view conversion we convert changes.
- // An attribute can be added or removed or changed.
- // The below code handles all 3 cases.
- if ( data.attributeNewValue ) {
- viewWriter.setAttribute( data.attributeKey, data.attributeNewValue, viewDiv );
- } else {
- viewWriter.removeAttribute( data.attributeKey, viewDiv );
- }
- } );
- } );
-}
-```
-
-Activate the plugin in the editor:
-
-```js
-ClassicEditor
- .create( ..., {
- extraPlugins: [ ConvertDivAttributes ],
- } )
- .then( editor => {
- // ...
- } )
- .catch( err => {
- console.error( err.stack );
- } );
-```
-
-## Parsing attribute values
-
-Some features, like {@link features/font Font}, allow only specific values for inline attributes. In this example you will add a converter that will parse any `font-size` value into one of the defined values.
-
-{@snippet framework/extending-content-arbitrary-attribute-values}
-
-Parsing any font value to the model requires adding a custom "upcast" converter that will override the default converter from `FontSize`. Unlike the default one, this converter parses values set in CSS nad sets them into the model.
-
-As the default "downcast" converter only operates on pre-defined values, you will also add a model-to-view converter that simply outputs any model value to font size using `px` units.
-
-```js
-function HandleFontSizeValue( editor ) {
- // Add a special catch-all converter for the font size feature.
- editor.conversion.for( 'upcast' ).elementToAttribute( {
- view: {
- name: 'span',
- styles: {
- 'font-size': /[\s\S]+/
- }
- },
- model: {
- key: 'fontSize',
- value: viewElement => {
- const value = parseFloat( viewElement.getStyle( 'font-size' ) ).toFixed( 0 );
-
- // It might be necessary to further convert the value to meet business requirements.
- // In the sample the font size is configured to handle only these sizes:
- // 12, 14, 'default', 18, 20, 22, 24, 26, 28, 30
- // Other sizes will be converted to the model but the UI might not be aware of them.
-
- // The font size feature expects numeric values to be Number, not String.
- return parseInt( value );
- }
- },
- converterPriority: 'high'
- } );
-
- // Add a special converter for the font size feature to convert all (even the not configured)
- // model attribute values.
- editor.conversion.for( 'downcast' ).attributeToElement( {
- model: {
- key: 'fontSize'
- },
- view: ( modelValue, { writer: viewWriter } ) => {
- return viewWriter.createAttributeElement( 'span', {
- style: `font-size:${ modelValue }px`
- } );
- },
- converterPriority: 'high'
- } );
-}
-```
-
-Activate the plugin in the editor:
-
-```js
-ClassicEditor
- .create( ..., {
- items: [ 'heading', '|', 'bold', 'italic', '|', 'fontSize' ],
- fontSize: {
- options: [ 10, 12, 14, 'default', 18, 20, 22 ]
- },
- extraPlugins: [ HandleFontSizeValue ],
- } )
- .then( editor => {
- // ...
- } )
- .catch( err => {
- console.error( err.stack );
- } );
-```
-
-## Adding extra attributes to elements contained in a figure
-
-The {@link features/images-overview image} and {@link features/table table} features wrap view elements (`` for image and `
` for table, respectively) in a `
` element. During the downcast conversion, the model element is mapped to `
` and not the inner element. In such cases the default `conversion.attributeToAttribute()` conversion helpers could lose information about the element that the attribute should be set on.
-
-To overcome this limitation it is sufficient to write a custom converter that adds custom attributes to elements already converted by base features. The key point is to add these converters with a lower priority than the base converters so they will be called after the base ones.
-
-{@snippet framework/extending-content-custom-figure-attributes}
-
-The sample below is extensible. To add your own attributes to preserve, just add another `setupCustomAttributeConversion()` call with desired names.
-
-```js
-/**
- * A plugin that converts custom attributes for elements that are wrapped in
in the view.
- */
-class CustomFigureAttributes {
- /**
- * Plugin's constructor - receives an editor instance on creation.
- */
- constructor( editor ) {
- // Save reference to the editor.
- this.editor = editor;
- }
-
- /**
- * Sets the conversion up and extends the table & image features schema.
- *
- * Schema extending must be done in the "afterInit()" call because plugins define their schema in "init()".
- */
- afterInit() {
- const editor = this.editor;
-
- // Define on which elements the CSS classes should be preserved:
- setupCustomClassConversion( 'img', 'imageBlock', editor );
- setupCustomClassConversion( 'img', 'imageInline', editor );
- setupCustomClassConversion( 'table', 'table', editor );
-
- editor.conversion.for( 'upcast' ).add( upcastCustomClasses( 'figure' ), { priority: 'low' } );
-
- // Define custom attributes that should be preserved.
- setupCustomAttributeConversion( 'img', 'imageBlock', 'id', editor );
- setupCustomAttributeConversion( 'img', 'imageInline', 'id', editor );
- setupCustomAttributeConversion( 'table', 'table', 'id', editor );
- }
-}
-
-/**
- * Sets up a conversion that preserves classes on and
elements.
- */
-function setupCustomClassConversion( viewElementName, modelElementName, editor ) {
- // The 'customClass' attribute stores custom classes from the data in the model so that schema definitions allow this attribute.
- editor.model.schema.extend( modelElementName, { allowAttributes: [ 'customClass' ] } );
-
- // Defines upcast converters for the and
elements with a "low" priority so they are run after the default converters.
- editor.conversion.for( 'upcast' ).add( upcastCustomClasses( viewElementName ), { priority: 'low' } );
-
- // Defines downcast converters for a model element with a "low" priority so they are run after the default converters.
- // Use `downcastCustomClassesToFigure` if you want to keep your classes on
element or `downcastCustomClassesToChild`
- // if you would like to keep your classes on a
child element, i.e. .
- editor.conversion.for( 'downcast' ).add( downcastCustomClassesToFigure( modelElementName ), { priority: 'low' } );
- // editor.conversion.for( 'downcast' ).add( downcastCustomClassesToChild( viewElementName, modelElementName ), { priority: 'low' } );
-}
-
-/**
- * Sets up a conversion for a custom attribute on the view elements contained inside a
.
- *
- * This method:
- * - Adds proper schema rules.
- * - Adds an upcast converter.
- * - Adds a downcast converter.
- */
-function setupCustomAttributeConversion( viewElementName, modelElementName, viewAttribute, editor ) {
- // Extends the schema to store an attribute in the model.
- const modelAttribute = `custom${ viewAttribute }`;
-
- editor.model.schema.extend( modelElementName, { allowAttributes: [ modelAttribute ] } );
-
- editor.conversion.for( 'upcast' ).add( upcastAttribute( viewElementName, viewAttribute, modelAttribute ) );
- editor.conversion.for( 'downcast' ).add( downcastAttribute( modelElementName, viewElementName, viewAttribute, modelAttribute ) );
-}
-
-/**
- * Creates an upcast converter that will pass all classes from the view element to the model element.
- */
-function upcastCustomClasses( elementName ) {
- return dispatcher => dispatcher.on( `element:${ elementName }`, ( evt, data, conversionApi ) => {
- const viewItem = data.viewItem;
- const modelRange = data.modelRange;
-
- const modelElement = modelRange && modelRange.start.nodeAfter;
-
- if ( !modelElement ) {
- return;
- }
-
- // The upcast conversion picks up classes from the base element and from the
element so it should be extensible.
- const currentAttributeValue = modelElement.getAttribute( 'customClass' ) || [];
-
- currentAttributeValue.push( ...viewItem.getClassNames() );
-
- conversionApi.writer.setAttribute( 'customClass', currentAttributeValue, modelElement );
- } );
-}
-
-/**
- * Creates a downcast converter that adds classes defined in the `customClass` attribute to a
element.
- *
- * This converter expects that the view element is nested in a
element.
- */
-function downcastCustomClassesToFigure( modelElementName ) {
- return dispatcher => dispatcher.on( `insert:${ modelElementName }`, ( evt, data, conversionApi ) => {
- const modelElement = data.item;
-
- const viewFigure = conversionApi.mapper.toViewElement( modelElement );
-
- if ( !viewFigure ) {
- return;
- }
-
- // The code below assumes that classes are set on the
element.
- conversionApi.writer.addClass( modelElement.getAttribute( 'customClass' ), viewFigure );
- } );
-}
-
-/**
- * Creates a downcast converter that adds classes defined in the `customClass` attribute to a
child element.
- *
- * This converter expects that the view element is nested in a
element.
- */
-function downcastCustomClassesToChild( viewElementName, modelElementName ) {
- return dispatcher => dispatcher.on( `insert:${ modelElementName }`, ( evt, data, conversionApi ) => {
- const modelElement = data.item;
-
- const viewFigure = conversionApi.mapper.toViewElement( modelElement );
-
- if ( !viewFigure ) {
- return;
- }
-
- // The code below assumes that classes are set on the element inside the
.
- const viewElement = findViewChild( viewFigure, viewElementName, conversionApi );
-
- conversionApi.writer.addClass( modelElement.getAttribute( 'customClass' ), viewElement );
- } );
-}
-
-/**
- * Helper method that searches for a given view element in all children of the model element.
- *
- * @param {module:engine/view/item~Item} viewElement
- * @param {String} viewElementName
- * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
- * @return {module:engine/view/item~Item}
- */
-function findViewChild( viewElement, viewElementName, conversionApi ) {
- const viewChildren = Array.from( conversionApi.writer.createRangeIn( viewElement ).getItems() );
-
- return viewChildren.find( item => item.is( 'element', viewElementName ) );
-}
-
-/**
- * Returns the custom attribute upcast converter.
- */
-function upcastAttribute( viewElementName, viewAttribute, modelAttribute ) {
- return dispatcher => dispatcher.on( `element:${ viewElementName }`, ( evt, data, conversionApi ) => {
- const viewItem = data.viewItem;
- const modelRange = data.modelRange;
-
- const modelElement = modelRange && modelRange.start.nodeAfter;
-
- if ( !modelElement ) {
- return;
- }
-
- conversionApi.writer.setAttribute( modelAttribute, viewItem.getAttribute( viewAttribute ), modelElement );
- } );
-}
-
-/**
- * Returns the custom attribute downcast converter.
- */
-function downcastAttribute( modelElementName, viewElementName, viewAttribute, modelAttribute ) {
- return dispatcher => dispatcher.on( `insert:${ modelElementName }`, ( evt, data, conversionApi ) => {
- const modelElement = data.item;
-
- const viewFigure = conversionApi.mapper.toViewElement( modelElement );
- const viewElement = findViewChild( viewFigure, viewElementName, conversionApi );
-
- if ( !viewElement ) {
- return;
- }
-
- conversionApi.writer.setAttribute( viewAttribute, modelElement.getAttribute( modelAttribute ), viewElement );
- } );
-}
-```
-
-Activate the plugin in the editor:
-
-```js
-ClassicEditor
- .create( ..., {
- extraPlugins: [ CustomFigureAttributes ],
- } )
- .then( editor => {
- // ...
- } )
- .catch( err => {
- console.error( err.stack );
- } );
-```
-
-## What's next?
-
-If you would like to read more about how to extend the output of existing CKEditor 5 features, refer to the {@link framework/guides/deep-dive/conversion-extending-output Extending the editor output} guide.
-
-If you want to learn how to create complex view structures or how to move from {@link module:engine/conversion/conversion~Conversion two-way} or {@link module:engine/conversion/conversion~Conversion#for one-way} converters to event-based ones, refer to the {@link framework/guides/deep-dive/custom-element-conversion Custom element conversion} guide.
diff --git a/packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion/downcast.md b/packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion/downcast.md
new file mode 100644
index 00000000000..2f01ca5e6d1
--- /dev/null
+++ b/packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion/downcast.md
@@ -0,0 +1,201 @@
+---
+category: framework-deep-dive-conversion
+menu-title: Model to view (downcast)
+order: 20
+since: 33.0.0
+---
+
+# Model to view (downcast)
+
+## Introduction
+
+The process of converting the **model** to the **view** is called a **downcast**.
+
+{@img assets/img/downcast-basic.svg 238 Basic downcast conversion diagram.}
+
+The downcast process happens every time a model node or attribute needs to be converted into a view node or attribute.
+
+The editor engine runs the conversion process and uses converters registered by plugins.
+
+{@snippet framework/mini-inspector}
+
+## Registering a converter
+
+In order to tell the engine how to convert a specific model element into a view element, you need to register a **downcast converter** by using the `editor.conversion.for( 'downcast' )` method:
+
+```js
+editor.conversion
+ .for( 'downcast' )
+ .elementToElement( {
+ model: 'paragraph',
+ view: 'p'
+ } );
+```
+
+The above converter will handle the conversion of every `` model element to a `
` view element.
+
+{@snippet framework/mini-inspector-paragraph}
+
+
+ This is just an example. Paragraph support is provided by the {@link api/paragraph paragraph plugin} so you don't have to write your own `` element to `
` element conversion.
+
+
+
+ You just learned about the `elementToElement()` **downcast** conversion helper method! More helpers are documented in the following chapters.
+
+
+## Downcast pipelines
+
+CKEditor 5 engine uses two different views: **data view** and **editing view**.
+
+**The data view** is used when generating editor output. This process is controlled by the data pipeline.
+
+**The editing view**, on the other hand, is what you see when you open the editor, which is controlled by the editing pipeline.
+
+{@img assets/img/downcast-pipelines.svg 444 Downcast conversion pipelines diagram.}
+
+The previous code example registers a converter for both pipelines at once. It means that `` model element will be converted to a `
` view element in both **data view** and **editing view**.
+
+Sometimes you may want to alter converter logic for a specific pipeline. For example, in editing view you want to add some additional class to the view element.
+
+```js
+// dataDowncast for data pipeline
+editor.conversion
+ .for( 'dataDowncast' )
+ .elementToElement( {
+ model: 'paragraph',
+ view: 'p'
+ } );
+
+// editingDowncast for editing pipeline
+editor.conversion
+ .for( 'editingDowncast' )
+ .elementToElement( {
+ model: 'paragraph',
+ view: {
+ name: 'p',
+ classes: 'paragraph-in-editing-view'
+ }
+ } );
+```
+
+{@snippet framework/mini-inspector-paragraph}
+
+## Converting text attributes
+
+As you may know from the chapter about the model, an **attribute** can be applied to a model text node.
+
+Such text nodes attributes can be converted into view elements.
+
+In order to do so, you can register a converter by using `attributeToElement()` conversion helper:
+
+```js
+editor.conversion
+ .for( 'downcast' )
+ .attributeToElement( {
+ model: 'bold',
+ view: 'strong'
+ } );
+```
+
+The above converter will handle the conversion of every `bold` model text node attribute to a `` view element.
+
+{@snippet framework/mini-inspector-bold}
+
+
+ This is just an example. Bold support is provided by the {@link features/basic-styles basic styles} plugin so you don't have to write your own bold attribute to strong element conversion.
+
+
+## Converting element to element
+
+Similar to the basic example, you can convert `` model element into `
` view element with `elementToElement()` conversion helper.
+
+```js
+editor.conversion
+ .for( 'downcast' )
+ .elementToElement( {
+ model: 'heading',
+ view: 'h1'
+ } );
+```
+
+Which is equivalent to:
+
+```js
+editor.conversion
+ .for( 'downcast' )
+ .elementToElement( {
+ model: 'heading',
+ view: ( modelElement, { writer } ) => {
+ return writer.createContainerElement(
+ 'h1'
+ );
+ }
+ } );
+```
+
+You learned that the `view` property can be a simple string or an object. The example above shows it is also possible to define a custom callback function to return created element instead.
+
+{@snippet framework/mini-inspector-heading}
+
+The `` element makes the most sense if you can set the heading level.
+
+From the previous chapter you learned that you can apply attributes to text nodes. It is also possible to add attributes to elements.
+
+```js
+editor.conversion
+ .for( 'downcast' )
+ .elementToElement( {
+ model: {
+ name: 'heading',
+ attributes: [ 'level' ]
+ },
+ view: ( modelElement, { writer } ) => {
+ return writer.createContainerElement(
+ 'h' + modelElement.getAttribute( 'level' )
+ );
+ }
+ } );
+```
+
+From now on, every time level attribute updates, the whole `` element will be converted to the `` element (for example `
`, `
`, etc).
+
+You can check this in action by using the example below:
+
+{@snippet framework/mini-inspector-heading-interactive}
+
+
+ This is just an example. Heading support is provided by the {@link features/headings headings feature} so you don't have to write your own `` to `
` element conversion.
+
+
+## Converting element to structure
+
+Sometimes you may want to convert a **single model** element into a more complex view structure consisting of a **single view element with children**.
+
+You can use `elementToStructure()` conversion helper for this purpose:
+
+```js
+editor.conversion
+ .for( 'downcast' ).elementToStructure( {
+ model: 'myElement',
+ view: ( modelElement, { writer } ) => {
+ return writer.createContainerElement( 'div', { class: 'wrapper' }, [
+ writer.createContainerElement( 'div', { class: 'inner-wrapper' }, [
+ writer.createSlot()
+ ] )
+ ] );
+ }
+ } );
+```
+
+The above converter will convert all `` model elements to `
...
` structures.
+
+{@snippet framework/mini-inspector-structure}
+
+
+ Using your own custom model element requires defining it in the schema.
+
+
+## Read next
+
+{@link framework/guides/deep-dive/conversion/upcast View to model (upcast)}
diff --git a/packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion/helpers/downcast.md b/packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion/helpers/downcast.md
new file mode 100644
index 00000000000..3c1bdeee6e7
--- /dev/null
+++ b/packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion/helpers/downcast.md
@@ -0,0 +1,376 @@
+---
+category: framework-deep-dive-conversion-helpers
+menu-title: Downcast helpers (model to view)
+order: 20
+since: 33.0.0
+---
+
+# Downcast helpers (model to view)
+
+## Element to element
+
+Converting a model element to a view element is the most common case of conversion. It is used to create view elements like `
` or `
` (that we call container elements).
+
+When using the `elementToElement()` helper, a **single model element** will be converted to a **single view element**. The children of this model element have to have their own converters defined and the engine will recursively convert them and insert into the created view element.
+
+### Basic element to element conversion
+
+If you want to convert a model element to a simple view element without additional attributes, simply provide their names:
+
+```js
+editor.conversion
+ .for( 'downcast' )
+ .elementToElement( {
+ model: 'paragraphSeparator',
+ view: 'hr'
+} );
+```
+
+### Using view element definition
+
+You might want to output a view element that has more attributes, e.g. a class name. To achieve that you can provide [element definition](https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_view_elementdefinition-ElementDefinition.html) in the `view` property:
+
+```js
+editor.conversion
+ .for( 'downcast' )
+ .elementToElement( {
+ model: 'fancyParagraph',
+ view: {
+ name: 'p',
+ classes: 'fancy'
+ }
+ } );
+```
+
+Check out the [ElementDefinition documentation](https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_view_elementdefinition-ElementDefinition.html) for more details.
+
+### Creating a view element using a callback
+
+Another way of writing a converter from the previous section using a callback would look like this:
+
+```js
+editor.conversion
+ .for( 'downcast' )
+ .elementToElement( {
+ model: 'fancyParagraph',
+ view: ( modelElement, { writer } ) => {
+ return writer.createContainerElement(
+ 'p', { class: 'fancy' }
+ );
+ }
+ } );
+```
+
+The second parameter of the view callback is the [DowncastConversionApi](https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_conversion_downcastdispatcher-DowncastConversionApi.html) object, that contains many properties and methods that can be useful when writing a more complex converters.
+
+The callback should return a single container element. That element should not contain any children except UI elements. If you want to create a richer structure, use `elementToStructure()`.
+
+### Handling model elements with attributes
+
+If the view element depends not only on the model element itself but also on its attributes you need to specify these attributes in the `model` property.
+
+```js
+editor.conversion
+ .for( 'downcast' )
+ .elementToElement( {
+ model: {
+ name: 'heading',
+ attributes: [ 'level' ]
+ },
+ view: ( modelElement, { writer } ) => {
+ return writer.createContainerElement(
+ 'h' + modelElement.getAttribute( 'level' )
+ );
+ }
+ } );
+```
+
+
+ If you forget about specifying these attributes, the converter will work for the insertion of the model element but it will not handle changes of the attribute value.
+
+
+### Changing converter priority
+
+In case there are other converters with the overlapping `model` patterns already present, you can prioritize your converter in order to override t. To do that use the `converterPriority` property:
+
+```js
+editor.conversion
+ .for( 'downcast' )
+ .elementToElement( {
+ model: 'userComment',
+ view: 'div'
+ } );
+
+editor.conversion
+ .for( 'downcast' )
+ .elementToElement( {
+ model: 'userComment',
+ view: 'article',
+ converterPriority: 'high'
+ } );
+```
+
+Above, the first converter has a default priority, `normal`. The second one overrides it by setting the priority to `high`. Using both of these converters at once will result in the `` element being converted to an `` element.
+
+Another case might be when you want your converter to act as a fallback when other converters for a given element are not present (e.g. a plugin has not been loaded). Achieving this is as simple as setting the `converterProperty` to `low`.
+
+## Element to structure
+
+Convert a single model element to many view elements (a structure of view elements).
+
+### Handling empty model elements
+
+To convert a single model element `horizontalLine` to a following structure:
+
+```html
+
+
+
+```
+
+you can use a converter similar to this:
+
+```js
+editor.conversion
+ .for( 'downcast' )
+ .elementToStructure( {
+ model: 'horizontalLine',
+ view: ( modelElement, { writer } ) => {
+ return writer.createContainerElement( 'div', { class: 'horizontal-line' }, [
+ writer.createEmptyElement( 'hr' )
+ ] );
+ }
+} );
+```
+
+Note that in this example we create two elements, which is not possible by using previously mentioned `elementToElement()` helper.
+
+Another thing to remember is that in the real life scenario it would be recommended for this element to be {@link framework/guides/tutorials/implementing-a-block-widget a widget}.
+
+### Handling model element’s children
+
+The example above uses an empty model element. If your model element may contain children you need to specify in the view where these children should be placed. To do that use `writer.createSlot()`
+
+```js
+editor.conversion
+ .for( 'downcast' )
+ .elementToStructure( {
+ model: 'wrappedParagraph',
+ view: ( modelElement, conversionApi ) => {
+ const { writer } = conversionApi;
+ const paragraphViewElement = writer.createContainerElement( 'p', {}, [
+ writer.createSlot()
+ ] );
+
+ return writer.createContainerElement( 'div', { class: 'wrapper' }, [
+ paragraphViewElement
+ ] );
+ }
+ } );
+```
+## Attribute to element
+
+The attribute to element conversion is used to create formatting view elements like `` or `` (that we call attribute elements). In this case, we don’t convert a model element but a text node’s attribute. It is important to note that text formatting such as bold or font size should be represented in the model as text nodes attributes.
+
+
+ In general, the model does not implement a concept of “inline elements” (in the sense in which they are defined by CSS). The only scenarios in which inline elements can be used are self-contained objects such as soft breaks (` `) or inline images.
+
+
+### Basic text attribute to model conversion
+
+```js
+editor.conversion
+ .for( 'downcast' )
+ .attributeToElement( {
+ model: 'bold',
+ view: 'strong'
+ } );
+```
+
+A model text node `"CKEditor 5"` with a `bold` attribute will become a `CKEditor 5` in the view.
+
+### Using view element definition
+
+You might want to output a view element that has more attributes, e.g. a class name. To achieve that you can provide [element definition](https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_view_elementdefinition-ElementDefinition.html) in the `view` property:
+
+```js
+editor.conversion
+ .for( 'downcast' )
+ .attributeToElement( {
+ model: 'invert',
+ view: {
+ name: 'span',
+ classes: [ 'font-light', 'bg-dark' ]
+ }
+ } );
+```
+
+Check out the [ElementDefinition documentation](https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_view_elementdefinition-ElementDefinition.html) for more details.
+
+### Creating a view element using a callback
+
+You can also generate the view element by a callback. This method is useful when the view element depends on the value of the model attribute.
+
+```js
+editor.conversion
+ .for( 'downcast' )
+ .attributeToElement( {
+ model: 'bold',
+ view: ( modelAttributeValue, conversionApi ) => {
+ const { writer } = conversionApi;
+
+ return writer.createAttributeElement( 'span', {
+ style: 'font-weight:' + modelAttributeValue
+ } );
+ }
+ } );
+```
+
+The second parameter of the view callback is the [DowncastConversionApi](https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_conversion_downcastdispatcher-DowncastConversionApi.html) object, that contains many properties and methods that can be useful when writing a more complex converters.
+
+### Changing converter priority
+
+In case there are other converters already present, you can prioritize your converter in order to override existing ones. To do that use the `converterPriority` property:
+
+```js
+editor.conversion
+ .for( 'downcast' )
+ .attributeToElement( {
+ model: 'bold',
+ view: 'strong'
+ } );
+
+editor.conversion
+ .for( 'downcast' )
+ .attributeToElement( {
+ model: 'bold',
+ view: 'b',
+ converterPriority: 'high'
+ } );
+```
+
+Above, the first converter has a default priority, `normal`. The second one overrides it by setting the priority to `high`. Using both of these converters at once will result in the `bold` attribute being converted to a `` element.
+
+## Attribute to attribute
+
+The `attributeToAttribute()` helper allows registering a converter that handles a specific attribute and converts it to an attribute of a view element.
+
+Usually, when registering converters for elements (e.g. by using `elementToElement()` or `elementToStructure()`), you will want to handle their attributes while handling the element itself.
+
+The `attributeToAttribute()` helper comes handy when for some reason you can’t cover a specific attribute inside `elementToElement()`. For instance, you are extending someone else’s plugin.
+
+
+ This type of converter helper works only if there is already an element converter provided. Trying to convert to an attribute while there is no receiving view element will cause an error.
+
+
+### Basic attribute to attribute conversion
+
+This conversion results in adding an attribute to a view node, basing on an attribute from a model node. For example, `` is converted to ``.
+
+```js
+editor.conversion
+ .for( 'downcast' )
+ .attributeToAttribute( {
+ model: 'source',
+ view: 'src'
+ } );
+```
+
+### Converting specific model element and attribute
+
+The converter in the example above will be convert all the `source` model attributes in the document. You can limit its scope by providing the model element name.
+
+```js
+editor.conversion
+ .for( 'downcast' )
+ .attributeToAttribute( {
+ model: {
+ name: 'imageInline',
+ key: 'source'
+ },
+ view: 'src'
+ } );
+```
+
+The converter above will convert all the `source` model attributes, but only those present on the `imageInline` model element.
+
+### Creating a custom view element from a selected list of model values
+
+Once you provide the array in the `model.values` property, the `view` property is expected to be an object with keys matching these values. This is best explained using the example below:
+
+```js
+editor.conversion
+ .for( 'downcast' )
+ .attributeToAttribute( {
+ model: {
+ name: 'styled',
+ values: [ 'dark', 'light' ]
+ },
+ view: {
+ dark: {
+ key: 'class',
+ value: [ 'styled', 'styled-dark' ]
+ },
+ light: {
+ key: 'class',
+ value: [ 'styled', 'styled-light' ]
+ }
+ }
+ } );
+```
+
+### Creating a view attribute with a custom value based on the model value
+
+The value of the view attribute can be modified in the converter. Below is a simple mapper, that sets the class attribute based on the model attribute value:
+
+```js
+editor.conversion
+ .for( 'downcast' )
+ .attributeToAttribute( {
+ model: 'styled',
+ view: modelAttributeValue => ( {
+ key: 'class',
+ value: 'styled-' + modelAttributeValue
+ } )
+ } );
+```
+
+It is worth noting that providing a style property in this manner requires the returned `value` to be an object:
+
+```js
+editor.conversion
+ .for( 'downcast' )
+ .attributeToAttribute( {
+ model: 'lineHeight',
+ view: modelAttributeValue => ( {
+ key: 'style',
+ value: {
+ 'line-height': modelAttributeValue,
+ 'border-bottom': '1px dotted #ba2'
+ }
+ } )
+ } );
+```
+
+### Changing converter priority
+
+You can override the existing converters by specifying higher priority, like in the example below:
+
+```js
+editor.conversion
+ .for( 'downcast' )
+ .attributeToAttribute( {
+ model: 'source',
+ view: 'href'
+ } );
+
+editor.conversion
+ .for( 'downcast' )
+ .attributeToAttribute( {
+ model: 'source',
+ view: 'src',
+ converterPriority: 'high'
+ } );
+```
+
+First converter has the default priority, `normal`. The second converter will be called earlier because of its higher priority, thus the `source` model attribute will get converted to `src` view attribute instead of `href`.
diff --git a/packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion/helpers/intro.md b/packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion/helpers/intro.md
new file mode 100644
index 00000000000..7630111cf18
--- /dev/null
+++ b/packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion/helpers/intro.md
@@ -0,0 +1,20 @@
+---
+category: framework-deep-dive-conversion-helpers
+menu-title: Introduction
+order: 10
+since: 33.0.0
+---
+
+# Introduction
+
+The editor is supporting out-of-the-box a vast amount of the most commonly used HTML elements via {@link features/index existing editor features}.
+
+If your aim is to easily enable common HTML features that are not explicitly supported by the dedicated CKEditor 5 features, use the {@link features/general-html-support General HTML Support feature}.
+
+Yet, there are cases where you might want to provide a rich editing experience for a custom HTML markup. The conversion helpers are the way to achieve that.
+
+## Helpers by category
+
+* **{@link framework/guides/deep-dive/conversion/helpers/downcast Downcast helpers (model to view)}**
+
+* **{@link framework/guides/deep-dive/conversion/helpers/upcast Upcast helpers (view to model)}**
diff --git a/packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion/helpers/upcast.md b/packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion/helpers/upcast.md
new file mode 100644
index 00000000000..27ed7ca9d86
--- /dev/null
+++ b/packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion/helpers/upcast.md
@@ -0,0 +1,362 @@
+---
+category: framework-deep-dive-conversion-helpers
+menu-title: Upcast helpers (view to model)
+order: 30
+since: 33.0.0
+---
+
+# Upcast helpers (view to model)
+
+## Element to element
+
+Converting a view element to a model element is the most common case of conversion. It is used to handle view elements like `
` or `
` (which needs to be converted to model elements).
+
+When using the `elementToElement()` helper, a **single view element** will be converted to a **single model element**. The children of this view element have to have their own converters defined and the engine will recursively convert them and insert into the created model element.
+
+### Basic element to element conversion
+
+The simplest case of an element to element conversion, where a view element becomes a paragraph model element can be achieved by providing their names:
+
+```js
+editor.conversion
+ .for( 'upcast' )
+ .elementToElement( {
+ view: 'p',
+ model: 'paragraph'
+ } );
+```
+
+The above example creates a model element `` from every `
` view element.
+
+### Using view element definition
+
+You can limit the view elements that qualify for the conversion by specifying their attributes, e.g. a class name. Provide respective element definition in the `view` property like in the example below:
+
+```js
+editor.conversion
+ .for( 'upcast' )
+ .elementToElement( {
+ view: {
+ name: 'p',
+ classes: 'fancy'
+ },
+ model: 'fancyParagraph'
+ } );
+```
+
+Check out the [ElementDefinition documentation](https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_view_elementdefinition-ElementDefinition.html) for more details.
+
+### Creating a model element using a callback
+
+Model element resulting from the conversion can be created manually using a callback provided as a `model` property.
+
+```js
+editor.conversion
+ .for( 'upcast' )
+ .elementToElement( {
+ view: {
+ name: 'p',
+ classes: 'heading'
+ },
+ model: ( viewElement, { writer } ) => {
+ return writer.createElement( 'heading' );
+ }
+ } );
+```
+
+In the example above the model element is created only from a view element `
`. The `
` elements without that class name will be omitted.
+
+The second parameter of the model callback is the [UpcastConversionApi](https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_conversion_upcastdispatcher-UpcastConversionApi.html) object, that contains many properties and methods useful when writing more a complex converters.
+
+### Handling view elements with attributes
+
+If the model element depends not only on the view element itself but also on its attributes, you need to specify those attributes in the `view` property.
+
+```js
+editor.conversion
+ .for( 'upcast' )
+ .elementToElement( {
+ view: {
+ name: 'p',
+ attributes: [ 'data-level' ]
+ },
+ model: ( viewElement, { writer } ) => {
+ return writer.createElement( 'heading', { level: viewElement.getAttribute( 'data-level' ) } );
+ }
+ } );
+```
+
+
+ If you forget about specifying these attributes, another converter e.g. from General HTML Support feature may also handle these attributes resulting in duplicating them in the model.
+
+
+### Changing converter priority
+
+In case there are other converters with the overlapping `view` patterns already present, you can prioritize your converter in order to override them. To do so use the `converterPriority` property:
+
+```js
+editor.conversion
+ .for( 'upcast' )
+ .elementToElement( {
+ view: 'div',
+ model: 'mainContent',
+ } );
+
+editor.conversion
+ .for( 'upcast' )
+ .elementToElement( {
+ view: 'div',
+ model: 'sideContent',
+ converterPriority: 'high'
+ } );
+```
+
+Above, the first converter has the default priority, `normal`. The second one override it by setting the priority to `high`. Using both of these converters at once will result in the `
` view element being converted to `sideContent`.
+
+Another case might be when you want your converter to act as a fallback when other converters for a given element are not present (e.g. a plugin has not been loaded) or existing converters were too specific. Achieving this is as simple as setting the `converterProperty` to `low`.
+
+## Element to attribute
+
+The element to attribute conversion is used to handle formatting view elements like `` or `` (which needs to be converted to text attributes). It is important to note that text formatting such as bold or font size should be represented in the model as text nodes attributes.
+
+
+ In general, the model does not implement a concept of “inline elements” (in the sense in which they are defined by CSS). The only scenarios in which inline elements can be used are self-contained objects such as soft breaks (` `) or inline images.
+
+
+### Basic element to attribute conversion
+
+```js
+editor.conversion
+ .for( 'upcast' )
+ .elementToAttribute( {
+ view: 'strong',
+ model: 'bold'
+ } );
+```
+
+A view `CKEditor 5` will become the `"CKEditor 5"` model text node with a `bold` attribute set to `true`.
+
+### Converting attribute in a specific view element
+
+You might want to convert only view elements with a specific class name or other attribute. To achieve that you can provide [element definition](https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_view_elementdefinition-ElementDefinition.html) in the `view` property.
+
+```js
+editor.conversion
+ .for( 'upcast' )
+ .elementToAttribute( {
+ view: {
+ name: 'span',
+ classes: 'bold'
+ },
+ model: 'bold'
+ } );
+```
+
+Check out the [ElementDefinition documentation](https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_view_elementdefinition-ElementDefinition.html) for more details.
+
+### Setting a predefined value to model attribute
+
+You can specify the value model attribute will take. To achieve that provide the name of the resulting model attribute as a `key` and its value as a `value` in `model` property object:
+
+```js
+editor.conversion
+ .for( 'upcast' )
+ .elementToAttribute( {
+ view: {
+ name: 'span',
+ classes: [ 'styled', 'styled-dark' ]
+ },
+ model: {
+ key: 'styled',
+ value: 'dark'
+ }
+ } );
+```
+
+The code above will convert `CKEditor5` into a model text node `CKEditor5` with `styled` attribute set to `dark`.
+
+### Handling attribute values via a callback
+
+In case when the value of an attribute needs additional processing (like mapping, filtering, etc.) you can define the `model.value` as a callback.
+
+```js
+editor.conversion
+ .for( 'upcast' )
+ .elementToAttribute( {
+ view: {
+ name: 'span',
+ styles: {
+ 'font-size': /[\s\S]+/
+ }
+ },
+ model: {
+ key: 'fontSize',
+ value: ( viewElement, conversionApi ) => {
+ const fontSize = viewElement.getStyle( 'font-size' );
+ const value = fontSize.substr( 0, fontSize.length - 2 );
+
+ if ( value <= 10 ) {
+ return 'small';
+ } else if ( value > 12 ) {
+ return 'big';
+ }
+
+ return null;
+ }
+ }
+ } );
+```
+
+In the above example we turn a numeric `font-size` inline style into an either `small` or `big` model attribute.
+
+### Changing converter priority
+
+You can override the existing converters by specifying higher priority, like in the example below:
+
+```js
+editor.conversion
+ .for( 'upcast' )
+ .elementToAttribute( {
+ view: 'strong',
+ model: 'bold'
+ } );
+
+editor.conversion
+ .for( 'upcast' )
+ .elementToAttribute( {
+ view: 'strong',
+ model: 'important',
+ converterPriority: 'high'
+ } );
+```
+
+Above, the first converter has the default priority, `normal`. The second one overrides it by setting the priority to `high`. Using both of these converters at once will result in the `` view element being converted to an `important` model attribute.
+
+## Attribute to attribute
+
+The `attributeToAttribute()` helper allows registering a converter that handles a specific attribute and converts it to an attribute of a model element.
+
+Usually, when registering converters for elements (e.g. by using `elementToElement()`), you will want to handle their attributes while handling the element itself.
+
+The `attributeToAttribute()` helper comes handy when for some reason you can’t cover a specific attribute inside `elementToElement()`. For instance, you are extending someone else’s plugin.
+
+
+ This type of converter helper works only if there is already an element converter provided. Trying to convert to an attribute while there is no receiving model element will cause an error.
+
+
+### Basic attribute to attribute conversion
+
+This conversion result in adding an attribute to a model element, based on an attribute from a view element. For example, the `src` attribute in `` will be converted to `source` in ``.
+
+```js
+editor.conversion
+ .for( 'upcast' )
+ .attributeToAttribute( {
+ view: 'src',
+ model: 'source'
+ } );
+```
+
+Another way of writing this converter is to provide a `view.key` property as in the example below:
+
+```js
+editor.conversion
+ .for( 'upcast' )
+ .attributeToAttribute( {
+ view: {
+ key: 'src'
+ },
+ model: 'source'
+ } );
+```
+
+Both snippets will result in the creating exactly the same converter.
+
+### Converting specific view element’s attribute
+
+You can limit the element holding the attribute as well as the value of that attributes. Such a converter will be executed only in case of a full match.
+
+```js
+editor.conversion
+ .for( 'upcast' )
+ .attributeToAttribute( {
+ view: {
+ name: 'p',
+ key: 'class',
+ value: 'styled-dark'
+ },
+ model: {
+ key: 'styled',
+ value: 'dark'
+ }
+ } );
+```
+
+In the example above only a `styled-dark` class of a `
` element will be converted to a model attribute `styled` with a predefined value `dark`.
+
+### Converting view attributes that match a more complex pattern
+
+The pattern provided in a `view` property can be much more elaborate. Besides a string, you can also provide a regexp or a function that takes the attribute value and returns `true` or `false`.
+
+```js
+editor.conversion
+ .for( 'upcast' )
+ .attributeToAttribute( {
+ view: {
+ key: 'data-style',
+ value: /\S+/
+ },
+ model: 'styled'
+ } );
+```
+
+In the example above we are utilizing regular expression to match only an attribute `data-style` that has no whitespace characters in its value. Attributes that match this expression will have their value assigned to a `styled` model attribute.
+
+### Processing attributes via callback
+
+In case when the value of an attribute needs additional processing (like mapping, filtering, etc.) you can define the `model.value` as a callback.
+
+```js
+editor.conversion
+ .for( 'upcast' )
+ .attributeToAttribute( {
+ view: {
+ key: 'class',
+ value: /styled-[\S]+/
+ },
+ model: {
+ key: 'styled'
+ value: viewElement => {
+ const regexp = /styled-([\S]+)/;
+ const match = viewElement.getAttribute( 'class' ).match( regexp );
+
+ return match[ 1 ];
+ }
+ }
+ } );
+```
+
+The converter in the example above will extract the style name from each `class` attribute that starts with `styled-` and assign it to a model attribute `styled`.
+
+### Changing converter priority
+
+You can override the existing converters by specifying higher priority, like in the example below:
+
+```js
+editor.conversion
+ .for( 'upcast' )
+ .attributeToAttribute( {
+ view: 'src',
+ model: 'source'
+ } );
+
+editor.conversion
+ .for( 'upcast' )
+ .attributeToAttribute( {
+ view: 'src',
+ model: 'sourceAddress',
+ converterPriority: 'high'
+ } );
+```
+
+First converter has the default priority, `normal`. The second converter will be called earlier because of its higher priority, thus the `src` view attribute will get converted to a `sourceAddress` model attribute (instead of `source`).
diff --git a/packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion/intro.md b/packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion/intro.md
new file mode 100644
index 00000000000..84c909ff97f
--- /dev/null
+++ b/packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion/intro.md
@@ -0,0 +1,32 @@
+---
+category: framework-deep-dive-conversion
+menu-title: Introduction
+order: 10
+since: 33.0.0
+---
+
+# Introduction
+
+## What is the conversion?
+
+As you may know, the editor works on two layers - model and view. The process of transforming one into the other is called conversion.
+
+When you load data into the editor, the view is created out of the markup, then, with the help of the upcast converters, the model is created. Once that is done, the model becomes the editor state.
+
+All changes (e.g. typing or pasting from the clipboard) are then applied directly to the model. In order to update the editing view (the one being displayed to the user), the engine transforms changes in the model to the view. The same process is executed when data needs to be generated (e.g. when you copy editor content or use `editor.getData()`).
+
+You can think about upcast and downcast as about processes working in opposite directions (symmetrical to each other).
+
+Following chapters will teach you how to create the right converter for each case, when creating your very own CKEditor 5 plugin.
+
+* **{@link framework/guides/deep-dive/conversion/downcast Model to view (downcast)}**
+
+ Model has to be transformed into the view. Learn how to achieve that by creating downcast converters.
+
+* **{@link framework/guides/deep-dive/conversion/upcast View to model (upcast)}**
+
+ Raw data coming into the editor has to be transformed into the model. Learn how to achieve that by creating upcast converters.
+
+* **{@link framework/guides/deep-dive/conversion/helpers/intro Conversion helpers}**
+
+ There are plenty of ways to transform data between model and view. To help you do this as efficiently as possible we provided many functions speeding up this process. This chapter will help you choose the right helper for the job.
diff --git a/packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion/upcast.md b/packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion/upcast.md
new file mode 100644
index 00000000000..f478c5bc840
--- /dev/null
+++ b/packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion/upcast.md
@@ -0,0 +1,209 @@
+---
+category: framework-deep-dive-conversion
+menu-title: View to model (upcast)
+order: 30
+since: 33.0.0
+---
+
+# View to model (upcast)
+
+## Introduction
+
+The process of converting the **view** to the **model** is called an **upcast**.
+
+{@img assets/img/upcast-basic.svg 214 Basic upcast conversion diagram.}
+
+The upcast process conversion happens every time any data is being loaded into the editor.
+
+Incoming data becomes the view which is then converted into the model via registered converters.
+
+{@snippet framework/mini-inspector}
+
+## Registering a converter
+
+In order to tell the engine how to convert a specific view element into a model element, you need to register an **upcast converter** by using the `editor.conversion.for( 'upcast' )` method:
+
+```js
+editor.conversion
+ .for( 'upcast' )
+ .elementToElement( {
+ view: 'p',
+ model: 'paragraph'
+ } );
+```
+
+The above converter will handle the conversion of every `
` view element to a `` model element.
+
+{@snippet framework/mini-inspector-paragraph}
+
+
+ This is just an example. Paragraph support is provided by the {@link api/paragraph paragraph plugin} so you don't have to write your own `
` element to `` element conversion.
+
+
+
+ You just learned about the `elementToElement()` **upcast** conversion helper method! More helpers are documented in the following chapters.
+
+
+## Upcast pipeline
+
+Contrary to the downcast, the upcast process happens only in the data pipeline and is called **data upcast.**
+
+The editing view may be changed only via changing the model first, hence editing pipeline needs only the downcast process.
+
+{@img assets/img/upcast-pipeline.svg 612 Upcast conversion pipeline diagram.}
+
+The previous code example registers a converter for both pipelines at once. It means that `` model element will be converted to a `
` view element in both **data view** and **editing view**.
+
+## Converting to text attribute
+
+View elements representing inline text formatting (such as `` or ``) need to be converted to an attribute on a model text node.
+
+To register such a converter, use `elementToAttribute()`:
+
+```js
+editor.conversion
+ .for( 'upcast' )
+ .elementToAttribute( {
+ view: 'strong',
+ model: 'bold'
+ } );
+```
+
+Text wrapped with the `` tag will be converted to a model text node with a `bold` attribute applied to it.
+
+{@snippet framework/mini-inspector-bold}
+
+
+ This is just an example. Bold support is provided by the {@link features/basic-styles basic styles} plugin so you don't have to write your own strong element to bold attribute conversion.
+
+
+If you need to “copy” an attribute from a view element to a model element, use `attributeToAttribute()`.
+
+Keep in mind that the model element must have its own converter registered, otherwise there is nothing the attribute can be copied to.
+
+```js
+editor.conversion
+ .for( 'upcast' )
+ .attributeToAttribute( {
+ view: 'src',
+ model: 'source'
+ } );
+```
+
+Assuming that in the editor some other feature did register the `` to `` model element upcast converter, you can extend this feature to allow `src` attribute. This attribute will be converted into `source` attribute on a model element.
+
+{@snippet framework/mini-inspector-upcast-attribute}
+
+
+ This is just an example. Image elements and source attributes support is provided by the {@link features/images-overview images feature} so you don't have to write your own `` to `` element conversion.
+
+
+## Converting to element
+
+Converting a view element to a corresponding model element can be achieved by registering the converter by using the `elementToElement()` method:
+
+```js
+editor.conversion
+ .for( 'upcast' )
+ .elementToElement( {
+ view: {
+ name: 'div',
+ classes: [ 'example' ]
+ },
+ model: 'example'
+ } );
+```
+
+The above converter will handle the conversion of every `
` view element into an `` model element.
+
+{@snippet framework/mini-inspector-upcast-element}
+
+
+ Using your own custom model element requires defining it in the schema.
+
+
+## Converting structures
+
+As you may learned in the {@link framework/guides/deep-dive/conversion/downcast previous chapter}, a single model element can be downcasted into a structure of multiple view elements.
+
+The opposite process will have to detect that structure (e.g. the main element) and convert that into a simple model element.
+
+There is no `structureToElement()` helper available for the upcast conversion. In order to register upcast converter for the entire structure and create just one model element, you must use the event based API like in the following example:
+
+```js
+editor.conversion.for( 'upcast' ).add( dispatcher => {
+ // Look for every view div element.
+ dispatcher.on( 'element:div', ( evt, data, conversionApi ) => {
+ // Get all the necessary items from the conversion API object.
+ const {
+ consumable,
+ writer,
+ safeInsert,
+ convertChildren,
+ updateConversionResult
+ } = conversionApi;
+
+ // Get view item from data object.
+ const { viewItem } = data;
+
+ // Define elements consumables.
+ const wrapper = { name: true, classes: 'wrapper' };
+ const innerWrapper = { name: true, classes: 'inner-wrapper' };
+
+ // Tests if the view element can be consumed.
+ if ( !consumable.test( viewItem, wrapper ) ) {
+ return;
+ }
+
+ // Check if there is only one child.
+ if ( viewItem.childCount !== 1 ) {
+ return;
+ }
+
+ // Get the first child element.
+ const firstChildItem = viewItem.getChild( 0 );
+
+ // Check if the first element is a div.
+ if ( !firstChildItem.is( 'element', 'div' ) ) {
+ return;
+ }
+
+ // Tests if the first child element can be consumed.
+ if ( !consumable.test( firstChildItem, innerWrapper ) ) {
+ return;
+ }
+
+ // Create model element.
+ const modelElement = writer.createElement( 'myElement' );
+
+ // Insert element on a current cursor location.
+ if ( !safeInsert( modelElement, data.modelCursor ) ) {
+ return;
+ }
+
+ // Consume the main outer wrapper element.
+ consumable.consume( viewItem, wrapper );
+ // Consume the inner wrapper element.
+ consumable.consume( firstChildItem, innerWrapper );
+
+ // Handle children conversion inside inner wrapper element.
+ convertChildren( firstChildItem, modelElement );
+
+ // Necessary function call to help setting model range and cursor
+ // for some specific cases when elements being split.
+ updateConversionResult( modelElement, data );
+ } );
+} );
+```
+
+The above converter will detect all `
...
` structures (by scanning for the outer `
` and turn those into a single `` model element).
+
+{@snippet framework/mini-inspector-structure}
+
+
+ Using your own custom model element requires defining it in the schema.
+
+
+## Read next
+
+{@link framework/guides/deep-dive/conversion/helpers/intro Conversion helpers}
diff --git a/packages/ckeditor5-engine/docs/framework/guides/deep-dive/custom-element-conversion.md b/packages/ckeditor5-engine/docs/framework/guides/deep-dive/custom-element-conversion.md
deleted file mode 100644
index 940eecdd503..00000000000
--- a/packages/ckeditor5-engine/docs/framework/guides/deep-dive/custom-element-conversion.md
+++ /dev/null
@@ -1,397 +0,0 @@
----
-category: framework-deep-dive-conversion
-menu-title: Custom element conversion
-order: 40
----
-
-{@snippet framework/build-custom-element-converter-source}
-
-There are three levels on which elements can be converted:
-
-* By using the two-way converter: {@link module:engine/conversion/conversion~Conversion#elementToElement `conversion.elementToElement()`}.
- This is a fully declarative API. It is the least powerful option but it is the easiest one to use.
-* By using one-way converters: for example {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `conversion.for( 'downcast' ).elementToElement()`} and {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToElement `conversion.for( 'upcast' ).elementToElement()`}.
- In this case, you need to define at least two converters (for upcast and downcast), but the "how" part becomes a callback, and hence you gain more control over it.
-* Finally, by using event-based converters.
- In this case, you need to listen to events fired by {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} and {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}. This method has full access to every bit of logic that a converter needs to implement and therefore it can be used to write the most complex conversion methods.
-
-This guide explains how to migrate from a simple two-way converter to an event-based converter as the requirements regarding the feature get more complex.
-
-## Introduction
-
-Let us assume that the content in your application contains "info boxes". As for now, it was only required to wrap a part of the content in a `
` element that would look like this in the data and editing views:
-
-```html
-
-
-
This is important!
-
-```
-
-The data is represented in the model as the following structure:
-
-```html
-
-
- <$text>This is $text><$text bold="true">important!$text>
-
-```
-
-This can be easily done with the below schema and converters in a simple `InfoBox` plugin:
-
-```js
-class InfoBox {
- constructor( editor ) {
- // 1. Define infoBox as an object that can contain any other content.
- editor.model.schema.register( 'infoBox', {
- allowWhere: '$block',
- allowContentOf: '$root',
- isObject: true
- } );
-
- // 2. The conversion is straightforward:
- editor.conversion.elementToElement( {
- model: 'infoBox',
- view: {
- name: 'div',
- classes: 'info-box'
- }
- } );
- }
-}
-```
-
-## Migrating to an event-based converter
-
-Let us now assume that the requirements have changed and there is a need for adding an additional element in the data and editing views that will display the type of the info box (warning, error, info, etc.).
-
-The new info box structure:
-
-```html
-
-
Warning
-
-
-
This is important!
-
-
-```
-
-The "Warning" part should not be editable. It defines the type of the info box so you can store this bit of information as an attribute of the `` element:
-
-```html
-
-
- <$text>This is $text><$text bold="true">important!$text>
-
-```
-
-Let us see how to update the basic implementation to cover these requirements.
-
-### Demo
-
-Below is a demo of the editor with a sample info box.
-
-{@snippet framework/extending-content-custom-element-converter}
-
-### Schema
-
-The type of the box is defined by an additional class on the main `
` but it is also represented as text in `
`. All the info box content must now be placed inside `
` instead of the main wrapper.
-
-For the above requirements you can see that the model structure of the `infoBox` does not need to change much. You can still use a single element in the model. The only addition to the model is an attribute that will store information about the info box type:
-
-```js
-editor.model.schema.register( 'infoBox', {
- allowWhere: '$block',
- allowContentOf: '$root',
- isObject: true,
- allowAttributes: [ 'infoBoxType' ] // Added.
-} );
-```
-
-### Event-based upcast converter
-
-The conversion of the type of the box itself can be achieved by using {@link module:engine/conversion/conversion~Conversion#attributeToAttribute `attributeToAttribute()`} (`info-box-*` CSS classes to the `infoBoxType` model attribute). However, two more changes were made to the data format that you need to handle:
-
-* There is a new `
` element that should be ignored during the upcast conversion as it duplicates the information conveyed by the main element's CSS class.
-* The content of the info box is now located inside another element. Previously it was located directly in the main wrapper.
-
-Neither two-way nor one-way converters can handle such conversion. Therefore, you need to use an event-based converter with the following behavior:
-
-1. Create a model `` element with the `infoBoxType` attribute.
-1. Skip the conversion of `
` as the information about type can be obtained from the wrapper's CSS classes.
-1. Convert the children of `
` and insert them directly into ``.
-
-```js
-function upcastConverter( event, data, conversionApi ) {
- const viewInfoBox = data.viewItem;
-
- // Check whether the view element is an info box
.
- // Otherwise, it should be handled by another converter.
- if ( !viewInfoBox.hasClass( 'info-box' ) ) {
- return;
- }
-
- // Create the model structure.
- const modelElement = conversionApi.writer.createElement( 'infoBox', {
- infoBoxType: getTypeFromViewElement( viewInfoBox )
- } );
-
- // Try to safely insert the element into the model structure.
- // If `safeInsert()` returns `false`, the element cannot be safely inserted
- // into the content and the conversion process must stop.
- // This may happen if the data that you are converting has an incorrect structure
- // (e.g. it was copied from an external website).
- if ( !conversionApi.safeInsert( modelElement, data.modelCursor ) ) {
- return;
- }
-
- // Mark the info box
as handled by this converter.
- conversionApi.consumable.consume( viewInfoBox, { name: true } );
-
- // Let us assume that the HTML structure is always the same.
- // Note: For full bulletproofing this converter, you should also check
- // whether these elements are the right ones.
- const viewInfoBoxTitle = viewInfoBox.getChild( 0 );
- const viewInfoBoxContent = viewInfoBox.getChild( 1 );
-
- // Mark info box inner elements (title and content
s) as handled by this converter.
- conversionApi.consumable.consume( viewInfoBoxTitle, { name: true } );
- conversionApi.consumable.consume( viewInfoBoxContent, { name: true } );
-
- // Let the editor handle the children of
.
- conversionApi.convertChildren( viewInfoBoxContent, modelElement );
-
- // Finally, update the conversion's modelRange and modelCursor.
- conversionApi.updateConversionResult( modelElement, data );
-}
-
-// A helper function to read the type from the view classes.
-function getTypeFromViewElement( viewElement ) {
- if ( viewElement.hasClass( 'info-box-info' ) ) {
- return 'Info';
- }
-
- if ( viewElement.hasClass( 'info-box-warning' ) ) {
- return 'Warning';
- }
-
- return 'None';
-}
-```
-
-This upcast converter callback can now be plugged by adding a listener to the {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#element `UpcastDispatcher#element` event}. You will listen to `element:div` to ensure that the callback is called only for `
` elements.
-
-```js
-editor.conversion.for( 'upcast' )
- .add( dispatcher => dispatcher.on( 'element:div', upcastConverter ) );
-```
-
-### Event-based downcast converter
-
-The missing bits are the downcast converters for the editing and data pipelines.
-
-You will want to use the widget system to make the info box behave like an "object". Another aspect that you need to take care of is the fact that the view structure has more elements than the model structure. In this case, you could actually use one-way converters. However, this tutorial will showcase how an event-based converter would look.
-
-
- See the {@link framework/guides/tutorials/implementing-a-block-widget Implementing a block widget guide} to learn about the widget system.
-
-
-The remaining downcast converters:
-
-```js
-function editingDowncastConverter( event, data, conversionApi ) {
- let { infoBox, infoBoxContent, infoBoxTitle } = createViewElements( data, conversionApi );
-
- // Decorate view items as a widget and widget editable area.
- infoBox = toWidget( infoBox, conversionApi.writer, { label: 'info box widget' } );
- infoBoxContent = toWidgetEditable( infoBoxContent, conversionApi.writer );
-
- insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBoxContent );
-}
-
-function dataDowncastConverter( event, data, conversionApi ) {
- const { infoBox, infoBoxContent, infoBoxTitle } = createViewElements( data, conversionApi );
-
- insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBoxContent );
-}
-
-function createViewElements( data, conversionApi ) {
- const type = data.item.getAttribute( 'infoBoxType' );
-
- const infoBox = conversionApi.writer.createContainerElement( 'div', {
- class: `info-box info-box-${ type.toLowerCase() }`
- } );
- const infoBoxContent = conversionApi.writer.createEditableElement( 'div', {
- class: 'info-box-content'
- } );
-
- const infoBoxTitle = conversionApi.writer.createUIElement( 'div',
- { class: 'info-box-title' },
- function( domDocument ) {
- const domElement = this.toDomElement( domDocument );
-
- domElement.innerText = type;
-
- return domElement;
- } );
-
- return { infoBox, infoBoxContent, infoBoxTitle };
-}
-
-function insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBoxContent ) {
- conversionApi.consumable.consume( data.item, 'insert' );
-
- conversionApi.writer.insert(
- conversionApi.writer.createPositionAt( infoBox, 0 ),
- infoBoxTitle
- );
- conversionApi.writer.insert(
- conversionApi.writer.createPositionAt( infoBox, 1 ),
- infoBoxContent
- );
-
- // The mapping between the model and its view representation.
- conversionApi.mapper.bindElements( data.item, infoBox );
-
- conversionApi.writer.insert(
- conversionApi.mapper.toViewPosition( data.range.start ),
- infoBox
- );
-}
-```
-
-These two converters need to be plugged as listeners into the {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#insert `DowncastDispatcher#insert` event}:
-
-```js
-editor.conversion.for( 'editingDowncast' )
- .add( dispatcher => dispatcher.on( 'insert:infoBox', editingDowncastConverter ) );
-editor.conversion.for( 'dataDowncast' )
- .add( dispatcher => dispatcher.on( 'insert:infoBox', dataDowncastConverter ) );
-```
-
-Due to the fact that the info box's view structure is more complex than its model structure, you need to take care of one additional aspect to make the converters work — position mapping.
-
-### The model-to-view position mapping
-
-The downcast converters shown in the previous section will not work correctly yet. This is what the given model would look like, after being downcasted:
-
-```
- ->
- ->
- Foobar -> Foobar
-
->
-
Info
-
- ->
-```
-
-This is not a correct view structure. The content of the model's `` element ended up directly inside the outer `
`. The ``'s content should be inside the `
`.
-
-You defined downcast conversion for `` itself, but you need to specify where its content should land in its view structure. By default, it is converted as direct children of `
` (as shown in the above snippet) but it should go into `
`. To achieve this, you need to register a callback for the {@link module:engine/conversion/mapper~Mapper#event:modelToViewPosition `Mapper#modelToViewPosition`} event, so the positions inside the model `` element would map to the positions inside the `
` view element.
-
-```
- ->
-
Info
-
- ->
- Foobar -> Foobar
-
->
-
- ->
-```
-
-Such a mapping can be achieved by registering this callback to the {@link module:engine/conversion/mapper~Mapper#event:modelToViewPosition `Mapper#modelToViewPosition`} event:
-
-```js
-function createModelToViewPositionMapper( view ) {
- return ( evt, data ) => {
- const modelPosition = data.modelPosition;
- const parent = modelPosition.parent;
-
- // Only the mapping of positions that are directly in
- // the model element should be modified.
- if ( !parent.is( 'element', 'infoBox' ) ) {
- return;
- }
-
- // Get the mapped view element
in it.
- const viewContentElement = findContentViewElement( view, viewElement );
-
- // Translate the model position offset to the view position offset.
- data.viewPosition = data.mapper.findPositionIn( viewContentElement, modelPosition.offset );
- };
-}
-
-// Returns the
nested in the info box view structure.
-function findContentViewElement( editingView, viewElement ) {
- for ( const value of editingView.createRangeIn( viewElement ) ) {
- if ( value.item.is( 'element', 'div' ) && value.item.hasClass( 'info-box-content' ) ) {
- return value.item;
- }
- }
-}
-```
-
-It needs to be plugged into the {@link module:engine/conversion/mapper~Mapper#event:modelToViewPosition `Mapper#modelToViewPosition`} event for both downcast pipelines:
-
-```js
-editor.editing.mapper.on( 'modelToViewPosition', createModelToViewPositionMapper( editor.editing.view ) );
-editor.data.mapper.on( 'modelToViewPosition', createModelToViewPositionMapper( editor.editing.view ) );
-```
-
-
- **Note**: You do not need the reverse position mapping ({@link module:engine/conversion/mapper~Mapper#event:viewToModelPosition from the view to the model}) because the default view-to-model position mapping looks for the {@link module:engine/conversion/mapper~Mapper#findMappedViewAncestor mapped view ancestor} and maps the offset in respect to the model element.
-
-
-### Updated plugin code
-
-The updated `InfoBox` plugin that glues the event-based converters together:
-
-```js
-class InfoBox {
- constructor( editor ) {
- // Schema definition.
- editor.model.schema.register( 'infoBox', {
- allowWhere: '$block',
- allowContentOf: '$root',
- isObject: true,
- allowAttributes: [ 'infoBoxType' ]
- } );
-
- // Upcast converter.
- editor.conversion.for( 'upcast' )
- .add( dispatcher => dispatcher.on( 'element:div', upcastConverter ) );
-
- // The downcast conversion must be split as you need a widget in the editing pipeline.
- editor.conversion.for( 'editingDowncast' )
- .add( dispatcher => dispatcher.on( 'insert:infoBox', editingDowncastConverter ) );
- editor.conversion.for( 'dataDowncast' )
- .add( dispatcher => dispatcher.on( 'insert:infoBox', dataDowncastConverter ) );
-
- // The model-to-view position mapper is needed since the model content needs to end up in the inner
- //
.
- editor.editing.mapper.on( 'modelToViewPosition', createModelToViewPositionMapper( editor.editing.view ) );
- editor.data.mapper.on( 'modelToViewPosition', createModelToViewPositionMapper( editor.editing.view ) );
- }
-}
-
-function upcastConverter() {
- // ...
-}
-
-function editingDowncastConverter() {
- // ...
-}
-
-function dataDowncastConverter() {
- // ...
-}
-
-function createModelToViewPositionMapper() {
- // ...
-}
-```
diff --git a/packages/ckeditor5-engine/docs/framework/guides/deep-dive/element-reconversion.md b/packages/ckeditor5-engine/docs/framework/guides/deep-dive/element-reconversion.md
deleted file mode 100644
index b278be05bbc..00000000000
--- a/packages/ckeditor5-engine/docs/framework/guides/deep-dive/element-reconversion.md
+++ /dev/null
@@ -1,626 +0,0 @@
----
-category: framework-deep-dive-conversion
-order: 50
-since: 24.0.0
----
-
-# Element reconversion
-
-{@snippet framework/build-element-reconversion-source}
-
-
- Element reconversion is currently in beta version. The API will be extended to support more cases and will be changing with time.
-
-
-This guide introduces the concept of the _reconversion of model elements_ during the downcast (model-to-view) {@link framework/guides/architecture/editing-engine#conversion conversion}.
-
-Reconversion allows simplifying downcast converters for model elements by merging multiple separate converters into a single converter that reacts to more types of model changes.
-
-## Prerequisites
-
-To better understand the concepts used in this guide, it is recommended to familiarize yourself with other conversion guides, too:
-
-* {@link framework/guides/tutorials/implementing-a-block-widget Implementing a block widget}
-* {@link framework/guides/deep-dive/custom-element-conversion Custom element conversion}
-
-## Atomic converters vs element reconversion
-
-In order to convert a model element to its view representation, you often write the following converters:
-
-* An `elementToElement()` converter. This converter reacts to the insertion of a model element specified in the `model` field.
-* If the model element has attributes and these attributes may change with time, you need to add the `attributeToAttribute()` converters for each attribute. These converters react to changes in the model element attributes and update the view accordingly.
-
-This granular approach to conversion is used by many editor features as it ensures extensibility of the base features and provides a separation of concerns. For example, the {@link features/images-overview base image feature} provides conversion for a simple `` model element, while the {@link features/images-resizing image resize feature} adds support for the `width` and `height` attributes, the {@link features/images-captions image caption feature} for the `` HTML element, and so on.
-
-Apart from the extensibility aspect, the above approach ensures that a change of a model attribute or structure requires minimal changes in the view.
-
-However, in some cases where granularity is not necessary this approach may be an overkill. Consider a case in which you need to create a multi-layer view structure for one model element, or a case in which the view structure depends on a value of a model attribute. In such cases, writing a separate converter for a model element and separate converters for each attribute becomes cumbersome.
-
-Thankfully, element reconversion allows merging these converters into a single converter that reacts to multiple types of model changes (element insertion, its attribute changes and changes in its direct children). This approach can be considered more "functional" as the `view` callback executed on any of these changes should produce the entire view structure (down to a certain level) without taking into account what state changes have just happened.
-
-An additional perk of using element reconversion is that the parts of the model tree that have not been changed, like paragraphs and text inside your feature element, will not be reconverted. In other words, their view elements are kept in memory and re-used inside the changed parent.
-
-To sum up, element reconversion comes in handy for cases where you need to convert a relatively simple model to a complex view structure. And also, writing a single functional converter is easier to grasp in your project.
-
-## Enabling element reconversion
-
-Element reconversion is enabled by setting the reconversion trigger configuration (`triggerBy`) for the {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`} downcast helper.
-
-The model element can be reconverted when:
-
-* one or many attributes change (using `triggerBy.attributes`) or
-* a child is inserted or removed (using `triggerBy.children`)
-
-
- Note that when using the `children` configuration option, the current implementation assumes that the downcast converter will either:
- * handle an element and its children conversion at once,
- * have a "flat" structure.
-
-
-A simple example of an element reconversion configuration is demonstrated below:
-
-```js
-editor.conversion.for( 'downcast' ).elementToElement( {
- model: 'myElement',
- view: ( modelElement, { writer } ) => {
- return writer.createContainerElement( 'div', {
- 'data-owner-id': modelElement.getAttribute( 'ownerId' ),
- class: `my-element my-element-${ modelElement.getAttribute( 'type' ) }`
- } );
- },
- triggerBy: {
- attributes: [ 'ownerId', 'type' ]
- }
-} )
-```
-
-In this example:
-
-* The downcast converter for `myElement` creates a `
` with a `data-owner-id` attribute and a set of CSS classes.
-* The value of `data-owner-id` is set from the `ownerId` model element's attribute.
-* The second CSS class is constructed off the `type` model element's attribute.
-* The `triggerBy.attributes` configuration defines that the element will be converted upon changes of the `onwerId` or `type` attributes.
-
-Before CKEditor version `23.1.0` you would have to define a set of atomic converters for the element and for each attribute:
-
-```js
-editor.conversion.for( 'downcast' )
- .elementToElement( {
- model: 'myElement',
- view: 'div'
- } )
- .attributeToAttribute( {
- model: 'owner-id',
- view: 'data-owner-id'
- } )
- .attributeToAttribute( {
- model: 'type',
- view: modelAttributeValue => ( {
- key: 'class',
- value: `my-element my-element-${ modelAttributeValue }`
- } )
- } );
-```
-
-## Example implementation
-
-In this example implementation you will implement a "card" box which is displayed beside the main article content. The card will contain a text-only title, one to four content sections and an optional URL. Additionally, the user can choose the type of the card.
-
-### Demo
-
-{@snippet framework/element-reconversion-demo}
-
-### Model and view structure
-
-A simplified model markup for the side card looks as follows:
-
-```html
-
- The title
-
- The content
-
-
-```
-
-This will be converted to the below view structure:
-
-```html
-
-```
-
-In the above example you can observe that the `'cardURL'` model attribute is converted as a view element inside the main view container while the type attribute is translated to a CSS class. Additionally, the UI controls are injected to the view after all other child views of the main container. Describing it using atomic converters would introduce a convoluted complexity.
-
-### Schema
-
-The side card model structure is represented in the editor's {@link framework/guides/deep-dive/schema schema} as follows:
-
-```js
-// The main element with attributes for type and URL:
-schema.register( 'sideCard', {
- allowWhere: '$block',
- isObject: true,
- allowAttributes: [ 'cardType', 'cardURL' ]
-} );
-// Disallow side card nesting.
-schema.addChildCheck( ( context, childDefinition ) => {
- if ( [ ...context.getNames() ].includes( 'sideCard' ) && childDefinition.name === 'sideCard' ) {
- return false;
- }
-} );
-
-// A text-only title.
-schema.register( 'sideCardTitle', {
- isLimit: true,
- allowIn: 'sideCard'
-} );
-// Allow text in title...
-schema.extend( '$text', { allowIn: 'sideCardTitle' } );
-// ...but disallow any text attribute inside.
-schema.addAttributeCheck( context => {
- if ( context.endsWith( 'sideCardTitle $text' ) ) {
- return false;
- }
-} );
-
-// A content block which can have any content allowed in $root.
-schema.register( 'sideCardSection', {
- isLimit: true,
- allowIn: 'sideCard',
- allowContentOf: '$root'
-} );
-```
-
-### Reconversion definition
-
-To enable element reconversion, define for which attribute and children modifications the main element will be converted:
-
-```js
-conversion.for( 'editingDowncast' ).elementToElement( {
- model: 'sideCard',
- view: downcastSideCard( editor, { asWidget: true } ),
- triggerBy: {
- attributes: [ 'cardType', 'cardURL' ],
- children: [ 'sideCardSection' ]
- }
-} );
-```
-
-The above definition will use the `downcastSideCard()` function to re-create the view when:
-
-* The `sideCard` element is inserted into the model.
-* One of `cardType` or `cardURL` has changed.
-* A child `sideCardSection` is added or removed from the parent `sideCard`.
-
-### Downcast converter details
-
-The function that creates a complete view for the model element:
-
-```js
-const downcastSideCard = ( editor, { asWidget } ) => {
- return ( modelElement, { writer, consumable, mapper } ) => {
- const type = modelElement.getAttribute( 'cardType' ) || 'default';
-
- // The main view element for the side card.
- const sideCardView = writer.createContainerElement( 'aside', {
- class: `side-card side-card-${ type }`
- } );
-
- // Create inner views from the side card children.
- for ( const child of modelElement.getChildren() ) {
- const childView = writer.createEditableElement( 'div' );
-
- // Child is either a "title" or "section".
- if ( child.is( 'element', 'sideCardTitle' ) ) {
- writer.addClass( 'side-card-title', childView );
- } else {
- writer.addClass( 'side-card-section', childView );
- }
-
- // It is important to consume and bind converted elements.
- consumable.consume( child, 'insert' );
- mapper.bindElements( child, childView );
-
- // Make it an editable part of the widget.
- if ( asWidget ) {
- toWidgetEditable( childView, writer );
- }
-
- writer.insert( writer.createPositionAt( sideCardView, 'end' ), childView );
- }
-
- const urlAttribute = modelElement.getAttribute( 'cardURL' );
-
- // Do not render an empty URL field
- if ( urlAttribute ) {
- const urlBox = writer.createRawElement( 'div', {
- class: 'side-card-url'
- }, function( domElement ) {
- domElement.innerText = `URL: "${ urlAttribute }"`;
- } );
-
- writer.insert( writer.createPositionAt( sideCardView, 'end' ), urlBox );
- }
-
- // Inner element used to render a simple UI that allows to change the side card's attributes.
- // It will only be needed in the editing view inside the widgetized element.
- // The data output should not contain this section.
- if ( asWidget ) {
- const actionsView = writer.createRawElement( 'div', {
- class: 'side-card-actions',
- contenteditable: 'false', // Prevents editing of the element.
- 'data-cke-ignore-events': 'true' // Allows using custom UI elements inside the editing view.
- }, createActionsView( editor, modelElement ) ); // See the full code for details.
-
- writer.insert( writer.createPositionAt( sideCardView, 'end' ), actionsView );
-
- toWidget( sideCardView, writer, { widgetLabel: 'Side card', hasSelectionHandle: true } );
- }
-
- return sideCardView;
- };
-};
-```
-
-By using `mapper.bindElements( child, childView )` for `` and `` you define which view elements correspond to which model elements. This allows the editor's conversion to re-use the existing view elements for the title and section children, so they will not be re-converted without a need.
-
-### Upcast conversion
-
-The upcast conversion uses standard element-to-element converters for the box and title, and a custom converter for the side card to extract metadata from the data.
-
-```js
-editor.conversion.for( 'upcast' )
- .elementToElement( {
- view: { name: 'aside', classes: [ 'side-card' ] },
- model: upcastCard // Details in the full source code.
- } )
- .elementToElement( {
- view: { name: 'div', classes: [ 'side-card-title' ] },
- model: 'sideCardTitle'
- } )
- .elementToElement( {
- view: { name: 'div', classes: [ 'side-card-section' ] },
- model: 'sideCardSection'
- } );
-```
-
-You can see the details of the upcast converter function (`upcastCard()`) in the full source code at the end of this guide.
-
-### Full source code
-
-```js
-import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
-import Command from '@ckeditor/ckeditor5-core/src/command';
-import { toWidget, toWidgetEditable, findOptimalInsertionRange } from '@ckeditor/ckeditor5-widget/src/utils';
-import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
-
-/**
- * Helper for extracting the side card type from a view element based on its CSS class.
- */
-const getTypeFromViewElement = viewElement => {
- for ( const type of [ 'default', 'alternate' ] ) {
- if ( viewElement.hasClass( `side-card-${ type }` ) ) {
- return type;
- }
- }
-
- return 'default';
-};
-
-/**
- * Single upcast converter to the element with all its attributes.
- */
-const upcastCard = ( viewElement, { writer } ) => {
- const sideCard = writer.createElement( 'sideCard' );
-
- const type = getTypeFromViewElement( viewElement );
- writer.setAttribute( 'cardType', type, sideCard );
-
- const urlWrapper = [ ...viewElement.getChildren() ].find( child => {
- return child.is( 'element', 'div' ) && child.hasClass( 'side-card-url' );
- } );
-
- if ( urlWrapper ) {
- writer.setAttribute( 'cardURL', urlWrapper.getChild( 0 ).data, sideCard );
- }
-
- return sideCard;
-};
-
-/**
- * Helper for creating a DOM button with an editor callback.
- */
-const addActionButton = ( text, callback, domElement, editor ) => {
- const domDocument = domElement.ownerDocument;
-
- const button = createElement( domDocument, 'button', {}, [ text ] );
-
- button.addEventListener( 'click', () => {
- editor.model.change( callback );
- } );
-
- domElement.appendChild( button );
-
- return button;
-};
-
-/**
- * Helper function that creates the card editing UI inside the card.
- */
-const createActionsView = ( editor, modelElement ) => function( domElement ) {
- //
- // Set the URL action button.
- //
- addActionButton( 'Set URL', writer => {
- // eslint-disable-next-line no-alert
- const newURL = prompt( 'Set URL', modelElement.getAttribute( 'cardURL' ) || '' );
-
- writer.setAttribute( 'cardURL', newURL, modelElement );
- }, domElement, editor );
-
- const currentType = modelElement.getAttribute( 'cardType' );
- const newType = currentType === 'default' ? 'alternate' : 'default';
-
- //
- // Change the card action button.
- //
- addActionButton( 'Change type', writer => {
- writer.setAttribute( 'cardType', newType, modelElement );
- }, domElement, editor );
-
- const childCount = modelElement.childCount;
-
- //
- // Add the content section to the card action button.
- //
- const addButton = addActionButton( 'Add section', writer => {
- writer.insertElement( 'sideCardSection', modelElement, 'end' );
- }, domElement, editor );
-
- // Disable the button so only 1-3 content boxes are in the card (there will always be a title).
- if ( childCount > 4 ) {
- addButton.setAttribute( 'disabled', 'disabled' );
- }
-
- //
- // Remove the content section from the card action button.
- //
- const removeButton = addActionButton( 'Remove section', writer => {
- writer.remove( modelElement.getChild( childCount - 1 ) );
- }, domElement, editor );
-
- // Disable the button so only 1-3 content boxes are in the card (there will always be a title).
- if ( childCount < 3 ) {
- removeButton.setAttribute( 'disabled', 'disabled' );
- }
-};
-
-/**
- * The downcast converter for the element.
- *
- * It returns the full view structure based on the current state of the model element.
- */
-const downcastSideCard = ( editor, { asWidget } ) => {
- return ( modelElement, { writer, consumable, mapper } ) => {
- const type = modelElement.getAttribute( 'cardType' ) || 'default';
-
- // The main view element for the side card.
- const sideCardView = writer.createContainerElement( 'aside', {
- class: `side-card side-card-${ type }`
- } );
-
- // Create inner views from the side card children.
- for ( const child of modelElement.getChildren() ) {
- const childView = writer.createEditableElement( 'div' );
-
- // Child is either a "title" or "section".
- if ( child.is( 'element', 'sideCardTitle' ) ) {
- writer.addClass( 'side-card-title', childView );
- } else {
- writer.addClass( 'side-card-section', childView );
- }
-
- // It is important to consume and bind converted elements.
- consumable.consume( child, 'insert' );
- mapper.bindElements( child, childView );
-
- // Make it an editable part of the widget.
- if ( asWidget ) {
- toWidgetEditable( childView, writer );
- }
-
- writer.insert( writer.createPositionAt( sideCardView, 'end' ), childView );
- }
-
- const urlAttribute = modelElement.getAttribute( 'cardURL' );
-
- // Do not render an empty URL field.
- if ( urlAttribute ) {
- const urlBox = writer.createRawElement( 'div', {
- class: 'side-card-url'
- }, function( domElement ) {
- domElement.innerText = `URL: "${ urlAttribute }"`;
- } );
-
- writer.insert( writer.createPositionAt( sideCardView, 'end' ), urlBox );
- }
-
- // Inner element used to render a simple UI that allows to change the side card's attributes.
- // It will only be needed in the editing view inside the widgetized element.
- // The data output should not contain this section.
- if ( asWidget ) {
- const actionsView = writer.createRawElement( 'div', {
- class: 'side-card-actions',
- contenteditable: 'false', // Prevents editing of the element.
- 'data-cke-ignore-events': 'true' // Allows using custom UI elements inside the editing view.
- }, createActionsView( editor, modelElement ) ); // See the full code for details.
-
- writer.insert( writer.createPositionAt( sideCardView, 'end' ), actionsView );
-
- toWidget( sideCardView, writer, { widgetLabel: 'Side card' } );
- }
-
- return sideCardView;
- };
-};
-
-class InsertCardCommand extends Command {
- /**
- * Refresh used schema definition to check if a side card can be inserted in the current selection.
- */
- refresh() {
- const model = this.editor.model;
- const range = findOptimalInsertionRange( model.document.selection, model );
-
- this.isEnabled = model.schema.checkChild( validParent, 'sideCard' );
- }
-
- /**
- * Creates a full side card element with all required children and attributes.
- */
- execute() {
- const model = this.editor.model;
- const selection = model.document.selection;
-
- const insertionRange = findOptimalInsertionRange( selection, model );
-
- model.change( writer => {
- const sideCard = writer.createElement( 'sideCard', { cardType: 'default' } );
- const title = writer.createElement( 'sideCardTitle' );
- const section = writer.createElement( 'sideCardSection' );
- const paragraph = writer.createElement( 'paragraph' );
-
- writer.insert( title, sideCard, 0 );
- writer.insert( section, sideCard, 1 );
- writer.insert( paragraph, section, 0 );
-
- model.insertContent( sideCard, insertionRange );
-
- writer.setSelection( writer.createPositionAt( title, 0 ) );
- } );
- }
-}
-
-class ComplexBox extends Plugin {
- constructor( editor ) {
- super( editor );
-
- this._defineSchema();
- this._defineConversion();
-
- editor.commands.add( 'insertCard', new InsertCardCommand( editor ) );
-
- this._defineUI();
- }
-
- _defineConversion() {
- const editor = this.editor;
- const conversion = editor.conversion;
-
- conversion.for( 'upcast' )
- .elementToElement( {
- view: { name: 'aside', classes: [ 'side-card' ] },
- model: upcastCard
- } )
- .elementToElement( {
- view: { name: 'div', classes: [ 'side-card-title' ] },
- model: 'sideCardTitle'
- } )
- .elementToElement( {
- view: { name: 'div', classes: [ 'side-card-section' ] },
- model: 'sideCardSection'
- } );
-
- // The downcast conversion must be split as you need a widget in the editing pipeline.
- conversion.for( 'editingDowncast' ).elementToElement( {
- model: 'sideCard',
- view: downcastSideCard( editor, { asWidget: true } ),
- triggerBy: {
- attributes: [ 'cardType', 'cardURL' ],
- children: [ 'sideCardSection' ]
- }
- } );
- // The data downcast is always executed from the current model stat, so `triggerBy` will take no effect.
- conversion.for( 'dataDowncast' ).elementToElement( {
- model: 'sideCard',
- view: downcastSideCard( editor, { asWidget: false } )
- } );
- }
-
- _defineSchema() {
- const schema = this.editor.model.schema;
-
- // The main element with attributes for type and URL:
- schema.register( 'sideCard', {
- allowWhere: '$block',
- isObject: true,
- allowAttributes: [ 'cardType', 'cardURL' ]
- } );
- // Disallow side card nesting.
- schema.addChildCheck( ( context, childDefinition ) => {
- if ( [ ...context.getNames() ].includes( 'sideCard' ) && childDefinition.name === 'sideCard' ) {
- return false;
- }
- } );
-
- // A text-only title.
- schema.register( 'sideCardTitle', {
- isLimit: true,
- allowIn: 'sideCard'
- } );
- // Allow text in title...
- schema.extend( '$text', { allowIn: 'sideCardTitle' } );
- // ...but disallow any text attribute inside.
- schema.addAttributeCheck( context => {
- if ( context.endsWith( 'sideCardTitle $text' ) ) {
- return false;
- }
- } );
-
- // A content block which can have any content allowed in $root.
- schema.register( 'sideCardSection', {
- isLimit: true,
- allowIn: 'sideCard',
- allowContentOf: '$root'
- } );
- }
-
- _defineUI() {
- const editor = this.editor;
-
- // Defines a simple text button.
- editor.ui.componentFactory.add( 'complexBox', locale => {
- const button = new ButtonView( locale );
-
- const command = editor.commands.get( 'insertComplexBox' );
-
- button.set( {
- withText: true,
- icon: false,
- label: 'Complex Box'
- } );
-
- button.bind( 'isEnabled' ).to( command );
-
- button.on( 'execute', () => {
- editor.execute( 'insertComplexBox' );
- editor.editing.view.focus();
- } );
-
- return button;
- } );
- }
-}
-```
diff --git a/packages/ckeditor5-engine/src/controller/datacontroller.js b/packages/ckeditor5-engine/src/controller/datacontroller.js
index 29013fdf483..de4a357a1da 100644
--- a/packages/ckeditor5-engine/src/controller/datacontroller.js
+++ b/packages/ckeditor5-engine/src/controller/datacontroller.js
@@ -14,7 +14,7 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
import Mapper from '../conversion/mapper';
import DowncastDispatcher from '../conversion/downcastdispatcher';
-import { insertText } from '../conversion/downcasthelpers';
+import { insertAttributesAndChildren, insertText } from '../conversion/downcasthelpers';
import UpcastDispatcher from '../conversion/upcastdispatcher';
import { convertText, convertToModelFragment } from '../conversion/upcasthelpers';
@@ -81,6 +81,7 @@ export default class DataController {
schema: model.schema
} );
this.downcastDispatcher.on( 'insert:$text', insertText(), { priority: 'lowest' } );
+ this.downcastDispatcher.on( 'insert', insertAttributesAndChildren(), { priority: 'lowest' } );
/**
* Upcast dispatcher used by the {@link #set set method}. Upcast converters should be attached to it.
@@ -243,27 +244,16 @@ export default class DataController {
this.mapper.bindElements( modelElementOrFragment, viewDocumentFragment );
- // Make additional options available during conversion process through `conversionApi`.
- this.downcastDispatcher.conversionApi.options = options;
-
- // We have no view controller and rendering to DOM in DataController so view.change() block is not used here.
- this.downcastDispatcher.convertInsert( modelRange, viewWriter );
-
- // Convert markers.
+ // Prepare list of markers.
// For document fragment, simply take the markers assigned to this document fragment.
// For model root, all markers in that root will be taken.
// For model element, we need to check which markers are intersecting with this element and relatively modify the markers' ranges.
// Collapsed markers at element boundary, although considered as not intersecting with the element, will also be returned.
const markers = modelElementOrFragment.is( 'documentFragment' ) ?
- Array.from( modelElementOrFragment.markers ) :
+ modelElementOrFragment.markers :
_getMarkersRelativeToElement( modelElementOrFragment );
- for ( const [ name, range ] of markers ) {
- this.downcastDispatcher.convertMarkerAdd( name, range, viewWriter );
- }
-
- // Clean `conversionApi`.
- delete this.downcastDispatcher.conversionApi.options;
+ this.downcastDispatcher.convert( modelRange, markers, viewWriter, options );
return viewDocumentFragment;
}
@@ -547,7 +537,7 @@ function _getMarkersRelativeToElement( element ) {
const doc = element.root.document;
if ( !doc ) {
- return [];
+ return new Map();
}
const elementRange = ModelRange._createIn( element );
@@ -581,7 +571,7 @@ function _getMarkersRelativeToElement( element ) {
// reverse DOM order, and intersecting ranges are in something approximating
// reverse DOM order (since reverse DOM order doesn't have a precise meaning
// when working with intersecting ranges).
- return result.sort( ( [ n1, r1 ], [ n2, r2 ] ) => {
+ result.sort( ( [ n1, r1 ], [ n2, r2 ] ) => {
if ( r1.end.compareWith( r2.start ) !== 'after' ) {
// m1.end <= m2.start -- m1 is entirely <= m2
return 1;
@@ -608,4 +598,6 @@ function _getMarkersRelativeToElement( element ) {
}
}
} );
+
+ return new Map( result );
}
diff --git a/packages/ckeditor5-engine/src/controller/editingcontroller.js b/packages/ckeditor5-engine/src/controller/editingcontroller.js
index 32bb537e006..5d7a4436329 100644
--- a/packages/ckeditor5-engine/src/controller/editingcontroller.js
+++ b/packages/ckeditor5-engine/src/controller/editingcontroller.js
@@ -11,10 +11,18 @@ import RootEditableElement from '../view/rooteditableelement';
import View from '../view/view';
import Mapper from '../conversion/mapper';
import DowncastDispatcher from '../conversion/downcastdispatcher';
-import { clearAttributes, convertCollapsedSelection, convertRangeSelection, insertText, remove } from '../conversion/downcasthelpers';
+import {
+ clearAttributes,
+ convertCollapsedSelection,
+ convertRangeSelection,
+ insertAttributesAndChildren,
+ insertText,
+ remove
+} from '../conversion/downcasthelpers';
import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
import mix from '@ckeditor/ckeditor5-utils/src/mix';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
import { convertSelectionChange } from '../conversion/upcasthelpers';
// @if CK_DEBUG_ENGINE // const { dumpTrees, initDocumentDumping } = require( '../dev-utils/utils' );
@@ -101,6 +109,7 @@ export default class EditingController {
// Attach default model converters.
this.downcastDispatcher.on( 'insert:$text', insertText(), { priority: 'lowest' } );
+ this.downcastDispatcher.on( 'insert', insertAttributesAndChildren(), { priority: 'lowest' } );
this.downcastDispatcher.on( 'remove', remove(), { priority: 'low' } );
// Attach default model selection converters.
@@ -144,6 +153,74 @@ export default class EditingController {
this.view.destroy();
this.stopListening();
}
+
+ /**
+ * Calling this method will refresh the marker by triggering the downcast conversion for it.
+ *
+ * Reconverting the marker is useful when you want to change its {@link module:engine/view/element~Element view element}
+ * without changing any marker data. For instance:
+ *
+ * let isCommentActive = false;
+ *
+ * model.conversion.markerToHighlight( {
+ * model: 'comment',
+ * view: data => {
+ * const classes = [ 'comment-marker' ];
+ *
+ * if ( isCommentActive ) {
+ * classes.push( 'comment-marker--active' );
+ * }
+ *
+ * return { classes };
+ * }
+ * } );
+ *
+ * // ...
+ *
+ * // Change the property that indicates if marker is displayed as active or not.
+ * isCommentActive = true;
+ *
+ * // Reconverting will downcast and synchronize the marker with the new isCommentActive state value.
+ * editor.editing.reconvertMarker( 'comment' );
+ *
+ * **Note**: If you want to reconvert a model item, use {@link #reconvertItem} instead.
+ *
+ * @param {String|module:engine/model/markercollection~Marker} markerOrName Name of a marker to update, or a marker instance.
+ */
+ reconvertMarker( markerOrName ) {
+ const markerName = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
+ const currentMarker = this.model.markers.get( markerName );
+
+ if ( !currentMarker ) {
+ /**
+ * The marker with provided name does not exist and cannot be reconverted.
+ *
+ * @error editingcontroller-reconvertmarker-marker-not-exist
+ * @param {String} markerName The name of the reconverted marker.
+ */
+ throw new CKEditorError( 'editingcontroller-reconvertmarker-marker-not-exist', this, { markerName } );
+ }
+
+ this.model.change( () => {
+ this.model.markers._refresh( currentMarker );
+ } );
+ }
+
+ /**
+ * Calling this method will downcast a model item on demand (by requesting a refresh in the {@link module:engine/model/differ~Differ}).
+ *
+ * You can use it if you want the view representation of a specific item updated as a response to external modifications. For instance,
+ * when the view structure depends not only on the associated model data but also on some external state.
+ *
+ * **Note**: If you want to reconvert a model marker, use {@link #reconvertMarker} instead.
+ *
+ * @param {module:engine/model/item~Item} item Item to refresh.
+ */
+ reconvertItem( item ) {
+ this.model.change( () => {
+ this.model.document.differ._refreshItem( item );
+ } );
+ }
}
mix( EditingController, ObservableMixin );
diff --git a/packages/ckeditor5-engine/src/conversion/conversion.js b/packages/ckeditor5-engine/src/conversion/conversion.js
index 57a28e8fac3..57def8a4f97 100644
--- a/packages/ckeditor5-engine/src/conversion/conversion.js
+++ b/packages/ckeditor5-engine/src/conversion/conversion.js
@@ -140,6 +140,7 @@ export default class Conversion {
* * downcast (model-to-view) conversion helpers:
*
* * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`},
+ * * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure `elementToStructure()`},
* * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement `attributeToElement()`},
* * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToAttribute `attributeToAttribute()`}.
* * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToElement `markerToElement()`}.
diff --git a/packages/ckeditor5-engine/src/conversion/downcastdispatcher.js b/packages/ckeditor5-engine/src/conversion/downcastdispatcher.js
index 989266d0ce2..f33739cb24b 100644
--- a/packages/ckeditor5-engine/src/conversion/downcastdispatcher.js
+++ b/packages/ckeditor5-engine/src/conversion/downcastdispatcher.js
@@ -9,7 +9,6 @@
import Consumable from './modelconsumable';
import Range from '../model/range';
-import Position, { getNodeAfterPosition, getTextNodeAtPosition } from '../model/position';
import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
import mix from '@ckeditor/ckeditor5-utils/src/mix';
@@ -49,8 +48,9 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
*
* Additionally, downcast dispatcher fires events for {@link module:engine/model/markercollection~Marker marker} changes:
*
- * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker} – If a marker was added.
- * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:removeMarker} – If a marker was removed.
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker `addMarker`} – If a marker was added.
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:removeMarker `removeMarker`} – If a marker was
+ * removed.
*
* Note that changing a marker is done through removing the marker from the old range and adding it to the new range,
* so both events are fired.
@@ -58,11 +58,11 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
* Finally, downcast dispatcher also handles firing events for the {@link module:engine/model/selection model selection}
* conversion:
*
- * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:selection}
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:selection `selection`}
* – Converts the selection from the model to the view.
- * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute}
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute `attribute`}
* – Fired for every selection attribute.
- * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker `addMarker`}
* – Fired for every marker that contains a selection.
*
* Unlike the model tree and the markers, the events for selection are not fired for changes but for a selection state.
@@ -70,18 +70,15 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
* When providing custom listeners for a downcast dispatcher, remember to check whether a given change has not been
* {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed} yet.
*
- * When providing custom listeners for downcast dispatcher, keep in mind that any callback that has
- * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed} a value from a consumable and
- * converted the change should also stop the event (for efficiency purposes).
+ * When providing custom listeners for a downcast dispatcher, keep in mind that you **should not** stop the event. If you stop it,
+ * then the default converter at the `lowest` priority will not trigger the conversion of this node's attributes and child nodes.
*
- * When providing custom listeners for downcast dispatcher, remember to use the provided
+ * When providing custom listeners for a downcast dispatcher, remember to use the provided
* {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer} to apply changes to the view document.
*
- * You can read more about conversion in the following guides:
+ * You can read more about conversion in the following guide:
*
- * * {@glink framework/guides/deep-dive/conversion/conversion-introduction Advanced conversion concepts — attributes}
- * * {@glink framework/guides/deep-dive/conversion/conversion-extending-output Extending the editor output }
- * * {@glink framework/guides/deep-dive/conversion/custom-element-conversion Custom element conversion}
+ * * {@glink framework/guides/deep-dive/conversion/downcast Downcast conversion}
*
* An example of a custom converter for the downcast dispatcher:
*
@@ -103,9 +100,6 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
*
* // Add the newly created view element to the view.
* conversionApi.writer.insert( viewPosition, viewElement );
- *
- * // Remember to stop the event propagation.
- * evt.stop();
* } );
*/
export default class DowncastDispatcher {
@@ -118,206 +112,102 @@ export default class DowncastDispatcher {
*/
constructor( conversionApi ) {
/**
- * An interface passed by the dispatcher to the event callbacks.
+ * A template for an interface passed by the dispatcher to the event callbacks.
*
+ * @protected
* @member {module:engine/conversion/downcastdispatcher~DowncastConversionApi}
*/
- this.conversionApi = Object.assign( { dispatcher: this }, conversionApi );
+ this._conversionApi = { dispatcher: this, ...conversionApi };
/**
- * Maps conversion event names that will trigger element reconversion for a given element name.
+ * A map of already fired events for a given `ModelConsumable`.
*
- * @type {Map}
* @private
+ * @member {WeakMap.}
*/
- this._reconversionEventsMapping = new Map();
+ this._firedEventsMap = new WeakMap();
}
/**
- * Takes a {@link module:engine/model/differ~Differ model differ} object with buffered changes and fires conversion basing on it.
+ * Converts changes buffered in the given {@link module:engine/model/differ~Differ model differ}
+ * and fires conversion events based on it.
*
+ * @fires insert
+ * @fires remove
+ * @fires attribute
+ * @fires addMarker
+ * @fires removeMarker
+ * @fires reduceChanges
* @param {module:engine/model/differ~Differ} differ The differ object with buffered changes.
- * @param {module:engine/model/markercollection~MarkerCollection} markers Markers connected with the converted model.
+ * @param {module:engine/model/markercollection~MarkerCollection} markers Markers related to the model fragment to convert.
* @param {module:engine/view/downcastwriter~DowncastWriter} writer The view writer that should be used to modify the view document.
*/
convertChanges( differ, markers, writer ) {
+ const conversionApi = this._createConversionApi( writer, differ.getRefreshedItems() );
+
// Before the view is updated, remove markers which have changed.
for ( const change of differ.getMarkersToRemove() ) {
- this.convertMarkerRemove( change.name, change.range, writer );
+ this._convertMarkerRemove( change.name, change.range, conversionApi );
}
- const changes = this._mapChangesWithAutomaticReconversion( differ );
+ // Let features modify the change list (for example to allow reconversion).
+ const changes = this._reduceChanges( differ.getChanges() );
// Convert changes that happened on model tree.
for ( const entry of changes ) {
if ( entry.type === 'insert' ) {
- this.convertInsert( Range._createFromPositionAndShift( entry.position, entry.length ), writer );
+ this._convertInsert( Range._createFromPositionAndShift( entry.position, entry.length ), conversionApi );
+ } else if ( entry.type === 'reinsert' ) {
+ this._convertReinsert( Range._createFromPositionAndShift( entry.position, entry.length ), conversionApi );
} else if ( entry.type === 'remove' ) {
- this.convertRemove( entry.position, entry.length, entry.name, writer );
- } else if ( entry.type === 'reconvert' ) {
- this.reconvertElement( entry.element, writer );
+ this._convertRemove( entry.position, entry.length, entry.name, conversionApi );
} else {
// Defaults to 'attribute' change.
- this.convertAttribute( entry.range, entry.attributeKey, entry.attributeOldValue, entry.attributeNewValue, writer );
+ this._convertAttribute( entry.range, entry.attributeKey, entry.attributeOldValue, entry.attributeNewValue, conversionApi );
}
}
- for ( const markerName of this.conversionApi.mapper.flushUnboundMarkerNames() ) {
+ for ( const markerName of conversionApi.mapper.flushUnboundMarkerNames() ) {
const markerRange = markers.get( markerName ).getRange();
- this.convertMarkerRemove( markerName, markerRange, writer );
- this.convertMarkerAdd( markerName, markerRange, writer );
+ this._convertMarkerRemove( markerName, markerRange, conversionApi );
+ this._convertMarkerAdd( markerName, markerRange, conversionApi );
}
// After the view is updated, convert markers which have changed.
for ( const change of differ.getMarkersToAdd() ) {
- this.convertMarkerAdd( change.name, change.range, writer );
- }
- }
-
- /**
- * Starts a conversion of a range insertion.
- *
- * For each node in the range, {@link #event:insert `insert` event is fired}. For each attribute on each node,
- * {@link #event:attribute `attribute` event is fired}.
- *
- * @fires insert
- * @fires attribute
- * @param {module:engine/model/range~Range} range The inserted range.
- * @param {module:engine/view/downcastwriter~DowncastWriter} writer The view writer that should be used to modify the view document.
- */
- convertInsert( range, writer ) {
- this.conversionApi.writer = writer;
-
- // Create a list of things that can be consumed, consisting of nodes and their attributes.
- this.conversionApi.consumable = this._createInsertConsumable( range );
-
- // Fire a separate insert event for each node and text fragment contained in the range.
- for ( const data of Array.from( range ).map( walkerValueToEventData ) ) {
- this._convertInsertWithAttributes( data );
+ this._convertMarkerAdd( change.name, change.range, conversionApi );
}
- this._clearConversionApi();
- }
-
- /**
- * Fires conversion of a single node removal. Fires {@link #event:remove remove event} with provided data.
- *
- * @param {module:engine/model/position~Position} position Position from which node was removed.
- * @param {Number} length Offset size of removed node.
- * @param {String} name Name of removed node.
- * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
- */
- convertRemove( position, length, name, writer ) {
- this.conversionApi.writer = writer;
-
- this.fire( 'remove:' + name, { position, length }, this.conversionApi );
-
- this._clearConversionApi();
- }
-
- /**
- * Starts a conversion of an attribute change on a given `range`.
- *
- * For each node in the given `range`, {@link #event:attribute attribute event} is fired with the passed data.
- *
- * @fires attribute
- * @param {module:engine/model/range~Range} range Changed range.
- * @param {String} key Key of the attribute that has changed.
- * @param {*} oldValue Attribute value before the change or `null` if the attribute has not been set before.
- * @param {*} newValue New attribute value or `null` if the attribute has been removed.
- * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
- */
- convertAttribute( range, key, oldValue, newValue, writer ) {
- this.conversionApi.writer = writer;
-
- // Create a list with attributes to consume.
- this.conversionApi.consumable = this._createConsumableForRange( range, `attribute:${ key }` );
-
- // Create a separate attribute event for each node in the range.
- for ( const value of range ) {
- const item = value.item;
- const itemRange = Range._createFromPositionAndShift( value.previousPosition, value.length );
- const data = {
- item,
- range: itemRange,
- attributeKey: key,
- attributeOldValue: oldValue,
- attributeNewValue: newValue
- };
-
- this._testAndFire( `attribute:${ key }`, data );
- }
+ // Remove mappings for all removed view elements.
+ conversionApi.mapper.flushDeferredBindings();
- this._clearConversionApi();
+ // Verify if all insert consumables were consumed.
+ conversionApi.consumable.verifyAllConsumed( 'insert' );
}
/**
- * Starts the reconversion of an element. It will:
- *
- * * Fire an {@link #event:insert `insert` event} for the element to reconvert.
- * * Fire an {@link #event:attribute `attribute` event} for element attributes.
- *
- * This will not reconvert children of the element if they have existing (already converted) views. For newly inserted child elements
- * it will behave the same as {@link #convertInsert}.
- *
- * Element reconversion is defined by the `triggerBy` configuration for the
- * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`} conversion helper.
+ * Starts a conversion of a model range and the provided markers.
*
* @fires insert
* @fires attribute
- * @param {module:engine/model/element~Element} element The element to be reconverted.
+ * @fires addMarker
+ * @param {module:engine/model/range~Range} range The inserted range.
+ * @param {Map} markers The map of markers that should be down-casted.
* @param {module:engine/view/downcastwriter~DowncastWriter} writer The view writer that should be used to modify the view document.
+ * @param {Object} [options] Optional options object passed to `convertionApi.options`.
*/
- reconvertElement( element, writer ) {
- const elementRange = Range._createOn( element );
-
- this.conversionApi.writer = writer;
-
- // Create a list of things that can be consumed, consisting of nodes and their attributes.
- this.conversionApi.consumable = this._createInsertConsumable( elementRange );
-
- const mapper = this.conversionApi.mapper;
- const currentView = mapper.toViewElement( element );
-
- // Remove the old view but do not remove mapper mappings - those will be used to revive existing elements.
- writer.remove( currentView );
-
- // Convert the element - without converting children.
- this._convertInsertWithAttributes( {
- item: element,
- range: elementRange
- } );
+ convert( range, markers, writer, options = {} ) {
+ const conversionApi = this._createConversionApi( writer, undefined, options );
- const convertedViewElement = mapper.toViewElement( element );
+ this._convertInsert( range, conversionApi );
- // Iterate over children of reconverted element in order to...
- for ( const value of Range._createIn( element ) ) {
- const { item } = value;
-
- const view = elementOrTextProxyToView( item, mapper );
-
- // ...either bring back previously converted view...
- if ( view ) {
- // Do not move views that are already in converted element - those might be created by the main element converter in case
- // when main element converts also its direct children.
- if ( view.root !== convertedViewElement.root ) {
- writer.move(
- writer.createRangeOn( view ),
- mapper.toViewPosition( Position._createBefore( item ) )
- );
- }
- }
- // ... or by converting newly inserted elements.
- else {
- this._convertInsertWithAttributes( walkerValueToEventData( value ) );
- }
+ for ( const [ name, range ] of markers ) {
+ this._convertMarkerAdd( name, range, conversionApi );
}
- // After reconversion is done we can unbind the old view.
- mapper.unbindViewElement( currentView );
-
- this._clearConversionApi();
+ // Verify if all insert consumables were consumed.
+ conversionApi.consumable.verifyAllConsumed( 'insert' );
}
/**
@@ -335,21 +225,20 @@ export default class DowncastDispatcher {
convertSelection( selection, markers, writer ) {
const markersAtSelection = Array.from( markers.getMarkersAtPosition( selection.getFirstPosition() ) );
- this.conversionApi.writer = writer;
- this.conversionApi.consumable = this._createSelectionConsumable( selection, markersAtSelection );
+ const conversionApi = this._createConversionApi( writer );
- this.fire( 'selection', { selection }, this.conversionApi );
+ this._addConsumablesForSelection( conversionApi.consumable, selection, markersAtSelection );
- if ( !selection.isCollapsed ) {
- this._clearConversionApi();
+ this.fire( 'selection', { selection }, conversionApi );
+ if ( !selection.isCollapsed ) {
return;
}
for ( const marker of markersAtSelection ) {
const markerRange = marker.getRange();
- if ( !shouldMarkerChangeBeConverted( selection.getFirstPosition(), marker, this.conversionApi.mapper ) ) {
+ if ( !shouldMarkerChangeBeConverted( selection.getFirstPosition(), marker, conversionApi.mapper ) ) {
continue;
}
@@ -359,8 +248,8 @@ export default class DowncastDispatcher {
markerRange
};
- if ( this.conversionApi.consumable.test( selection, 'addMarker:' + marker.name ) ) {
- this.fire( 'addMarker:' + marker.name, data, this.conversionApi );
+ if ( conversionApi.consumable.test( selection, 'addMarker:' + marker.name ) ) {
+ this.fire( 'addMarker:' + marker.name, data, conversionApi );
}
}
@@ -374,130 +263,218 @@ export default class DowncastDispatcher {
};
// Do not fire event if the attribute has been consumed.
- if ( this.conversionApi.consumable.test( selection, 'attribute:' + data.attributeKey ) ) {
- this.fire( 'attribute:' + data.attributeKey + ':$text', data, this.conversionApi );
+ if ( conversionApi.consumable.test( selection, 'attribute:' + data.attributeKey ) ) {
+ this.fire( 'attribute:' + data.attributeKey + ':$text', data, conversionApi );
}
}
+ }
+
+ /**
+ * Fires insertion conversion of a range of nodes.
+ *
+ * For each node in the range, {@link #event:insert `insert` event is fired}. For each attribute on each node,
+ * {@link #event:attribute `attribute` event is fired}.
+ *
+ * @protected
+ * @fires insert
+ * @fires attribute
+ * @param {module:engine/model/range~Range} range The inserted range.
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
+ * @param {Object} [options]
+ * @param {Boolean} [options.doNotAddConsumables=false] Whether the ModelConsumable should not get populated
+ * for items in the provided range.
+ */
+ _convertInsert( range, conversionApi, options = {} ) {
+ if ( !options.doNotAddConsumables ) {
+ // Collect a list of things that can be consumed, consisting of nodes and their attributes.
+ this._addConsumablesForInsert( conversionApi.consumable, Array.from( range ) );
+ }
+
+ // Fire a separate insert event for each node and text fragment contained in the range.
+ for ( const data of Array.from( range.getWalker( { shallow: true } ) ).map( walkerValueToEventData ) ) {
+ this._testAndFire( 'insert', data, conversionApi );
+ }
+ }
+
+ /**
+ * Fires conversion of a single node removal. Fires {@link #event:remove remove event} with provided data.
+ *
+ * @protected
+ * @param {module:engine/model/position~Position} position Position from which node was removed.
+ * @param {Number} length Offset size of removed node.
+ * @param {String} name Name of removed node.
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
+ */
+ _convertRemove( position, length, name, conversionApi ) {
+ this.fire( 'remove:' + name, { position, length }, conversionApi );
+ }
+
+ /**
+ * Starts a conversion of an attribute change on a given `range`.
+ *
+ * For each node in the given `range`, {@link #event:attribute attribute event} is fired with the passed data.
+ *
+ * @protected
+ * @fires attribute
+ * @param {module:engine/model/range~Range} range Changed range.
+ * @param {String} key Key of the attribute that has changed.
+ * @param {*} oldValue Attribute value before the change or `null` if the attribute has not been set before.
+ * @param {*} newValue New attribute value or `null` if the attribute has been removed.
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
+ */
+ _convertAttribute( range, key, oldValue, newValue, conversionApi ) {
+ // Create a list with attributes to consume.
+ this._addConsumablesForRange( conversionApi.consumable, range, `attribute:${ key }` );
+
+ // Create a separate attribute event for each node in the range.
+ for ( const value of range ) {
+ const data = {
+ item: value.item,
+ range: Range._createFromPositionAndShift( value.previousPosition, value.length ),
+ attributeKey: key,
+ attributeOldValue: oldValue,
+ attributeNewValue: newValue
+ };
+
+ this._testAndFire( `attribute:${ key }`, data, conversionApi );
+ }
+ }
+
+ /**
+ * Fires re-insertion conversion (with a `reconversion` flag passed to `insert` events)
+ * of a range of elements (only elements on the range depth, without children).
+ *
+ * For each node in the range on its depth (without children), {@link #event:insert `insert` event} is fired.
+ * For each attribute on each node, {@link #event:attribute `attribute` event} is fired.
+ *
+ * @protected
+ * @fires insert
+ * @fires attribute
+ * @param {module:engine/model/range~Range} range The range to reinsert.
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
+ */
+ _convertReinsert( range, conversionApi ) {
+ // Convert the elements - without converting children.
+ const walkerValues = Array.from( range.getWalker( { shallow: true } ) );
+
+ // Collect a list of things that can be consumed, consisting of nodes and their attributes.
+ this._addConsumablesForInsert( conversionApi.consumable, walkerValues );
- this._clearConversionApi();
+ // Fire a separate insert event for each node and text fragment contained shallowly in the range.
+ for ( const data of walkerValues.map( walkerValueToEventData ) ) {
+ this._testAndFire( 'insert', { ...data, reconversion: true }, conversionApi );
+ }
}
/**
* Converts the added marker. Fires the {@link #event:addMarker `addMarker`} event for each item
* in the marker's range. If the range is collapsed, a single event is dispatched. See the event description for more details.
*
+ * @protected
* @fires addMarker
* @param {String} markerName Marker name.
* @param {module:engine/model/range~Range} markerRange The marker range.
- * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify the view document.
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
*/
- convertMarkerAdd( markerName, markerRange, writer ) {
+ _convertMarkerAdd( markerName, markerRange, conversionApi ) {
// Do not convert if range is in graveyard.
if ( markerRange.root.rootName == '$graveyard' ) {
return;
}
- this.conversionApi.writer = writer;
-
// In markers' case, event name == consumable name.
const eventName = 'addMarker:' + markerName;
//
// First, fire an event for the whole marker.
//
- const consumable = new Consumable();
- consumable.add( markerRange, eventName );
+ conversionApi.consumable.add( markerRange, eventName );
- this.conversionApi.consumable = consumable;
-
- this.fire( eventName, { markerName, markerRange }, this.conversionApi );
+ this.fire( eventName, { markerName, markerRange }, conversionApi );
//
// Do not fire events for each item inside the range if the range got consumed.
+ // Also consume the whole marker consumable if it wasn't consumed.
//
- if ( !consumable.test( markerRange, eventName ) ) {
- this._clearConversionApi();
-
+ if ( !conversionApi.consumable.consume( markerRange, eventName ) ) {
return;
}
//
// Then, fire an event for each item inside the marker range.
//
- this.conversionApi.consumable = this._createConsumableForRange( markerRange, eventName );
+ this._addConsumablesForRange( conversionApi.consumable, markerRange, eventName );
for ( const item of markerRange.getItems() ) {
// Do not fire event for already consumed items.
- if ( !this.conversionApi.consumable.test( item, eventName ) ) {
+ if ( !conversionApi.consumable.test( item, eventName ) ) {
continue;
}
const data = { item, range: Range._createOn( item ), markerName, markerRange };
- this.fire( eventName, data, this.conversionApi );
+ this.fire( eventName, data, conversionApi );
}
-
- this._clearConversionApi();
}
/**
* Fires the conversion of the marker removal. Fires the {@link #event:removeMarker `removeMarker`} event with the provided data.
*
+ * @protected
* @fires removeMarker
* @param {String} markerName Marker name.
* @param {module:engine/model/range~Range} markerRange The marker range.
- * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify the view document.
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
*/
- convertMarkerRemove( markerName, markerRange, writer ) {
+ _convertMarkerRemove( markerName, markerRange, conversionApi ) {
// Do not convert if range is in graveyard.
if ( markerRange.root.rootName == '$graveyard' ) {
return;
}
- this.conversionApi.writer = writer;
-
- this.fire( 'removeMarker:' + markerName, { markerName, markerRange }, this.conversionApi );
-
- this._clearConversionApi();
+ this.fire( 'removeMarker:' + markerName, { markerName, markerRange }, conversionApi );
}
/**
- * Maps the model element "insert" reconversion for given event names. The event names must be fully specified:
+ * Fires the reduction of changes buffered in the {@link module:engine/model/differ~Differ `Differ`}.
*
- * * For "attribute" change event, it should include the main element name, i.e: `'attribute:attributeName:elementName'`.
- * * For child node change events, these should use the child event name as well, i.e:
- * * For adding a node: `'insert:childElementName'`.
- * * For removing a node: `'remove:childElementName'`.
+ * Features can replace selected {@link module:engine/model/differ~DiffItem `DiffItem`}s with `reinsert` entries to trigger
+ * reconversion. The {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure
+ * `DowncastHelpers.elementToStructure()`} is using this event to trigger reconversion.
*
- * **Note**: This method should not be used directly. The reconversion is defined by the `triggerBy()` configuration of the
- * `elementToElement()` conversion helper.
- *
- * @protected
- * @param {String} modelName The name of the main model element for which the events will trigger the reconversion.
- * @param {String} eventName The name of an event that would trigger conversion for a given model element.
+ * @private
+ * @fires reduceChanges
+ * @param {Iterable.} changes
+ * @returns {Iterable.}
*/
- _mapReconversionTriggerEvent( modelName, eventName ) {
- this._reconversionEventsMapping.set( eventName, modelName );
+ _reduceChanges( changes ) {
+ const data = { changes };
+
+ this.fire( 'reduceChanges', data );
+
+ return data.changes;
}
/**
- * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with values to consume from a given range,
+ * Populates provided {@link module:engine/conversion/modelconsumable~ModelConsumable} with values to consume from a given range,
* assuming that the range has just been inserted to the model.
*
* @private
- * @param {module:engine/model/range~Range} range The inserted range.
+ * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable The consumable.
+ * @param {Iterable.} walkerValues The walker values for the inserted range.
* @returns {module:engine/conversion/modelconsumable~ModelConsumable} The values to consume.
*/
- _createInsertConsumable( range ) {
- const consumable = new Consumable();
-
- for ( const value of range ) {
+ _addConsumablesForInsert( consumable, walkerValues ) {
+ for ( const value of walkerValues ) {
const item = value.item;
- consumable.add( item, 'insert' );
+ // Add consumable if it wasn't there yet.
+ if ( consumable.test( item, 'insert' ) === null ) {
+ consumable.add( item, 'insert' );
- for ( const key of item.getAttributeKeys() ) {
- consumable.add( item, 'attribute:' + key );
+ for ( const key of item.getAttributeKeys() ) {
+ consumable.add( item, 'attribute:' + key );
+ }
}
}
@@ -505,16 +482,15 @@ export default class DowncastDispatcher {
}
/**
- * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with values to consume for a given range.
+ * Populates provided {@link module:engine/conversion/modelconsumable~ModelConsumable} with values to consume for a given range.
*
* @private
+ * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable The consumable.
* @param {module:engine/model/range~Range} range The affected range.
* @param {String} type Consumable type.
* @returns {module:engine/conversion/modelconsumable~ModelConsumable} The values to consume.
*/
- _createConsumableForRange( range, type ) {
- const consumable = new Consumable();
-
+ _addConsumablesForRange( consumable, range, type ) {
for ( const item of range.getItems() ) {
consumable.add( item, type );
}
@@ -523,16 +499,15 @@ export default class DowncastDispatcher {
}
/**
- * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with selection consumable values.
+ * Populates provided {@link module:engine/conversion/modelconsumable~ModelConsumable} with selection consumable values.
*
* @private
+ * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable The consumable.
* @param {module:engine/model/selection~Selection} selection The selection to create the consumable from.
* @param {Iterable.} markers Markers that contain the selection.
* @returns {module:engine/conversion/modelconsumable~ModelConsumable} The values to consume.
*/
- _createSelectionConsumable( selection, markers ) {
- const consumable = new Consumable();
-
+ _addConsumablesForSelection( consumable, selection, markers ) {
consumable.add( selection, 'selection' );
for ( const marker of markers ) {
@@ -547,152 +522,97 @@ export default class DowncastDispatcher {
}
/**
- * Tests passed `consumable` to check whether given event can be fired and if so, fires it.
+ * Tests whether given event wasn't already fired and if so, fires it.
*
* @private
* @fires insert
* @fires attribute
* @param {String} type Event type.
* @param {Object} data Event data.
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
*/
- _testAndFire( type, data ) {
- if ( !this.conversionApi.consumable.test( data.item, type ) ) {
- // Do not fire event if the item was consumed.
+ _testAndFire( type, data, conversionApi ) {
+ const eventName = getEventName( type, data );
+ const itemKey = data.item.is( '$textProxy' ) ? conversionApi.consumable._getSymbolForTextProxy( data.item ) : data.item;
+
+ const eventsFiredForConversion = this._firedEventsMap.get( conversionApi );
+ const eventsFiredForItem = eventsFiredForConversion.get( itemKey );
+
+ if ( !eventsFiredForItem ) {
+ eventsFiredForConversion.set( itemKey, new Set( [ eventName ] ) );
+ } else if ( !eventsFiredForItem.has( eventName ) ) {
+ eventsFiredForItem.add( eventName );
+ } else {
return;
}
- this.fire( getEventName( type, data ), data, this.conversionApi );
- }
-
- /**
- * Clears the conversion API object.
- *
- * @private
- */
- _clearConversionApi() {
- delete this.conversionApi.writer;
- delete this.conversionApi.consumable;
+ this.fire( eventName, data, conversionApi );
}
/**
- * Internal method for converting element insertion. It will fire events for the inserted element and events for its attributes.
+ * Fires not already fired events for setting attributes on just inserted item.
*
* @private
- * @fires insert
- * @fires attribute
- * @param {Object} data Event data.
+ * @param {module:engine/model/item~Item} item The model item to convert attributes for.
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
*/
- _convertInsertWithAttributes( data ) {
- this._testAndFire( 'insert', data );
+ _testAndFireAddAttributes( item, conversionApi ) {
+ const data = {
+ item,
+ range: Range._createOn( item )
+ };
- // Fire a separate addAttribute event for each attribute that was set on inserted items.
- // This is important because most attributes converters will listen only to add/change/removeAttribute events.
- // If we would not add this part, attributes on inserted nodes would not be converted.
for ( const key of data.item.getAttributeKeys() ) {
data.attributeKey = key;
data.attributeOldValue = null;
data.attributeNewValue = data.item.getAttribute( key );
- this._testAndFire( `attribute:${ key }`, data );
+ this._testAndFire( `attribute:${ key }`, data, conversionApi );
}
}
/**
- * Returns differ changes together with added "reconvert" type changes for {@link #reconvertElement}. These are defined by
- * a the `triggerBy()` configuration for the
- * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`} conversion helper.
- *
- * This method will remove every mapped insert or remove change with a single "reconvert" change.
- *
- * For instance: Having a `triggerBy()` configuration defined for the `` element that issues this element reconversion on
- * `foo` and `bar` attributes change, and a set of changes for this element:
- *
- * const differChanges = [
- * { type: 'attribute', attributeKey: 'foo', ... },
- * { type: 'attribute', attributeKey: 'bar', ... },
- * { type: 'attribute', attributeKey: 'baz', ... }
- * ];
+ * Builds an instance of the {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi} from a template and a given
+ * {@link module:engine/view/downcastwriter~DowncastWriter `DowncastWriter`} and options object.
*
- * This method will return:
- *
- * const updatedChanges = [
- * { type: 'reconvert', element: complexElementInstance },
- * { type: 'attribute', attributeKey: 'baz', ... }
- * ];
- *
- * In the example above, the `'baz'` attribute change will fire an {@link #event:attribute attribute event}
- *
- * @param {module:engine/model/differ~Differ} differ The differ object with buffered changes.
- * @returns {Array.