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

Commit 1961395

Browse files
author
Piotr Jasiun
authored
Merge pull request #1256 from ckeditor/t/1213
Feature: Convert view to model using position. Closes #1213. Closes #1250. BREAKING CHANGE: `DataController#parse`, `DataController#toModel`, `ViewConversionDispatcher#convert` gets `SchemaContextDefinition` as a contex instead of `String`. BREAKING CHANGE: `ViewConversionApi#splitToAllowedParent` has been introduced. BREAKING CHANGE: `ViewConversionApi#storage` has been introduced. BREAKING CHANGE: `ViewConsumable` has been merged to `ViewConversionApi`. BREAKING CHANGE: Format od data object passed across conversion callback has been changed. Feature: `Schema#findAllowedParent` has been introduced. Feature: `SchemaContext#concat` has been introduced.
2 parents 4331e01 + 7cb1fc4 commit 1961395

18 files changed

+1239
-1041
lines changed

src/controller/datacontroller.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -209,11 +209,11 @@ export default class DataController {
209209
*
210210
* @see #set
211211
* @param {String} data Data to parse.
212-
* @param {String} [context='$root'] Base context in which the view will be converted to the model. See:
213-
* {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#convert}.
212+
* @param {module:engine/model/schema~SchemaContextDefinition} [context=['$root']] Base context in which the view will
213+
* be converted to the model. See: {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#convert}.
214214
* @returns {module:engine/model/documentfragment~DocumentFragment} Parsed data.
215215
*/
216-
parse( data, context = '$root' ) {
216+
parse( data, context = [ '$root' ] ) {
217217
// data -> view
218218
const viewDocumentFragment = this.processor.toView( data );
219219

@@ -231,12 +231,12 @@ export default class DataController {
231231
*
232232
* @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} viewElementOrFragment
233233
* Element or document fragment whose content will be converted.
234-
* @param {String} [context='$root'] Base context in which the view will be converted to the model. See:
235-
* {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#convert}.
234+
* @param {module:engine/model/schema~SchemaContextDefinition} [context=['$root']] Base context in which the view will
235+
* be converted to the model. See: {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#convert}.
236236
* @returns {module:engine/model/documentfragment~DocumentFragment} Output document fragment.
237237
*/
238-
toModel( viewElementOrFragment, context = '$root' ) {
239-
return this.viewToModel.convert( viewElementOrFragment, { context: [ context ] } );
238+
toModel( viewElementOrFragment, context = [ '$root' ] ) {
239+
return this.viewToModel.convert( viewElementOrFragment, context );
240240
}
241241

242242
/**

src/conversion/buildviewconverter.js

Lines changed: 72 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99

1010
import Matcher from '../view/matcher';
1111
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
12-
import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
12+
import Position from '../model/position';
13+
import Range from '../model/range';
1314

1415
/**
1516
* Provides chainable, high-level API to easily build basic view-to-model converters that are appended to given
@@ -269,12 +270,12 @@ class ViewConverterBuilder {
269270
*/
270271
toElement( element ) {
271272
function eventCallbackGen( from ) {
272-
return ( evt, data, consumable, conversionApi ) => {
273+
return ( evt, data, conversionApi ) => {
273274
const writer = conversionApi.writer;
274275

275276
// There is one callback for all patterns in the matcher.
276277
// This will be usually just one pattern but we support matchers with many patterns too.
277-
const matchAll = from.matcher.matchAll( data.input );
278+
const matchAll = from.matcher.matchAll( data.viewItem );
278279

279280
// If there is no match, this callback should not do anything.
280281
if ( !matchAll ) {
@@ -284,38 +285,60 @@ class ViewConverterBuilder {
284285
// Now, for every match between matcher and actual element, we will try to consume the match.
285286
for ( const match of matchAll ) {
286287
// Create model element basing on creator function or element name.
287-
const modelElement = element instanceof Function ? element( data.input, writer ) : writer.createElement( element );
288+
const modelElement = element instanceof Function ? element( data.viewItem, writer ) : writer.createElement( element );
288289

289290
// Do not convert if element building function returned falsy value.
290291
if ( !modelElement ) {
291292
continue;
292293
}
293294

294-
if ( !conversionApi.schema.checkChild( data.context, modelElement ) ) {
295+
// When element was already consumed then skip it.
296+
if ( !conversionApi.consumable.test( data.viewItem, from.consume || match.match ) ) {
295297
continue;
296298
}
297299

298-
// Try to consume appropriate values from consumable values list.
299-
if ( !consumable.consume( data.input, from.consume || match.match ) ) {
300+
// Find allowed parent for element that we are going to insert.
301+
// If current parent does not allow to insert element but one of the ancestors does
302+
// then split nodes to allowed parent.
303+
const splitResult = conversionApi.splitToAllowedParent( modelElement, data.cursorPosition );
304+
305+
// When there is no split result it means that we can't insert element to model tree, so let's skip it.
306+
if ( !splitResult ) {
300307
continue;
301308
}
302309

303-
// If everything is fine, we are ready to start the conversion.
304-
// Add newly created `modelElement` to the parents stack.
305-
data.context.push( modelElement );
310+
// Insert element on allowed position.
311+
conversionApi.writer.insert( modelElement, splitResult.position );
306312

307-
// Convert children of converted view element and append them to `modelElement`.
308-
const modelChildren = conversionApi.convertChildren( data.input, consumable, data );
313+
// Convert children and insert to element.
314+
const childrenResult = conversionApi.convertChildren( data.viewItem, Position.createAt( modelElement ) );
309315

310-
for ( const child of Array.from( modelChildren ) ) {
311-
writer.append( child, modelElement );
312-
}
316+
// Consume appropriate value from consumable values list.
317+
conversionApi.consumable.consume( data.viewItem, from.consume || match.match );
318+
319+
// Set conversion result range.
320+
data.modelRange = new Range(
321+
// Range should start before inserted element
322+
Position.createBefore( modelElement ),
323+
// Should end after but we need to take into consideration that children could split our
324+
// element, so we need to move range after parent of the last converted child.
325+
// before: <allowed>[]</allowed>
326+
// after: <allowed>[<converted><child></child></converted><child></child><converted>]</converted></allowed>
327+
Position.createAfter( childrenResult.cursorPosition.parent )
328+
);
313329

314-
// Remove created `modelElement` from the parents stack.
315-
data.context.pop();
330+
// Now we need to check where the cursorPosition should be.
331+
// If we had to split parent to insert our element then we want to continue conversion inside split parent.
332+
//
333+
// before: <allowed><notAllowed>[]</notAllowed></allowed>
334+
// after: <allowed><notAllowed></notAllowed><converted></converted><notAllowed>[]</notAllowed></allowed>
335+
if ( splitResult.cursorParent ) {
336+
data.cursorPosition = Position.createAt( splitResult.cursorParent );
316337

317-
// Add `modelElement` as a result.
318-
data.output = modelElement;
338+
// Otherwise just continue after inserted element.
339+
} else {
340+
data.cursorPosition = data.modelRange.end;
341+
}
319342

320343
// Prevent multiple conversion if there are other correct matches.
321344
break;
@@ -345,10 +368,10 @@ class ViewConverterBuilder {
345368
*/
346369
toAttribute( keyOrCreator, value ) {
347370
function eventCallbackGen( from ) {
348-
return ( evt, data, consumable, conversionApi ) => {
371+
return ( evt, data, conversionApi ) => {
349372
// There is one callback for all patterns in the matcher.
350373
// This will be usually just one pattern but we support matchers with many patterns too.
351-
const matchAll = from.matcher.matchAll( data.input );
374+
const matchAll = from.matcher.matchAll( data.viewItem );
352375

353376
// If there is no match, this callback should not do anything.
354377
if ( !matchAll ) {
@@ -358,34 +381,39 @@ class ViewConverterBuilder {
358381
// Now, for every match between matcher and actual element, we will try to consume the match.
359382
for ( const match of matchAll ) {
360383
// Try to consume appropriate values from consumable values list.
361-
if ( !consumable.consume( data.input, from.consume || match.match ) ) {
384+
if ( !conversionApi.consumable.consume( data.viewItem, from.consume || match.match ) ) {
362385
continue;
363386
}
364387

365-
// Since we are converting to attribute we need an output on which we will set the attribute.
366-
// If the output is not created yet, we will create it.
367-
if ( !data.output ) {
368-
data.output = conversionApi.convertChildren( data.input, consumable, data );
388+
// Since we are converting to attribute we need an range on which we will set the attribute.
389+
// If the range is not created yet, we will create it.
390+
if ( !data.modelRange ) {
391+
// Convert children and set conversion result as a current data.
392+
data = Object.assign( data, conversionApi.convertChildren( data.viewItem, data.cursorPosition ) );
369393
}
370394

371395
// Use attribute creator function, if provided.
372396
let attribute;
373397

374398
if ( keyOrCreator instanceof Function ) {
375-
attribute = keyOrCreator( data.input );
399+
attribute = keyOrCreator( data.viewItem );
376400

377401
if ( !attribute ) {
378402
return;
379403
}
380404
} else {
381405
attribute = {
382406
key: keyOrCreator,
383-
value: value ? value : data.input.getAttribute( from.attributeKey )
407+
value: value ? value : data.viewItem.getAttribute( from.attributeKey )
384408
};
385409
}
386410

387-
// Set attribute on current `output`. `Schema` is checked inside this helper function.
388-
setAttributeOn( data.output, attribute, data, conversionApi );
411+
// Set attribute on each item in range according to Schema.
412+
for ( const node of Array.from( data.modelRange.getItems() ) ) {
413+
if ( conversionApi.schema.checkAttribute( node, attribute.key ) ) {
414+
conversionApi.writer.setAttribute( attribute.key, attribute.value, node );
415+
}
416+
}
389417

390418
// Prevent multiple conversion if there are other correct matches.
391419
break;
@@ -431,12 +459,12 @@ class ViewConverterBuilder {
431459
*/
432460
toMarker( creator ) {
433461
function eventCallbackGen( from ) {
434-
return ( evt, data, consumable, conversionApi ) => {
462+
return ( evt, data, conversionApi ) => {
435463
const writer = conversionApi.writer;
436464

437465
// There is one callback for all patterns in the matcher.
438466
// This will be usually just one pattern but we support matchers with many patterns too.
439-
const matchAll = from.matcher.matchAll( data.input );
467+
const matchAll = from.matcher.matchAll( data.viewItem );
440468

441469
// If there is no match, this callback should not do anything.
442470
if ( !matchAll ) {
@@ -447,10 +475,10 @@ class ViewConverterBuilder {
447475

448476
// When creator is provided then create model element basing on creator function.
449477
if ( creator instanceof Function ) {
450-
modelElement = creator( data.input );
478+
modelElement = creator( data.viewItem );
451479
// When there is no creator then create model element basing on data from view element.
452480
} else {
453-
modelElement = writer.createElement( '$marker', { 'data-name': data.input.getAttribute( 'data-name' ) } );
481+
modelElement = writer.createElement( '$marker', { 'data-name': data.viewItem.getAttribute( 'data-name' ) } );
454482
}
455483

456484
// Check if model element is correct (has proper name and property).
@@ -463,11 +491,19 @@ class ViewConverterBuilder {
463491
// Now, for every match between matcher and actual element, we will try to consume the match.
464492
for ( const match of matchAll ) {
465493
// Try to consume appropriate values from consumable values list.
466-
if ( !consumable.consume( data.input, from.consume || match.match ) ) {
494+
if ( !conversionApi.consumable.consume( data.viewItem, from.consume || match.match ) ) {
467495
continue;
468496
}
469497

470-
data.output = modelElement;
498+
// Tmp fix because multiple matchers are not properly matched and consumed.
499+
// See https://github.com/ckeditor/ckeditor5-engine/issues/1257.
500+
if ( data.modelRange ) {
501+
continue;
502+
}
503+
504+
writer.insert( modelElement, data.cursorPosition );
505+
data.modelRange = Range.createOn( modelElement );
506+
data.cursorPosition = data.modelRange.end;
471507

472508
// Prevent multiple conversion if there are other correct matches.
473509
break;
@@ -504,22 +540,6 @@ class ViewConverterBuilder {
504540
}
505541
}
506542

507-
// Helper function that sets given attributes on given `module:engine/model/node~Node` or
508-
// `module:engine/model/documentfragment~DocumentFragment`.
509-
function setAttributeOn( toChange, attribute, data, conversionApi ) {
510-
if ( isIterable( toChange ) ) {
511-
for ( const node of toChange ) {
512-
setAttributeOn( node, attribute, data, conversionApi );
513-
}
514-
515-
return;
516-
}
517-
518-
if ( conversionApi.schema.checkAttribute( toChange, attribute.key ) ) {
519-
conversionApi.writer.setAttribute( attribute.key, attribute.value, toChange );
520-
}
521-
}
522-
523543
/**
524544
* Entry point for view-to-model converters builder. This chainable API makes it easy to create basic, most common
525545
* view-to-model converters and attach them to provided dispatchers. The method returns an instance of

src/conversion/view-to-model-converters.js

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
* For licensing, see LICENSE.md.
44
*/
55

6+
import Range from '../model/range';
7+
68
/**
79
* Contains {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for
810
* {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher}.
@@ -26,10 +28,11 @@
2628
* {@link module:engine/model/documentfragment~DocumentFragment model fragment} with children of converted view item.
2729
*/
2830
export function convertToModelFragment() {
29-
return ( evt, data, consumable, conversionApi ) => {
31+
return ( evt, data, conversionApi ) => {
3032
// Second argument in `consumable.consume` is discarded for ViewDocumentFragment but is needed for ViewElement.
31-
if ( !data.output && consumable.consume( data.input, { name: true } ) ) {
32-
data.output = conversionApi.convertChildren( data.input, consumable, data );
33+
if ( !data.modelRange && conversionApi.consumable.consume( data.viewItem, { name: true } ) ) {
34+
data = Object.assign( data, conversionApi.convertChildren( data.viewItem, data.cursorPosition ) );
35+
data.cursorPosition = data.modelRange.end;
3336
}
3437
};
3538
}
@@ -40,10 +43,15 @@ export function convertToModelFragment() {
4043
* @returns {Function} {@link module:engine/view/text~Text View text} converter.
4144
*/
4245
export function convertText() {
43-
return ( evt, data, consumable, conversionApi ) => {
44-
if ( conversionApi.schema.checkChild( data.context, '$text' ) ) {
45-
if ( consumable.consume( data.input ) ) {
46-
data.output = conversionApi.writer.createText( data.input.data );
46+
return ( evt, data, conversionApi ) => {
47+
if ( conversionApi.schema.checkChild( data.cursorPosition, '$text' ) ) {
48+
if ( conversionApi.consumable.consume( data.viewItem ) ) {
49+
const text = conversionApi.writer.createText( data.viewItem.data );
50+
51+
conversionApi.writer.insert( text, data.cursorPosition );
52+
53+
data.modelRange = Range.createFromPositionAndShift( data.cursorPosition, text.offsetSize );
54+
data.cursorPosition = data.modelRange.end;
4755
}
4856
}
4957
};

0 commit comments

Comments
 (0)