Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Preserve correct order of block elements inside list items during view to model conversion #113

Merged
merged 7 commits into from Oct 18, 2018

Conversation

f1ames
Copy link
Contributor

@f1ames f1ames commented Oct 15, 2018

Suggested merge commit message (convention)

Fix: Preserve correct order of block elements inside list items during view to model conversion. Closes ckeditor/ckeditor5#1263.


Additional information

The cause of the issue is described in ckeditor/ckeditor5#1263 (comment) and ckeditor/ckeditor5#1263 (comment). and more complex mechanics is explained in the comments inside the introduced function.

@f1ames
Copy link
Contributor Author

f1ames commented Oct 16, 2018

I have removed some unnecessary code which was part of the logic of the first approach to this issue (see 76f50d0).

@coveralls
Copy link

coveralls commented Oct 16, 2018

Coverage Status

Coverage remained the same at 100.0% when pulling 3eb7417 on t/ckeditor5/1263 into d05f64f on master.

// Check all children of the converted `<li>`. At this point we assume there are no "whitespace" view text nodes
// in view list, between view list items. This should be handled by `<ul>` and `<ol>` converters.
for ( const child of viewChildren ) {
// If this is a view list element, we will convert it after last `listItem` model element.
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel that some comments are outdated now. They might have been written by me (I don't remember) and then, after a few refactorings, they lost their meaning a bit. I'd change it to: "If this is a view list element, we will insert its conversion result after currently handled listItem.".

if ( child.name == 'ul' || child.name == 'ol' ) {
nextPosition = conversionApi.convertItem( child, nextPosition ).modelCursor;
}
// If it was not a list view element it was a "regular" list item content. Convert it to a `listItem`.
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. In this function we convert children of listItem, not the listItem itself... So it doesn't make sense?

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 first part (if ( child.name == 'ul'...) refers to list view element:

If this is a view list element, we will insert its conversion result after currently handled listItem.

and else refers to any element which is not a view list element, what the comment describes:

If it was not a list view element it was a "regular" list item content.

Only second part of it:

Convert it to a listItem.

seems to be confusing as children are not converted to listItem (it was already inserted) but to listItem children. I will update this part.

const result = conversionApi.convertItem( child, ModelPosition.createAt( lastListItem, 'end' ) );
const convertedChild = Array.from( result.modelRange )[ 0 ];

nextPosition = ModelPosition.createAt( lastListItem, 'end' );
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't it the same as result.modelCursor? I honestly don't know, I think I haven't yet written a view-to-model converter after the refactor :P.

// If it was not a list view element it was a "regular" list item content. Convert it to a `listItem`.
else {
const result = conversionApi.convertItem( child, ModelPosition.createAt( lastListItem, 'end' ) );
const convertedChild = Array.from( result.modelRange )[ 0 ];
Copy link
Contributor

Choose a reason for hiding this comment

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

Ugh, I hate that variable name! You don't obtain a child here, instead you obtain a tree walker value. The name is very misleading. Later in the code I was like "convertedChild.type === 'elementStart'? What's going on?". Then I got back and realized that above is not returning a child.

If you don't need tree walker value but just a node, you can use result.modelRange.start.nodeAfter. This should return you a node unless the range for some reason starts inside a text node, but that would mean that we had two view.Text nodes next to each other, which shouldn't happen.

Later in the code I see that you check convertedChild.type === 'elementStart' -- is it really needed? Won't it always be elementStart or text? If so, isn't it enough to check .is()?

// so we need to update reference to `lastListItem` and `nextPosition`.
if ( convertedChild.type === 'elementStart' && convertedChild.item.is( 'element' ) ) {
nextPosition = result.modelCursor;
lastListItem = nextPosition.getAncestors().pop();
Copy link
Contributor

Choose a reason for hiding this comment

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

nextPosition.parent?

@@ -325,8 +325,8 @@ describe( 'ListEditing', () => {

describe( 'flat lists', () => {
describe( 'setting data', () => {
function test( testName, string, expectedString = null ) {
it( testName, () => {
function test( testName, string, expectedString = null, skip = false ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of this you could just comment out a test or wrap it in describe.skip().

Copy link
Contributor

Choose a reason for hiding this comment

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

BTW. it reminds that something similar was somewhere... probably undo tests in lists were skipped this way... But there at least half of a test was performed.

// <listItem></listItem><paragraph>Foo</paragraph><listItem></listItem>
//
// so we need to update reference to `lastListItem` and `nextPosition`.
if ( convertedChild.type === 'elementStart' && convertedChild.item.is( 'element' ) ) {
Copy link
Contributor

@scofalik scofalik Oct 17, 2018

Choose a reason for hiding this comment

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

One thing that I am not sure I like is future-proof of this if. Let's say that we will allow some kind of element in lists, or we introduce "document lists" (blocks allowed in lists). Maybe we could use Schema somehow to check if given model element is allowed at given position? But I am not sure now if conversionApi has reference to Schema.

@f1ames
Copy link
Contributor Author

f1ames commented Oct 18, 2018

@scofalik I have refactored the code according to your suggestions. It is ready for another review.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
3 participants