Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redesign and introduce new reconversion system #10294

Closed
niegowski opened this issue Aug 3, 2021 · 7 comments · Fixed by #11211
Closed

Redesign and introduce new reconversion system #10294

niegowski opened this issue Aug 3, 2021 · 7 comments · Fixed by #11211
Assignees
Labels
domain:dx This issue reports a developer experience problem or possible improvement. Epic package:engine squad:core Issue to be handled by the Core team. support:2 An issue reported by a commercially licensed client. type:feature This issue reports a feature request (an idea for a new functionality or a missing option).

Comments

@niegowski
Copy link
Contributor

niegowski commented Aug 3, 2021

EDIT (Summary of changes)

You can find a summary of changes in the conversion system in this post: #11268 (comment)

There's also a migration guide that covers related changes: Migration to CKEditor 5 v33.0.0.

Why

What do we have now

There is a downcast helper for elementToElement with a triggerBy option that triggers reconversion of a specific model element by removing its view representation (but keeping mappings), triggering the new view structure conversion callbacks (for that model element only), and reusing the child elements' old view representations. 

What’s wrong with what we have now

To downcast a single model element to a slightly more complex structure we need to use the low-level API (so we need to take care of consuming what we converted and to bind model elements with view elements). That's because we are converting a subtree of a model structure. The reusing of view elements works sort of in a "magical" way. 

There is also an issue about the current implementation conflicting different triggerBy converters: #9641.

What could we improve in the current solution

It could be more understandable if a single elementToElement converter would handle a single model element and do not touch its children elements. The conversion callback could provide a view structure with the special placeholders (slots) that model child elements should be downcasted into.

Why do we think it can be done better?

The current triggerBy usage is very confusing. It requires to convert (in a single converter) not only an element itself but also its model child elements (and only their content is reused). This would be more declarative if we could provide conversion for a single model element to a bigger view structure with placeholders/slots for model child elements so that it could take care of inserting view elements in the correct positions in the view structure (not only as a direct child of the main element).

That slots could also have conditions for example to handle specific table rows (thead vs tbody that depends on headingRows attribute value).

What do we have prototyped so far? Where’s that code?

The early PoC is in the list reconversion branch (that includes a lot of other experiments for rangeToStructure conversion). That branch is currently broken (because of changes in progress). The most interesting function is insertSlotted that is supposed to get triggered after the old view was removed (but the view mapping was temporarily kept until the end of the conversion process to be reused while converting slots' content).

What

A high-level overview of a new mechanism

The solution could be split into layers:

  • DowncastDispatcher could fire a new reduceChanges event before the conversion process starts to allow features (or conversion helpers) to alter the list of changes provided by the Differ so it could detect cases where the reconversion should be used. The re-conversion would be split into 2 events - one to remove the old view representation of a model element and one to insert a new representation. The old view elements would get removed from the view and unbound in the Mapper but it could keep temporal mapping to allow reusing view elements that got removed in the same conversion process. Those would get flushed at the end of the whole conversion.
  • DowncastHelpers elementToElement and elementToStructure would listen for the reduceChanges event to register callbacks for triggerBy changes to trigger re-conversion is a specified situation.
  • elementToStructure downcast helper could provide a factory for placeholders/slots so the simple view generator could put those into the structure and the helper itself would take care of converting those slot's content or reusing the view that was previously there.

Example API proposal

The high-level API:

editor.conversion.for( 'downcast' ).elementToStructure( {
    model: 'table', 
    view: ( modelElement, conversionApi ) => {
        const viewElement = conversionApi.writer.createContainerElement( 'div' );

        // ...

        writer.insert( writer.createPositionAt( viewElement, 0 ), conversionApi.slotFor( modelElement, 'children' ) );

        // ...

        return viewElement;
    }, 
    triggerBy: { 
        attributes: [ 'headingRows' ]
    }
} );

Success criteria

  • Simple conversion for a single model element to a bigger view structure without taking care of mapping or consumables.
  • Can be used in tables to simplify the current downcast of the whole table and enable its reconversion with the preservation of table rows and cells.
  • Should be adjustable to use with a rangeToStructure future helper that will be used for the document list conversion.

Risks, other things to change.

  • Temporal mappings kept for the conversion process are not yet in PoC (it was a simple one converter solution).
  • It seems that markers downcasted to view should properly reconvert themself (as it works for the current reconversion).

When

Why now

The PoC was prepared while working on #9784 and #9783. This could be extracted from that PoC as it's a separate feature that would be later used by rangeToStructure downcast helper.

What’s next

The next step would be to implement rangeToStructure downcast helper and use it for document list conversion.


If you'd like to see this feature implemented, add a 👍 reaction to this post.

@niegowski niegowski added type:feature This issue reports a feature request (an idea for a new functionality or a missing option). domain:dx This issue reports a developer experience problem or possible improvement. squad:dx labels Aug 3, 2021
@niegowski niegowski self-assigned this Aug 3, 2021
@niegowski
Copy link
Contributor Author

The elementToStructure PoC is now extracted to a new branch: https://github.com/ckeditor/ckeditor5/compare/ck/10294-elementToStructure

It also implements the logic of remove + reconvert diff items.

@Mgsy Mgsy added this to the iteration 46 milestone Aug 5, 2021
@Reinmar
Copy link
Member

Reinmar commented Aug 5, 2021

There is also an issue about the current implementation conflicting different triggerBy converters: #9641.

Another issue: #10306 (not sure if related).

@niegowski
Copy link
Contributor Author

I just noticed another bug in the current reconversion implementation: #10314

@Mgsy Mgsy added the Epic label Aug 16, 2021
@Mgsy Mgsy removed the squad:dx label Aug 16, 2021
@Reinmar Reinmar changed the title Introduce an elementToStructure downcast helpers with slots reconversion Improve downcast helpers Aug 16, 2021
@Reinmar Reinmar changed the title Improve downcast helpers Reintroduce reconversion functionality for downcast Aug 16, 2021
@Reinmar Reinmar changed the title Reintroduce reconversion functionality for downcast Redesign and introduce new reconversion system Aug 16, 2021
@AnnaTomanek AnnaTomanek modified the milestones: iteration 46, iteration 47 Sep 6, 2021
@dawidurbanski
Copy link
Contributor

dawidurbanski commented Sep 21, 2021

I have couple thoughts after working on adding support for reconversion in elementToElement downcast helper.

Let me put them all together in this ticket so they are at least noted somewhere.

Children prop Boolean|Function

The new API for describing model in elementToElement config object looks like this:

model: {
	name: 'items',
	attributes: [ 'mode' ],
	children: true
},

I found some obscure use cases where it could be useful if children prop accepted not only boolean but also a function returning boolean (ideally with modelElement passed). This way we could set children dynamically based on model element attribute / element type / it's children type etc.

model: {
	name: 'items',
	attributes: [ 'mode' ],
	children: ( modelElement ) => modelElement.getAttribute( 'mode' ) !== 'no-reconvert'
},

I don't have any realistic use cases for this at the moment, so it's just for the records. Probably not very important nor needed.

Docs / comments

* @param {Array.<String>} [config.model.attributes] The list of attribute names that should be consumed while creating
* the view structure. Note that the view will be reconverted if any of the listed attributes will change.

Do we have to mention consumables here? Isn't enough to say the second part:

View will be reconverted if any of the listed attributes will change.

Also, to keep it consistent with other parts of this API, shouldn't we accept both array of strings and string for single attribute?

@param {String|Array.<String>} [config.model.attributes]

* Note: The children of a model element that's being converted must be allocated in the same order in the view
* in which they are placed in the model.

I know it has been added as part of "improving docs" commit, but this note is still a bit unclear to me. Yeah it describes the constraint but does not say why nor what happens if you don't follow it. Will it raise an error? Will it crash?

I guess this probably might be a part of improving conversion docs and be just a matter of putting a link near this note.

@Reinmar @niegowski FYI

@niegowski
Copy link
Contributor Author

Also, to keep it consistent with other parts of this API, shouldn't we accept both array of strings and string for single attribute?

@param {String|Array.<String>} [config.model.attributes]

The normalization accepts it, maybe it's just missing JSDoc?

@Inviz
Copy link
Contributor

Inviz commented Oct 20, 2021

Hopefully this will solve issues like #10306

@lslowikowska lslowikowska added the support:2 An issue reported by a commercially licensed client. label Nov 9, 2021
@Reinmar Reinmar removed this from the iteration 49 milestone Dec 1, 2021
dawidurbanski added a commit that referenced this issue Feb 9, 2022
Feature (engine): The DowncastWriter#createContainerElement() should accept a list of children so bigger view structures can be created in one call. Closes #10714.

Feature (engine): The elementToElement downcast helper will log a console warning if multiple elements have been created. Closes #10610.

Feature (engine): The DowncastDispatcher will throw an error if any of the model items were not consumed while converting. Closes #10377.

Feature (engine): Introducing convertItem(), convertChildren() and convertAttributes() in the downcast conversion API interface.

Feature (engine): Added support for reconversion in DowncastHelpers#elementToElement() downcast helper. Closes #10359.

Feature (engine): Added DowncastHelpers#elementToStructure() downcast helper with reconversion support. Closes #10358.

Feature (engine): It's now possible to trigger a nested conversion while downcasting an element.

Docs (engine): Overhauled the conversion documentation and introduced the documentation for conversion helpers. Closes #10294.

Internal (engine): elementToStructure() should throw when invoked for an element that allows $text. Closes #11163.

Internal (engine): Replaced conversionApi.slotFor() in elementToStructure with writer.createSlot(). Closes #11179.

Internal (engine): Reconversion helpers can coexist with Differ#refreshItem() children.

Other (engine): Implemented the EditingController#reconvertMarker() method to be used instead of Writer#updateMarker() for marker reconversion purposes. Implemented the EditingController#reconvertItem() method to replace Differ#refreshItem(). Closes #10659.

Other (engine): The attribute and child nodes conversion events are fired by the lowest priority handler for the insert event instead of by DowncastDispatcher itself. Closes #10376.

Other (engine): Events are fired by the DowncastDispatcher even if they were previously consumed. It's the conversion handler's responsibility to check if it can be consumed or if it was already consumed by other converters.

Other (engine): The DowncastDispatcher#convert() method introduced as a replacement to previously used convertInsert(). The new method handles not only nodes conversion but also markers.

Internal (horizontal-line): The horizontalLine element downcast conversion has been changed from elementToElement to elementToStructure.

Internal (html-support): HTML elements downcast conversion have been changed from elementToElement to elementToStructure.

Internal (image): The imageBlock and imageInline elements downcast conversion have been changed from elementToElement to elementToStructure.

Internal (link): Link inlineWidget element downcast conversion has been changed from elementToElement to elementToStructure.

Internal (media-embed): The media element downcast conversion has been changed from elementToElement to elementToStructure.

Internal (page-break): The pageBreak element downcast conversion has been changed from elementToElement to elementToStructure.

Internal (widget): The blockWidget and inlineWidget elements downcast conversion have been changed from elementToElement to elementToStructure.

Internal (heading): Added missing consuming converter.

Internal (heading): Code adjusted to changes in DowncastDispatcher API.

Internal (html-embed): Code adjusted to the removal of triggerBy conversion option.

Other (list): The ckeditor5-list package was restructured into subdirectories. Closes #10811.

Other (list): Downcast conversion should consume downcasted attributes.

Other (table): Table downcast conversion migrated to elementToStructure() downcast helper. Closes #10502.

Tests (clipboard): Added missing downcast conversion.

Tests (image): Tests updated to properly handle non-consumed errors fired by the DowncastDispatcher.

Tests (alignment, autoformat, block-quote, engine, html-support, image, indent, table, word-count): Tests updated after list package restructuring.

Tests (html-support): Fixed test for checking if an attribute was consumed while converting.

Docs (ckeditor5): Links in the migration guide point to the new conversion documentation.

MAJOR BREAKING CHANGE (list): The ListEditing, ListUI, ListStyleEditing, ListStyleUI, TodoListEditing, TodoListUI plugins were moved to the dedicated subdirectories (list, liststyle, todolist).

MAJOR BREAKING CHANGE (engine): Removed the Differ#refreshItem() method from the public API. Replaced by EditingController#reconvertItem() (see #10659).

MAJOR BREAKING CHANGE (engine): The DowncastDispatcher will throw an error if any of the model items were not consumed while converting. Read the conversion-model-consumable-not-consumed error documentation for more information.

MAJOR BREAKING CHANGE (engine): The DowncastDispatcher#conversionApi property is no longer available. The instances of DowncastConversionApi being created at the start of conversion.

MAJOR BREAKING CHANGE (engine): The support for the triggerBy option for downcast helpers is removed and replaced with the new elementToStructure() options.

MINOR BREAKING CHANGE (media-embed): The createMediaFigureElement helper function first argument has been changed from writer object to conversionApi object.

MINOR BREAKING CHANGE (table): The downcast converters of the table feature has been rewritten with the use of elementToStructure() and the re-conversion mechanism. See #10502.
@AnnaTomanek AnnaTomanek added this to the iteration 51 milestone Feb 10, 2022
@dawidurbanski
Copy link
Contributor

Crosslinking the summary of changes for this huge feature: #11268 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
domain:dx This issue reports a developer experience problem or possible improvement. Epic package:engine squad:core Issue to be handled by the Core team. support:2 An issue reported by a commercially licensed client. type:feature This issue reports a feature request (an idea for a new functionality or a missing option).
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants