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

Introduced markers serialization #845

Merged
merged 45 commits into from
Mar 7, 2017
Merged

Introduced markers serialization #845

merged 45 commits into from
Mar 7, 2017

Conversation

oskarwrobel
Copy link
Contributor

@oskarwrobel oskarwrobel commented Feb 26, 2017

Suggested merge commit message (convention)

Feature: Introduced markers serialization. Closes ckeditor/ckeditor5#3961. Closes ckeditor/ckeditor5#4003.

BREAKING CHANGES: BuildModelConverter#fromMarkerCollapsed is removed. Use BuildModelConverter#fromMarker instead. Format of data returned by ViewConversionDispatcher#convert has changed from single model item to object with model item and list of static markers.

NOTE: insertUIElement model to view converter now supports collapsed and non-collapsed ranges.


Related to: ckeditor/ckeditor5-clipboard#15

Follow-ups:

@@ -180,7 +183,10 @@ export default class DataController {

/**
* Sets input data parsed by the {@link #processor data processor} and
* converted by the {@link #viewToModel view to model converters}.
* converted by the {@link #viewToModel view to model converters}. When markers where converted
Copy link
Contributor

Choose a reason for hiding this comment

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

If

@@ -180,7 +183,10 @@ export default class DataController {

/**
* Sets input data parsed by the {@link #processor data processor} and
* converted by the {@link #viewToModel view to model converters}.
* converted by the {@link #viewToModel view to model converters}. When markers where converted
* from view to model as temporary {@link module:engine/model/element/~Element model elements} then those element
Copy link
Contributor

Choose a reason for hiding this comment

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

elements

markerData.endPath ? new ModelPosition( modelRoot, markerData.endPath ) : null
);

batch.setMarker( this.model.markers.set( markerName, range ) );
Copy link
Contributor

Choose a reason for hiding this comment

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

AFAIR you can directly use setMarker - no need to add that marker to MarkerCollection first. MarkerOperation, upon execution, will do it.

@@ -306,6 +335,57 @@ export default class DataController {

mix( DataController, EmitterMixin );

// Traverses given DocumentFragment and searches elements which marks marker range. Founded element is removed from
Copy link
Contributor

Choose a reason for hiding this comment

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

Found

@@ -306,6 +335,57 @@ export default class DataController {

mix( DataController, EmitterMixin );

// Traverses given DocumentFragment and searches elements which marks marker range. Founded element is removed from
// DocumentFragment but path of this element is stored in Map.
Copy link
Contributor

@scofalik scofalik Feb 27, 2017

Choose a reason for hiding this comment

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

in a Map which is then returned.

//
// @param {module:engine/view/documentfragment~DocumentFragment} documentFragment Model DocumentFragment.
// @returns {Object} Object with markers data and cleaned up document fragment.
function extractMarkersFromModelFragment( documentFragment ) {
Copy link
Contributor

@scofalik scofalik Feb 27, 2017

Choose a reason for hiding this comment

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

To simplify this function and avoid refreshing walker, you could simply first find all elements to be extracted, and then remove them one by one after tree walker already did it's job:

const markerStamps = new Set();

// ...

for ( const value of walker ) {
    // ...
    markerStamps.add( value.item )
    // ...
}

for ( const stamp of markerStamps ) {
    // ...
}

AFAIR, Set remembers the order in which items where stored.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, since documentFragment is passed by reference, you only need to return the markers. It will be more elegant to use.

Copy link
Contributor

Choose a reason for hiding this comment

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

Finally, this could be a private method too, but I also have this "problem" of using sometimes private "static-like" methods and sometimes not exported functions, so as you wish :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also, since documentFragment is passed by reference, you only need to return the markers. It will be more elegant to use.

That's how it looked in my first implementation but I assumed that it'll be more readable to return data.

viewWriter.insert( mapper.toViewPosition( data.range.start ), viewElement );

if ( !data.range.isCollapsed ) {
viewWriter.insert( mapper.toViewPosition( data.range.end ), viewElement.clone( true ) );
Copy link
Contributor

Choose a reason for hiding this comment

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

We've been discussing that it would be nice if closing element is different than opening element, so this information would have to be send to creator and two view elements would have to be created.

If that is not too complicated, it would be nice to have this, but it is not a strict requirement. Take both model->view and view->model conversion into consideration.

Copy link
Contributor Author

@oskarwrobel oskarwrobel Feb 27, 2017

Choose a reason for hiding this comment

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

@scofalik
Copy link
Contributor

Changed proposed commit message from

NOTE: BuildModelConverter#fromMarker and BuildModelConverter#fromCollapsedMarker are unified, insertUIElement method of model -> view converter is enhanced to supports collapsed and non-collapsed ranges.

to

BREAKING CHANGES: BuildModelConverter#fromMarker and #fromCollapsedMarker are unified, and the latter is removed.
NOTE: insertUIElement model->view converter now supports collapsed and non-collapsed ranges.

@Reinmar
Copy link
Member

Reinmar commented Feb 27, 2017

I added missing empty line between both blocks :D

}

// Walk through collected marker elements store its path and remove its from the DocumentFragment.
return Array.from( markerStamps ).reduce( ( result, stamp ) => {
Copy link

Choose a reason for hiding this comment

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

Iterators FTW!

Copy link
Member

Choose a reason for hiding this comment

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

For var i = 0; i < length; ++i FTW!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was really curious what will you say guys about this reduce :)

}

// Walk through collected marker elements store its path and remove its from the DocumentFragment.
return Array.from( markerStamps ).reduce( ( result, stamp ) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Loops FTW!

On serious note, this looks overcomplicated. Simple loop over markerStamps would be okay too.

Copy link

@pjasiun pjasiun Feb 27, 2017

Choose a reason for hiding this comment

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

Anyway, the whole function should be moved from the DataController to conversion view dispatcher or view converter helper.

* converted by the {@link #viewToModel view to model converters}. If markers where converted
* from view to model as temporary {@link module:engine/model/element/~Element model elements} then those elements
* will be removed from parsed {@link module:engine/model/element/~DocumentFragment} and added to the
* {@link module:engine/model/document~Document#markers markers collection}.
Copy link

Choose a reason for hiding this comment

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

This should not be here. All what DataController is, is a facade for the complex API. I should not have any complex logic because quickly it became uber-class.

Copy link

Choose a reason for hiding this comment

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

Especially since now parse does not return any useful data, and the whole processing is done outside this function.

* @fires insertContent
* @param {module:engine/model/documentfragment~DocumentFragment} content The content to insert.
* @param {module:engine/model/selection~Selection} selection Selection into which the content should be inserted.
* @param {module:engine/model/batch~Batch} [batch] Batch to which deltas will be added. If not specified, then
* changes will be added to a new batch.
*/
insertContent( content, selection, batch ) {
extractMarkersFromModelFragment( content );
Copy link

Choose a reason for hiding this comment

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

Do we need to extract anything on insert content?

Copy link
Member

Choose a reason for hiding this comment

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

Why is it here? Any clean up should be done beforehand, using the clipboard events (#paste or #clipboardInput).

@Reinmar
Copy link
Member

Reinmar commented Feb 27, 2017

This branch has conflicts! Git FTW!

@@ -198,10 +198,28 @@ export default class DataController {
this.model.selection.removeAllRanges();
this.model.selection.clearAttributes();

// Parse data to model and extract markers from parsed document fragment.
const { modelItem, markersData } = this.parse( data );
Copy link

Choose a reason for hiding this comment

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

I was thinking a while about the return format. I don't like the code bellow needed to translate markersData to document.makers. It's not short nor simple, and you will have to write it every time you need a custom handling of parsed data. Of course, we could have a helper function, but where-ever we put such function it will be ugly. Then I thought that maybe markers should be returned as MarkersCollection.

At the same time, it's strange that you can not return one object representing a parsed fragment of the document, something like... DocumentFragment?

Markers are integrated part of the document data. They are not like selection or history, they are more like attributes or children, so maybe we should have DocumentFragment.makers? Then, when you add document fragment to another document, makers from it should be taken and added to the parent document, the same way like children are moved to the parent element. It could be done by the model.writer or element.insert (I hope soon we will get rid off model.writer anyway).

Copy link

Choose a reason for hiding this comment

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

After this change, the code here will not change much.

Copy link

Choose a reason for hiding this comment

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

I know I am usually the one who is against magic, but the possibility of having all data in one object is worth trying.

Copy link
Contributor Author

@oskarwrobel oskarwrobel Mar 1, 2017

Choose a reason for hiding this comment

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

I like this idea. Converter still will be returning Object with conversion result and markers data, but parser will merge it together into DocumentFragment with its own MarkersCollection ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Creating the real MarkersCollection on DocumentFragment might be more compilated because Marker keeps LiveRange inside but LiveRange work only for Document. We can:

  • try to improve DocumentFragment and LiveRange to work each other but it might be overengineering.
  • don't create the real MarkersCollection on DocumentFragmentbut only something to store markers data. I don't like it because every change on DocFrag will make markers positions out of date.

* @param {Function} [creator] Creator function.
*/
toMarker( creator ) {
const eventCallbackGen = function( from ) {
Copy link

Choose a reason for hiding this comment

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

Why not function eventCallbackGen( from )?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm also for named function instead of anonymous function assigned to const, but this is consistent with the other methods. I'll fix it.

const walker = new ModelTreeWalker( {
startPosition: ModelPosition.createAt( modelItem, 0 ),
ignoreElementEnd: true,
shallow: false
Copy link

Choose a reason for hiding this comment

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

AFAIR shallow is false by default.

@@ -236,7 +237,19 @@ export default class DataController {
* @returns {module:engine/model/documentfragment~DocumentFragment} Output document fragment.
*/
toModel( viewElementOrFragment, context = '$root' ) {
return this.viewToModel.convert( viewElementOrFragment, { context: [ context ] } );
const { conversionResult, markers } = this.viewToModel.convert( viewElementOrFragment, { context: [ context ] } );
Copy link

Choose a reason for hiding this comment

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

I think convert should simply return documentFragmet with markers which can be added instead of transforming it here. DataController should not contain any transformation logic, it should be as simple as possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

* buildModelConverter().for( dispatcher )
* .fromMarker( 'search' )
* .toStamp( ( data, isOpening ) => {
* if ( isOpening ) {
Copy link

Choose a reason for hiding this comment

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

isOpening should be a part of the data (data.isOpening).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

*/
toStamp( element ) {
for ( let dispatcher of this._dispatchers ) {
if ( this._from.type == 'element' || this._from.type == 'attribute' ) {
Copy link

Choose a reason for hiding this comment

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

Why not if ( this._from.type != 'marker' )? If we will introduce a new type it most probably should not be transformed to stamp as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

} else {
log.warn( 'model-writer-insert-lose-markers: Element containing markers is set to element without MarkersCollection.' );
}
}
Copy link

Choose a reason for hiding this comment

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

Will it work fine if I insert DocumentFragment with markers to another DocumentFragment?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did a small fix and wrote test for it and it works.

// When conversion output is not a Node or Element we just return it.
if ( !( conversionResult instanceof ModelNode || conversionResult instanceof ModelDocumentFragment ) ) {
return conversionResult;
}
Copy link

Choose a reason for hiding this comment

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

I don't get this condition. Is it for text nodes? I think that there is nothing wrong if we will wrap everything in the document fragment.

Copy link
Contributor Author

@oskarwrobel oskarwrobel Mar 7, 2017

Choose a reason for hiding this comment

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

This is because conversion output doesn't have to be a node. Theoretically it can be any value but we use it to provide converted element. @scofalik knows the details :) I know that it might be confusing especially when there is no explanation in docs.

Copy link

Choose a reason for hiding this comment

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

Yea, I know. It was even my idea, but it was a very theoretical idea. You can remove these tests because at this point it's pretty clear that we will not use dispatcher this way, soon.

Copy link
Contributor Author

Choose a reason for hiding this comment

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


return this._convertItem( viewItem, consumable, additionalData );
// When conversion output is not a Node or Element we just return it.
Copy link

Choose a reason for hiding this comment

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

This comment is useless. It tells exactly what the next line do. I can read code instead. What comment should do is tell me why this code is needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

* {@link module:engine/view/documentfragment~DocumentFragment view document fragment} converted by the
* {@link #viewToModel view to model converters} to a
* {@link module:engine/model/documentfragment~DocumentFragment model document fragment}.
* {@link #viewToModel view to model converters}.
Copy link

Choose a reason for hiding this comment

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

You could add a note to toModel and viewToModel.convert that the document fragment may contain 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.

Done.

@@ -25,6 +29,15 @@ export default class DocumentFragment {
*/
constructor( children ) {
/**
* DocumentFragment static markers list. This is a list of names and {@link module:engine/model/range~Range ranges}
Copy link

Choose a reason for hiding this comment

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

It's map, not list.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

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