diff --git a/packages/ckeditor5-engine/src/controller/datacontroller.js b/packages/ckeditor5-engine/_src/controller/datacontroller.js
similarity index 100%
rename from packages/ckeditor5-engine/src/controller/datacontroller.js
rename to packages/ckeditor5-engine/_src/controller/datacontroller.js
diff --git a/packages/ckeditor5-engine/src/controller/editingcontroller.js b/packages/ckeditor5-engine/_src/controller/editingcontroller.js
similarity index 100%
rename from packages/ckeditor5-engine/src/controller/editingcontroller.js
rename to packages/ckeditor5-engine/_src/controller/editingcontroller.js
diff --git a/packages/ckeditor5-engine/src/conversion/conversion.js b/packages/ckeditor5-engine/_src/conversion/conversion.js
similarity index 100%
rename from packages/ckeditor5-engine/src/conversion/conversion.js
rename to packages/ckeditor5-engine/_src/conversion/conversion.js
diff --git a/packages/ckeditor5-engine/src/conversion/conversionhelpers.js b/packages/ckeditor5-engine/_src/conversion/conversionhelpers.js
similarity index 100%
rename from packages/ckeditor5-engine/src/conversion/conversionhelpers.js
rename to packages/ckeditor5-engine/_src/conversion/conversionhelpers.js
diff --git a/packages/ckeditor5-engine/src/conversion/downcastdispatcher.js b/packages/ckeditor5-engine/_src/conversion/downcastdispatcher.js
similarity index 100%
rename from packages/ckeditor5-engine/src/conversion/downcastdispatcher.js
rename to packages/ckeditor5-engine/_src/conversion/downcastdispatcher.js
diff --git a/packages/ckeditor5-engine/src/conversion/downcasthelpers.js b/packages/ckeditor5-engine/_src/conversion/downcasthelpers.js
similarity index 100%
rename from packages/ckeditor5-engine/src/conversion/downcasthelpers.js
rename to packages/ckeditor5-engine/_src/conversion/downcasthelpers.js
diff --git a/packages/ckeditor5-engine/src/conversion/mapper.js b/packages/ckeditor5-engine/_src/conversion/mapper.js
similarity index 100%
rename from packages/ckeditor5-engine/src/conversion/mapper.js
rename to packages/ckeditor5-engine/_src/conversion/mapper.js
diff --git a/packages/ckeditor5-engine/src/conversion/modelconsumable.js b/packages/ckeditor5-engine/_src/conversion/modelconsumable.js
similarity index 100%
rename from packages/ckeditor5-engine/src/conversion/modelconsumable.js
rename to packages/ckeditor5-engine/_src/conversion/modelconsumable.js
diff --git a/packages/ckeditor5-engine/src/conversion/upcastdispatcher.js b/packages/ckeditor5-engine/_src/conversion/upcastdispatcher.js
similarity index 100%
rename from packages/ckeditor5-engine/src/conversion/upcastdispatcher.js
rename to packages/ckeditor5-engine/_src/conversion/upcastdispatcher.js
diff --git a/packages/ckeditor5-engine/src/conversion/upcasthelpers.js b/packages/ckeditor5-engine/_src/conversion/upcasthelpers.js
similarity index 100%
rename from packages/ckeditor5-engine/src/conversion/upcasthelpers.js
rename to packages/ckeditor5-engine/_src/conversion/upcasthelpers.js
diff --git a/packages/ckeditor5-engine/src/conversion/viewconsumable.js b/packages/ckeditor5-engine/_src/conversion/viewconsumable.js
similarity index 100%
rename from packages/ckeditor5-engine/src/conversion/viewconsumable.js
rename to packages/ckeditor5-engine/_src/conversion/viewconsumable.js
diff --git a/packages/ckeditor5-engine/src/dataprocessor/basichtmlwriter.js b/packages/ckeditor5-engine/_src/dataprocessor/basichtmlwriter.js
similarity index 100%
rename from packages/ckeditor5-engine/src/dataprocessor/basichtmlwriter.js
rename to packages/ckeditor5-engine/_src/dataprocessor/basichtmlwriter.js
diff --git a/packages/ckeditor5-engine/src/dataprocessor/dataprocessor.jsdoc b/packages/ckeditor5-engine/_src/dataprocessor/dataprocessor.jsdoc
similarity index 100%
rename from packages/ckeditor5-engine/src/dataprocessor/dataprocessor.jsdoc
rename to packages/ckeditor5-engine/_src/dataprocessor/dataprocessor.jsdoc
diff --git a/packages/ckeditor5-engine/src/dataprocessor/htmldataprocessor.js b/packages/ckeditor5-engine/_src/dataprocessor/htmldataprocessor.js
similarity index 100%
rename from packages/ckeditor5-engine/src/dataprocessor/htmldataprocessor.js
rename to packages/ckeditor5-engine/_src/dataprocessor/htmldataprocessor.js
diff --git a/packages/ckeditor5-engine/src/dataprocessor/htmlwriter.js b/packages/ckeditor5-engine/_src/dataprocessor/htmlwriter.js
similarity index 100%
rename from packages/ckeditor5-engine/src/dataprocessor/htmlwriter.js
rename to packages/ckeditor5-engine/_src/dataprocessor/htmlwriter.js
diff --git a/packages/ckeditor5-engine/src/dataprocessor/xmldataprocessor.js b/packages/ckeditor5-engine/_src/dataprocessor/xmldataprocessor.js
similarity index 100%
rename from packages/ckeditor5-engine/src/dataprocessor/xmldataprocessor.js
rename to packages/ckeditor5-engine/_src/dataprocessor/xmldataprocessor.js
diff --git a/packages/ckeditor5-engine/src/dev-utils/model.js b/packages/ckeditor5-engine/_src/dev-utils/model.js
similarity index 100%
rename from packages/ckeditor5-engine/src/dev-utils/model.js
rename to packages/ckeditor5-engine/_src/dev-utils/model.js
diff --git a/packages/ckeditor5-engine/src/dev-utils/operationreplayer.js b/packages/ckeditor5-engine/_src/dev-utils/operationreplayer.js
similarity index 100%
rename from packages/ckeditor5-engine/src/dev-utils/operationreplayer.js
rename to packages/ckeditor5-engine/_src/dev-utils/operationreplayer.js
diff --git a/packages/ckeditor5-engine/src/dev-utils/utils.js b/packages/ckeditor5-engine/_src/dev-utils/utils.js
similarity index 100%
rename from packages/ckeditor5-engine/src/dev-utils/utils.js
rename to packages/ckeditor5-engine/_src/dev-utils/utils.js
diff --git a/packages/ckeditor5-engine/src/dev-utils/view.js b/packages/ckeditor5-engine/_src/dev-utils/view.js
similarity index 100%
rename from packages/ckeditor5-engine/src/dev-utils/view.js
rename to packages/ckeditor5-engine/_src/dev-utils/view.js
diff --git a/packages/ckeditor5-engine/src/index.js b/packages/ckeditor5-engine/_src/index.js
similarity index 100%
rename from packages/ckeditor5-engine/src/index.js
rename to packages/ckeditor5-engine/_src/index.js
diff --git a/packages/ckeditor5-engine/src/model/batch.js b/packages/ckeditor5-engine/_src/model/batch.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/batch.js
rename to packages/ckeditor5-engine/_src/model/batch.js
diff --git a/packages/ckeditor5-engine/src/model/differ.js b/packages/ckeditor5-engine/_src/model/differ.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/differ.js
rename to packages/ckeditor5-engine/_src/model/differ.js
diff --git a/packages/ckeditor5-engine/src/model/document.js b/packages/ckeditor5-engine/_src/model/document.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/document.js
rename to packages/ckeditor5-engine/_src/model/document.js
diff --git a/packages/ckeditor5-engine/src/model/documentfragment.js b/packages/ckeditor5-engine/_src/model/documentfragment.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/documentfragment.js
rename to packages/ckeditor5-engine/_src/model/documentfragment.js
diff --git a/packages/ckeditor5-engine/src/model/documentselection.js b/packages/ckeditor5-engine/_src/model/documentselection.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/documentselection.js
rename to packages/ckeditor5-engine/_src/model/documentselection.js
diff --git a/packages/ckeditor5-engine/src/model/element.js b/packages/ckeditor5-engine/_src/model/element.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/element.js
rename to packages/ckeditor5-engine/_src/model/element.js
diff --git a/packages/ckeditor5-engine/src/model/history.js b/packages/ckeditor5-engine/_src/model/history.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/history.js
rename to packages/ckeditor5-engine/_src/model/history.js
diff --git a/packages/ckeditor5-engine/_src/model/item.jsdoc b/packages/ckeditor5-engine/_src/model/item.jsdoc
new file mode 100644
index 00000000000..4ef8ecba1d5
--- /dev/null
+++ b/packages/ckeditor5-engine/_src/model/item.jsdoc
@@ -0,0 +1,14 @@
+/**
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module engine/model/item
+ */
+
+/**
+ * Item is a {@link module:engine/model/node~Node} or {@link module:engine/model/textproxy~TextProxy}.
+ *
+ * @typedef {module:engine/model/node~Node|module:engine/model/textproxy~TextProxy} module:engine/model/item~Item
+ */
diff --git a/packages/ckeditor5-engine/src/model/liveposition.js b/packages/ckeditor5-engine/_src/model/liveposition.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/liveposition.js
rename to packages/ckeditor5-engine/_src/model/liveposition.js
diff --git a/packages/ckeditor5-engine/src/model/liverange.js b/packages/ckeditor5-engine/_src/model/liverange.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/liverange.js
rename to packages/ckeditor5-engine/_src/model/liverange.js
diff --git a/packages/ckeditor5-engine/src/model/markercollection.js b/packages/ckeditor5-engine/_src/model/markercollection.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/markercollection.js
rename to packages/ckeditor5-engine/_src/model/markercollection.js
diff --git a/packages/ckeditor5-engine/src/model/model.js b/packages/ckeditor5-engine/_src/model/model.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/model.js
rename to packages/ckeditor5-engine/_src/model/model.js
diff --git a/packages/ckeditor5-engine/src/model/node.js b/packages/ckeditor5-engine/_src/model/node.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/node.js
rename to packages/ckeditor5-engine/_src/model/node.js
diff --git a/packages/ckeditor5-engine/src/model/nodelist.js b/packages/ckeditor5-engine/_src/model/nodelist.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/nodelist.js
rename to packages/ckeditor5-engine/_src/model/nodelist.js
diff --git a/packages/ckeditor5-engine/src/model/operation/attributeoperation.js b/packages/ckeditor5-engine/_src/model/operation/attributeoperation.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/operation/attributeoperation.js
rename to packages/ckeditor5-engine/_src/model/operation/attributeoperation.js
diff --git a/packages/ckeditor5-engine/src/model/operation/detachoperation.js b/packages/ckeditor5-engine/_src/model/operation/detachoperation.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/operation/detachoperation.js
rename to packages/ckeditor5-engine/_src/model/operation/detachoperation.js
diff --git a/packages/ckeditor5-engine/src/model/operation/insertoperation.js b/packages/ckeditor5-engine/_src/model/operation/insertoperation.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/operation/insertoperation.js
rename to packages/ckeditor5-engine/_src/model/operation/insertoperation.js
diff --git a/packages/ckeditor5-engine/src/model/operation/markeroperation.js b/packages/ckeditor5-engine/_src/model/operation/markeroperation.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/operation/markeroperation.js
rename to packages/ckeditor5-engine/_src/model/operation/markeroperation.js
diff --git a/packages/ckeditor5-engine/src/model/operation/mergeoperation.js b/packages/ckeditor5-engine/_src/model/operation/mergeoperation.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/operation/mergeoperation.js
rename to packages/ckeditor5-engine/_src/model/operation/mergeoperation.js
diff --git a/packages/ckeditor5-engine/src/model/operation/moveoperation.js b/packages/ckeditor5-engine/_src/model/operation/moveoperation.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/operation/moveoperation.js
rename to packages/ckeditor5-engine/_src/model/operation/moveoperation.js
diff --git a/packages/ckeditor5-engine/src/model/operation/nooperation.js b/packages/ckeditor5-engine/_src/model/operation/nooperation.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/operation/nooperation.js
rename to packages/ckeditor5-engine/_src/model/operation/nooperation.js
diff --git a/packages/ckeditor5-engine/src/model/operation/operation.js b/packages/ckeditor5-engine/_src/model/operation/operation.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/operation/operation.js
rename to packages/ckeditor5-engine/_src/model/operation/operation.js
diff --git a/packages/ckeditor5-engine/src/model/operation/operationfactory.js b/packages/ckeditor5-engine/_src/model/operation/operationfactory.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/operation/operationfactory.js
rename to packages/ckeditor5-engine/_src/model/operation/operationfactory.js
diff --git a/packages/ckeditor5-engine/src/model/operation/renameoperation.js b/packages/ckeditor5-engine/_src/model/operation/renameoperation.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/operation/renameoperation.js
rename to packages/ckeditor5-engine/_src/model/operation/renameoperation.js
diff --git a/packages/ckeditor5-engine/src/model/operation/rootattributeoperation.js b/packages/ckeditor5-engine/_src/model/operation/rootattributeoperation.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/operation/rootattributeoperation.js
rename to packages/ckeditor5-engine/_src/model/operation/rootattributeoperation.js
diff --git a/packages/ckeditor5-engine/src/model/operation/splitoperation.js b/packages/ckeditor5-engine/_src/model/operation/splitoperation.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/operation/splitoperation.js
rename to packages/ckeditor5-engine/_src/model/operation/splitoperation.js
diff --git a/packages/ckeditor5-engine/src/model/operation/transform.js b/packages/ckeditor5-engine/_src/model/operation/transform.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/operation/transform.js
rename to packages/ckeditor5-engine/_src/model/operation/transform.js
diff --git a/packages/ckeditor5-engine/src/model/operation/utils.js b/packages/ckeditor5-engine/_src/model/operation/utils.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/operation/utils.js
rename to packages/ckeditor5-engine/_src/model/operation/utils.js
diff --git a/packages/ckeditor5-engine/src/model/position.js b/packages/ckeditor5-engine/_src/model/position.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/position.js
rename to packages/ckeditor5-engine/_src/model/position.js
diff --git a/packages/ckeditor5-engine/src/model/range.js b/packages/ckeditor5-engine/_src/model/range.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/range.js
rename to packages/ckeditor5-engine/_src/model/range.js
diff --git a/packages/ckeditor5-engine/src/model/rootelement.js b/packages/ckeditor5-engine/_src/model/rootelement.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/rootelement.js
rename to packages/ckeditor5-engine/_src/model/rootelement.js
diff --git a/packages/ckeditor5-engine/src/model/schema.js b/packages/ckeditor5-engine/_src/model/schema.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/schema.js
rename to packages/ckeditor5-engine/_src/model/schema.js
diff --git a/packages/ckeditor5-engine/src/model/selection.js b/packages/ckeditor5-engine/_src/model/selection.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/selection.js
rename to packages/ckeditor5-engine/_src/model/selection.js
diff --git a/packages/ckeditor5-engine/src/model/text.js b/packages/ckeditor5-engine/_src/model/text.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/text.js
rename to packages/ckeditor5-engine/_src/model/text.js
diff --git a/packages/ckeditor5-engine/src/model/textproxy.js b/packages/ckeditor5-engine/_src/model/textproxy.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/textproxy.js
rename to packages/ckeditor5-engine/_src/model/textproxy.js
diff --git a/packages/ckeditor5-engine/src/model/treewalker.js b/packages/ckeditor5-engine/_src/model/treewalker.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/treewalker.js
rename to packages/ckeditor5-engine/_src/model/treewalker.js
diff --git a/packages/ckeditor5-engine/src/model/utils/autoparagraphing.js b/packages/ckeditor5-engine/_src/model/utils/autoparagraphing.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/utils/autoparagraphing.js
rename to packages/ckeditor5-engine/_src/model/utils/autoparagraphing.js
diff --git a/packages/ckeditor5-engine/src/model/utils/deletecontent.js b/packages/ckeditor5-engine/_src/model/utils/deletecontent.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/utils/deletecontent.js
rename to packages/ckeditor5-engine/_src/model/utils/deletecontent.js
diff --git a/packages/ckeditor5-engine/src/model/utils/findoptimalinsertionrange.js b/packages/ckeditor5-engine/_src/model/utils/findoptimalinsertionrange.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/utils/findoptimalinsertionrange.js
rename to packages/ckeditor5-engine/_src/model/utils/findoptimalinsertionrange.js
diff --git a/packages/ckeditor5-engine/src/model/utils/getselectedcontent.js b/packages/ckeditor5-engine/_src/model/utils/getselectedcontent.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/utils/getselectedcontent.js
rename to packages/ckeditor5-engine/_src/model/utils/getselectedcontent.js
diff --git a/packages/ckeditor5-engine/src/model/utils/insertcontent.js b/packages/ckeditor5-engine/_src/model/utils/insertcontent.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/utils/insertcontent.js
rename to packages/ckeditor5-engine/_src/model/utils/insertcontent.js
diff --git a/packages/ckeditor5-engine/src/model/utils/insertobject.js b/packages/ckeditor5-engine/_src/model/utils/insertobject.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/utils/insertobject.js
rename to packages/ckeditor5-engine/_src/model/utils/insertobject.js
diff --git a/packages/ckeditor5-engine/src/model/utils/modifyselection.js b/packages/ckeditor5-engine/_src/model/utils/modifyselection.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/utils/modifyselection.js
rename to packages/ckeditor5-engine/_src/model/utils/modifyselection.js
diff --git a/packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js b/packages/ckeditor5-engine/_src/model/utils/selection-post-fixer.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js
rename to packages/ckeditor5-engine/_src/model/utils/selection-post-fixer.js
diff --git a/packages/ckeditor5-engine/src/model/writer.js b/packages/ckeditor5-engine/_src/model/writer.js
similarity index 100%
rename from packages/ckeditor5-engine/src/model/writer.js
rename to packages/ckeditor5-engine/_src/model/writer.js
diff --git a/packages/ckeditor5-engine/src/view/attributeelement.js b/packages/ckeditor5-engine/_src/view/attributeelement.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/attributeelement.js
rename to packages/ckeditor5-engine/_src/view/attributeelement.js
diff --git a/packages/ckeditor5-engine/src/view/containerelement.js b/packages/ckeditor5-engine/_src/view/containerelement.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/containerelement.js
rename to packages/ckeditor5-engine/_src/view/containerelement.js
diff --git a/packages/ckeditor5-engine/src/view/document.js b/packages/ckeditor5-engine/_src/view/document.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/document.js
rename to packages/ckeditor5-engine/_src/view/document.js
diff --git a/packages/ckeditor5-engine/src/view/documentfragment.js b/packages/ckeditor5-engine/_src/view/documentfragment.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/documentfragment.js
rename to packages/ckeditor5-engine/_src/view/documentfragment.js
diff --git a/packages/ckeditor5-engine/src/view/documentselection.js b/packages/ckeditor5-engine/_src/view/documentselection.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/documentselection.js
rename to packages/ckeditor5-engine/_src/view/documentselection.js
diff --git a/packages/ckeditor5-engine/src/view/domconverter.js b/packages/ckeditor5-engine/_src/view/domconverter.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/domconverter.js
rename to packages/ckeditor5-engine/_src/view/domconverter.js
diff --git a/packages/ckeditor5-engine/src/view/downcastwriter.js b/packages/ckeditor5-engine/_src/view/downcastwriter.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/downcastwriter.js
rename to packages/ckeditor5-engine/_src/view/downcastwriter.js
diff --git a/packages/ckeditor5-engine/src/view/editableelement.js b/packages/ckeditor5-engine/_src/view/editableelement.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/editableelement.js
rename to packages/ckeditor5-engine/_src/view/editableelement.js
diff --git a/packages/ckeditor5-engine/src/view/element.js b/packages/ckeditor5-engine/_src/view/element.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/element.js
rename to packages/ckeditor5-engine/_src/view/element.js
diff --git a/packages/ckeditor5-engine/src/view/elementdefinition.jsdoc b/packages/ckeditor5-engine/_src/view/elementdefinition.jsdoc
similarity index 100%
rename from packages/ckeditor5-engine/src/view/elementdefinition.jsdoc
rename to packages/ckeditor5-engine/_src/view/elementdefinition.jsdoc
diff --git a/packages/ckeditor5-engine/src/view/emptyelement.js b/packages/ckeditor5-engine/_src/view/emptyelement.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/emptyelement.js
rename to packages/ckeditor5-engine/_src/view/emptyelement.js
diff --git a/packages/ckeditor5-engine/src/view/filler.js b/packages/ckeditor5-engine/_src/view/filler.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/filler.js
rename to packages/ckeditor5-engine/_src/view/filler.js
diff --git a/packages/ckeditor5-engine/src/view/item.jsdoc b/packages/ckeditor5-engine/_src/view/item.jsdoc
similarity index 100%
rename from packages/ckeditor5-engine/src/view/item.jsdoc
rename to packages/ckeditor5-engine/_src/view/item.jsdoc
diff --git a/packages/ckeditor5-engine/src/view/matcher.js b/packages/ckeditor5-engine/_src/view/matcher.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/matcher.js
rename to packages/ckeditor5-engine/_src/view/matcher.js
diff --git a/packages/ckeditor5-engine/src/view/node.js b/packages/ckeditor5-engine/_src/view/node.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/node.js
rename to packages/ckeditor5-engine/_src/view/node.js
diff --git a/packages/ckeditor5-engine/src/view/observer/arrowkeysobserver.js b/packages/ckeditor5-engine/_src/view/observer/arrowkeysobserver.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/observer/arrowkeysobserver.js
rename to packages/ckeditor5-engine/_src/view/observer/arrowkeysobserver.js
diff --git a/packages/ckeditor5-engine/src/view/observer/bubblingemittermixin.js b/packages/ckeditor5-engine/_src/view/observer/bubblingemittermixin.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/observer/bubblingemittermixin.js
rename to packages/ckeditor5-engine/_src/view/observer/bubblingemittermixin.js
diff --git a/packages/ckeditor5-engine/src/view/observer/bubblingeventinfo.js b/packages/ckeditor5-engine/_src/view/observer/bubblingeventinfo.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/observer/bubblingeventinfo.js
rename to packages/ckeditor5-engine/_src/view/observer/bubblingeventinfo.js
diff --git a/packages/ckeditor5-engine/src/view/observer/clickobserver.js b/packages/ckeditor5-engine/_src/view/observer/clickobserver.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/observer/clickobserver.js
rename to packages/ckeditor5-engine/_src/view/observer/clickobserver.js
diff --git a/packages/ckeditor5-engine/src/view/observer/compositionobserver.js b/packages/ckeditor5-engine/_src/view/observer/compositionobserver.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/observer/compositionobserver.js
rename to packages/ckeditor5-engine/_src/view/observer/compositionobserver.js
diff --git a/packages/ckeditor5-engine/src/view/observer/domeventdata.js b/packages/ckeditor5-engine/_src/view/observer/domeventdata.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/observer/domeventdata.js
rename to packages/ckeditor5-engine/_src/view/observer/domeventdata.js
diff --git a/packages/ckeditor5-engine/src/view/observer/domeventobserver.js b/packages/ckeditor5-engine/_src/view/observer/domeventobserver.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/observer/domeventobserver.js
rename to packages/ckeditor5-engine/_src/view/observer/domeventobserver.js
diff --git a/packages/ckeditor5-engine/src/view/observer/fakeselectionobserver.js b/packages/ckeditor5-engine/_src/view/observer/fakeselectionobserver.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/observer/fakeselectionobserver.js
rename to packages/ckeditor5-engine/_src/view/observer/fakeselectionobserver.js
diff --git a/packages/ckeditor5-engine/src/view/observer/focusobserver.js b/packages/ckeditor5-engine/_src/view/observer/focusobserver.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/observer/focusobserver.js
rename to packages/ckeditor5-engine/_src/view/observer/focusobserver.js
diff --git a/packages/ckeditor5-engine/src/view/observer/inputobserver.js b/packages/ckeditor5-engine/_src/view/observer/inputobserver.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/observer/inputobserver.js
rename to packages/ckeditor5-engine/_src/view/observer/inputobserver.js
diff --git a/packages/ckeditor5-engine/src/view/observer/keyobserver.js b/packages/ckeditor5-engine/_src/view/observer/keyobserver.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/observer/keyobserver.js
rename to packages/ckeditor5-engine/_src/view/observer/keyobserver.js
diff --git a/packages/ckeditor5-engine/src/view/observer/mouseobserver.js b/packages/ckeditor5-engine/_src/view/observer/mouseobserver.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/observer/mouseobserver.js
rename to packages/ckeditor5-engine/_src/view/observer/mouseobserver.js
diff --git a/packages/ckeditor5-engine/src/view/observer/mutationobserver.js b/packages/ckeditor5-engine/_src/view/observer/mutationobserver.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/observer/mutationobserver.js
rename to packages/ckeditor5-engine/_src/view/observer/mutationobserver.js
diff --git a/packages/ckeditor5-engine/src/view/observer/observer.js b/packages/ckeditor5-engine/_src/view/observer/observer.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/observer/observer.js
rename to packages/ckeditor5-engine/_src/view/observer/observer.js
diff --git a/packages/ckeditor5-engine/src/view/observer/selectionobserver.js b/packages/ckeditor5-engine/_src/view/observer/selectionobserver.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/observer/selectionobserver.js
rename to packages/ckeditor5-engine/_src/view/observer/selectionobserver.js
diff --git a/packages/ckeditor5-engine/src/view/observer/tabobserver.js b/packages/ckeditor5-engine/_src/view/observer/tabobserver.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/observer/tabobserver.js
rename to packages/ckeditor5-engine/_src/view/observer/tabobserver.js
diff --git a/packages/ckeditor5-engine/src/view/placeholder.js b/packages/ckeditor5-engine/_src/view/placeholder.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/placeholder.js
rename to packages/ckeditor5-engine/_src/view/placeholder.js
diff --git a/packages/ckeditor5-engine/src/view/position.js b/packages/ckeditor5-engine/_src/view/position.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/position.js
rename to packages/ckeditor5-engine/_src/view/position.js
diff --git a/packages/ckeditor5-engine/src/view/range.js b/packages/ckeditor5-engine/_src/view/range.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/range.js
rename to packages/ckeditor5-engine/_src/view/range.js
diff --git a/packages/ckeditor5-engine/src/view/rawelement.js b/packages/ckeditor5-engine/_src/view/rawelement.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/rawelement.js
rename to packages/ckeditor5-engine/_src/view/rawelement.js
diff --git a/packages/ckeditor5-engine/src/view/renderer.js b/packages/ckeditor5-engine/_src/view/renderer.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/renderer.js
rename to packages/ckeditor5-engine/_src/view/renderer.js
diff --git a/packages/ckeditor5-engine/src/view/rooteditableelement.js b/packages/ckeditor5-engine/_src/view/rooteditableelement.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/rooteditableelement.js
rename to packages/ckeditor5-engine/_src/view/rooteditableelement.js
diff --git a/packages/ckeditor5-engine/src/view/selection.js b/packages/ckeditor5-engine/_src/view/selection.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/selection.js
rename to packages/ckeditor5-engine/_src/view/selection.js
diff --git a/packages/ckeditor5-engine/src/view/styles/background.js b/packages/ckeditor5-engine/_src/view/styles/background.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/styles/background.js
rename to packages/ckeditor5-engine/_src/view/styles/background.js
diff --git a/packages/ckeditor5-engine/src/view/styles/border.js b/packages/ckeditor5-engine/_src/view/styles/border.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/styles/border.js
rename to packages/ckeditor5-engine/_src/view/styles/border.js
diff --git a/packages/ckeditor5-engine/src/view/styles/margin.js b/packages/ckeditor5-engine/_src/view/styles/margin.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/styles/margin.js
rename to packages/ckeditor5-engine/_src/view/styles/margin.js
diff --git a/packages/ckeditor5-engine/src/view/styles/padding.js b/packages/ckeditor5-engine/_src/view/styles/padding.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/styles/padding.js
rename to packages/ckeditor5-engine/_src/view/styles/padding.js
diff --git a/packages/ckeditor5-engine/src/view/styles/utils.js b/packages/ckeditor5-engine/_src/view/styles/utils.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/styles/utils.js
rename to packages/ckeditor5-engine/_src/view/styles/utils.js
diff --git a/packages/ckeditor5-engine/src/view/stylesmap.js b/packages/ckeditor5-engine/_src/view/stylesmap.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/stylesmap.js
rename to packages/ckeditor5-engine/_src/view/stylesmap.js
diff --git a/packages/ckeditor5-engine/src/view/text.js b/packages/ckeditor5-engine/_src/view/text.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/text.js
rename to packages/ckeditor5-engine/_src/view/text.js
diff --git a/packages/ckeditor5-engine/src/view/textproxy.js b/packages/ckeditor5-engine/_src/view/textproxy.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/textproxy.js
rename to packages/ckeditor5-engine/_src/view/textproxy.js
diff --git a/packages/ckeditor5-engine/src/view/treewalker.js b/packages/ckeditor5-engine/_src/view/treewalker.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/treewalker.js
rename to packages/ckeditor5-engine/_src/view/treewalker.js
diff --git a/packages/ckeditor5-engine/src/view/uielement.js b/packages/ckeditor5-engine/_src/view/uielement.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/uielement.js
rename to packages/ckeditor5-engine/_src/view/uielement.js
diff --git a/packages/ckeditor5-engine/src/view/upcastwriter.js b/packages/ckeditor5-engine/_src/view/upcastwriter.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/upcastwriter.js
rename to packages/ckeditor5-engine/_src/view/upcastwriter.js
diff --git a/packages/ckeditor5-engine/src/view/view.js b/packages/ckeditor5-engine/_src/view/view.js
similarity index 100%
rename from packages/ckeditor5-engine/src/view/view.js
rename to packages/ckeditor5-engine/_src/view/view.js
diff --git a/packages/ckeditor5-engine/package.json b/packages/ckeditor5-engine/package.json
index 299bc2b75bf..65c52981030 100644
--- a/packages/ckeditor5-engine/package.json
+++ b/packages/ckeditor5-engine/package.json
@@ -21,7 +21,7 @@
"ckeditor5-lib",
"ckeditor5-dll"
],
- "main": "src/index.js",
+ "main": "src/index.ts",
"dependencies": {
"@ckeditor/ckeditor5-utils": "^35.0.1",
"lodash-es": "^4.17.15"
@@ -47,6 +47,7 @@
"@ckeditor/ckeditor5-ui": "^35.0.1",
"@ckeditor/ckeditor5-undo": "^35.0.1",
"@ckeditor/ckeditor5-widget": "^35.0.1",
+ "typescript": "^4.6.4",
"webpack": "^5.58.1",
"webpack-cli": "^4.9.0"
},
@@ -65,9 +66,14 @@
},
"files": [
"lang",
- "src",
+ "src/**/*.js",
+ "src/**/*.d.ts",
"theme",
"ckeditor5-metadata.json",
"CHANGELOG.md"
- ]
+ ],
+ "scripts": {
+ "build": "tsc -p ./tsconfig.release.json",
+ "postversion": "npm run build"
+ }
}
diff --git a/packages/ckeditor5-engine/src/controller/datacontroller.ts b/packages/ckeditor5-engine/src/controller/datacontroller.ts
new file mode 100644
index 00000000000..45217a500e0
--- /dev/null
+++ b/packages/ckeditor5-engine/src/controller/datacontroller.ts
@@ -0,0 +1,641 @@
+/**
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module engine/controller/datacontroller
+ */
+
+import { Observable } from '@ckeditor/ckeditor5-utils/src/observablemixin';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import { Emitter } from '@ckeditor/ckeditor5-utils/src/emittermixin';
+
+import Mapper from '../conversion/mapper';
+
+import DowncastDispatcher, { type DowncastInsertEvent } from '../conversion/downcastdispatcher';
+import { insertAttributesAndChildren, insertText } from '../conversion/downcasthelpers';
+
+import UpcastDispatcher, {
+ type UpcastDocumentFragmentEvent,
+ type UpcastElementEvent,
+ type UpcastTextEvent
+} from '../conversion/upcastdispatcher';
+import { convertText, convertToModelFragment } from '../conversion/upcasthelpers';
+
+import ViewDocumentFragment from '../view/documentfragment';
+import ViewDocument from '../view/document';
+import ViewDowncastWriter from '../view/downcastwriter';
+import type ViewElement from '../view/element';
+import { type StylesProcessor } from '../view/stylesmap';
+import { type MatcherPattern } from '../view/matcher';
+
+import ModelRange from '../model/range';
+import type Model from '../model/model';
+import type ModelText from '../model/text';
+import type ModelElement from '../model/element';
+import type ModelTextProxy from '../model/textproxy';
+import type ModelDocumentFragment from '../model/documentfragment';
+import { type SchemaContextDefinition } from '../model/schema';
+import { type BatchType } from '../model/batch';
+import { autoParagraphEmptyRoots } from '../model/utils/autoparagraphing';
+
+import HtmlDataProcessor from '../dataprocessor/htmldataprocessor';
+import type DataProcessor from '../dataprocessor/dataprocessor';
+
+/**
+ * Controller for the data pipeline. The data pipeline controls how data is retrieved from the document
+ * and set inside it. Hence, the controller features two methods which allow to {@link ~DataController#get get}
+ * and {@link ~DataController#set set} data of the {@link ~DataController#model model}
+ * using the given:
+ *
+ * * {@link module:engine/dataprocessor/dataprocessor~DataProcessor data processor},
+ * * downcast converters,
+ * * upcast converters.
+ *
+ * An instance of the data controller is always available in the {@link module:core/editor/editor~Editor#data `editor.data`}
+ * property:
+ *
+ * editor.data.get( { rootName: 'customRoot' } ); // -> '
Hello!
'
+ *
+ * @mixes module:utils/emittermixin~EmitterMixin
+ */
+export default class DataController extends Emitter {
+ public readonly model: Model;
+ public readonly mapper: Mapper;
+ public readonly downcastDispatcher: DowncastDispatcher;
+ public readonly upcastDispatcher: UpcastDispatcher;
+ public readonly viewDocument: ViewDocument;
+ public readonly stylesProcessor: StylesProcessor;
+ public readonly htmlProcessor: HtmlDataProcessor;
+ public processor: DataProcessor;
+
+ private readonly _viewWriter: ViewDowncastWriter;
+
+ /**
+ * Creates a data controller instance.
+ *
+ * @param {module:engine/model/model~Model} model Data model.
+ * @param {module:engine/view/stylesmap~StylesProcessor} stylesProcessor The styles processor instance.
+ */
+ constructor( model: Model, stylesProcessor: StylesProcessor ) {
+ super();
+
+ /**
+ * Data model.
+ *
+ * @readonly
+ * @member {module:engine/model/model~Model}
+ */
+ this.model = model;
+
+ /**
+ * Mapper used for the conversion. It has no permanent bindings, because these are created while getting data and
+ * ae cleared directly after the data are converted. However, the mapper is defined as a class property, because
+ * it needs to be passed to the `DowncastDispatcher` as a conversion API.
+ *
+ * @readonly
+ * @member {module:engine/conversion/mapper~Mapper}
+ */
+ this.mapper = new Mapper();
+
+ /**
+ * Downcast dispatcher used by the {@link #get get method}. Downcast converters should be attached to it.
+ *
+ * @readonly
+ * @member {module:engine/conversion/downcastdispatcher~DowncastDispatcher}
+ */
+ this.downcastDispatcher = new DowncastDispatcher( {
+ mapper: this.mapper,
+ 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.
+ *
+ * @readonly
+ * @member {module:engine/conversion/upcastdispatcher~UpcastDispatcher}
+ */
+ this.upcastDispatcher = new UpcastDispatcher( {
+ schema: model.schema
+ } );
+
+ /**
+ * The view document used by the data controller.
+ *
+ * @readonly
+ * @member {module:engine/view/document~Document}
+ */
+ this.viewDocument = new ViewDocument( stylesProcessor );
+
+ /**
+ * Styles processor used during the conversion.
+ *
+ * @readonly
+ * @member {module:engine/view/stylesmap~StylesProcessor}
+ */
+ this.stylesProcessor = stylesProcessor;
+
+ /**
+ * Data processor used specifically for HTML conversion.
+ *
+ * @readonly
+ * @member {module:engine/dataprocessor/htmldataprocessor~HtmlDataProcessor} #htmlProcessor
+ */
+ this.htmlProcessor = new HtmlDataProcessor( this.viewDocument );
+
+ /**
+ * Data processor used during the conversion.
+ * Same instance as {@link #htmlProcessor} by default. Can be replaced at run time to handle different format, e.g. XML or Markdown.
+ *
+ * @member {module:engine/dataprocessor/dataprocessor~DataProcessor} #processor
+ */
+ this.processor = this.htmlProcessor;
+
+ /**
+ * The view downcast writer just for data conversion purposes, i.e. to modify
+ * the {@link #viewDocument}.
+ *
+ * @private
+ * @readonly
+ * @member {module:engine/view/downcastwriter~DowncastWriter}
+ */
+ this._viewWriter = new ViewDowncastWriter( this.viewDocument );
+
+ // Define default converters for text and elements.
+ //
+ // Note that if there is no default converter for the element it will be skipped, for instance `foo` will be
+ // converted to nothing. We therefore add `convertToModelFragment` as a last converter so it converts children of that
+ // element to the document fragment so `foo` will still be converted to `foo` even if there is no converter for ``.
+ this.upcastDispatcher.on( 'text', convertText(), { priority: 'lowest' } );
+ this.upcastDispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
+ this.upcastDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
+
+ Observable.prototype.decorate.call( this, 'init' as any );
+ Observable.prototype.decorate.call( this, 'set' as any );
+ Observable.prototype.decorate.call( this, 'get' as any );
+
+ // Fire the `ready` event when the initialization has completed. Such low-level listener offers the possibility
+ // to plug into the initialization pipeline without interrupting the initialization flow.
+ this.on( 'init', () => {
+ this.fire( 'ready' );
+ }, { priority: 'lowest' } );
+
+ // Fix empty roots after DataController is 'ready' (note that the init method could be decorated and stopped).
+ // We need to handle this event because initial data could be empty and the post-fixer would not get triggered.
+ this.on( 'ready', () => {
+ this.model.enqueueChange( { isUndoable: false }, autoParagraphEmptyRoots );
+ }, { priority: 'lowest' } );
+ }
+
+ /**
+ * Returns the model's data converted by downcast dispatchers attached to {@link #downcastDispatcher} and
+ * formatted by the {@link #processor data processor}.
+ *
+ * @fires get
+ * @param {Object} [options] Additional configuration for the retrieved data. `DataController` provides two optional
+ * properties: `rootName` and `trim`. Other properties of this object are specified by various editor features.
+ * @param {String} [options.rootName='main'] Root name.
+ * @param {String} [options.trim='empty'] Whether returned data should be trimmed. This option is set to `empty` by default,
+ * which means whenever editor content is considered empty, an empty string will be returned. To turn off trimming completely
+ * use `'none'`. In such cases the exact content will be returned (for example a `
` for an empty editor).
+ * @returns {String} Output data.
+ */
+ public get( options: Record = {} ): string {
+ const { rootName = 'main', trim = 'empty' } = options as Record;
+
+ if ( !this._checkIfRootsExists( [ rootName ] ) ) {
+ /**
+ * Cannot get data from a non-existing root. This error is thrown when {@link #get DataController#get() method}
+ * is called with a non-existent root name. For example, if there is an editor instance with only `main` root,
+ * calling {@link #get} like:
+ *
+ * data.get( { rootName: 'root2' } );
+ *
+ * will throw this error.
+ *
+ * @error datacontroller-get-non-existent-root
+ */
+ throw new CKEditorError( 'datacontroller-get-non-existent-root', this );
+ }
+
+ const root = this.model.document.getRoot( rootName )!;
+
+ if ( trim === 'empty' && !this.model.hasContent( root, { ignoreWhitespaces: true } ) ) {
+ return '';
+ }
+
+ return this.stringify( root, options );
+ }
+
+ /**
+ * Returns the content of the given {@link module:engine/model/element~Element model's element} or
+ * {@link module:engine/model/documentfragment~DocumentFragment model document fragment} converted by the downcast converters
+ * attached to the {@link #downcastDispatcher} and formatted by the {@link #processor data processor}.
+ *
+ * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} modelElementOrFragment
+ * The element whose content will be stringified.
+ * @param {Object} [options] Additional configuration passed to the conversion process.
+ * @returns {String} Output data.
+ */
+ public stringify(
+ modelElementOrFragment: ModelElement | ModelDocumentFragment,
+ options: Record = {}
+ ): string {
+ // Model -> view.
+ const viewDocumentFragment = this.toView( modelElementOrFragment, options );
+
+ // View -> data.
+ return this.processor.toData( viewDocumentFragment );
+ }
+
+ /**
+ * Returns the content of the given {@link module:engine/model/element~Element model element} or
+ * {@link module:engine/model/documentfragment~DocumentFragment model document fragment} converted by the downcast
+ * converters attached to {@link #downcastDispatcher} into a
+ * {@link module:engine/view/documentfragment~DocumentFragment view document fragment}.
+ *
+ * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} modelElementOrFragment
+ * Element or document fragment whose content will be converted.
+ * @param {Object} [options={}] Additional configuration that will be available through the
+ * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi#options} during the conversion process.
+ * @returns {module:engine/view/documentfragment~DocumentFragment} Output view DocumentFragment.
+ */
+ public toView(
+ modelElementOrFragment: ModelElement | ModelDocumentFragment,
+ options: Record = {}
+ ): ViewDocumentFragment {
+ const viewDocument = this.viewDocument;
+ const viewWriter = this._viewWriter;
+
+ // Clear bindings so the call to this method returns correct results.
+ this.mapper.clearBindings();
+
+ // First, convert elements.
+ const modelRange = ModelRange._createIn( modelElementOrFragment );
+ const viewDocumentFragment = new ViewDocumentFragment( viewDocument );
+
+ this.mapper.bindElements( modelElementOrFragment, viewDocumentFragment );
+
+ // 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' ) ?
+ modelElementOrFragment.markers :
+ _getMarkersRelativeToElement( modelElementOrFragment );
+
+ this.downcastDispatcher.convert( modelRange, markers, viewWriter, options );
+
+ return viewDocumentFragment;
+ }
+
+ /**
+ * Sets the initial input data parsed by the {@link #processor data processor} and
+ * converted by the {@link #upcastDispatcher view-to-model converters}.
+ * Initial data can be only set to a document whose {@link module:engine/model/document~Document#version} is equal 0.
+ *
+ * **Note** This method is {@link module:utils/observablemixin~ObservableMixin#decorate decorated} which is
+ * used by e.g. collaborative editing plugin that syncs remote data on init.
+ *
+ * When data is passed as a string, it is initialized on the default `main` root:
+ *
+ * dataController.init( '
Foo
' ); // Initializes data on the `main` root only, as no other is specified.
+ *
+ * To initialize data on a different root or multiple roots at once, an object containing `rootName` - `data` pairs should be passed:
+ *
+ * dataController.init( { main: '
Foo
', title: '
Bar
' } ); // Initializes data on both the `main` and `title` roots.
+ *
+ * @fires init
+ * @param {String|Object.} data Input data as a string or an object containing the `rootName` - `data`
+ * pairs to initialize data on multiple roots at once.
+ * @returns {Promise} Promise that is resolved after the data is set on the editor.
+ */
+ public init( data: string | Record ): Promise {
+ if ( this.model.document.version ) {
+ /**
+ * Cannot set initial data to a non-empty {@link module:engine/model/document~Document}.
+ * Initial data should be set once, during the {@link module:core/editor/editor~Editor} initialization,
+ * when the {@link module:engine/model/document~Document#version} is equal 0.
+ *
+ * @error datacontroller-init-document-not-empty
+ */
+ throw new CKEditorError( 'datacontroller-init-document-not-empty', this );
+ }
+
+ let initialData: Record = {};
+
+ if ( typeof data === 'string' ) {
+ initialData.main = data; // Default root is 'main'. To initiate data on a different root, object should be passed.
+ } else {
+ initialData = data;
+ }
+
+ if ( !this._checkIfRootsExists( Object.keys( initialData ) ) ) {
+ /**
+ * Cannot init data on a non-existent root. This error is thrown when {@link #init DataController#init() method}
+ * is called with non-existent root name. For example, if there is an editor instance with only `main` root,
+ * calling {@link #init} like:
+ *
+ * data.init( { main: '
Foo
', root2: '
Bar
' } );
+ *
+ * will throw this error.
+ *
+ * @error datacontroller-init-non-existent-root
+ */
+ throw new CKEditorError( 'datacontroller-init-non-existent-root', this );
+ }
+
+ this.model.enqueueChange( { isUndoable: false }, writer => {
+ for ( const rootName of Object.keys( initialData ) ) {
+ const modelRoot = this.model.document.getRoot( rootName )!;
+
+ writer.insert( this.parse( initialData[ rootName ], modelRoot ), modelRoot, 0 );
+ }
+ } );
+
+ return Promise.resolve();
+ }
+
+ /**
+ * Sets the input data parsed by the {@link #processor data processor} and
+ * converted by the {@link #upcastDispatcher view-to-model converters}.
+ * This method can be used any time to replace existing editor data with the new one without clearing the
+ * {@link module:engine/model/document~Document#history document history}.
+ *
+ * This method also creates a batch with all the changes applied. If all you need is to parse data, use
+ * the {@link #parse} method.
+ *
+ * When data is passed as a string it is set on the default `main` root:
+ *
+ * dataController.set( '
Foo
' ); // Sets data on the `main` root, as no other is specified.
+ *
+ * To set data on a different root or multiple roots at once, an object containing `rootName` - `data` pairs should be passed:
+ *
+ * dataController.set( { main: '
Foo
', title: '
Bar
' } ); // Sets data on the `main` and `title` roots as specified.
+ *
+ * To set the data with a preserved undo stack and add the change to the undo stack, set `{ isUndoable: true }` as a `batchType` option.
+ *
+ * dataController.set( '
Foo
', { batchType: { isUndoable: true } } );
+ *
+ * @fires set
+ * @param {String|Object.} data Input data as a string or an object containing the `rootName` - `data`
+ * pairs to set data on multiple roots at once.
+ * @param {Object} [options={}] Options for setting data.
+ * @param {Object} [options.batchType] The batch type that will be used to create a batch for the changes applied by this method.
+ * By default, the batch will be set as {@link module:engine/model/batch~Batch#isUndoable not undoable} and the undo stack will be
+ * cleared after the new data is applied (all undo steps will be removed). If the batch type `isUndoable` flag is be set to `true`,
+ * the undo stack will be preserved instead and not cleared when new data is applied.
+ */
+ public set( data: string | Record, options: { batchType?: BatchType } = {} ): void {
+ let newData: Record = {};
+
+ if ( typeof data === 'string' ) {
+ newData.main = data; // The default root is 'main'. To set data on a different root, an object should be passed.
+ } else {
+ newData = data;
+ }
+
+ if ( !this._checkIfRootsExists( Object.keys( newData ) ) ) {
+ /**
+ * Cannot set data on a non-existent root. This error is thrown when the {@link #set DataController#set() method}
+ * is called with non-existent root name. For example, if there is an editor instance with only the default `main` root,
+ * calling {@link #set} like:
+ *
+ * data.set( { main: '
Foo
', root2: '
Bar
' } );
+ *
+ * will throw this error.
+ *
+ * @error datacontroller-set-non-existent-root
+ */
+ throw new CKEditorError( 'datacontroller-set-non-existent-root', this );
+ }
+
+ this.model.enqueueChange( options.batchType || {}, writer => {
+ writer.setSelection( null );
+ writer.removeSelectionAttribute( this.model.document.selection.getAttributeKeys() );
+
+ for ( const rootName of Object.keys( newData ) ) {
+ // Save to model.
+ const modelRoot = this.model.document.getRoot( rootName )!;
+
+ writer.remove( writer.createRangeIn( modelRoot ) );
+ writer.insert( this.parse( newData[ rootName ], modelRoot ), modelRoot, 0 );
+ }
+ } );
+ }
+
+ /**
+ * Returns the data parsed by the {@link #processor data processor} and then converted by upcast converters
+ * attached to the {@link #upcastDispatcher}.
+ *
+ * @see #set
+ * @param {String} data Data to parse.
+ * @param {module:engine/model/schema~SchemaContextDefinition} [context='$root'] Base context in which the view will
+ * be converted to the model. See: {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#convert}.
+ * @returns {module:engine/model/documentfragment~DocumentFragment} Parsed data.
+ */
+ public parse( data: string, context: SchemaContextDefinition = '$root' ): ModelDocumentFragment {
+ // data -> view
+ const viewDocumentFragment = this.processor.toView( data );
+
+ // view -> model
+ return this.toModel( viewDocumentFragment, context );
+ }
+
+ /**
+ * Returns the result of the given {@link module:engine/view/element~Element view element} or
+ * {@link module:engine/view/documentfragment~DocumentFragment view document fragment} converted by the
+ * {@link #upcastDispatcher view-to-model converters}, wrapped by {@link module:engine/model/documentfragment~DocumentFragment}.
+ *
+ * When marker elements were converted during the conversion process, it will be set as a document fragment's
+ * {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}.
+ *
+ * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} viewElementOrFragment
+ * The element or document fragment whose content will be converted.
+ * @param {module:engine/model/schema~SchemaContextDefinition} [context='$root'] Base context in which the view will
+ * be converted to the model. See: {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#convert}.
+ * @returns {module:engine/model/documentfragment~DocumentFragment} Output document fragment.
+ */
+ public toModel(
+ viewElementOrFragment: ViewElement | ViewDocumentFragment,
+ context: SchemaContextDefinition = '$root'
+ ): ModelDocumentFragment {
+ return this.model.change( writer => {
+ return this.upcastDispatcher.convert( viewElementOrFragment, writer, context );
+ } );
+ }
+
+ /**
+ * Adds the style processor normalization rules.
+ *
+ * You can implement your own rules as well as use one of the available processor rules:
+ *
+ * * background: {@link module:engine/view/styles/background~addBackgroundRules}
+ * * border: {@link module:engine/view/styles/border~addBorderRules}
+ * * margin: {@link module:engine/view/styles/margin~addMarginRules}
+ * * padding: {@link module:engine/view/styles/padding~addPaddingRules}
+ *
+ * @param {Function} callback
+ */
+ public addStyleProcessorRules( callback: ( stylesProcessor: StylesProcessor ) => void ): void {
+ callback( this.stylesProcessor );
+ }
+
+ /**
+ * Registers a {@link module:engine/view/matcher~MatcherPattern} on an {@link #htmlProcessor htmlProcessor}
+ * and a {@link #processor processor} for view elements whose content should be treated as raw data
+ * and not processed during the conversion from DOM to view elements.
+ *
+ * The raw data can be later accessed by the {@link module:engine/view/element~Element#getCustomProperty view element custom property}
+ * `"$rawContent"`.
+ *
+ * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all view elements whose content should
+ * be treated as a raw data.
+ */
+ public registerRawContentMatcher( pattern: MatcherPattern ): void {
+ // No need to register the pattern if both the `htmlProcessor` and `processor` are the same instances.
+ if ( this.processor && this.processor !== this.htmlProcessor ) {
+ this.processor.registerRawContentMatcher( pattern );
+ }
+
+ this.htmlProcessor.registerRawContentMatcher( pattern );
+ }
+
+ /**
+ * Removes all event listeners set by the DataController.
+ */
+ public destroy(): void {
+ this.stopListening();
+ }
+
+ /**
+ * Checks whether all provided root names are actually existing editor roots.
+ *
+ * @private
+ * @param {Array.} rootNames Root names to check.
+ * @returns {Boolean} Whether all provided root names are existing editor roots.
+ */
+ private _checkIfRootsExists( rootNames: string[] ): boolean {
+ for ( const rootName of rootNames ) {
+ if ( !this.model.document.getRootNames().includes( rootName ) ) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Event fired once the data initialization has finished.
+ *
+ * @event ready
+ */
+
+ /**
+ * An event fired after the {@link #init `init()` method} was run. It can be {@link #listenTo listened to} in order to adjust or modify
+ * the initialization flow. However, if the `init` event is stopped or prevented, the {@link #event:ready `ready` event}
+ * should be fired manually.
+ *
+ * The `init` event is fired by the decorated {@link #init} method.
+ * See {@link module:utils/observablemixin~ObservableMixin#decorate} for more information and samples.
+ *
+ * @event init
+ */
+
+ /**
+ * An event fired after {@link #set set() method} has been run.
+ *
+ * The `set` event is fired by the decorated {@link #set} method.
+ * See {@link module:utils/observablemixin~ObservableMixin#decorate} for more information and samples.
+ *
+ * @event set
+ */
+
+ /**
+ * Event fired after the {@link #get get() method} has been run.
+ *
+ * The `get` event is fired by the decorated {@link #get} method.
+ * See {@link module:utils/observablemixin~ObservableMixin#decorate} for more information and samples.
+ *
+ * @event get
+ */
+}
+
+// Helper function for downcast conversion.
+//
+// Takes a document element (element that is added to a model document) and checks which markers are inside it. If the marker is collapsed
+// at element boundary, it is considered as contained inside the element and marker range is returned. Otherwise, if the marker is
+// intersecting with the element, the intersection is returned.
+function _getMarkersRelativeToElement( element: ModelElement ): Map {
+ const result: [ string, ModelRange ][] = [];
+ const doc = element.root.document;
+
+ if ( !doc ) {
+ return new Map();
+ }
+
+ const elementRange = ModelRange._createIn( element );
+
+ for ( const marker of doc.model.markers ) {
+ const markerRange = marker.getRange();
+
+ const isMarkerCollapsed = markerRange.isCollapsed;
+ const isMarkerAtElementBoundary = markerRange.start.isEqual( elementRange.start ) || markerRange.end.isEqual( elementRange.end );
+
+ if ( isMarkerCollapsed && isMarkerAtElementBoundary ) {
+ result.push( [ marker.name, markerRange ] );
+ } else {
+ const updatedMarkerRange = elementRange.getIntersection( markerRange );
+
+ if ( updatedMarkerRange ) {
+ result.push( [ marker.name, updatedMarkerRange ] );
+ }
+ }
+ }
+
+ // Sort the markers in a stable fashion to ensure that the order in which they are
+ // added to the model's marker collection does not affect how they are
+ // downcast. One particular use case that we are targeting here, is one where
+ // two markers are adjacent but not overlapping, such as an insertion/deletion
+ // suggestion pair representing the replacement of a range of text. In this
+ // case, putting the markers in DOM order causes the first marker's end to be
+ // serialized right after the second marker's start, while putting the markers
+ // in reverse DOM order causes it to be right before the second marker's
+ // start. So, we sort these in a way that ensures non-intersecting ranges are in
+ // 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).
+ result.sort( ( [ n1, r1 ], [ n2, r2 ] ) => {
+ if ( r1.end.compareWith( r2.start ) !== 'after' ) {
+ // m1.end <= m2.start -- m1 is entirely <= m2
+ return 1;
+ } else if ( r1.start.compareWith( r2.end ) !== 'before' ) {
+ // m1.start >= m2.end -- m1 is entirely >= m2
+ return -1;
+ } else {
+ // they overlap, so use their start positions as the primary sort key and
+ // end positions as the secondary sort key
+ switch ( r1.start.compareWith( r2.start ) ) {
+ case 'before':
+ return 1;
+ case 'after':
+ return -1;
+ default:
+ switch ( r1.end.compareWith( r2.end ) ) {
+ case 'before':
+ return 1;
+ case 'after':
+ return -1;
+ default:
+ return n2.localeCompare( n1 );
+ }
+ }
+ }
+ } );
+
+ return new Map( result );
+}
diff --git a/packages/ckeditor5-engine/src/controller/editingcontroller.ts b/packages/ckeditor5-engine/src/controller/editingcontroller.ts
new file mode 100644
index 00000000000..7845d42f257
--- /dev/null
+++ b/packages/ckeditor5-engine/src/controller/editingcontroller.ts
@@ -0,0 +1,243 @@
+/**
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module engine/controller/editingcontroller
+ */
+
+import RootEditableElement from '../view/rooteditableelement';
+import View from '../view/view';
+import Mapper from '../conversion/mapper';
+import DowncastDispatcher, {
+ type DowncastInsertEvent,
+ type DowncastRemoveEvent,
+ type DowncastSelectionEvent
+} from '../conversion/downcastdispatcher';
+import {
+ clearAttributes,
+ convertCollapsedSelection,
+ convertRangeSelection,
+ insertAttributesAndChildren,
+ insertText,
+ remove
+} from '../conversion/downcasthelpers';
+
+import { Observable } from '@ckeditor/ckeditor5-utils/src/observablemixin';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import { convertSelectionChange } from '../conversion/upcasthelpers';
+
+import type Model from '../model/model';
+import type ModelItem from '../model/item';
+import type ModelText from '../model/text';
+import type ModelTextProxy from '../model/textproxy';
+import type { ChangeEvent } from '../model/document';
+import type { Marker } from '../model/markercollection';
+import type { StylesProcessor } from '../view/stylesmap';
+import type { SelectionObserverEvent } from '../view/observer/selectionobserver';
+
+// @if CK_DEBUG_ENGINE // const { dumpTrees, initDocumentDumping } = require( '../dev-utils/utils' );
+
+/**
+ * A controller for the editing pipeline. The editing pipeline controls the {@link ~EditingController#model model} rendering,
+ * including selection handling. It also creates the {@link ~EditingController#view view} which builds a
+ * browser-independent virtualization over the DOM elements. The editing controller also attaches default converters.
+ *
+ * @mixes module:utils/observablemixin~ObservableMixin
+ */
+export default class EditingController extends Observable {
+ public readonly model: Model;
+ public readonly view: View;
+ public readonly mapper: Mapper;
+ public readonly downcastDispatcher: DowncastDispatcher;
+
+ /**
+ * Creates an editing controller instance.
+ *
+ * @param {module:engine/model/model~Model} model Editing model.
+ * @param {module:engine/view/stylesmap~StylesProcessor} stylesProcessor The styles processor instance.
+ */
+ constructor( model: Model, stylesProcessor: StylesProcessor ) {
+ super();
+
+ /**
+ * Editor model.
+ *
+ * @readonly
+ * @member {module:engine/model/model~Model}
+ */
+ this.model = model;
+
+ /**
+ * Editing view controller.
+ *
+ * @readonly
+ * @member {module:engine/view/view~View}
+ */
+ this.view = new View( stylesProcessor );
+
+ /**
+ * A mapper that describes the model-view binding.
+ *
+ * @readonly
+ * @member {module:engine/conversion/mapper~Mapper}
+ */
+ this.mapper = new Mapper();
+
+ /**
+ * Downcast dispatcher that converts changes from the model to the {@link #view editing view}.
+ *
+ * @readonly
+ * @member {module:engine/conversion/downcastdispatcher~DowncastDispatcher} #downcastDispatcher
+ */
+ this.downcastDispatcher = new DowncastDispatcher( {
+ mapper: this.mapper,
+ schema: model.schema
+ } );
+
+ const doc = this.model.document;
+ const selection = doc.selection;
+ const markers = this.model.markers;
+
+ // When plugins listen on model changes (on selection change, post fixers, etc.) and change the view as a result of
+ // the model's change, they might trigger view rendering before the conversion is completed (e.g. before the selection
+ // is converted). We disable rendering for the length of the outermost model change() block to prevent that.
+ //
+ // See https://github.com/ckeditor/ckeditor5-engine/issues/1528
+ this.listenTo( this.model, '_beforeChanges', () => {
+ this.view._disableRendering( true );
+ }, { priority: 'highest' } );
+
+ this.listenTo( this.model, '_afterChanges', () => {
+ this.view._disableRendering( false );
+ }, { priority: 'lowest' } );
+
+ // Whenever model document is changed, convert those changes to the view (using model.Document#differ).
+ // Do it on 'low' priority, so changes are converted after other listeners did their job.
+ // Also convert model selection.
+ this.listenTo( doc, 'change', () => {
+ this.view.change( writer => {
+ this.downcastDispatcher.convertChanges( doc.differ, markers, writer );
+ this.downcastDispatcher.convertSelection( selection, markers, writer );
+ } );
+ }, { priority: 'low' } );
+
+ // Convert selection from the view to the model when it changes in the view.
+ this.listenTo( this.view.document, 'selectionChange', convertSelectionChange( this.model, this.mapper ) );
+
+ // 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.
+ this.downcastDispatcher.on( 'selection', clearAttributes(), { priority: 'high' } );
+ this.downcastDispatcher.on( 'selection', convertRangeSelection(), { priority: 'low' } );
+ this.downcastDispatcher.on( 'selection', convertCollapsedSelection(), { priority: 'low' } );
+
+ // Binds {@link module:engine/view/document~Document#roots view roots collection} to
+ // {@link module:engine/model/document~Document#roots model roots collection} so creating
+ // model root automatically creates corresponding view root.
+ this.view.document.roots.bindTo( this.model.document.roots ).using( root => {
+ // $graveyard is a special root that has no reflection in the view.
+ if ( root.rootName == '$graveyard' ) {
+ return null;
+ }
+
+ const viewRoot = new RootEditableElement( this.view.document, root.name );
+
+ viewRoot.rootName = root.rootName;
+ this.mapper.bindElements( root, viewRoot );
+
+ return viewRoot;
+ } );
+
+ // @if CK_DEBUG_ENGINE // initDocumentDumping( this.model.document );
+ // @if CK_DEBUG_ENGINE // initDocumentDumping( this.view.document );
+
+ // @if CK_DEBUG_ENGINE // dumpTrees( this.model.document, this.model.document.version );
+ // @if CK_DEBUG_ENGINE // dumpTrees( this.view.document, this.model.document.version );
+
+ // @if CK_DEBUG_ENGINE // this.model.document.on( 'change', () => {
+ // @if CK_DEBUG_ENGINE // dumpTrees( this.view.document, this.model.document.version );
+ // @if CK_DEBUG_ENGINE // }, { priority: 'lowest' } );
+ }
+
+ /**
+ * Removes all event listeners attached to the `EditingController`. Destroys all objects created
+ * by `EditingController` that need to be destroyed.
+ */
+ public destroy(): void {
+ 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.
+ */
+ public reconvertMarker( markerOrName: Marker | string ): void {
+ const markerName = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
+ const currentMarker = this.model.markers.get( markerName );
+
+ if ( !currentMarker ) {
+ /**
+ * The marker with the 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.
+ */
+ public reconvertItem( item: ModelItem ): void {
+ this.model.change( () => {
+ this.model.document.differ._refreshItem( item );
+ } );
+ }
+}
diff --git a/packages/ckeditor5-engine/src/conversion/conversion.ts b/packages/ckeditor5-engine/src/conversion/conversion.ts
new file mode 100644
index 00000000000..542b49e9826
--- /dev/null
+++ b/packages/ckeditor5-engine/src/conversion/conversion.ts
@@ -0,0 +1,731 @@
+/**
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module engine/conversion/conversion
+ */
+
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import UpcastHelpers from './upcasthelpers';
+import DowncastHelpers, {
+ type AttributeCreatorFunction,
+ type AttributeDescriptor
+} from './downcasthelpers';
+import toArray, { type ArrayOrItem } from '@ckeditor/ckeditor5-utils/src/toarray';
+
+import type DowncastDispatcher from './downcastdispatcher';
+import type UpcastDispatcher from './upcastdispatcher';
+import type { PriorityString } from '@ckeditor/ckeditor5-utils/src/priorities';
+import type ElementDefinition from '../view/elementdefinition';
+import type { MatcherPattern } from '../view/matcher';
+
+/**
+ * A utility class that helps add converters to upcast and downcast dispatchers.
+ *
+ * We recommend reading the {@glink framework/guides/deep-dive/conversion/intro editor conversion} guide first to
+ * understand the core concepts of the conversion mechanisms.
+ *
+ * An instance of the conversion manager is available in the
+ * {@link module:core/editor/editor~Editor#conversion `editor.conversion`} property
+ * and by default has the following groups of dispatchers (i.e. directions of conversion):
+ *
+ * * `downcast` (editing and data downcasts)
+ * * `editingDowncast`
+ * * `dataDowncast`
+ * * `upcast`
+ *
+ * # One-way converters
+ *
+ * To add a converter to a specific group, use the {@link module:engine/conversion/conversion~Conversion#for `for()`}
+ * method:
+ *
+ * // Add a converter to editing downcast and data downcast.
+ * editor.conversion.for( 'downcast' ).elementToElement( config ) );
+ *
+ * // Add a converter to the data pipepline only:
+ * editor.conversion.for( 'dataDowncast' ).elementToElement( dataConversionConfig ) );
+ *
+ * // And a slightly different one for the editing pipeline:
+ * editor.conversion.for( 'editingDowncast' ).elementToElement( editingConversionConfig ) );
+ *
+ * See {@link module:engine/conversion/conversion~Conversion#for `for()`} method documentation to learn more about
+ * available conversion helpers and how to use your custom ones.
+ *
+ * # Two-way converters
+ *
+ * Besides using one-way converters via the `for()` method, you can also use other methods available in this
+ * class to add two-way converters (upcast and downcast):
+ *
+ * * {@link module:engine/conversion/conversion~Conversion#elementToElement `elementToElement()`} –
+ * Model element to view element and vice versa.
+ * * {@link module:engine/conversion/conversion~Conversion#attributeToElement `attributeToElement()`} –
+ * Model attribute to view element and vice versa.
+ * * {@link module:engine/conversion/conversion~Conversion#attributeToAttribute `attributeToAttribute()`} –
+ * Model attribute to view attribute and vice versa.
+ */
+export default class Conversion {
+ private readonly _helpers: Map;
+ private readonly _downcast: DowncastDispatcher[];
+ private readonly _upcast: UpcastDispatcher[];
+
+ /**
+ * Creates a new conversion instance.
+ *
+ * @param {module:engine/conversion/downcastdispatcher~DowncastDispatcher|
+ * Array.} downcastDispatchers
+ * @param {module:engine/conversion/upcastdispatcher~UpcastDispatcher|
+ * Array.} upcastDispatchers
+ */
+ constructor(
+ downcastDispatchers: ArrayOrItem,
+ upcastDispatchers: ArrayOrItem
+ ) {
+ /**
+ * Maps dispatchers group name to ConversionHelpers instances.
+ *
+ * @private
+ * @member {Map.}
+ */
+ this._helpers = new Map();
+
+ // Define default 'downcast' & 'upcast' dispatchers groups. Those groups are always available as two-way converters needs them.
+ this._downcast = toArray( downcastDispatchers );
+ this._createConversionHelpers( { name: 'downcast', dispatchers: this._downcast, isDowncast: true } );
+
+ this._upcast = toArray( upcastDispatchers );
+ this._createConversionHelpers( { name: 'upcast', dispatchers: this._upcast, isDowncast: false } );
+ }
+
+ public addAlias(
+ alias: `${ string }Downcast`,
+ dispatcher: DowncastDispatcher
+ ): void;
+ public addAlias(
+ alias: `${ string }Upcast`,
+ dispatcher: UpcastDispatcher
+ ): void;
+ public addAlias(
+ alias: string,
+ dispatcher: DowncastDispatcher | UpcastDispatcher
+ ): void;
+
+ /**
+ * Define an alias for registered dispatcher.
+ *
+ * const conversion = new Conversion(
+ * [ dataDowncastDispatcher, editingDowncastDispatcher ],
+ * upcastDispatcher
+ * );
+ *
+ * conversion.addAlias( 'dataDowncast', dataDowncastDispatcher );
+ *
+ * @param {String} alias An alias of a dispatcher.
+ * @param {module:engine/conversion/downcastdispatcher~DowncastDispatcher|
+ * module:engine/conversion/upcastdispatcher~UpcastDispatcher} dispatcher Dispatcher which should have an alias.
+ */
+ public addAlias(
+ alias: string,
+ dispatcher: DowncastDispatcher | UpcastDispatcher
+ ): void {
+ const isDowncast = this._downcast.includes( dispatcher as any );
+ const isUpcast = this._upcast.includes( dispatcher as any );
+
+ if ( !isUpcast && !isDowncast ) {
+ /**
+ * Trying to register an alias for a dispatcher that nas not been registered.
+ *
+ * @error conversion-add-alias-dispatcher-not-registered
+ */
+ throw new CKEditorError(
+ 'conversion-add-alias-dispatcher-not-registered',
+ this
+ );
+ }
+
+ this._createConversionHelpers( { name: alias, dispatchers: [ dispatcher ], isDowncast } );
+ }
+
+ public for( groupName: 'downcast' | `${ string }Downcast` ): DowncastHelpers;
+ public for( groupName: 'upcast' | `${ string }Upcast` ): UpcastHelpers;
+ public for( groupName: string ): DowncastHelpers | UpcastHelpers;
+
+ /**
+ * Provides a chainable API to assign converters to a conversion dispatchers group.
+ *
+ * If the given group name has not been registered, the
+ * {@link module:utils/ckeditorerror~CKEditorError `conversion-for-unknown-group` error} is thrown.
+ *
+ * You can use conversion helpers available directly in the `for()` chain or your custom ones via
+ * the {@link module:engine/conversion/conversionhelpers~ConversionHelpers#add `add()`} method.
+ *
+ * # Using built-in conversion helpers
+ *
+ * The `for()` chain comes with a set of conversion helpers which you can use like this:
+ *
+ * editor.conversion.for( 'downcast' )
+ * .elementToElement( config1 ) // Adds an element-to-element downcast converter.
+ * .attributeToElement( config2 ); // Adds an attribute-to-element downcast converter.
+ *
+ * editor.conversion.for( 'upcast' )
+ * .elementToAttribute( config3 ); // Adds an element-to-attribute upcast converter.
+ *
+ * Refer to the documentation of built-in conversion helpers to learn about their configuration options.
+ *
+ * * downcast (model-to-view) conversion helpers:
+ *
+ * * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`},
+ * * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement `attributeToElement()`},
+ * * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToAttribute `attributeToAttribute()`}.
+ * * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToElement `markerToElement()`}.
+ * * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToHighlight `markerToHighlight()`}.
+ *
+ * * upcast (view-to-model) conversion helpers:
+ *
+ * * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToElement `elementToElement()`},
+ * * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToAttribute `elementToAttribute()`},
+ * * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#attributeToAttribute `attributeToAttribute()`}.
+ * * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToMarker `elementToMarker()`}.
+ *
+ * # Using custom conversion helpers
+ *
+ * If you need to implement an atypical converter, you can do so by calling:
+ *
+ * editor.conversion.for( direction ).add( customHelper );
+ *
+ * The `.add()` method takes exactly one parameter, which is a function. This function should accept one parameter that
+ * is a dispatcher instance. The function should add an actual converter to the passed dispatcher instance.
+ *
+ * Example:
+ *
+ * editor.conversion.for( 'upcast' ).add( dispatcher => {
+ * dispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
+ * // Do something with a view element.
+ * } );
+ * } );
+ *
+ * Refer to the documentation of {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}
+ * and {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} to learn how to write
+ * custom converters.
+ *
+ * @param {String} groupName The name of dispatchers group to add the converters to.
+ * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers|module:engine/conversion/upcasthelpers~UpcastHelpers}
+ */
+ public for( groupName: string ): DowncastHelpers | UpcastHelpers {
+ if ( !this._helpers.has( groupName ) ) {
+ /**
+ * Trying to add a converter to an unknown dispatchers group.
+ *
+ * @error conversion-for-unknown-group
+ */
+ throw new CKEditorError( 'conversion-for-unknown-group', this );
+ }
+
+ return this._helpers.get( groupName )!;
+ }
+
+ /**
+ * Sets up converters between the model and the view that convert a model element to a view element (and vice versa).
+ * For example, the model `Foo` is turned into `