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

#10358 Introducing a downcast helper elementToStructure with reconversion support #10466

Merged
merged 38 commits into from
Sep 9, 2021

Conversation

niegowski
Copy link
Contributor

@niegowski niegowski commented Sep 1, 2021

Suggested merge commit message (convention)

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.

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 (heading): Code adjusted to changes in DowncastDispatcher API.

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

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

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


Additional information

This PR removes support for previous reconversion implementation and it's a part of an epic to reintroduce it in some downcast helpers.

@Reinmar
Copy link
Member

Reinmar commented Sep 8, 2021

Things to verify manually (cc @ckeditor/qa-team):

  • HTML embed feature
  • The getBody() function of the Title plugin combined with stuff that uses markers (TC, comments, etc)

Missing (cc @niegowski):

  • Tests for deferring removing mappings in the Mapper

Asking @scofalik to review this:

  • packages/ckeditor5-engine/src/conversion/modelconsumable.js
  • Overview of changes in DowncastHelpers

@niegowski
Copy link
Contributor Author

  • Tests for deferring removing mappings in the Mapper

Added.

} );

const convertedViewElement = mapper.toViewElement( element );
convert( range, markers, writer, options = {} ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to pass markers here or can they be taken from range (range.root to be precise). I am not sure if RootElement has a reference to Model instance. Wouldn't it be nice if you didn't have to evaluate those markers on your own (like in data processor)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem there is for example with Title plugin that for getBody() finds only intersecting parts of markers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And in DataController we pass there only _getMarkersRelativeToElement.

Copy link
Contributor

@scofalik scofalik Sep 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't we evaluate it in the dispatcher? Relative to range in this case, I guess, or let passing range or element to the dispatcher?

Not sure how Title works.

@@ -335,21 +217,20 @@ export default class DowncastDispatcher {
convertSelection( selection, markers, writer ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, would be nice if we could skip markers parameter ;).

@Reinmar
Copy link
Member

Reinmar commented Sep 9, 2021

TBD:

  • Regarding slotFor(), I'm still unsure about:
    • slotFor( 'children' ) – can this accept something else than 'children'? I know it can be a callback too, but then it's still children, just not all. It's a bit odd to me.
    • Do we check whether multiple slots with callbacks will not filter children in such a way that the order of them in the view will differ from the order in which they are in the model? This is a potentially dangerous case because mapping of a selection from the model to the view may and back may give weird results (like reversed selection positions).
    • What will happen if a callback for slotFor() allocates text nodes in a weird manner (split to multiple places)? Maybe we should limit this and only allow for elements that has only elements as their children?
  • Consider splitting downcasthelpers.js into multiple files. It got too long for my taste ;| (and will get longer)

I'll ping you f2f about this. None of this is a blocker for merging the PR.

@Reinmar Reinmar merged commit eeb30a3 into ck/10294/reconversion Sep 9, 2021
@Reinmar Reinmar deleted the ck/10358-elementToStructure branch September 9, 2021 20:31
}
}

return [ node ];
Copy link
Contributor

@scofalik scofalik Sep 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why null or single-item array instead of true or false?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this function is used as default parameter later in the code. Will there be cases when the user will write this function on their own, and then the function would return bigger arrays?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or is this for future purposes? Or artifact from the past :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was for future purposes but in a recent spike, I found a better way to handle it. In the next PoC it's true/false ck/10294/reconversion...ck/10381-imperative-reconversion

return false;
}

return events.every( event => consumable.consume( node, event ) );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that forEach() + return true would be more clear.

// @param {module:engine/model/element~Element} element
// @param {Map.<module:engine/view/element~Element,Array.<module:engine/model/node~Node>>} slotsMap
// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
// @returns {Function}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am missing here at least a sentence about the parameter which the function can take.

Comment on lines +2129 to +2136
if ( options.reconversion && viewChildNode && viewChildNode.root != viewElement.root ) {
writer.move(
writer.createRangeOn( viewChildNode ),
mapper.toViewPosition( ModelPosition._createBefore( modelChildNode ) )
);
} else {
conversionApi.convertInsert( ModelRange._createOn( modelChildNode ) );
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarification needed.

  1. Are there two modes because this function is sometimes used for the "first insertion" and sometimes for reconversion?
  2. Do we check viewChildNode when there's reconversion flag because of text nodes?
  3. Why do we check roots?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. This is for cases when reconversion is triggered by adding/removing child nodes. The old view elements are restored and only the added child is converted.
  2. We want to restore old view elements only for reconversion and not while the initial insert.
  3. To make sure that that view element is not already added to the view structure.

@scofalik
Copy link
Contributor

scofalik commented Sep 10, 2021

I like the API, I like or am neutral with almost all of the changes.

Other than what I commented:

  • Good points by PK.
  • I agree that slotFor( 'children' ) could simply be slotFor().
  • I don't like slotFor() name, I'd go with createSlot().
  • I also don't like "reduce" name, but I'd have to re-read this PR to be sure about that.
  • Sometimes I wasn't sure how given part of code works with text nodes (or whether text nodes (as a value of a variable) are even possible at given place in code), but I would have to re-read again to understand those cases.
  • Please re-read all comments and API docs, there are a lot of mistakes there (by a lot I mean more than one :D). Also, maybe you will spot places that can be further clarified.

Also, maybe you already considered that, but the sample with table is still a bit complex. Maybe slots could be named, so that if there is anything to put in the slot, a proper view element would be created automatically?

// second parameter is attributes, the usage looks like creating a regular view element
const tableHead = createSlot(
    'thead',
    null,
    element => element.is( 'element', 'tableRow' ) && element.index < headingRows
);

writer.insert( writer.createPositionAt( tableElement, 'end' ), tableHead );

Instead of:

if ( headingRows > 0 ) {
    const tableHead = writer.createContainerElement( 'thead' );
    const headSlot = slotFor( element => element.is( 'element', 'tableRow' ) && element.index < headingRows );

    writer.insert( writer.createPositionAt( tableElement, 'end' ), tableHead );
    writer.insert( writer.createPositionAt( tableHead, 0 ), headSlot );
}

I think it looks cool that you can create "view element" that takes callback instead of children ;). However it may be confusing (if you don't know how slots work, you may think that you will get an empty element, OTOH, you might want to have an empty element and in such case you need to go with the "regular way" to create elements).

@niegowski
Copy link
Contributor Author

Also, maybe you already considered that, but the sample with table is still a bit complex. Maybe slots could be named, so that if there is anything to put in the slot, a proper view element would be created automatically?

This would force to wrap elements with a container element (thead in the example) and we would not be able to create such structure:

<figure class="image">
	<img/>
	<$slot for="children"/>
</figure>

In such case the slot does not need a wrapper, view elements should be inserted directly into the figure element at the specified view positions (so after the img).

@scofalik
Copy link
Contributor

I never meant this should be the only way to create slots.

createSlot();
createSlot( callback );
createSlot( name, attributes, callback );

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants