From 8e344050d4bafedf29bfc42bec795421d49d52cc Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Tue, 27 Nov 2018 14:26:49 +0100 Subject: [PATCH 01/84] Changed name of event during selection attributes conversion. --- src/conversion/downcastdispatcher.js | 2 +- tests/conversion/downcastdispatcher.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/conversion/downcastdispatcher.js b/src/conversion/downcastdispatcher.js index 217676f05..5163487e4 100644 --- a/src/conversion/downcastdispatcher.js +++ b/src/conversion/downcastdispatcher.js @@ -294,7 +294,7 @@ export default class DowncastDispatcher { // Do not fire event if the attribute has been consumed. if ( this.conversionApi.consumable.test( selection, 'attribute:' + data.attributeKey ) ) { - this.fire( 'attribute:' + data.attributeKey, data, this.conversionApi ); + this.fire( 'attribute:' + data.attributeKey + ':$text', data, this.conversionApi ); } } diff --git a/tests/conversion/downcastdispatcher.js b/tests/conversion/downcastdispatcher.js index 05568471d..7bd63ca8f 100644 --- a/tests/conversion/downcastdispatcher.js +++ b/tests/conversion/downcastdispatcher.js @@ -319,7 +319,7 @@ describe( 'DowncastDispatcher', () => { dispatcher.convertSelection( doc.selection, model.markers, [] ); - expect( dispatcher.fire.calledWith( 'attribute:bold' ) ).to.be.true; + expect( dispatcher.fire.calledWith( 'attribute:bold:$text' ) ).to.be.true; } ); it( 'should not fire attributes events if attribute has been consumed', () => { From 3b19330db383bb19f256f322fa67b02c9f6ad437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 6 Dec 2018 13:51:14 +0100 Subject: [PATCH 02/84] Update selection post-fixer tests to reflect tables structure. --- tests/model/utils/selection-post-fixer.js | 194 +++++++++++++++++----- 1 file changed, 150 insertions(+), 44 deletions(-) diff --git a/tests/model/utils/selection-post-fixer.js b/tests/model/utils/selection-post-fixer.js index 60e25898a..2a45c778f 100644 --- a/tests/model/utils/selection-post-fixer.js +++ b/tests/model/utils/selection-post-fixer.js @@ -27,8 +27,9 @@ describe( 'Selection post-fixer', () => { model.schema.register( 'table', { allowWhere: '$block', - isObject: true, - isLimit: true + allowAttributes: [ 'headingRows', 'headingColumns' ], + isLimit: true, + isObject: true } ); model.schema.register( 'tableRow', { @@ -38,15 +39,18 @@ describe( 'Selection post-fixer', () => { model.schema.register( 'tableCell', { allowIn: 'tableRow', - allowContentOf: '$block', + allowAttributes: [ 'colspan', 'rowspan' ], isLimit: true } ); model.schema.register( 'image', { - allowIn: '$root', - isObject: true + isObject: true, + isBlock: true, + allowWhere: '$block' } ); + model.schema.extend( '$block', { allowIn: 'tableCell' } ); + model.schema.register( 'caption', { allowIn: 'image', allowContentOf: '$block', @@ -98,13 +102,16 @@ describe( 'Selection post-fixer', () => { setModelData( model, '[]foo' + '' + - 'aaabbb' + + '' + + 'aaa' + + 'bbb' + + '' + '
' + 'bar' ); } ); - it( 'should fix #1', () => { + it( 'should fix #1 - range start outside table, end on table cell', () => { // f[oo]... model.change( writer => { writer.setSelection( writer.createRange( @@ -116,13 +123,16 @@ describe( 'Selection post-fixer', () => { expect( getModelData( model ) ).to.equal( 'f[oo' + '
' + - 'aaabbb' + + '' + + 'aaa' + + 'bbb' + + '' + '
]' + 'bar' ); } ); - it( 'should fix #2', () => { + it( 'should fix #2 - range start on table cell, end outside table', () => { // ...[
b]ar model.change( writer => { writer.setSelection( writer.createRange( @@ -134,7 +144,10 @@ describe( 'Selection post-fixer', () => { expect( getModelData( model ) ).to.equal( 'foo' + '[' + - 'aaabbb' + + '' + + 'aaa' + + 'bbb' + + '' + '
' + 'b]ar' ); @@ -152,7 +165,10 @@ describe( 'Selection post-fixer', () => { expect( getModelData( model ) ).to.equal( 'f[oo' + '' + - 'aaabbb' + + '' + + 'aaa' + + 'bbb' + + '' + '
]' + 'bar' ); @@ -162,15 +178,18 @@ describe( 'Selection post-fixer', () => { // fooa[aab]bb model.change( writer => { writer.setSelection( writer.createRange( - writer.createPositionAt( modelRoot.getChild( 1 ).getChild( 0 ).getChild( 0 ), 1 ), - writer.createPositionAt( modelRoot.getChild( 1 ).getChild( 0 ).getChild( 1 ), 2 ) + writer.createPositionAt( modelRoot.getNodeByPath( [ 1, 0, 0, 0 ] ), 1 ), + writer.createPositionAt( modelRoot.getNodeByPath( [ 1, 0, 1, 0 ] ), 2 ) ) ); } ); expect( getModelData( model ) ).to.equal( 'foo' + '[
' + - 'aaabbb' + + '' + + 'aaa' + + 'bbb' + + '' + '
]' + 'bar' ); @@ -180,11 +199,17 @@ describe( 'Selection post-fixer', () => { setModelData( model, 'foo' + '' + - 'aaabbb' + + '' + + 'aaa' + + 'bbb' + + '' + '
' + '[]' + '' + - 'xxxyyy' + + '' + + 'xxx' + + 'yyy' + + '' + '
' + 'baz' ); @@ -192,10 +217,16 @@ describe( 'Selection post-fixer', () => { expect( getModelData( model ) ).to.equal( 'foo' + '[' + - 'aaabbb' + + '' + + 'aaa' + + 'bbb' + + '' + '
]' + '' + - 'xxxyyy' + + '' + + 'xxx' + + 'yyy' + + '' + '
' + 'baz' ); @@ -208,7 +239,10 @@ describe( 'Selection post-fixer', () => { setModelData( model, 'foo' + '' + - '[aaabbb]' + + '[' + + 'aaa' + + 'bbb' + + ']' + '
' + 'baz' ); @@ -216,7 +250,10 @@ describe( 'Selection post-fixer', () => { expect( getModelData( model ) ).to.equal( 'foo' + '[' + - 'aaabbb' + + '' + + 'aaa' + + 'bbb' + + '' + '
]' + 'baz' ); @@ -226,9 +263,18 @@ describe( 'Selection post-fixer', () => { setModelData( model, 'foo' + '' + - '[12' + - '34]' + - '56' + + '[' + + '1' + + '2' + + '' + + '' + + '3' + + '4]' + + '' + + '' + + '5' + + '6' + + '' + '
' + 'baz' ); @@ -236,9 +282,18 @@ describe( 'Selection post-fixer', () => { expect( getModelData( model ) ).to.equal( 'foo' + '[' + - '12' + - '34' + - '56' + + '' + + '1' + + '2' + + '' + + '' + + '3' + + '4' + + '' + + '' + + '5' + + '6' + + '' + '
]' + 'baz' ); @@ -270,11 +325,14 @@ describe( 'Selection post-fixer', () => { ); } ); - it( 'should not fix #1', () => { + it( 'should not fix #1 - selection over paragraphs outside table', () => { setModelData( model, 'foo' + '' + - 'aaabbb' + + '' + + 'aaa' + + 'bbb' + + '' + '
' + 'b[ar' + 'ba]z' @@ -283,7 +341,10 @@ describe( 'Selection post-fixer', () => { expect( getModelData( model ) ).to.equal( 'foo' + '' + - 'aaabbb' + + '' + + 'aaa' + + 'bbb' + + '' + '
' + 'b[ar' + 'ba]z' @@ -308,7 +369,10 @@ describe( 'Selection post-fixer', () => { expect( getModelData( model ) ).to.equal( 'f[oo' + '' + - 'aaabbb' + + '' + + 'aaa' + + 'bbb' + + '' + '
]' + 'bar' ); @@ -333,7 +397,10 @@ describe( 'Selection post-fixer', () => { expect( getModelData( model ) ).to.equal( 'f[oo' + '' + - 'aaabbb' + + '' + + 'aaa' + + 'bbb' + + '' + '
' + 'ba]r' ); @@ -343,10 +410,22 @@ describe( 'Selection post-fixer', () => { setModelData( model, 'foo' + '' + - '[aaabbb' + - '][aaabbb' + - '][aaabbb' + - '][aaabbb' + + '' + + '[aaa' + + 'bbb' + + '' + + ']' + + '[aaa' + + 'bbb' + + '' + + ']' + + '[aaa' + + 'bbb' + + '' + + ']' + + '[aaa' + + 'bbb' + + '' + '
' + 'b]az' ); @@ -354,10 +433,22 @@ describe( 'Selection post-fixer', () => { expect( getModelData( model ) ).to.equal( 'foo' + '[' + - 'aaabbb' + - 'aaabbb' + - 'aaabbb' + - 'aaabbb' + + '' + + 'aaa' + + 'bbb' + + '' + + '' + + 'aaa' + + 'bbb' + + '' + + '' + + 'aaa' + + 'bbb' + + '' + + '' + + 'aaa' + + 'bbb' + + '' + '
' + 'b]az' ); @@ -386,7 +477,10 @@ describe( 'Selection post-fixer', () => { expect( getModelData( model ) ).to.equal( 'f[oo' + '' + - 'aaabbb' + + '' + + 'aaa' + + 'bbb' + + '' + '
' + 'bar]' ); @@ -722,7 +816,10 @@ describe( 'Selection post-fixer', () => { setModelData( model, '[]foo' + '' + - 'aaabbb' + + '' + + 'aaa' + + 'bbb' + + '' + '
' + 'bar' ); @@ -739,7 +836,10 @@ describe( 'Selection post-fixer', () => { expect( getModelData( model ) ).to.equal( 'foo[]' + '' + - 'aaabbb' + + '' + + 'aaa' + + 'bbb' + + '' + '
' + 'bar' ); @@ -758,7 +858,10 @@ describe( 'Selection post-fixer', () => { expect( getModelData( model ) ).to.equal( 'foo' + '' + - '[]aaabbb' + + '' + + '[]aaa' + + 'bbb' + + '' + '
' + 'bar' ); @@ -778,7 +881,10 @@ describe( 'Selection post-fixer', () => { expect( getModelData( model ) ).to.equal( '[foo]' + '' + - 'aaabbb' + + '' + + 'aaa' + + 'bbb' + + '' + '
' + 'bar' ); From efa318b32e664eec6589d92981fd8b61d95189ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 6 Dec 2018 14:28:08 +0100 Subject: [PATCH 03/84] Remove redundant plugins from table manual test. --- src/model/utils/selection-post-fixer.js | 47 ++++++++++++++++------- tests/model/utils/selection-post-fixer.js | 28 ++++++++++++++ 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/src/model/utils/selection-post-fixer.js b/src/model/utils/selection-post-fixer.js index abfa23fea..dc9da0391 100644 --- a/src/model/utils/selection-post-fixer.js +++ b/src/model/utils/selection-post-fixer.js @@ -159,6 +159,7 @@ function tryFixingCollapsedRange( range, schema ) { // // @param {module:engine/model/range~Range} range Expanded range to fix. // @param {module:engine/model/schema~Schema} schema + // @returns {module:engine/model/range~Range|null} Returns fixed range or null if range is valid. function tryFixingNonCollapsedRage( range, schema ) { const start = range.start; @@ -197,13 +198,23 @@ function tryFixingNonCollapsedRage( range, schema ) { // At this point we eliminated valid positions on text nodes so if one of range positions is placed inside a limit element // then the range crossed limit element boundaries and needs to be fixed. if ( isStartInLimit || isEndInLimit ) { + const bothInSameParent = ( !!start.nodeAfter && !!end.nodeBefore ) && start.nodeAfter === end.nodeBefore; + + const expandStart = isStartInLimit && ( !bothInSameParent || !isInObject( start.nodeAfter, schema ) ); + const expandEnd = isEndInLimit && ( !bothInSameParent || !isInObject( end.nodeBefore, schema ) ); + // Although we've already found limit element on start/end positions we must find the outer-most limit element. // as limit elements might be nested directly inside (ie table > tableRow > tableCell). - const startPosition = Position._createAt( startLimitElement, 0 ); - const endPosition = Position._createAt( endLimitElement, 0 ); + let fixedStart = start; + let fixedEnd = end; - const fixedStart = isStartInLimit ? expandSelectionOnIsLimitNode( startPosition, schema, 'start' ) : start; - const fixedEnd = isEndInLimit ? expandSelectionOnIsLimitNode( endPosition, schema, 'end' ) : end; + if ( expandStart ) { + fixedStart = Position._createBefore( findOuterMostIsLimitAncestor( startLimitElement, schema ) ); + } + + if ( expandEnd ) { + fixedEnd = Position._createAfter( findOuterMostIsLimitAncestor( endLimitElement, schema ) ); + } return new Range( fixedStart, fixedEnd ); } @@ -212,24 +223,23 @@ function tryFixingNonCollapsedRage( range, schema ) { return null; } -// Expands selection so it contains whole limit node. +// Finds the outer-most ancestor. // -// @param {module:engine/model/position~Position} position +// @param {module:engine/model/node~Node} startingNode // @param {module:engine/model/schema~Schema} schema // @param {String} expandToDirection Direction of expansion - either 'start' or 'end' of the range. -// @returns {module:engine/model/position~Position} -function expandSelectionOnIsLimitNode( position, schema, expandToDirection ) { - let node = position.parent; - let parent = node; +// @returns {module:engine/model/node~Node} +function findOuterMostIsLimitAncestor( startingNode, schema ) { + let isLimitNode = startingNode; + let parent = isLimitNode; // Find outer most isLimit block as such blocks might be nested (ie. in tables). while ( schema.isLimit( parent ) && parent.parent ) { - node = parent; + isLimitNode = parent; parent = parent.parent; } - // Depending on direction of expanding selection return position before or after found node. - return expandToDirection === 'start' ? Position._createBefore( node ) : Position._createAfter( node ); + return isLimitNode; } // Checks whether both range ends are placed around non-limit elements. @@ -237,9 +247,20 @@ function expandSelectionOnIsLimitNode( position, schema, expandToDirection ) { // @param {module:engine/model/position~Position} start // @param {module:engine/model/position~Position} end // @param {module:engine/model/schema~Schema} schema +// @returns {Boolean} function checkSelectionOnNonLimitElements( start, end, schema ) { const startIsOnBlock = ( start.nodeAfter && !schema.isLimit( start.nodeAfter ) ) || schema.checkChild( start, '$text' ); const endIsOnBlock = ( end.nodeBefore && !schema.isLimit( end.nodeBefore ) ) || schema.checkChild( end, '$text' ); return startIsOnBlock && endIsOnBlock; } + +// Checks if node exists and if it's an object. +// +// @param {module:engine/model/node~Node} node +// @param {module:engine/model/schema~Schema} schema +// @returns {Boolean} +function isInObject( node, schema ) { + return node && schema.isObject( node ); +} + diff --git a/tests/model/utils/selection-post-fixer.js b/tests/model/utils/selection-post-fixer.js index 2a45c778f..0addabea3 100644 --- a/tests/model/utils/selection-post-fixer.js +++ b/tests/model/utils/selection-post-fixer.js @@ -351,6 +351,34 @@ describe( 'Selection post-fixer', () => { ); } ); + it( 'should not fix #2 - selection over image in table', () => { + setModelData( model, + 'foo' + + '' + + '' + + 'foo' + + '[]bbb' + + '' + + '
' + ); + + model.change( writer => { + const image = model.document.getRoot().getNodeByPath( [ 1, 0, 0, 1 ] ); + + writer.setSelection( writer.createRangeOn( image ) ); + } ); + + expect( getModelData( model ) ).to.equal( + 'foo' + + '' + + '' + + 'foo[]' + + 'bbb' + + '' + + '
' + ); + } ); + it( 'should fix multiple ranges #1', () => { model.change( writer => { const ranges = [ From ecffa73dd1c911b9cb81ce5fe233c47992cf7700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 6 Dec 2018 18:21:57 +0100 Subject: [PATCH 04/84] Make selection post-fixer should allow selection of an objects. --- src/model/utils/selection-post-fixer.js | 13 ++++-- tests/model/utils/selection-post-fixer.js | 56 +++++++++++++++++++++++ 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/src/model/utils/selection-post-fixer.js b/src/model/utils/selection-post-fixer.js index dc9da0391..99ced7437 100644 --- a/src/model/utils/selection-post-fixer.js +++ b/src/model/utils/selection-post-fixer.js @@ -184,9 +184,14 @@ function tryFixingNonCollapsedRage( range, schema ) { // - [foo] -> [foo] // - [foo] -> [foo] // - f[oo] -> f[oo] + // TODO: make it nicer + // - [foo] -> [foo] if ( checkSelectionOnNonLimitElements( start, end, schema ) ) { - const fixedStart = schema.getNearestSelectionRange( start, 'forward' ); - const fixedEnd = schema.getNearestSelectionRange( end, 'backward' ); + const isStartObject = start.nodeAfter && schema.isObject( start.nodeAfter ); + const fixedStart = isStartObject ? null : schema.getNearestSelectionRange( start, 'forward' ); + + const isEndObject = end.nodeBefore && schema.isObject( end.nodeBefore ); + const fixedEnd = isEndObject ? null : schema.getNearestSelectionRange( end, 'backward' ); return new Range( fixedStart ? fixedStart.start : start, fixedEnd ? fixedEnd.start : end ); } @@ -249,8 +254,8 @@ function findOuterMostIsLimitAncestor( startingNode, schema ) { // @param {module:engine/model/schema~Schema} schema // @returns {Boolean} function checkSelectionOnNonLimitElements( start, end, schema ) { - const startIsOnBlock = ( start.nodeAfter && !schema.isLimit( start.nodeAfter ) ) || schema.checkChild( start, '$text' ); - const endIsOnBlock = ( end.nodeBefore && !schema.isLimit( end.nodeBefore ) ) || schema.checkChild( end, '$text' ); + const startIsOnBlock = ( start.nodeAfter && schema.isBlock( start.nodeAfter ) ) || schema.checkChild( start, '$text' ); + const endIsOnBlock = ( end.nodeBefore && schema.isBlock( end.nodeBefore ) ) || schema.checkChild( end, '$text' ); return startIsOnBlock && endIsOnBlock; } diff --git a/tests/model/utils/selection-post-fixer.js b/tests/model/utils/selection-post-fixer.js index 0addabea3..3fc26c609 100644 --- a/tests/model/utils/selection-post-fixer.js +++ b/tests/model/utils/selection-post-fixer.js @@ -379,6 +379,62 @@ describe( 'Selection post-fixer', () => { ); } ); + it( 'should not fix #3 - selection over paragraph & image in table', () => { + setModelData( model, + 'foo' + + '' + + '' + + 'foo' + + '[]bbb' + + '' + + '
' + ); + + model.change( writer => { + const tableCell = model.document.getRoot().getNodeByPath( [ 1, 0, 0 ] ); + + writer.setSelection( writer.createRangeIn( tableCell ) ); + } ); + + expect( getModelData( model ) ).to.equal( + 'foo' + + '' + + '' + + '[foo]' + + 'bbb' + + '' + + '
' + ); + } ); + + it( 'should not fix #3 - selection over image & paragraph in table', () => { + setModelData( model, + 'foo' + + '' + + '' + + 'foo' + + '[]bbb' + + '' + + '
' + ); + + model.change( writer => { + const tableCell = model.document.getRoot().getNodeByPath( [ 1, 0, 0 ] ); + + writer.setSelection( writer.createRangeIn( tableCell ) ); + } ); + + expect( getModelData( model ) ).to.equal( + 'foo' + + '' + + '' + + '[foo]' + + 'bbb' + + '' + + '
' + ); + } ); + it( 'should fix multiple ranges #1', () => { model.change( writer => { const ranges = [ From 6e4794558c27ac49f11b3a2d4eec0b4cd27141b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 7 Dec 2018 11:39:23 +0100 Subject: [PATCH 05/84] Add tests for blockQuote in table in selection post-fixer tests. --- tests/model/utils/selection-post-fixer.js | 35 ++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/tests/model/utils/selection-post-fixer.js b/tests/model/utils/selection-post-fixer.js index 3fc26c609..2561fb700 100644 --- a/tests/model/utils/selection-post-fixer.js +++ b/tests/model/utils/selection-post-fixer.js @@ -407,7 +407,7 @@ describe( 'Selection post-fixer', () => { ); } ); - it( 'should not fix #3 - selection over image & paragraph in table', () => { + it( 'should not fix #4 - selection over image & paragraph in table', () => { setModelData( model, 'foo' + '' + @@ -435,6 +435,39 @@ describe( 'Selection post-fixer', () => { ); } ); + it( 'should not fix #4 - selection over blockQuote in table', () => { + model.schema.register( 'blockQuote', { + allowWhere: '$block', + allowContentOf: '$root' + } ); + + setModelData( model, + 'foo' + + '
' + + '' + + '
foo
' + + '[]bbb' + + '
' + + '
' + ); + + model.change( writer => { + const tableCell = model.document.getRoot().getNodeByPath( [ 1, 0, 0 ] ); + + writer.setSelection( writer.createRangeIn( tableCell ) ); + } ); + + expect( getModelData( model ) ).to.equal( + 'foo' + + '' + + '' + + '
[foo]
' + + 'bbb' + + '
' + + '
' + ); + } ); + it( 'should fix multiple ranges #1', () => { model.change( writer => { const ranges = [ From ef8352cb39b506514c5d34375555fc892817221f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 7 Dec 2018 12:32:49 +0100 Subject: [PATCH 06/84] Fix requirements checking on general elements in selection post-fixer. --- src/model/utils/selection-post-fixer.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/model/utils/selection-post-fixer.js b/src/model/utils/selection-post-fixer.js index 99ced7437..6bfa98c0b 100644 --- a/src/model/utils/selection-post-fixer.js +++ b/src/model/utils/selection-post-fixer.js @@ -184,8 +184,7 @@ function tryFixingNonCollapsedRage( range, schema ) { // - [foo] -> [foo] // - [foo] -> [foo] // - f[oo] -> f[oo] - // TODO: make it nicer - // - [foo] -> [foo] + // - [foo] -> [foo] if ( checkSelectionOnNonLimitElements( start, end, schema ) ) { const isStartObject = start.nodeAfter && schema.isObject( start.nodeAfter ); const fixedStart = isStartObject ? null : schema.getNearestSelectionRange( start, 'forward' ); @@ -247,17 +246,18 @@ function findOuterMostIsLimitAncestor( startingNode, schema ) { return isLimitNode; } -// Checks whether both range ends are placed around non-limit elements. +// Checks whether one of range ends is placed around non-limit elements. // // @param {module:engine/model/position~Position} start // @param {module:engine/model/position~Position} end // @param {module:engine/model/schema~Schema} schema // @returns {Boolean} function checkSelectionOnNonLimitElements( start, end, schema ) { - const startIsOnBlock = ( start.nodeAfter && schema.isBlock( start.nodeAfter ) ) || schema.checkChild( start, '$text' ); - const endIsOnBlock = ( end.nodeBefore && schema.isBlock( end.nodeBefore ) ) || schema.checkChild( end, '$text' ); + const startIsOnBlock = ( start.nodeAfter && !schema.isLimit( start.nodeAfter ) ) || schema.checkChild( start, '$text' ); + const endIsOnBlock = ( end.nodeBefore && !schema.isLimit( end.nodeBefore ) ) || schema.checkChild( end, '$text' ); - return startIsOnBlock && endIsOnBlock; + // We should fix such selection when one of those nodes needs fixing. + return startIsOnBlock || endIsOnBlock; } // Checks if node exists and if it's an object. From 1d54e748b5c6d62f0be169ed2f9943030806bb64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 7 Dec 2018 12:56:42 +0100 Subject: [PATCH 07/84] Add test case for object in object case. --- tests/model/utils/selection-post-fixer.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/model/utils/selection-post-fixer.js b/tests/model/utils/selection-post-fixer.js index 2561fb700..9a2c9ad12 100644 --- a/tests/model/utils/selection-post-fixer.js +++ b/tests/model/utils/selection-post-fixer.js @@ -435,7 +435,7 @@ describe( 'Selection post-fixer', () => { ); } ); - it( 'should not fix #4 - selection over blockQuote in table', () => { + it( 'should not fix #5 - selection over blockQuote in table', () => { model.schema.register( 'blockQuote', { allowWhere: '$block', allowContentOf: '$root' @@ -926,6 +926,25 @@ describe( 'Selection post-fixer', () => { 'fo[ob]ar' ); } ); + + it( 'should not fix #4 - object in object', () => { + model.schema.register( 'div', { + allowWhere: '$block', + isObject: true + } ); + + model.schema.extend( 'div', { allowIn: 'div' } ); + + setModelData( model, '
[
]
' ); + + model.change( writer => { + const innerDiv = model.document.getRoot().getNodeByPath( [ 0, 0 ] ); + + writer.setSelection( writer.createRangeOn( innerDiv ) ); + } ); + + expect( getModelData( model ) ).to.equal( '
[
]
' ); + } ); } ); describe( 'collapsed selection', () => { From 33aa691d08f9f59ad967082c6c1107acc87c7178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 7 Dec 2018 12:59:16 +0100 Subject: [PATCH 08/84] Simplify 'should not fix #4 - object in object' test. --- tests/model/utils/selection-post-fixer.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/model/utils/selection-post-fixer.js b/tests/model/utils/selection-post-fixer.js index 9a2c9ad12..44311b627 100644 --- a/tests/model/utils/selection-post-fixer.js +++ b/tests/model/utils/selection-post-fixer.js @@ -929,12 +929,10 @@ describe( 'Selection post-fixer', () => { it( 'should not fix #4 - object in object', () => { model.schema.register( 'div', { - allowWhere: '$block', + allowIn: [ '$root', 'div' ], isObject: true } ); - model.schema.extend( 'div', { allowIn: 'div' } ); - setModelData( model, '
[
]
' ); model.change( writer => { From 5ab60f7dcaeb01fdbcd58f453fdbdfe62188f302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 12 Dec 2018 13:31:54 +0100 Subject: [PATCH 09/84] Make conversion._dispatchersGroups store objects with metadata. --- src/conversion/conversion.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/conversion/conversion.js b/src/conversion/conversion.js index e3c102144..2771fab10 100644 --- a/src/conversion/conversion.js +++ b/src/conversion/conversion.js @@ -96,7 +96,12 @@ export default class Conversion { throw new CKEditorError( 'conversion-register-group-exists: Trying to register a group name that was already registered.' ); } - this._dispatchersGroups.set( groupName, dispatchers ); + const group = { + name: groupName, + dispatchers + }; + + this._dispatchersGroups.set( groupName, group ); } /** @@ -553,9 +558,7 @@ export default class Conversion { * module:engine/conversion/upcastdispatcher~UpcastDispatcher>} */ _getDispatchers( groupName ) { - const dispatchers = this._dispatchersGroups.get( groupName ); - - if ( !dispatchers ) { + if ( !this._dispatchersGroups.has( groupName ) ) { /** * Trying to add a converter to an unknown dispatchers group. * @@ -564,6 +567,8 @@ export default class Conversion { throw new CKEditorError( 'conversion-for-unknown-group: Trying to add a converter to an unknown dispatchers group.' ); } + const { dispatchers } = this._dispatchersGroups.get( groupName ); + return dispatchers; } } From 876e349530c1acd9246496aad3cdea45dc018ba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Krzto=C5=84?= Date: Wed, 12 Dec 2018 13:41:09 +0100 Subject: [PATCH 10/84] Upcast element to attribute defaults to 'low' priority instead of 'normal'. --- src/conversion/conversion.js | 2 +- src/conversion/upcast-converters.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/conversion/conversion.js b/src/conversion/conversion.js index e3c102144..55ff2d576 100644 --- a/src/conversion/conversion.js +++ b/src/conversion/conversion.js @@ -410,7 +410,7 @@ export default class Conversion { upcastElementToAttribute( { view, model, - priority: definition.priority + converterPriority: definition.priority } ) ); } diff --git a/src/conversion/upcast-converters.js b/src/conversion/upcast-converters.js index 2caabdb10..21709ebd6 100644 --- a/src/conversion/upcast-converters.js +++ b/src/conversion/upcast-converters.js @@ -151,7 +151,7 @@ export function upcastElementToAttribute( config ) { const eventName = elementName ? 'element:' + elementName : 'element'; return dispatcher => { - dispatcher.on( eventName, converter, { priority: config.converterPriority || 'normal' } ); + dispatcher.on( eventName, converter, { priority: config.converterPriority || 'low' } ); }; } From 5358880493caad4fb540953dc580837e462d8d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 12 Dec 2018 14:13:42 +0100 Subject: [PATCH 11/84] Refactor conversion.register() parameters to an options object. --- src/conversion/conversion.js | 18 ++++++++++-------- tests/conversion/conversion.js | 12 ++++++------ tests/conversion/downcast-converters.js | 2 +- tests/conversion/upcast-converters.js | 2 +- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/conversion/conversion.js b/src/conversion/conversion.js index 2771fab10..2e658a0e9 100644 --- a/src/conversion/conversion.js +++ b/src/conversion/conversion.js @@ -81,13 +81,15 @@ export default class Conversion { * If a given group name is used for the second time, the * {@link module:utils/ckeditorerror~CKEditorError `conversion-register-group-exists` error} is thrown. * - * @param {String} groupName The name for dispatchers group. - * @param {Array.} dispatchers Dispatchers to register + * @param {Object} options + * @param {String} options.name The name for dispatchers group. + * @param {module:engine/conversion/downcastdispatcher~DowncastDispatcher| + * module:engine/conversion/upcastdispatcher~UpcastDispatcher|Array.} options.dispatcher Dispatcher or array of dispatchers to register * under the given name. */ - register( groupName, dispatchers ) { - if ( this._dispatchersGroups.has( groupName ) ) { + register( options ) { + if ( this._dispatchersGroups.has( options.name ) ) { /** * Trying to register a group name that was already registered. * @@ -97,11 +99,11 @@ export default class Conversion { } const group = { - name: groupName, - dispatchers + name: options.name, + dispatchers: Array.isArray( options.dispatcher ) ? options.dispatcher : [ options.dispatcher ] }; - this._dispatchersGroups.set( groupName, group ); + this._dispatchersGroups.set( options.name, group ); } /** diff --git a/tests/conversion/conversion.js b/tests/conversion/conversion.js index 948c08a01..03bc826d8 100644 --- a/tests/conversion/conversion.js +++ b/tests/conversion/conversion.js @@ -27,15 +27,15 @@ describe( 'Conversion', () => { dispA = Symbol( 'dispA' ); dispB = Symbol( 'dispB' ); - conversion.register( 'ab', [ dispA, dispB ] ); - conversion.register( 'a', [ dispA ] ); - conversion.register( 'b', [ dispB ] ); + conversion.register( { name: 'ab', dispatcher: [ dispA, dispB ] } ); + conversion.register( { name: 'a', dispatcher: dispA } ); + conversion.register( { name: 'b', dispatcher: dispB } ); } ); describe( 'register()', () => { it( 'should throw when trying to use same group name twice', () => { expect( () => { - conversion.register( 'ab' ); + conversion.register( { name: 'ab' } ); } ).to.throw( CKEditorError, /conversion-register-group-exists/ ); } ); } ); @@ -113,8 +113,8 @@ describe( 'Conversion', () => { viewDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } ); conversion = new Conversion(); - conversion.register( 'upcast', [ viewDispatcher ] ); - conversion.register( 'downcast', [ controller.downcastDispatcher ] ); + conversion.register( { name: 'upcast', dispatcher: [ viewDispatcher ] } ); + conversion.register( { name: 'downcast', dispatcher: [ controller.downcastDispatcher ] } ); } ); describe( 'elementToElement', () => { diff --git a/tests/conversion/downcast-converters.js b/tests/conversion/downcast-converters.js index b61824d7b..bba8208bb 100644 --- a/tests/conversion/downcast-converters.js +++ b/tests/conversion/downcast-converters.js @@ -45,7 +45,7 @@ describe( 'downcast-helpers', () => { viewRoot = controller.view.document.getRoot(); conversion = new Conversion(); - conversion.register( 'downcast', [ controller.downcastDispatcher ] ); + conversion.register( { name: 'downcast', dispatcher: controller.downcastDispatcher } ); } ); describe( 'downcastElementToElement', () => { diff --git a/tests/conversion/upcast-converters.js b/tests/conversion/upcast-converters.js index 32fdb4a7b..3e70aa83c 100644 --- a/tests/conversion/upcast-converters.js +++ b/tests/conversion/upcast-converters.js @@ -49,7 +49,7 @@ describe( 'upcast-helpers', () => { upcastDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } ); conversion = new Conversion(); - conversion.register( 'upcast', [ upcastDispatcher ] ); + conversion.register( { name: 'upcast', dispatcher: upcastDispatcher } ); } ); describe( 'upcastElementToElement', () => { From 471fb485777511271e4fa36857010190396b2c22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 12 Dec 2018 15:13:14 +0100 Subject: [PATCH 12/84] Introduce conversion helpers for dispatcher groups. --- src/conversion/conversion.js | 44 +++++++++++++++++++++------ src/conversion/downcast-converters.js | 14 +++++++++ 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/src/conversion/conversion.js b/src/conversion/conversion.js index 2e658a0e9..930885b4f 100644 --- a/src/conversion/conversion.js +++ b/src/conversion/conversion.js @@ -149,18 +149,21 @@ export default class Conversion { * conversion.for( 'downcast' ).add( conversion.customConverter( 'insert:paragraph', myConverter ) ); * * @param {String} groupName The name of dispatchers group to add the converters to. - * @returns {Object} An object with the `.add()` method, providing a way to add converters. + * @returns {module:engine/conversion/downcast-converters~DowncastHelpers} + * An object with the `.add()` method, providing a way to add converters. */ for( groupName ) { - const dispatchers = this._getDispatchers( groupName ); + const { dispatchers, helpers } = this._getDispatchersGroup( groupName ); - return { + const baseRetVal = { add( conversionHelper ) { _addToDispatchers( dispatchers, conversionHelper ); return this; } }; + + return Object.assign( {}, baseRetVal, helpers ); } /** @@ -549,17 +552,16 @@ export default class Conversion { } /** - * Returns dispatchers registered under a given group name. + * Returns dispatchers group registered under a given group name. * * If the given group name has not been registered, the * {@link module:utils/ckeditorerror~CKEditorError `conversion-for-unknown-group` error} is thrown. * * @private * @param {String} groupName - * @returns {Array.} + * @returns {module:engine/conversion/conversion~DispatchersGroup} */ - _getDispatchers( groupName ) { + _getDispatchersGroup( groupName ) { if ( !this._dispatchersGroups.has( groupName ) ) { /** * Trying to add a converter to an unknown dispatchers group. @@ -569,9 +571,7 @@ export default class Conversion { throw new CKEditorError( 'conversion-for-unknown-group: Trying to add a converter to an unknown dispatchers group.' ); } - const { dispatchers } = this._dispatchersGroups.get( groupName ); - - return dispatchers; + return this._dispatchersGroups.get( groupName ); } } @@ -592,6 +592,30 @@ export default class Conversion { * @property {module:utils/priorities~PriorityString} [converterPriority] The converter priority. */ +/** + * @typedef {Object} module:engine/conversion/conversion~DispatchersGroup + * @property {String} name Group name + * @property {Array.} dispatchers + * @property {module:engine/conversion/downcast-converters~DowncastHelpers} helpers + */ + +/** + * Base class for conversion utilises. + * + * @interface ConversionHelpers + */ + +/** + * Registers a conversion helper. + * + * **Note**: See full usage example in the `{@link module:engine/conversion/conversion~Conversion#for conversion.for()}` method description + * + * @method module:engine/conversion/conversion~ConversionHelpers#add + * @param {Function} conversionHelper The function to be called on event. + * @returns {module:engine/conversion/conversion~Conversion} + */ + // Helper function for the `Conversion` `.add()` method. // // Calls `conversionHelper` on each dispatcher from the group specified earlier in the `.for()` call, effectively diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index e9b9c1e5a..abe92f2c9 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -1103,3 +1103,17 @@ export function createViewElementFromHighlightDescriptor( descriptor ) { * attribute element. If the descriptor is applied to an element, usually these attributes will be set on that element, however, * this depends on how the element converts the descriptor. */ + +/** + * Downcast conversion helper functions. + * + * @typedef {Object} DowncastHelpers + */ +export const helpers = { + elementToElement: downcastElementToElement, + attributeToElement: downcastAttributeToElement, + attributeToAttribute: downcastAttributeToAttribute, + markerToElement: downcastMarkerToElement, + markerToHighlight: downcastMarkerToHighlight, + createViewElementFromHighlightDescriptor +}; From 26f51e42823a614d3655fb7e951ea8f12d411542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 12 Dec 2018 16:10:22 +0100 Subject: [PATCH 13/84] Introduce conversion.for( 'downcast' ).elementToElement() helper. --- src/conversion/conversion.js | 3 +- src/conversion/downcast-converters.js | 95 +++++++++++++++++---------- tests/conversion/conversion.js | 23 ++++++- 3 files changed, 84 insertions(+), 37 deletions(-) diff --git a/src/conversion/conversion.js b/src/conversion/conversion.js index 930885b4f..bf583d82f 100644 --- a/src/conversion/conversion.js +++ b/src/conversion/conversion.js @@ -100,7 +100,8 @@ export default class Conversion { const group = { name: options.name, - dispatchers: Array.isArray( options.dispatcher ) ? options.dispatcher : [ options.dispatcher ] + dispatchers: Array.isArray( options.dispatcher ) ? options.dispatcher : [ options.dispatcher ], + helpers: options.helpers }; this._dispatchersGroups.set( options.name, group ); diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index abe92f2c9..5dc67a834 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -36,10 +36,10 @@ import { cloneDeep } from 'lodash-es'; * } * } ); * - * downcastElementToElement( { - * model: 'heading', - * view: ( modelElement, viewWriter ) => viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) ) - * } ); + * downcastElementToElement( { + * model: 'heading', + * view: ( modelElement, viewWriter ) => viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) ) + * } ); * * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter * to the conversion process. @@ -100,12 +100,12 @@ export function downcastElementToElement( config ) { * } * } ); * - * downcastAttributeToElement( { - * model: 'bold', - * view: ( modelAttributeValue, viewWriter ) => { - * return viewWriter.createAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } ); - * } - * } ); + * downcastAttributeToElement( { + * model: 'bold', + * view: ( modelAttributeValue, viewWriter ) => { + * return viewWriter.createAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } ); + * } + * } ); * * downcastAttributeToElement( { * model: { @@ -255,12 +255,12 @@ export function downcastAttributeToAttribute( config ) { * } * } ); * - * downcastMarkerToElement( { - * model: 'search', - * view: ( markerData, viewWriter ) => { - * return viewWriter.createUIElement( 'span', { 'data-marker': 'search', 'data-start': markerData.isOpening } ); - * } - * } ); + * downcastMarkerToElement( { + * model: 'search', + * view: ( markerData, viewWriter ) => { + * return viewWriter.createUIElement( 'span', { 'data-marker': 'search', 'data-start': markerData.isOpening } ); + * } + * } ); * * If a function is passed as the `config.view` parameter, it will be used to generate both boundary elements. The function * receives the `data` object as a parameter and should return an instance of the @@ -314,17 +314,17 @@ export function downcastMarkerToElement( config ) { * * downcastMarkerToHighlight( { model: 'comment', view: { classes: 'new-comment' }, converterPriority: 'high' } ); * - * downcastMarkerToHighlight( { - * model: 'comment', - * view: data => { - * // Assuming that the marker name is in a form of comment:commentType. - * const commentType = data.markerName.split( ':' )[ 1 ]; + * downcastMarkerToHighlight( { + * model: 'comment', + * view: data => { + * // Assuming that the marker name is in a form of comment:commentType. + * const commentType = data.markerName.split( ':' )[ 1 ]; * - * return { - * classes: [ 'comment', 'comment-' + commentType ] - * }; - * } - * } ); + * return { + * classes: [ 'comment', 'comment-' + commentType ] + * }; + * } + * } ); * * If a function is passed as the `config.view` parameter, it will be used to generate the highlight descriptor. The function * receives the `data` object as a parameter and should return a @@ -1107,13 +1107,42 @@ export function createViewElementFromHighlightDescriptor( descriptor ) { /** * Downcast conversion helper functions. * - * @typedef {Object} DowncastHelpers + * @interface module:engine/conversion/downcast-converters~DowncastHelpers */ export const helpers = { - elementToElement: downcastElementToElement, - attributeToElement: downcastAttributeToElement, - attributeToAttribute: downcastAttributeToAttribute, - markerToElement: downcastMarkerToElement, - markerToHighlight: downcastMarkerToHighlight, - createViewElementFromHighlightDescriptor + /** + * Model element to view element conversion helper. + * + * This conversion results in creating a view element. For example, model `Foo` becomes `

Foo

` in the view. + * + * editor.conversion.for( 'downcast' ).elementToElement( { model: 'paragraph', view: 'p' } ); + * + * editor.conversion.for( 'downcast' ).elementToElement( { model: 'paragraph', view: 'div', converterPriority: 'high' } ); + * + * editor.conversion.for( 'downcast' ).elementToElement( { + * model: 'fancyParagraph', + * view: { + * name: 'p', + * classes: 'fancy' + * } + * } ); + * + * editor.conversion.for( 'downcast' ).elementToElement( { + * model: 'heading', + * view: ( modelElement, viewWriter ) => viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) ) + * } ); + * + * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter + * to the conversion process. + * + * @method #elementToElement + * @param {Object} config Conversion configuration. + * @param {String} config.model The name of the model element to convert. + * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function + * that takes the model element and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer} + * as parameters and returns a view container element. + */ + elementToElement( config ) { + return this.add( downcastElementToElement( config ) ); + } }; diff --git a/tests/conversion/conversion.js b/tests/conversion/conversion.js index 03bc826d8..17b5a536b 100644 --- a/tests/conversion/conversion.js +++ b/tests/conversion/conversion.js @@ -14,8 +14,9 @@ import EditingController from '../../src/controller/editingcontroller'; import Model from '../../src/model/model'; -import { stringify as viewStringify, parse as viewParse } from '../../src/dev-utils/view'; -import { stringify as modelStringify } from '../../src/dev-utils/model'; +import { parse as viewParse, stringify as viewStringify } from '../../src/dev-utils/view'; +import { setData, stringify as modelStringify } from '../../src/dev-utils/model'; +import { helpers as downcastHelpers } from '../../src/conversion/downcast-converters'; describe( 'Conversion', () => { let conversion, dispA, dispB; @@ -114,7 +115,7 @@ describe( 'Conversion', () => { conversion = new Conversion(); conversion.register( { name: 'upcast', dispatcher: [ viewDispatcher ] } ); - conversion.register( { name: 'downcast', dispatcher: [ controller.downcastDispatcher ] } ); + conversion.register( { name: 'downcast', dispatcher: [ controller.downcastDispatcher ], helpers: downcastHelpers } ); } ); describe( 'elementToElement', () => { @@ -627,6 +628,22 @@ describe( 'Conversion', () => { } ); } ); + describe( 'for( \'downcast\' )', () => { + describe( 'elementToElement()', () => { + it( 'adds downcast converter', () => { + conversion.for( 'downcast' ).elementToElement( { model: 'paragraph', view: 'p' } ); + + testDowncast( 'foo', '

foo

' ); + } ); + } ); + } ); + + function testDowncast( input, expectedView ) { + setData( model, input ); + + expect( viewStringify( viewRoot, null, { ignoreRoot: true } ) ).to.equal( expectedView ); + } + function test( input, expectedModel, expectedView = null ) { loadData( input ); From 5d653a76eb5e18369c059a0bbba79b50c988fae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 12 Dec 2018 16:19:39 +0100 Subject: [PATCH 14/84] Update returned types in conversion.for() method. --- src/conversion/conversion.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/conversion/conversion.js b/src/conversion/conversion.js index bf583d82f..e52ceb4b8 100644 --- a/src/conversion/conversion.js +++ b/src/conversion/conversion.js @@ -87,6 +87,7 @@ export default class Conversion { * module:engine/conversion/upcastdispatcher~UpcastDispatcher|Array.} options.dispatcher Dispatcher or array of dispatchers to register * under the given name. + * @param {module:engine/conversion/downcast-converters~DowncastHelpers} helpers */ register( options ) { if ( this._dispatchersGroups.has( options.name ) ) { @@ -150,7 +151,7 @@ export default class Conversion { * conversion.for( 'downcast' ).add( conversion.customConverter( 'insert:paragraph', myConverter ) ); * * @param {String} groupName The name of dispatchers group to add the converters to. - * @returns {module:engine/conversion/downcast-converters~DowncastHelpers} + * @returns {module:engine/conversion/conversion~ConversionHelpers|module:engine/conversion/downcast-converters~DowncastHelpers} * An object with the `.add()` method, providing a way to add converters. */ for( groupName ) { From 9fd639b054d196f86669c2dc5f8b20a97fa06a44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 12 Dec 2018 16:20:06 +0100 Subject: [PATCH 15/84] Add extends tag to DowncastHelpers interface docs. --- src/conversion/downcast-converters.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index 5dc67a834..c224990ea 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -1108,6 +1108,7 @@ export function createViewElementFromHighlightDescriptor( descriptor ) { * Downcast conversion helper functions. * * @interface module:engine/conversion/downcast-converters~DowncastHelpers + * @extends module:engine/conversion/conversion~ConversionHelpers */ export const helpers = { /** From 1dc85dc02c15d9e2725a502a18a0ec4bb6c7b496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 12 Dec 2018 16:24:28 +0100 Subject: [PATCH 16/84] Introduce conversion.for( 'downcast' ).attributeToElement() helper. --- src/conversion/downcast-converters.js | 73 +++++++++++++++++++++++++++ tests/conversion/conversion.js | 9 ++++ 2 files changed, 82 insertions(+) diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index c224990ea..945376e97 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -1145,5 +1145,78 @@ export const helpers = { */ elementToElement( config ) { return this.add( downcastElementToElement( config ) ); + }, + + /** + * Model attribute to view element conversion helper. + * + * + * This conversion results in wrapping view nodes with a view attribute element. For example, a model text node with + * `"Foo"` as data and the `bold` attribute becomes `Foo` in the view. + * + * editor.conversion.for( 'downcast' ).attributeToElement( { model: 'bold', view: 'strong' } ); + * + * editor.conversion.for( 'downcast' ).attributeToElement( { model: 'bold', view: 'b', converterPriority: 'high' } ); + * + * editor.conversion.for( 'downcast' ).attributeToElement( { + * model: 'invert', + * view: { + * name: 'span', + * classes: [ 'font-light', 'bg-dark' ] + * } + * } ); + * + * editor.conversion.for( 'downcast' ).attributeToElement( { + * model: { + * key: 'fontSize', + * values: [ 'big', 'small' ] + * }, + * view: { + * big: { + * name: 'span', + * styles: { + * 'font-size': '1.2em' + * } + * }, + * small: { + * name: 'span', + * styles: { + * 'font-size': '0.8em' + * } + * } + * } + * } ); + * + * editor.conversion.for( 'downcast' ).attributeToElement( { + * model: 'bold', + * view: ( modelAttributeValue, viewWriter ) => { + * return viewWriter.createAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } ); + * } + * } ); + * + * editor.conversion.for( 'downcast' ).attributeToElement( { + * model: { + * key: 'color', + * name: '$text' + * }, + * view: ( modelAttributeValue, viewWriter ) => { + * return viewWriter.createAttributeElement( 'span', { style: 'color:' + modelAttributeValue } ); + * } + * } ); + * + * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter + * to the conversion process. + * @method #attributeToAttribute + * @param {Object} config Conversion configuration. + * @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array + * of `String`s with possible values if the model attribute is an enumerable. + * @param {module:engine/view/elementdefinition~ElementDefinition|Function|Object} config.view A view element definition or a function + * that takes the model attribute value and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer} + * as parameters and returns a view attribute element. If `config.model.values` is + * given, `config.view` should be an object assigning values from `config.model.values` to view element definitions or functions. + * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. + */ + attributeToElement( config ) { + return this.add( downcastAttributeToElement( config ) ); } }; diff --git a/tests/conversion/conversion.js b/tests/conversion/conversion.js index 17b5a536b..e1292b0e9 100644 --- a/tests/conversion/conversion.js +++ b/tests/conversion/conversion.js @@ -636,6 +636,15 @@ describe( 'Conversion', () => { testDowncast( 'foo', '

foo

' ); } ); } ); + + describe( 'attributeToElement()', () => { + it( 'adds downcast converter', () => { + conversion.for( 'downcast' ).elementToElement( { model: 'paragraph', view: 'p' } ); + conversion.for( 'downcast' ).attributeToElement( { model: 'bold', view: 'strong' } ); + + testDowncast( '<$text bold="true">Foo bar', '

Foo bar

' ); + } ); + } ); } ); function testDowncast( input, expectedView ) { From 862342688fd38ef9d27796341a47b0a0c9dbba67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 12 Dec 2018 16:35:27 +0100 Subject: [PATCH 17/84] Introduce conversion.for( 'downcast' ).attributeToAttribute() helper. --- src/conversion/downcast-converters.js | 60 +++++++++++++++++++++++++++ tests/conversion/conversion.js | 14 +++++++ 2 files changed, 74 insertions(+) diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index 945376e97..4f476287c 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -1204,6 +1204,7 @@ export const helpers = { * } * } ); * + * @method #attributeToElement * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter * to the conversion process. * @method #attributeToAttribute @@ -1218,5 +1219,64 @@ export const helpers = { */ attributeToElement( config ) { return this.add( downcastAttributeToElement( config ) ); + }, + + /** + * Model attribute to view attribute conversion helper. + * + * This conversion results in adding an attribute to a view node, basing on an attribute from a model node. For example, + * `` is converted to ``. + * + * conversion.for( 'downcast' ).attributeToAttribute( { model: 'source', view: 'src' } ); + * + * conversion.for( 'downcast' ).attributeToAttribute( { model: 'source', view: 'href', converterPriority: 'high' } ); + * + * conversion.for( 'downcast' ).attributeToAttribute( { + * model: { + * name: 'image', + * key: 'source' + * }, + * view: 'src' + * } ); + * + * conversion.for( 'downcast' ).attributeToAttribute( { + * model: { + * name: 'styled', + * values: [ 'dark', 'light' ] + * }, + * view: { + * dark: { + * key: 'class', + * value: [ 'styled', 'styled-dark' ] + * }, + * light: { + * key: 'class', + * value: [ 'styled', 'styled-light' ] + * } + * } + * } ); + * + * conversion.for( 'downcast' ).attributeToAttribute( { + * model: 'styled', + * view: modelAttributeValue => ( { key: 'class', value: 'styled-' + modelAttributeValue } ) + * } ); + * + * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter + * to the conversion process. + * + * @method #attributeToAttribute + * @param {Object} config Conversion configuration. + * @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values, [ name ] }` object describing + * the attribute key, possible values and, optionally, an element name to convert from. + * @param {String|Object|Function} config.view A view attribute key, or a `{ key, value }` object or a function that takes + * the model attribute value and returns a `{ key, value }` object. If `key` is `'class'`, `value` can be a `String` or an + * array of `String`s. If `key` is `'style'`, `value` is an object with key-value pairs. In other cases, `value` is a `String`. + * If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to + * `{ key, value }` objects or a functions. + * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. + * @returns {Function} Conversion helper. + */ + attributeToAttribute( config ) { + return this.add( downcastAttributeToAttribute( config ) ); } }; diff --git a/tests/conversion/conversion.js b/tests/conversion/conversion.js index e1292b0e9..ee25bbd75 100644 --- a/tests/conversion/conversion.js +++ b/tests/conversion/conversion.js @@ -645,6 +645,20 @@ describe( 'Conversion', () => { testDowncast( '<$text bold="true">Foo bar', '

Foo bar

' ); } ); } ); + + describe( 'attributeToAttribute()', () => { + it( 'adds downcast converter', () => { + schema.register( 'image', { + inheritAllFrom: '$block', + allowAttributes: [ 'source' ] + } ); + + conversion.for( 'downcast' ).elementToElement( { model: 'image', view: 'img' } ); + conversion.for( 'downcast' ).attributeToAttribute( { model: 'source', view: 'src' } ); + + testDowncast( '', '' ); + } ); + } ); } ); function testDowncast( input, expectedView ) { From 3883ae1b4448d907d9f28d312eea16e401eda09d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 12 Dec 2018 16:38:00 +0100 Subject: [PATCH 18/84] Fix docs of conversion.for( 'downcast' ).attributeToElement() method. --- src/conversion/downcast-converters.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index 4f476287c..390dde984 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -1204,10 +1204,10 @@ export const helpers = { * } * } ); * - * @method #attributeToElement * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter * to the conversion process. - * @method #attributeToAttribute + * + * @method #attributeToElement * @param {Object} config Conversion configuration. * @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array * of `String`s with possible values if the model attribute is an enumerable. From 2c97e0e7ede2c70932530ec566ee4ea99e10a7bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 12 Dec 2018 16:45:59 +0100 Subject: [PATCH 19/84] Introduce conversion.for( 'downcast' ).markerToElement() helper. --- src/conversion/downcast-converters.js | 51 +++++++++++++++++++++++++++ tests/conversion/conversion.js | 19 ++++++++++ 2 files changed, 70 insertions(+) diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index 390dde984..10f1b3da6 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -1278,5 +1278,56 @@ export const helpers = { */ attributeToAttribute( config ) { return this.add( downcastAttributeToAttribute( config ) ); + }, + + /** + * Model marker to view element conversion helper. + * + * This conversion results in creating a view element on the boundaries of the converted marker. If the converted marker + * is collapsed, only one element is created. For example, model marker set like this: `F[oo b]ar` + * becomes `

Foo bar

` in the view. + * + * conversion.for( 'downcast' ).markerToElement( { model: 'search', view: 'marker-search' } ); + * + * conversion.for( 'downcast' ).markerToElement( { model: 'search', view: 'search-result', converterPriority: 'high' } ); + * + * conversion.for( 'downcast' ).markerToElement( { + * model: 'search', + * view: { + * name: 'span', + * attributes: { + * 'data-marker': 'search' + * } + * } + * } ); + * + * conversion.for( 'downcast' ).markerToElement( { + * model: 'search', + * view: ( markerData, viewWriter ) => { + * return viewWriter.createUIElement( 'span', { 'data-marker': 'search', 'data-start': markerData.isOpening } ); + * } + * } ); + * + * If a function is passed as the `config.view` parameter, it will be used to generate both boundary elements. The function + * receives the `data` object as a parameter and should return an instance of the + * {@link module:engine/view/uielement~UIElement view UI element}. The `data` and `conversionApi` objects are passed from + * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. Additionally, + * the `data.isOpening` parameter is passed, which is set to `true` for the marker start boundary element, and `false` to + * the marker end boundary element. + * + * This kind of conversion is useful for saving data into the database, so it should be used in the data conversion pipeline. + * + * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add a converter to the conversion process. + * + * @method #markerToElement + * @param {Object} config Conversion configuration. + * @param {String} config.model The name of the model marker (or model marker group) to convert. + * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function + * that takes the model marker data as a parameter and returns a view UI element. + * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. + * @returns {Function} Conversion helper. + */ + markerToElement( config ) { + return this.add( downcastMarkerToElement( config ) ); } }; diff --git a/tests/conversion/conversion.js b/tests/conversion/conversion.js index ee25bbd75..79ddcebd9 100644 --- a/tests/conversion/conversion.js +++ b/tests/conversion/conversion.js @@ -659,6 +659,25 @@ describe( 'Conversion', () => { testDowncast( '', '' ); } ); } ); + + describe( 'markerToElement()', () => { + it( 'adds downcast converter', () => { + conversion.for( 'downcast' ).markerToElement( { model: 'search', view: 'marker-search' } ); + + model.change( writer => { + writer.insertText( 'foo', modelRoot, 0 ); + + const range = writer.createRange( + writer.createPositionAt( modelRoot, 1 ), + writer.createPositionAt( modelRoot, 2 ) + ); + writer.addMarker( 'search', { range, usingOperation: false } ); + } ); + + expect( viewStringify( viewRoot, null, { ignoreRoot: true } ) ) + .to.equal( 'foo' ); + } ); + } ); } ); function testDowncast( input, expectedView ) { From fabf41af0e78fc239f6b543897e125be9644d4c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 12 Dec 2018 16:56:06 +0100 Subject: [PATCH 20/84] Introduce conversion.for( 'downcast' ).markerToHighlight() helper. --- src/conversion/downcast-converters.js | 58 +++++++++++++++++++++++++++ tests/conversion/conversion.js | 18 +++++++++ 2 files changed, 76 insertions(+) diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index 10f1b3da6..f6b812014 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -1329,5 +1329,63 @@ export const helpers = { */ markerToElement( config ) { return this.add( downcastMarkerToElement( config ) ); + }, + + /** + * Model marker to highlight conversion helper. + * + * This conversion results in creating a highlight on view nodes. For this kind of conversion, + * {@link module:engine/conversion/downcast-converters~HighlightDescriptor} should be provided. + * + * For text nodes, a `` {@link module:engine/view/attributeelement~AttributeElement} is created and it wraps all text nodes + * in the converted marker range. For example, a model marker set like this: `F[oo b]ar` becomes + * `

Foo bar

` in the view. + * + * {@link module:engine/view/containerelement~ContainerElement} may provide a custom way of handling highlight. Most often, + * the element itself is given classes and attributes described in the highlight descriptor (instead of being wrapped in ``). + * For example, a model marker set like this: `[]` becomes `` + * in the view. + * + * For container elements, the conversion is two-step. While the converter processes the highlight descriptor and passes it + * to a container element, it is the container element instance itself that applies values from the highlight descriptor. + * So, in a sense, the converter takes care of stating what should be applied on what, while the element decides how to apply that. + * + * conversion.for( 'downcast' ).markerToHighlight( { model: 'comment', view: { classes: 'comment' } } ); + * + * conversion.for( 'downcast' ).markerToHighlight( { + * model: 'comment', + * view: { classes: 'new-comment' }, + * converterPriority: 'high' + * } ); + * + * conversion.for( 'downcast' ).markerToHighlight( { + * model: 'comment', + * view: data => { + * // Assuming that the marker name is in a form of comment:commentType. + * const commentType = data.markerName.split( ':' )[ 1 ]; + * + * return { + * classes: [ 'comment', 'comment-' + commentType ] + * }; + * } + * } ); + * + * If a function is passed as the `config.view` parameter, it will be used to generate the highlight descriptor. The function + * receives the `data` object as a parameter and should return a + * {@link module:engine/conversion/downcast-converters~HighlightDescriptor highlight descriptor}. + * The `data` object properties are passed from {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. + * + * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add a converter to the conversion process. + * + * @method #markerToHighlight + * @param {Object} config Conversion configuration. + * @param {String} config.model The name of the model marker (or model marker group) to convert. + * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} config.view A highlight descriptor + * that will be used for highlighting or a function that takes the model marker data as a parameter and returns a highlight descriptor. + * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. + * @returns {Function} Conversion helper. + */ + markerToHighlight( config ) { + return this.add( downcastMarkerToHighlight( config ) ); } }; diff --git a/tests/conversion/conversion.js b/tests/conversion/conversion.js index 79ddcebd9..1de6cd12c 100644 --- a/tests/conversion/conversion.js +++ b/tests/conversion/conversion.js @@ -678,6 +678,24 @@ describe( 'Conversion', () => { .to.equal( 'foo' ); } ); } ); + + describe( 'markerToHighlight()', () => { + it( 'adds downcast converter', () => { + conversion.for( 'downcast' ).markerToHighlight( { model: 'comment', view: { classes: 'comment' } } ); + + model.change( writer => { + writer.insertText( 'foo', modelRoot, 0 ); + const range = writer.createRange( + writer.createPositionAt( modelRoot, 0 ), + writer.createPositionAt( modelRoot, 3 ) + ); + writer.addMarker( 'comment', { range, usingOperation: false } ); + } ); + + expect( viewStringify( viewRoot, null, { ignoreRoot: true } ) ) + .to.equal( 'foo' ); + } ); + } ); } ); function testDowncast( input, expectedView ) { From 612ac67a846890869fc1125f904a924bd2463e8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 12 Dec 2018 17:19:02 +0100 Subject: [PATCH 21/84] Introduce UpcastHelpers and conversion.for( 'upcast' ).elementToElement() helper. --- src/conversion/conversion.js | 8 +++-- src/conversion/upcast-converters.js | 53 +++++++++++++++++++++++++++++ tests/conversion/conversion.js | 22 ++++++++++-- 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/src/conversion/conversion.js b/src/conversion/conversion.js index e52ceb4b8..d3ddc37b7 100644 --- a/src/conversion/conversion.js +++ b/src/conversion/conversion.js @@ -87,7 +87,8 @@ export default class Conversion { * module:engine/conversion/upcastdispatcher~UpcastDispatcher|Array.} options.dispatcher Dispatcher or array of dispatchers to register * under the given name. - * @param {module:engine/conversion/downcast-converters~DowncastHelpers} helpers + * @param {module:engine/conversion/downcast-converters~DowncastHelpers| + * module:engine/conversion/upcast-converters~UpcastHelpers} helpers */ register( options ) { if ( this._dispatchersGroups.has( options.name ) ) { @@ -151,7 +152,8 @@ export default class Conversion { * conversion.for( 'downcast' ).add( conversion.customConverter( 'insert:paragraph', myConverter ) ); * * @param {String} groupName The name of dispatchers group to add the converters to. - * @returns {module:engine/conversion/conversion~ConversionHelpers|module:engine/conversion/downcast-converters~DowncastHelpers} + * @returns {module:engine/conversion/conversion~ConversionHelpers|module:engine/conversion/downcast-converters~DowncastHelpers| + * module:engine/conversion/upcast-converters~UpcastHelpers} * An object with the `.add()` method, providing a way to add converters. */ for( groupName ) { @@ -599,7 +601,7 @@ export default class Conversion { * @property {String} name Group name * @property {Array.} dispatchers - * @property {module:engine/conversion/downcast-converters~DowncastHelpers} helpers + * @property {module:engine/conversion/downcast-converters~DowncastHelpers|module:engine/conversion/upcast-converters~UpcastHelpers} helpers */ /** diff --git a/src/conversion/upcast-converters.js b/src/conversion/upcast-converters.js index 2caabdb10..8459fd21b 100644 --- a/src/conversion/upcast-converters.js +++ b/src/conversion/upcast-converters.js @@ -607,3 +607,56 @@ export function convertText() { } }; } + +/** + * Upcast conversion helper functions. + * + * @interface module:engine/conversion/upcast-converters~UpcastHelpers + * @extends module:engine/conversion/conversion~ConversionHelpers + */ +export const helpers = { + /** + * View element to model element conversion helper. + * + * This conversion results in creating a model element. For example, + * view `

Foo

` becomes `Foo` in the model. + * + * Keep in mind that the element will be inserted only if it is allowed + * by {@link module:engine/model/schema~Schema schema} configuration. + * + * conversion.for( 'upcast' ).elementToElement( { view: 'p', model: 'paragraph' } ); + * + * conversion.for( 'upcast' ).elementToElement( { view: 'p', model: 'paragraph', converterPriority: 'high' } ); + * + * conversion.for( 'upcast' ).elementToElement( { + * view: { + * name: 'p', + * classes: 'fancy' + * }, + * model: 'fancyParagraph' + * } ); + * + * conversion.for( 'upcast' ).elementToElement( { + * view: { + * name: 'p', + * classes: 'heading' + * }, + * model: ( viewElement, modelWriter ) => { + * return modelWriter.createElement( 'heading', { level: viewElement.getAttribute( 'data-level' ) } ); + * } + * } ); + * + * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process. + * + * @method #elementToElement + * @param {Object} config Conversion configuration. + * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted. + * @param {String|module:engine/model/element~Element|Function} config.model Name of the model element, a model element + * instance or a function that takes a view element and returns a model element. The model element will be inserted in the model. + * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. + * @returns {Function} Conversion helper. + */ + elementToElement( config ) { + return this.add( upcastElementToElement( config ) ); + } +}; diff --git a/tests/conversion/conversion.js b/tests/conversion/conversion.js index 1de6cd12c..fd7c939e7 100644 --- a/tests/conversion/conversion.js +++ b/tests/conversion/conversion.js @@ -8,7 +8,7 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import UpcastDispatcher from '../../src/conversion/upcastdispatcher'; -import { convertText, convertToModelFragment } from '../../src/conversion/upcast-converters'; +import { helpers as upcastHelpers, convertText, convertToModelFragment } from '../../src/conversion/upcast-converters'; import EditingController from '../../src/controller/editingcontroller'; @@ -114,7 +114,7 @@ describe( 'Conversion', () => { viewDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } ); conversion = new Conversion(); - conversion.register( { name: 'upcast', dispatcher: [ viewDispatcher ] } ); + conversion.register( { name: 'upcast', dispatcher: [ viewDispatcher ], helpers: upcastHelpers } ); conversion.register( { name: 'downcast', dispatcher: [ controller.downcastDispatcher ], helpers: downcastHelpers } ); } ); @@ -698,12 +698,30 @@ describe( 'Conversion', () => { } ); } ); + describe( 'for( \'upcast\' )', () => { + describe( 'elementToElement()', () => { + it( 'adds downcast converter', () => { + conversion.for( 'upcast' ).elementToElement( { model: 'paragraph', view: 'p' } ); + // TODO this shouldn't be required + conversion.for( 'downcast' ).elementToElement( { model: 'paragraph', view: 'p' } ); + + testUpcast( '

foo

', 'foo' ); + } ); + } ); + } ); + function testDowncast( input, expectedView ) { setData( model, input ); expect( viewStringify( viewRoot, null, { ignoreRoot: true } ) ).to.equal( expectedView ); } + function testUpcast( input, expectedModel ) { + loadData( input ); + + expect( modelStringify( model.document.getRoot() ) ).to.equal( expectedModel ); + } + function test( input, expectedModel, expectedView = null ) { loadData( input ); From fd148bb61fca159c2cf9e5afc4c3c82c0426ed55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 17 Dec 2018 12:24:19 +0100 Subject: [PATCH 22/84] Introduce conversion.( 'upcast' ).elementToAttribute() helper. --- src/conversion/upcast-converters.js | 77 +++++++++++++++++++++++++++++ tests/conversion/conversion.js | 16 +++++- 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/src/conversion/upcast-converters.js b/src/conversion/upcast-converters.js index 8459fd21b..91567b4a3 100644 --- a/src/conversion/upcast-converters.js +++ b/src/conversion/upcast-converters.js @@ -658,5 +658,82 @@ export const helpers = { */ elementToElement( config ) { return this.add( upcastElementToElement( config ) ); + }, + + /** + * View element to model attribute conversion helper. + * + * This conversion results in setting an attribute on a model node. For example, view `Foo` becomes + * `Foo` {@link module:engine/model/text~Text model text node} with `bold` attribute set to `true`. + * + * This helper is meant to set a model attribute on all the elements that are inside the converted element: + * + * Foo -->

Foo

--> <$text bold="true">Foo + * + * Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step). + * Even though `` is over `

` element, `bold="true"` was added to the text. See + * {@link module:engine/conversion/upcast-converters~UpcastHelpers#attributeToAttribute} for comparison. + * + * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration. + * + * conversion.for( 'upcast' ).elementToAttribute( { view: 'strong', model: 'bold' } ); + * + * conversion.for( 'upcast' ).elementToAttribute( { view: 'strong', model: 'bold', converterPriority: 'high' } ); + * + * conversion.for( 'upcast' ).elementToAttribute( { + * view: { + * name: 'span', + * classes: 'bold' + * }, + * model: 'bold' + * } ); + * + * conversion.for( 'upcast' ).elementToAttribute( { + * view: { + * name: 'span', + * classes: [ 'styled', 'styled-dark' ] + * }, + * model: { + * key: 'styled', + * value: 'dark' + * } + * } ); + * + * conversion.for( 'upcast' ).elementToAttribute( { + * view: { + * name: 'span', + * styles: { + * 'font-size': /[\s\S]+/ + * } + * }, + * model: { + * key: 'fontSize', + * value: viewElement => { + * const fontSize = viewElement.getStyle( 'font-size' ); + * const value = fontSize.substr( 0, fontSize.length - 2 ); + * + * if ( value <= 10 ) { + * return 'small'; + * } else if ( value > 12 ) { + * return 'big'; + * } + * + * return null; + * } + * } + * } ); + * + * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process. + * + * @param {Object} config Conversion configuration. + * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted. + * @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing + * the model attribute. `value` property may be set as a function that takes a view element and returns the value. + * If `String` is given, the model attribute value will be set to `true`. + * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. + * @returns {module:engine/conversion/conversion~Conversion} Conversion helper. + */ + elementToAttribute( config ) { + return this.add( upcastElementToAttribute( config ) ); } }; diff --git a/tests/conversion/conversion.js b/tests/conversion/conversion.js index fd7c939e7..c7e5d0520 100644 --- a/tests/conversion/conversion.js +++ b/tests/conversion/conversion.js @@ -83,7 +83,7 @@ describe( 'Conversion', () => { } ); } ); - describe( 'converters', () => { + describe.only( 'converters', () => { let viewDispatcher, model, schema, conversion, modelRoot, viewRoot; beforeEach( () => { @@ -700,7 +700,7 @@ describe( 'Conversion', () => { describe( 'for( \'upcast\' )', () => { describe( 'elementToElement()', () => { - it( 'adds downcast converter', () => { + it( 'adds upcast converter', () => { conversion.for( 'upcast' ).elementToElement( { model: 'paragraph', view: 'p' } ); // TODO this shouldn't be required conversion.for( 'downcast' ).elementToElement( { model: 'paragraph', view: 'p' } ); @@ -708,6 +708,18 @@ describe( 'Conversion', () => { testUpcast( '

foo

', 'foo' ); } ); } ); + + describe( 'elementToAttribute()', () => { + it( 'adds upcast converter', () => { + conversion.for( 'upcast' ).elementToElement( { model: 'paragraph', view: 'p' } ); + conversion.for( 'downcast' ).elementToElement( { model: 'paragraph', view: 'p' } ); + + conversion.for( 'upcast' ).elementToAttribute( { model: 'bold', view: 'strong' } ); + conversion.for( 'downcast' ).attributeToElement( { model: 'bold', view: 'strong' } ); + + testUpcast( '

Foo bar

', '<$text bold="true">Foo bar' ); + } ); + } ); } ); function testDowncast( input, expectedView ) { From 474e86680b5cf4d0129f7837dba93c42d0bae653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 17 Dec 2018 12:34:44 +0100 Subject: [PATCH 23/84] Remove .only from conversion tests. --- tests/conversion/conversion.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conversion/conversion.js b/tests/conversion/conversion.js index c7e5d0520..e2fbb020e 100644 --- a/tests/conversion/conversion.js +++ b/tests/conversion/conversion.js @@ -83,7 +83,7 @@ describe( 'Conversion', () => { } ); } ); - describe.only( 'converters', () => { + describe( 'converters', () => { let viewDispatcher, model, schema, conversion, modelRoot, viewRoot; beforeEach( () => { From e72502fe223bf080a41a73bb5aa3c1057f27856b Mon Sep 17 00:00:00 2001 From: Szymon Cofalik Date: Fri, 30 Nov 2018 15:39:15 +0100 Subject: [PATCH 24/84] Added: `DowncastDispatcher` will fire an additional event for the whole marker range before events for items in that range are fired. --- src/conversion/downcast-converters.js | 8 ++- src/conversion/downcastdispatcher.js | 22 +++++--- tests/conversion/downcastdispatcher.js | 75 +++++++++++++++++++++----- 3 files changed, 82 insertions(+), 23 deletions(-) diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index e9b9c1e5a..e361f104e 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -564,6 +564,10 @@ export function remove() { */ export function insertUIElement( elementCreator ) { return ( evt, data, conversionApi ) => { + if ( !data.markerRange ) { + return; + } + // Create two view elements. One will be inserted at the beginning of marker, one at the end. // If marker is collapsed, only "opening" element will be inserted. data.isOpening = true; @@ -858,7 +862,7 @@ export function wrap( elementCreator ) { */ export function highlightText( highlightDescriptor ) { return ( evt, data, conversionApi ) => { - if ( data.markerRange.isCollapsed ) { + if ( !data.item ) { return; } @@ -921,7 +925,7 @@ export function highlightText( highlightDescriptor ) { */ export function highlightElement( highlightDescriptor ) { return ( evt, data, conversionApi ) => { - if ( data.markerRange.isCollapsed ) { + if ( !data.item ) { return; } diff --git a/src/conversion/downcastdispatcher.js b/src/conversion/downcastdispatcher.js index 5163487e4..25311eb74 100644 --- a/src/conversion/downcastdispatcher.js +++ b/src/conversion/downcastdispatcher.js @@ -321,22 +321,28 @@ export default class DowncastDispatcher { // In markers' case, event name == consumable name. const eventName = 'addMarker:' + markerName; - // When range is collapsed - fire single event with collapsed range in consumable. - if ( markerRange.isCollapsed ) { - const consumable = new Consumable(); - consumable.add( markerRange, eventName ); + // + // First, fire an event for the whole marker. + // + const consumable = new Consumable(); + consumable.add( markerRange, eventName ); - this.conversionApi.consumable = consumable; + this.conversionApi.consumable = consumable; - this.fire( eventName, { markerName, markerRange }, this.conversionApi ); + this.fire( eventName, { markerName, markerRange }, this.conversionApi ); + // + // Do not fire events for each item inside the range if the range got consumed. + // + if ( !consumable.test( markerRange, eventName ) ) { return; } - // Create consumable for each item in range. + // + // Then, fire an event for each item inside the marker range. + // this.conversionApi.consumable = this._createConsumableForRange( markerRange, eventName ); - // Create separate event for each node in the range. for ( const item of markerRange.getItems() ) { // Do not fire event for already consumed items. if ( !this.conversionApi.consumable.test( item, eventName ) ) { diff --git a/tests/conversion/downcastdispatcher.js b/tests/conversion/downcastdispatcher.js index 7bd63ca8f..263ed5114 100644 --- a/tests/conversion/downcastdispatcher.js +++ b/tests/conversion/downcastdispatcher.js @@ -446,22 +446,29 @@ describe( 'DowncastDispatcher', () => { } ); describe( 'convertMarkerAdd', () => { - let range, element, text; + let element, text; beforeEach( () => { text = new ModelText( 'foo bar baz' ); element = new ModelElement( 'paragraph', null, [ text ] ); root._appendChild( [ element ] ); - - range = model.createRange( model.createPositionAt( element, 0 ), model.createPositionAt( element, 4 ) ); } ); - it( 'should fire addMarker event', () => { - sinon.spy( dispatcher, 'fire' ); + it( 'should fire addMarker event for whole collapsed marker', () => { + const range = model.createRange( model.createPositionAt( element, 2 ), model.createPositionAt( element, 2 ) ); + + const spy = sinon.spy(); + + dispatcher.on( 'addMarker:name', ( evt, data ) => { + spy(); + + expect( data.markerName ).to.equal( 'name' ); + expect( data.markerRange.isEqual( range ) ).to.be.true; + } ); dispatcher.convertMarkerAdd( 'name', range ); - expect( dispatcher.fire.calledWith( 'addMarker:name' ) ).to.be.true; + expect( spy.calledOnce ).to.be.true; } ); it( 'should not convert marker if it is in graveyard', () => { @@ -483,27 +490,69 @@ describe( 'DowncastDispatcher', () => { expect( dispatcher.fire.called ).to.be.false; } ); - it( 'should fire conversion for each item in the range', () => { - range = model.createRangeIn( root ); + it( 'should fire addMarker event for whole non-collapsed marker and for each item in the range', () => { + const range = model.createRangeIn( root ); + + const spyWholeRange = sinon.spy(); + + dispatcher.on( 'addMarker:name', ( evt, data ) => { + if ( !data.item ) { + spyWholeRange(); + expect( data.markerName ).to.equal( 'name' ); + expect( data.markerRange.isEqual( range ) ).to.be.true; + } + } ); + + const spyItems = sinon.spy(); const items = []; dispatcher.on( 'addMarker:name', ( evt, data, conversionApi ) => { - expect( data.markerName ).to.equal( 'name' ); - expect( data.markerRange.isEqual( range ) ).to.be.true; - expect( conversionApi.consumable.test( data.item, 'addMarker:name' ) ); + if ( data.item ) { + spyItems(); - items.push( data.item ); + expect( data.markerName ).to.equal( 'name' ); + expect( data.markerRange.isEqual( range ) ).to.be.true; + expect( conversionApi.consumable.test( data.item, 'addMarker:name' ) ); + + items.push( data.item ); + } } ); dispatcher.convertMarkerAdd( 'name', range ); + expect( spyWholeRange.calledOnce ).to.be.true; + expect( spyItems.calledTwice ).to.be.true; + expect( items[ 0 ] ).to.equal( element ); expect( items[ 1 ].data ).to.equal( text.data ); } ); + it( 'should not fire conversion for non-collapsed marker items if marker was consumed in earlier event', () => { + const range = model.createRangeIn( root ); + + dispatcher.on( 'addMarker:name', ( evt, data, conversionApi ) => { + if ( !data.item ) { + conversionApi.consumable.consume( data.markerRange, evt.name ); + } + }, { priority: 'high' } ); + + const spyItems = sinon.spy(); + const items = []; + + dispatcher.on( 'addMarker:name', ( evt, data ) => { + if ( data.item ) { + spyItems(); + } + } ); + + dispatcher.convertMarkerAdd( 'name', range ); + + expect( spyItems.called ).to.be.false; + } ); + it( 'should be possible to override', () => { - range = model.createRangeIn( root ); + const range = model.createRangeIn( root ); const addMarkerSpy = sinon.spy(); const highAddMarkerSpy = sinon.spy(); From 3af5e6261e5a252a8bf9c185f8fd5544b935431a Mon Sep 17 00:00:00 2001 From: Szymon Cofalik Date: Fri, 30 Nov 2018 18:22:47 +0100 Subject: [PATCH 25/84] Changed: `model.Writer` will add marker operation if move, remove or merge affected a marker. --- src/model/writer.js | 78 +++++++++++++++++++---- tests/model/writer.js | 144 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 206 insertions(+), 16 deletions(-) diff --git a/src/model/writer.js b/src/model/writer.js index b17684103..452afb850 100644 --- a/src/model/writer.js +++ b/src/model/writer.js @@ -466,6 +466,14 @@ export default class Writer { const position = Position._createAt( itemOrPosition, offset ); + // Do not move anything if the move target is same as moved range start. + if ( position.isEqual( range.start ) ) { + return; + } + + // If part of the marker is removed, create additional marker operation for undo purposes. + this._addOperationForAffectedMarkers( 'move', range ); + if ( !isSameTree( range.root, position.root ) ) { /** * Range is going to be moved within not the same document. Please use @@ -491,17 +499,15 @@ export default class Writer { remove( itemOrRange ) { this._assertWriterUsedCorrectly(); - if ( itemOrRange instanceof Range ) { - // The array is reversed, so the ranges to remove are in correct order and do not have to be updated. - const ranges = itemOrRange.getMinimalFlatRanges().reverse(); + let rangeToRemove = itemOrRange instanceof Range ? itemOrRange : Range._createOn( itemOrRange ); - for ( const flat of ranges ) { - applyRemoveOperation( flat.start, flat.end.offset - flat.start.offset, this.batch, this.model ); - } - } else { - const howMany = itemOrRange.is( 'text' ) ? itemOrRange.offsetSize : 1; + // If part of the marker is removed, create additional marker operation for undo purposes. + this._addOperationForAffectedMarkers( 'move', rangeToRemove ); - applyRemoveOperation( Position._createBefore( itemOrRange ), howMany, this.batch, this.model ); + const ranges = rangeToRemove.getMinimalFlatRanges().reverse(); + + for ( const flat of ranges ) { + applyRemoveOperation( flat.start, flat.end.offset - flat.start.offset, this.batch, this.model ); } } @@ -519,6 +525,9 @@ export default class Writer { const nodeBefore = position.nodeBefore; const nodeAfter = position.nodeAfter; + // If part of the marker is removed, create additional marker operation for undo purposes. + this._addOperationForAffectedMarkers( 'merge', position ); + if ( !( nodeBefore instanceof Element ) ) { /** * Node before merge position must be an element. @@ -888,12 +897,12 @@ export default class Writer { if ( !options || typeof options.usingOperation != 'boolean' ) { /** - * The `options.usingOperations` parameter is required when adding a new marker. + * The `options.usingOperation` parameter is required when adding a new marker. * - * @error writer-addMarker-no-usingOperations + * @error writer-addMarker-no-usingOperation */ throw new CKEditorError( - 'writer-addMarker-no-usingOperations: The options.usingOperations parameter is required when adding a new marker.' + 'writer-addMarker-no-usingOperation: The options.usingOperation parameter is required when adding a new marker.' ); } @@ -1294,6 +1303,51 @@ export default class Writer { throw new CKEditorError( 'writer-incorrect-use: Trying to use a writer outside the change() block.' ); } } + + /** + * For given action `type` and `positionOrRange` where the action happens, this function finds all affected markers + * and applies a marker operation with the new marker range equal to the current range. Thanks to this, the marker range + * can be later correctly processed during undo. + * + * @private + * @param {'move'|'merge'} type Writer action type. + * @param {module:engine/model/position~Position|module:engine/model/range~Range} positionOrRange Position or range + * where the writer action happens. + */ + _addOperationForAffectedMarkers( type, positionOrRange ) { + for ( const marker of this.model.markers ) { + if ( !marker.managedUsingOperations ) { + continue; + } + + const markerRange = marker.getRange(); + let isAffected = false; + + if ( type == 'move' ) { + const intersecting = + positionOrRange.containsPosition( markerRange.start ) || + positionOrRange.start.isEqual( markerRange.start ) || + positionOrRange.containsPosition( markerRange.end ) || + positionOrRange.end.isEqual( markerRange.end ); + + isAffected = intersecting && !positionOrRange.containsRange( markerRange ); + } else { + debugger; + // if type == 'merge'. + const elementBefore = positionOrRange.nodeBefore; + const elementAfter = positionOrRange.nodeAfter; + + const affectedOnLeft = markerRange.start.parent == elementBefore && markerRange.start.isAtEnd; + const affectedOnRight = markerRange.end.parent == elementAfter && markerRange.end.offset == 0; + + isAffected = affectedOnLeft || affectedOnRight; + } + + if ( isAffected ) { + this.updateMarker( marker.name, { range: markerRange } ); + } + } + } } // Sets given attribute to each node in given range. When attribute value is null then attribute will be removed. diff --git a/tests/model/writer.js b/tests/model/writer.js index fb90ce884..7aea82a35 100644 --- a/tests/model/writer.js +++ b/tests/model/writer.js @@ -1413,6 +1413,51 @@ describe( 'Writer', () => { expect( docFrag.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foobar' ); } ); + it( 'should create a marker operation if a marker was affected', () => { + const markerRange = new Range( Position._createAt( p2, 0 ), Position._createAt( p2, 0 ) ); + + addMarker( 'name', { + range: markerRange, + usingOperation: true + } ); + + const documentVersion = model.document.version; + + merge( Position._createAfter( p1 ) ); + + const history = model.document.history; + + const lastOperation = history._operations[ history._operations.length - 1 ]; + const secondLastOperation = history._operations[ history._operations.length - 2 ]; + + expect( secondLastOperation.type ).to.equal( 'marker' ); + expect( secondLastOperation.oldRange.isEqual( markerRange ) ); + expect( secondLastOperation.newRange.isEqual( markerRange ) ); + + expect( lastOperation.type ).to.equal( 'merge' ); + expect( model.document.version ).to.equal( documentVersion + 2 ); + } ); + + it( 'should not create a marker operation if affected marker was not using operations', () => { + const markerRange = new Range( Position._createAt( p2, 0 ), Position._createAt( p2, 2 ) ); + + addMarker( 'name', { + range: markerRange, + usingOperation: false + } ); + + const documentVersion = model.document.version; + + merge( Position._createAfter( p1 ) ); + + const history = model.document.history; + + const lastOperation = history._operations[ history._operations.length - 1 ]; + + expect( lastOperation.type ).to.equal( 'merge' ); + expect( model.document.version ).to.equal( documentVersion + 1 ); + } ); + it( 'should throw if there is no element after', () => { expect( () => { merge( new Position( root, [ 2 ] ) ); @@ -1458,6 +1503,51 @@ describe( 'Writer', () => { expect( getNodesAndText( Range._createIn( root.getChild( 1 ) ) ) ).to.equal( 'abcobarxyz' ); } ); + it( 'should create a marker operation if a marker was affected', () => { + const markerRange = new Range( Position._createAt( p, 1 ), Position._createAt( p, 4 ) ); + + addMarker( 'name', { + range: markerRange, + usingOperation: true + } ); + + const documentVersion = model.document.version; + + move( new Range( Position._createAt( p, 0 ), Position._createAt( p, 2 ) ), Position._createAt( div, 0 ) ); + + const history = model.document.history; + + const lastOperation = history._operations[ history._operations.length - 1 ]; + const secondLastOperation = history._operations[ history._operations.length - 2 ]; + + expect( secondLastOperation.type ).to.equal( 'marker' ); + expect( secondLastOperation.oldRange.isEqual( markerRange ) ); + expect( secondLastOperation.newRange.isEqual( markerRange ) ); + + expect( lastOperation.type ).to.equal( 'move' ); + expect( model.document.version ).to.equal( documentVersion + 2 ); + } ); + + it( 'should not create a marker operation if affected marker was not using operations', () => { + const markerRange = new Range( Position._createAt( p, 1 ), Position._createAt( p, 4 ) ); + + addMarker( 'name', { + range: markerRange, + usingOperation: false + } ); + + const documentVersion = model.document.version; + + move( new Range( Position._createAt( p, 0 ), Position._createAt( p, 2 ) ), Position._createAt( div, 0 ) ); + + const history = model.document.history; + + const lastOperation = history._operations[ history._operations.length - 1 ]; + + expect( lastOperation.type ).to.equal( 'move' ); + expect( model.document.version ).to.equal( documentVersion + 1 ); + } ); + it( 'should throw if object to move is not a range', () => { expect( () => { move( root.getChild( 0 ), new Position( root, [ 1, 3 ] ) ); @@ -1555,6 +1645,52 @@ describe( 'Writer', () => { expect( batch.operations[ 0 ].type ).to.equal( 'remove' ); } ); + it( 'should create a marker operation if a marker was affected', () => { + const markerRange = new Range( Position._createAt( p, 1 ), Position._createAt( p, 4 ) ); + + addMarker( 'name', { + range: markerRange, + usingOperation: true + } ); + + const documentVersion = model.document.version; + + remove( new Range( Position._createAt( p, 0 ), Position._createAt( p, 2 ) ) ); + + const history = model.document.history; + + const lastOperation = history._operations[ history._operations.length - 1 ]; + const secondLastOperation = history._operations[ history._operations.length - 2 ]; + + expect( secondLastOperation.type ).to.equal( 'marker' ); + expect( secondLastOperation.oldRange.isEqual( markerRange ) ); + expect( secondLastOperation.newRange.isEqual( markerRange ) ); + + expect( lastOperation.type ).to.equal( 'remove' ); + + expect( model.document.version ).to.equal( documentVersion + 2 ); + } ); + + it( 'should not create a marker operation if affected marker was not using operations', () => { + const markerRange = new Range( Position._createAt( p, 1 ), Position._createAt( p, 4 ) ); + + addMarker( 'name', { + range: markerRange, + usingOperation: false + } ); + + const documentVersion = model.document.version; + + remove( new Range( Position._createAt( p, 0 ), Position._createAt( p, 2 ) ) ); + + const history = model.document.history; + + const lastOperation = history._operations[ history._operations.length - 1 ]; + + expect( lastOperation.type ).to.equal( 'remove' ); + expect( model.document.version ).to.equal( documentVersion + 1 ); + } ); + it( 'should throw when trying to use detached writer', () => { const writer = new Writer( model, batch ); @@ -1922,16 +2058,16 @@ describe( 'Writer', () => { range = Range._createIn( root ); } ); - it( 'should throw if options.usingOperations is not defined', () => { + it( 'should throw if options.usingOperation is not defined', () => { expect( () => { addMarker( 'name' ); - } ).to.throw( CKEditorError, /^writer-addMarker-no-usingOperations/ ); + } ).to.throw( CKEditorError, /^writer-addMarker-no-usingOperation/ ); } ); - it( 'should throw if name and range is defined but options.usingOperations is not defined', () => { + it( 'should throw if name and range is defined but options.usingOperation is not defined', () => { expect( () => { addMarker( 'name', { range } ); - } ).to.throw( CKEditorError, /^writer-addMarker-no-usingOperations/ ); + } ).to.throw( CKEditorError, /^writer-addMarker-no-usingOperation/ ); } ); it( 'should add marker to the document marker collection', () => { From 2328966e9d841915f03d5bfd76dae445d9869750 Mon Sep 17 00:00:00 2001 From: Szymon Cofalik Date: Fri, 30 Nov 2018 18:23:23 +0100 Subject: [PATCH 26/84] Fixed: Added special case in `model.Range#_getTransformedByMergeOperation`. --- src/model/range.js | 15 +++++++++++++++ tests/model/range.js | 21 +++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/model/range.js b/src/model/range.js index 521145551..93d6f1819 100644 --- a/src/model/range.js +++ b/src/model/range.js @@ -562,6 +562,21 @@ export default class Range { * @returns {module:engine/model/range~Range} */ _getTransformedByMergeOperation( operation ) { + // Special case when the marker is set on "the closing tag" of an element. Marker can be set like that during + // transformations, especially when a content of a few block elements were removed. For example: + // + // {} is the transformed range, [] is the removed range. + //

F[o{o

B}ar

Xy]z

+ // + //

Fo{o

B}ar

z

+ //

F{

B}ar

z

+ //

F{

}

z

+ //

F{}z

+ // + if ( this.start.isEqual( operation.targetPosition ) && this.end.isEqual( operation.deletionPosition ) ) { + return new Range( this.start ); + } + let start = this.start._getTransformedByMergeOperation( operation ); let end = this.end._getTransformedByMergeOperation( operation ); diff --git a/tests/model/range.js b/tests/model/range.js index 6fccc7a03..258170f64 100644 --- a/tests/model/range.js +++ b/tests/model/range.js @@ -1179,6 +1179,27 @@ describe( 'Range', () => { expect( transformed[ 0 ].start.path ).to.deep.equal( [ 0, 2 ] ); expect( transformed[ 0 ].end.path ).to.deep.equal( [ 2 ] ); } ); + + it( 'range is set on closing tag of merge target element', () => { + //

aa{

}

bb

+ const range = new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 1 ] ) ); + + const op = new MergeOperation( + new Position( root, [ 1, 0 ] ), + 2, + new Position( root, [ 0, 2 ] ), + gyPos, + 1 + ); + + const transformed = range.getTransformedByOperation( op ); + + expect( transformed.length ).to.equal( 1 ); + + //

aa{}bb

+ expect( transformed[ 0 ].start.path ).to.deep.equal( [ 0, 2 ] ); + expect( transformed[ 0 ].end.path ).to.deep.equal( [ 0, 2 ] ); + } ); } ); } ); From a2cdaedb9d9211e8cbe291d3b16bcd5861e89feb Mon Sep 17 00:00:00 2001 From: Szymon Cofalik Date: Fri, 30 Nov 2018 18:23:46 +0100 Subject: [PATCH 27/84] Tests: Added missing tests for new marker conversion. --- tests/conversion/downcastdispatcher.js | 42 +++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/tests/conversion/downcastdispatcher.js b/tests/conversion/downcastdispatcher.js index 263ed5114..6f44eace6 100644 --- a/tests/conversion/downcastdispatcher.js +++ b/tests/conversion/downcastdispatcher.js @@ -551,18 +551,50 @@ describe( 'DowncastDispatcher', () => { expect( spyItems.called ).to.be.false; } ); - it( 'should be possible to override', () => { + it( 'should be possible to override #1', () => { const range = model.createRangeIn( root ); const addMarkerSpy = sinon.spy(); const highAddMarkerSpy = sinon.spy(); - dispatcher.on( 'addMarker:marker', addMarkerSpy ); + dispatcher.on( 'addMarker:marker', ( evt, data ) => { + if ( !data.item ) { + addMarkerSpy(); + } + } ); - dispatcher.on( 'addMarker:marker', evt => { - highAddMarkerSpy(); + dispatcher.on( 'addMarker:marker', ( evt, data ) => { + if ( !data.item ) { + highAddMarkerSpy(); - evt.stop(); + evt.stop(); + } + }, { priority: 'high' } ); + + dispatcher.convertMarkerAdd( 'marker', range ); + + expect( addMarkerSpy.called ).to.be.false; + expect( highAddMarkerSpy.calledOnce ).to.be.true; + } ); + + it( 'should be possible to override #2', () => { + const range = model.createRangeIn( root ); + + const addMarkerSpy = sinon.spy(); + const highAddMarkerSpy = sinon.spy(); + + dispatcher.on( 'addMarker:marker', ( evt, data ) => { + if ( data.item ) { + addMarkerSpy(); + } + } ); + + dispatcher.on( 'addMarker:marker', ( evt, data ) => { + if ( data.item ) { + highAddMarkerSpy(); + + evt.stop(); + } }, { priority: 'high' } ); dispatcher.convertMarkerAdd( 'marker', range ); From 7d8f3ec805dac6b41c05e9ed778a671a5d0ee028 Mon Sep 17 00:00:00 2001 From: Szymon Cofalik Date: Fri, 30 Nov 2018 18:24:03 +0100 Subject: [PATCH 28/84] Changed: Removed not needed condition in `insertUIElement` converter. --- src/conversion/downcast-converters.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index e361f104e..4ae9ae3b2 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -564,10 +564,6 @@ export function remove() { */ export function insertUIElement( elementCreator ) { return ( evt, data, conversionApi ) => { - if ( !data.markerRange ) { - return; - } - // Create two view elements. One will be inserted at the beginning of marker, one at the end. // If marker is collapsed, only "opening" element will be inserted. data.isOpening = true; From feb90922ecc7a9383151f8f62d47fc41764e8760 Mon Sep 17 00:00:00 2001 From: Szymon Cofalik Date: Fri, 30 Nov 2018 18:38:57 +0100 Subject: [PATCH 29/84] Fixed: Removed missed debugger statement. --- src/model/writer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/model/writer.js b/src/model/writer.js index 452afb850..9c9cd2a13 100644 --- a/src/model/writer.js +++ b/src/model/writer.js @@ -1332,7 +1332,6 @@ export default class Writer { isAffected = intersecting && !positionOrRange.containsRange( markerRange ); } else { - debugger; // if type == 'merge'. const elementBefore = positionOrRange.nodeBefore; const elementAfter = positionOrRange.nodeAfter; From 3fd75330f2405264a78ba1ddc6cf52616c8f804a Mon Sep 17 00:00:00 2001 From: Szymon Cofalik Date: Wed, 5 Dec 2018 16:31:44 +0100 Subject: [PATCH 30/84] Changed: Use context in marker operations transformations to better handle markers during undo. --- src/model/operation/transform.js | 72 ++++++++++++++++++++++- tests/model/operation/transform/marker.js | 46 +++++++++++++++ tests/model/operation/transform/undo.js | 70 ++++++++++++++++++++++ 3 files changed, 186 insertions(+), 2 deletions(-) diff --git a/src/model/operation/transform.js b/src/model/operation/transform.js index 0504f9d95..d8d1c8532 100644 --- a/src/model/operation/transform.js +++ b/src/model/operation/transform.js @@ -515,6 +515,48 @@ class ContextFactory { break; } + + case MarkerOperation: { + const markerRange = opA.newRange; + + if ( !markerRange ) { + return; + } + + switch ( opB.constructor ) { + case MoveOperation: { + const movedRange = Range._createFromPositionAndShift( opB.sourcePosition, opB.howMany ); + + const affectedLeft = movedRange.containsPosition( markerRange.start ) || + movedRange.start.isEqual( markerRange.start ); + + const affectedRight = movedRange.containsPosition( markerRange.end ) || + movedRange.end.isEqual( markerRange.end ); + + if ( ( affectedLeft || affectedRight ) && !movedRange.containsRange( markerRange ) ) { + this._setRelation( opA, opB, { + side: affectedLeft ? 'left' : 'right', + offset: affectedLeft ? markerRange.start.offset : markerRange.end.offset + } ); + } + + break; + } + + case MergeOperation: { + const wasInLeftElement = markerRange.start.isEqual( opB.targetPosition ); + const wasInRightElement = markerRange.end.isEqual( opB.sourcePosition ); + + if ( wasInLeftElement || wasInRightElement ) { + this._setRelation( opA, opB, { wasInLeftElement, wasInRightElement } ); + } + + break; + } + } + + break; + } } } @@ -1068,24 +1110,49 @@ setTransformation( MarkerOperation, MergeOperation, ( a, b ) => { return [ a ]; } ); -setTransformation( MarkerOperation, MoveOperation, ( a, b ) => { +setTransformation( MarkerOperation, MoveOperation, ( a, b, context ) => { if ( a.oldRange ) { a.oldRange = Range._createFromRanges( a.oldRange._getTransformedByMoveOperation( b ) ); } if ( a.newRange ) { + if ( context.abRelation ) { + if ( context.abRelation.side == 'left' && b.targetPosition.isEqual( a.newRange.start ) ) { + a.newRange.start.offset = context.abRelation.offset; + a.newRange.end.offset += b.howMany; + + return [ a ]; + } else if ( context.abRelation.side == 'right' && b.targetPosition.isEqual( a.newRange.end ) ) { + a.newRange.end.offset = context.abRelation.offset; + + return [ a ]; + } + } + a.newRange = Range._createFromRanges( a.newRange._getTransformedByMoveOperation( b ) ); } return [ a ]; } ); -setTransformation( MarkerOperation, SplitOperation, ( a, b ) => { +setTransformation( MarkerOperation, SplitOperation, ( a, b, context ) => { if ( a.oldRange ) { a.oldRange = a.oldRange._getTransformedBySplitOperation( b ); } if ( a.newRange ) { + if ( context.abRelation ) { + if ( a.newRange.start.isEqual( b.splitPosition ) && !context.abRelation.wasInLeftElement ) { + a.newRange.start = Position._createAt( b.moveTargetPosition ); + } + + if ( a.newRange.end.isEqual( b.splitPosition ) && context.abRelation.wasInRightElement ) { + a.newRange.end = Position._createAt( b.moveTargetPosition ); + } + + return [ a ]; + } + a.newRange = a.newRange._getTransformedBySplitOperation( b ); } @@ -1719,6 +1786,7 @@ setTransformation( MoveOperation, MergeOperation, ( a, b, context ) => { targetPositionPath.push( 0 ); const splitNodesMoveTarget = new Position( gyMove.targetPosition.root, targetPositionPath ); + splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove( gyMoveSource, gyMoveTarget, 1 ); const splitNodesMove = new MoveOperation( splitNodesMoveSource, b.howMany, splitNodesMoveTarget, 0 ); results.push( gyMove ); diff --git a/tests/model/operation/transform/marker.js b/tests/model/operation/transform/marker.js index cb97882a8..9ee14dc93 100644 --- a/tests/model/operation/transform/marker.js +++ b/tests/model/operation/transform/marker.js @@ -421,6 +421,52 @@ describe( 'transform', () => { 'Foo Bar' ); } ); + + it( 'left side of marker moved, insertion at the moved range start, move undo', () => { + john.setData( 'Foo[bar]' ); + kate.setData( 'Foo[bar]' ); + + john.setMarker( 'm1' ); + john.setSelection( [ 0, 2 ], [ 0, 4 ] ); + john.move( [ 1, 0 ] ); + + syncClients(); + + kate.setSelection( [ 0, 2 ] ); + kate.type( 'xx' ); + + syncClients(); + + expectClients( 'Foxxarob' ); + + john.undo(); + syncClients(); + + expectClients( 'Foobxxar' ); + } ); + + it( 'right side of marker moved, insertion at the moved range start, move undo', () => { + john.setData( '[Foo]bar' ); + kate.setData( '[Foo]bar' ); + + john.setMarker( 'm1' ); + john.setSelection( [ 0, 2 ], [ 0, 4 ] ); + john.move( [ 1, 0 ] ); + + syncClients(); + + kate.setSelection( [ 0, 2 ] ); + kate.type( 'xx' ); + + syncClients(); + + expectClients( 'Foxxarob' ); + + john.undo(); + syncClients(); + + expectClients( 'Foobxxar' ); + } ); } ); describe( 'by remove', () => { diff --git a/tests/model/operation/transform/undo.js b/tests/model/operation/transform/undo.js index 63b328337..78b73f63e 100644 --- a/tests/model/operation/transform/undo.js +++ b/tests/model/operation/transform/undo.js @@ -388,4 +388,74 @@ describe( 'transform', () => { john.undo(); expectClients( 'FooBar' ); } ); + + it( 'collapsed marker at the beginning of merged element then undo', () => { + john.setData( 'Foo[]Bar' ); + + john.setMarker( 'm1' ); + john.setSelection( [ 1 ] ); + john.merge(); + + expectClients( 'FooBar' ); + + john.undo(); + + expectClients( 'FooBar' ); + } ); + + it( 'collapsed marker at the end of merge-target element then undo', () => { + john.setData( 'Foo[]Bar' ); + + john.setMarker( 'm1' ); + john.setSelection( [ 1 ] ); + john.merge(); + + expectClients( 'FooBar' ); + + john.undo(); + + expectClients( 'FooBar' ); + } ); + + it( 'empty marker between merged elements then undo', () => { + john.setData( 'Foo[]Bar' ); + + john.setMarker( 'm1' ); + john.setSelection( [ 1 ] ); + john.merge(); + + expectClients( 'FooBar' ); + + john.undo(); + + expectClients( 'FooBar' ); + } ); + + it( 'left side of marker moved then undo', () => { + john.setData( 'Foo[bar]' ); + + john.setMarker( 'm1' ); + john.setSelection( [ 0, 2 ], [ 0, 4 ] ); + john.move( [ 1, 0 ] ); + + expectClients( 'Foarob' ); + + john.undo(); + + expectClients( 'Foobar' ); + } ); + + it( 'right side of marker moved then undo', () => { + john.setData( '[Foo]bar' ); + + john.setMarker( 'm1' ); + john.setSelection( [ 0, 2 ], [ 0, 4 ] ); + john.move( [ 1, 0 ] ); + + expectClients( 'Foarob' ); + + john.undo(); + + expectClients( 'Foobar' ); + } ); } ); From b52fd1d48cc7cd601dc0580040c435c14f55ea2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 17 Dec 2018 12:38:22 +0100 Subject: [PATCH 31/84] Introduce conversion.( 'upcast' ).attributeToAttribute() helper. --- src/conversion/upcast-converters.js | 81 +++++++++++++++++++++++++++++ tests/conversion/conversion.js | 17 ++++++ 2 files changed, 98 insertions(+) diff --git a/src/conversion/upcast-converters.js b/src/conversion/upcast-converters.js index 91567b4a3..935379617 100644 --- a/src/conversion/upcast-converters.js +++ b/src/conversion/upcast-converters.js @@ -735,5 +735,86 @@ export const helpers = { */ elementToAttribute( config ) { return this.add( upcastElementToAttribute( config ) ); + }, + + /** + * View attribute to model attribute conversion helper. + * + * This conversion results in setting an attribute on a model node. For example, view `` becomes + * `` in the model. + * + * This helper is meant to convert view attributes from view elements which got converted to the model, so the view attribute + * is set only on the corresponding model node: + * + *
foo
-->
foo
+ * + * Above, `class="dark"` attribute is added only to the `
` elements that has it. This is in contrary to + * {@link module:engine/conversion/upcast-converters~upcastElementToAttribute} which sets attributes for all the children in the model: + * + * Foo -->

Foo

--> <$text bold="true">Foo + * + * Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step). + * Even though `` is over `

` element, `bold="true"` was added to the text. + * + * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration. + * + * conversion.for( 'upcast' ).attributeToAttribute( { view: 'src', model: 'source' } ); + * + * conversion.for( 'upcast' ).attributeToAttribute( { view: { key: 'src' }, model: 'source' } ); + * + * conversion.for( 'upcast' ).attributeToAttribute( { view: { key: 'src' }, model: 'source', converterPriority: 'normal' } ); + * + * conversion.for( 'upcast' ).attributeToAttribute( { + * view: { + * key: 'data-style', + * value: /[\s\S]+/ + * }, + * model: 'styled' + * } ); + * + * conversion.for( 'upcast' ).attributeToAttribute( { + * view: { + * name: 'img', + * key: 'class', + * value: 'styled-dark' + * }, + * model: { + * key: 'styled', + * value: 'dark' + * } + * } ); + * + * conversion.for( 'upcast' ).attributeToAttribute( { + * view: { + * key: 'class', + * value: /styled-[\S]+/ + * }, + * model: { + * key: 'styled' + * value: viewElement => { + * const regexp = /styled-([\S]+)/; + * const match = viewElement.getAttribute( 'class' ).match( regexp ); + * + * return match[ 1 ]; + * } + * } + * } ); + * + * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process. + * + * @param {Object} config Conversion configuration. + * @param {String|Object} config.view Specifies which view attribute will be converted. If a `String` is passed, + * attributes with given key will be converted. If an `Object` is passed, it must have a required `key` property, + * specifying view attribute key, and may have an optional `value` property, specifying view attribute value and optional `name` + * property specifying a view element name from/on which the attribute should be converted. `value` can be given as a `String`, + * a `RegExp` or a function callback, that takes view attribute value as the only parameter and returns `Boolean`. + * @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing + * the model attribute. `value` property may be set as a function that takes a view element and returns the value. + * If `String` is given, the model attribute value will be same as view attribute value. + * @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority. + * @returns {Function} Conversion helper. + */ + attributeToAttribute( config ) { + return this.add( upcastAttributeToAttribute( config ) ); } }; diff --git a/tests/conversion/conversion.js b/tests/conversion/conversion.js index e2fbb020e..d675c1907 100644 --- a/tests/conversion/conversion.js +++ b/tests/conversion/conversion.js @@ -720,6 +720,23 @@ describe( 'Conversion', () => { testUpcast( '

Foo bar

', '<$text bold="true">Foo bar' ); } ); } ); + + describe( 'attributeToAttribute()', () => { + it( 'adds upcast converter', () => { + schema.register( 'image', { + inheritAllFrom: '$block', + allowAttributes: [ 'source' ] + } ); + + conversion.for( 'downcast' ).elementToElement( { model: 'image', view: 'img' } ); + conversion.for( 'downcast' ).attributeToAttribute( { model: 'source', view: 'src' } ); + + conversion.for( 'upcast' ).elementToElement( { model: 'image', view: 'img' } ); + conversion.for( 'upcast' ).attributeToAttribute( { model: 'source', view: 'src' } ); + + testUpcast( '', '' ); + } ); + } ); } ); function testDowncast( input, expectedView ) { From baa575333b786c3a68ca9a6d9dd52ef143e69d88 Mon Sep 17 00:00:00 2001 From: Szymon Cofalik Date: Mon, 17 Dec 2018 12:44:50 +0100 Subject: [PATCH 32/84] Docs: Fixed missing param name. --- src/model/markercollection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/markercollection.js b/src/model/markercollection.js index 63411115d..4d6e63321 100644 --- a/src/model/markercollection.js +++ b/src/model/markercollection.js @@ -230,7 +230,7 @@ export default class MarkerCollection { * Fired whenever marker is added, updated or removed from `MarkerCollection`. * * @event update - * @param {module:engine/model/markercollection~Marker} Updated Marker. + * @param {module:engine/model/markercollection~Marker} marker Updated Marker. * @param {module:engine/model/range~Range|null} oldRange Marker range before the update. When is not defined it * means that marker is just added. * @param {module:engine/model/range~Range|null} newRange Marker range after update. When is not defined it From 7913772c65dadffee0e0d42af0e86aaa454e885c Mon Sep 17 00:00:00 2001 From: Szymon Cofalik Date: Mon, 17 Dec 2018 12:45:33 +0100 Subject: [PATCH 33/84] Fix: Lint errors. --- src/model/writer.js | 2 +- tests/conversion/downcastdispatcher.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/model/writer.js b/src/model/writer.js index 9c9cd2a13..20202721f 100644 --- a/src/model/writer.js +++ b/src/model/writer.js @@ -499,7 +499,7 @@ export default class Writer { remove( itemOrRange ) { this._assertWriterUsedCorrectly(); - let rangeToRemove = itemOrRange instanceof Range ? itemOrRange : Range._createOn( itemOrRange ); + const rangeToRemove = itemOrRange instanceof Range ? itemOrRange : Range._createOn( itemOrRange ); // If part of the marker is removed, create additional marker operation for undo purposes. this._addOperationForAffectedMarkers( 'move', rangeToRemove ); diff --git a/tests/conversion/downcastdispatcher.js b/tests/conversion/downcastdispatcher.js index 6f44eace6..13f8d4a37 100644 --- a/tests/conversion/downcastdispatcher.js +++ b/tests/conversion/downcastdispatcher.js @@ -538,7 +538,6 @@ describe( 'DowncastDispatcher', () => { }, { priority: 'high' } ); const spyItems = sinon.spy(); - const items = []; dispatcher.on( 'addMarker:name', ( evt, data ) => { if ( data.item ) { From 6b2f6f22a8ffffffa1fce34809188a85144e9994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 17 Dec 2018 12:57:18 +0100 Subject: [PATCH 34/84] Introduce conversion.( 'upcast' ).elementToMarker() helper. --- src/conversion/upcast-converters.js | 40 +++++++++++++++++++++++++++++ tests/conversion/conversion.js | 38 +++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/src/conversion/upcast-converters.js b/src/conversion/upcast-converters.js index 935379617..4341aee53 100644 --- a/src/conversion/upcast-converters.js +++ b/src/conversion/upcast-converters.js @@ -816,5 +816,45 @@ export const helpers = { */ attributeToAttribute( config ) { return this.add( upcastAttributeToAttribute( config ) ); + }, + + /** + * View element to model marker conversion helper. + * + * This conversion results in creating a model marker. For example, if the marker was stored in a view as an element: + * `

Foo

Bar

`, + * after the conversion is done, the marker will be available in + * {@link module:engine/model/model~Model#markers model document markers}. + * + * conversion.for( 'upcast' ).elementToMarker( { view: 'marker-search', model: 'search' } ); + * + * conversion.for( 'upcast' ).elementToMarker( { view: 'marker-search', model: 'search', converterPriority: 'high' } ); + * + * conversion.for( 'upcast' ).elementToMarker( { + * view: 'marker-search', + * model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' ) + * } ); + * + * conversion.for( 'upcast' ).elementToMarker( { + * view: { + * name: 'span', + * attributes: { + * 'data-marker': 'search' + * } + * }, + * model: 'search' + * } ); + * + * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process. + * + * @param {Object} config Conversion configuration. + * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted. + * @param {String|Function} config.model Name of the model marker, or a function that takes a view element and returns + * a model marker name. + * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. + * @returns {Function} Conversion helper. + */ + elementToMarker( config ) { + return this.add( upcastElementToMarker( config ) ); } }; diff --git a/tests/conversion/conversion.js b/tests/conversion/conversion.js index d675c1907..779739e65 100644 --- a/tests/conversion/conversion.js +++ b/tests/conversion/conversion.js @@ -17,6 +17,9 @@ import Model from '../../src/model/model'; import { parse as viewParse, stringify as viewStringify } from '../../src/dev-utils/view'; import { setData, stringify as modelStringify } from '../../src/dev-utils/model'; import { helpers as downcastHelpers } from '../../src/conversion/downcast-converters'; +import ViewDocumentFragment from '../../src/view/documentfragment'; +import ViewText from '../../src/view/text'; +import ViewUIElement from '../../src/view/uielement'; describe( 'Conversion', () => { let conversion, dispA, dispB; @@ -101,6 +104,7 @@ describe( 'Conversion', () => { schema = model.schema; schema.extend( '$text', { + allowIn: '$root', allowAttributes: [ 'bold' ] } ); @@ -737,6 +741,25 @@ describe( 'Conversion', () => { testUpcast( '', '' ); } ); } ); + + describe( 'markerToElement()', () => { + it( 'adds upcast converter', () => { + conversion.for( 'upcast' ).elementToMarker( { model: 'search', view: 'marker-search' } ); + conversion.for( 'downcast' ).markerToElement( { model: 'search', view: 'marker-search' } ); + + const frag = new ViewDocumentFragment( [ + new ViewText( 'fo' ), + new ViewUIElement( 'marker-search' ), + new ViewText( 'oba' ), + new ViewUIElement( 'marker-search' ), + new ViewText( 'r' ) + ] ); + + const marker = { name: 'search', start: [ 2 ], end: [ 5 ] }; + + testUpcastMarker( frag, 'foobar', marker ); + } ); + } ); } ); function testDowncast( input, expectedView ) { @@ -774,5 +797,20 @@ describe( 'Conversion', () => { writer.insert( convertedModel, modelRoot, 0 ); } ); } + + function testUpcastMarker( viewToConvert, modelString, marker ) { + const conversionResult = model.change( writer => viewDispatcher.convert( viewToConvert, writer ) ); + + if ( marker ) { + expect( conversionResult.markers.has( marker.name ) ).to.be.true; + + const convertedMarker = conversionResult.markers.get( marker.name ); + + expect( convertedMarker.start.path ).to.deep.equal( marker.start ); + expect( convertedMarker.end.path ).to.deep.equal( marker.end ); + } + + expect( viewStringify( conversionResult ) ).to.equal( modelString ); + } } ); } ); From a22aec56b0f5866a506790ea633ba32460e53f5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 17 Dec 2018 13:08:06 +0100 Subject: [PATCH 35/84] Fix code blocks in upcast-converters tests. --- src/conversion/upcast-converters.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/conversion/upcast-converters.js b/src/conversion/upcast-converters.js index 4341aee53..198275a99 100644 --- a/src/conversion/upcast-converters.js +++ b/src/conversion/upcast-converters.js @@ -826,16 +826,16 @@ export const helpers = { * after the conversion is done, the marker will be available in * {@link module:engine/model/model~Model#markers model document markers}. * - * conversion.for( 'upcast' ).elementToMarker( { view: 'marker-search', model: 'search' } ); + * conversion.for( 'upcast' ).elementToMarker( { view: 'marker-search', model: 'search' } ); * - * conversion.for( 'upcast' ).elementToMarker( { view: 'marker-search', model: 'search', converterPriority: 'high' } ); + * conversion.for( 'upcast' ).elementToMarker( { view: 'marker-search', model: 'search', converterPriority: 'high' } ); * - * conversion.for( 'upcast' ).elementToMarker( { + * conversion.for( 'upcast' ).elementToMarker( { * view: 'marker-search', * model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' ) * } ); * - * conversion.for( 'upcast' ).elementToMarker( { + * conversion.for( 'upcast' ).elementToMarker( { * view: { * name: 'span', * attributes: { From d6f3a044569cf67913cfc95fd99fcc1c33e8d610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 17 Dec 2018 13:59:03 +0100 Subject: [PATCH 36/84] Make downcast conversion helpers protected. --- src/conversion/conversion.js | 26 +++---- src/conversion/downcast-converters.js | 67 +++++++++-------- tests/controller/datacontroller.js | 32 ++++----- tests/controller/editingcontroller.js | 9 ++- tests/conversion/downcast-converters.js | 96 +++++++++++++------------ tests/tickets/699.js | 8 +-- 6 files changed, 118 insertions(+), 120 deletions(-) diff --git a/src/conversion/conversion.js b/src/conversion/conversion.js index d3ddc37b7..ef909de3b 100644 --- a/src/conversion/conversion.js +++ b/src/conversion/conversion.js @@ -9,12 +9,6 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; -import { - downcastElementToElement, - downcastAttributeToElement, - downcastAttributeToAttribute -} from './downcast-converters'; - import { upcastElementToElement, upcastElementToAttribute, @@ -40,12 +34,12 @@ import { * method: * * // Add a converter to editing downcast and data downcast. - * editor.conversion.for( 'downcast' ).add( downcastElementToElement( config ) ); + * editor.conversion.for( 'downcast' ).for( 'downcast' ).elementToElement( config ) ); * * // Add a converter to the data pipepline only: - * editor.conversion.for( 'dataDowncast' ).add( downcastElementToElement( dataConversionConfig ) ); + * editor.conversion.for( 'dataDowncast' ).for( 'downcast' ).elementToElement( dataConversionConfig ) ); * // And a slightly different one for the editing pipeline: - * editor.conversion.for( 'editingDowncast' ).add( downcastElementToElement( editingConversionConfig ) ); + * editor.conversion.for( 'editingDowncast' ).for( 'downcast' ).elementToElement( editingConversionConfig ) ); * * The functions used in `add()` calls are one-way converters (i.e. you need to remember yourself to add * a converter in the other direction, if your feature requires that). They are also called "conversion helpers". @@ -127,9 +121,9 @@ export default class Conversion { * * For downcast (model-to-view conversion), these are: * - * * {@link module:engine/conversion/downcast-converters~downcastElementToElement Downcast element-to-element converter}, - * * {@link module:engine/conversion/downcast-converters~downcastAttributeToElement Downcast attribute-to-element converter}, - * * {@link module:engine/conversion/downcast-converters~downcastAttributeToAttribute Downcast attribute-to-attribute converter}. + * * {@link module:engine/conversion/downcast-converters~_downcastElementToElement Downcast element-to-element converter}, + * * {@link module:engine/conversion/downcast-converters~_downcastAttributeToElement Downcast attribute-to-element converter}, + * * {@link module:engine/conversion/downcast-converters~_downcastAttributeToAttribute Downcast attribute-to-attribute converter}. * * For upcast (view-to-model conversion), these are: * @@ -143,7 +137,7 @@ export default class Conversion { * const config = { model: 'paragraph', view: 'p' }; * * // Add converters to proper dispatchers using conversion helpers. - * conversion.for( 'downcast' ).add( downcastElementToElement( config ) ); + * conversion.for( 'downcast' ).for( 'downcast' ).elementToElement( config ) ); * conversion.for( 'upcast' ).add( upcastElementToElement( config ) ); * * An example of providing a custom conversion helper that uses a custom converter function: @@ -243,7 +237,7 @@ export default class Conversion { */ elementToElement( definition ) { // Set up downcast converter. - this.for( 'downcast' ).add( downcastElementToElement( definition ) ); + this.for( 'downcast' ).elementToElement( definition ); // Set up upcast converter. for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) { @@ -416,7 +410,7 @@ export default class Conversion { */ attributeToElement( definition ) { // Set up downcast converter. - this.for( 'downcast' ).add( downcastAttributeToElement( definition ) ); + this.for( 'downcast' ).attributeToElement( definition ); // Set up upcast converter. for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) { @@ -542,7 +536,7 @@ export default class Conversion { */ attributeToAttribute( definition ) { // Set up downcast converter. - this.for( 'downcast' ).add( downcastAttributeToAttribute( definition ) ); + this.for( 'downcast' ).attributeToAttribute( definition ); // Set up upcast converter. for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) { diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index f6b812014..64f669350 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -24,11 +24,11 @@ import { cloneDeep } from 'lodash-es'; * * This conversion results in creating a view element. For example, model `Foo` becomes `

Foo

` in the view. * - * downcastElementToElement( { model: 'paragraph', view: 'p' } ); + * _downcastElementToElement( { model: 'paragraph', view: 'p' } ); * - * downcastElementToElement( { model: 'paragraph', view: 'div', converterPriority: 'high' } ); + * _downcastElementToElement( { model: 'paragraph', view: 'div', converterPriority: 'high' } ); * - * downcastElementToElement( { + * _downcastElementToElement( { * model: 'fancyParagraph', * view: { * name: 'p', @@ -36,7 +36,7 @@ import { cloneDeep } from 'lodash-es'; * } * } ); * - * downcastElementToElement( { + * _downcastElementToElement( { * model: 'heading', * view: ( modelElement, viewWriter ) => viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) ) * } ); @@ -44,6 +44,7 @@ import { cloneDeep } from 'lodash-es'; * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter * to the conversion process. * + * @protected * @param {Object} config Conversion configuration. * @param {String} config.model The name of the model element to convert. * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function @@ -51,7 +52,7 @@ import { cloneDeep } from 'lodash-es'; * as parameters and returns a view container element. * @returns {Function} Conversion helper. */ -export function downcastElementToElement( config ) { +export function _downcastElementToElement( config ) { config = cloneDeep( config ); config.view = _normalizeToElementConfig( config.view, 'container' ); @@ -67,11 +68,11 @@ export function downcastElementToElement( config ) { * This conversion results in wrapping view nodes with a view attribute element. For example, a model text node with * `"Foo"` as data and the `bold` attribute becomes `Foo` in the view. * - * downcastAttributeToElement( { model: 'bold', view: 'strong' } ); + * _downcastAttributeToElement( { model: 'bold', view: 'strong' } ); * - * downcastAttributeToElement( { model: 'bold', view: 'b', converterPriority: 'high' } ); + * _downcastAttributeToElement( { model: 'bold', view: 'b', converterPriority: 'high' } ); * - * downcastAttributeToElement( { + * _downcastAttributeToElement( { * model: 'invert', * view: { * name: 'span', @@ -79,7 +80,7 @@ export function downcastElementToElement( config ) { * } * } ); * - * downcastAttributeToElement( { + * _downcastAttributeToElement( { * model: { * key: 'fontSize', * values: [ 'big', 'small' ] @@ -100,14 +101,14 @@ export function downcastElementToElement( config ) { * } * } ); * - * downcastAttributeToElement( { + * _downcastAttributeToElement( { * model: 'bold', * view: ( modelAttributeValue, viewWriter ) => { * return viewWriter.createAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } ); * } * } ); * - * downcastAttributeToElement( { + * _downcastAttributeToElement( { * model: { * key: 'color', * name: '$text' @@ -120,6 +121,7 @@ export function downcastElementToElement( config ) { * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter * to the conversion process. * + * @protected * @param {Object} config Conversion configuration. * @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array * of `String`s with possible values if the model attribute is an enumerable. @@ -130,7 +132,7 @@ export function downcastElementToElement( config ) { * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. * @returns {Function} Conversion helper. */ -export function downcastAttributeToElement( config ) { +export function _downcastAttributeToElement( config ) { config = cloneDeep( config ); const modelKey = config.model.key ? config.model.key : config.model; @@ -161,11 +163,11 @@ export function downcastAttributeToElement( config ) { * This conversion results in adding an attribute to a view node, basing on an attribute from a model node. For example, * `` is converted to ``. * - * downcastAttributeToAttribute( { model: 'source', view: 'src' } ); + * _downcastAttributeToAttribute( { model: 'source', view: 'src' } ); * - * downcastAttributeToAttribute( { model: 'source', view: 'href', converterPriority: 'high' } ); + * _downcastAttributeToAttribute( { model: 'source', view: 'href', converterPriority: 'high' } ); * - * downcastAttributeToAttribute( { + * _downcastAttributeToAttribute( { * model: { * name: 'image', * key: 'source' @@ -173,7 +175,7 @@ export function downcastAttributeToElement( config ) { * view: 'src' * } ); * - * downcastAttributeToAttribute( { + * _downcastAttributeToAttribute( { * model: { * name: 'styled', * values: [ 'dark', 'light' ] @@ -190,7 +192,7 @@ export function downcastAttributeToElement( config ) { * } * } ); * - * downcastAttributeToAttribute( { + * _downcastAttributeToAttribute( { * model: 'styled', * view: modelAttributeValue => ( { key: 'class', value: 'styled-' + modelAttributeValue } ) * } ); @@ -198,6 +200,7 @@ export function downcastAttributeToElement( config ) { * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter * to the conversion process. * + * @protected * @param {Object} config Conversion configuration. * @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values, [ name ] }` object describing * the attribute key, possible values and, optionally, an element name to convert from. @@ -209,7 +212,7 @@ export function downcastAttributeToElement( config ) { * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. * @returns {Function} Conversion helper. */ -export function downcastAttributeToAttribute( config ) { +export function _downcastAttributeToAttribute( config ) { config = cloneDeep( config ); const modelKey = config.model.key ? config.model.key : config.model; @@ -241,11 +244,11 @@ export function downcastAttributeToAttribute( config ) { * is collapsed, only one element is created. For example, model marker set like this: `F[oo b]ar` * becomes `

Foo bar

` in the view. * - * downcastMarkerToElement( { model: 'search', view: 'marker-search' } ); + * _downcastMarkerToElement( { model: 'search', view: 'marker-search' } ); * - * downcastMarkerToElement( { model: 'search', view: 'search-result', converterPriority: 'high' } ); + * _downcastMarkerToElement( { model: 'search', view: 'search-result', converterPriority: 'high' } ); * - * downcastMarkerToElement( { + * _downcastMarkerToElement( { * model: 'search', * view: { * name: 'span', @@ -255,7 +258,7 @@ export function downcastAttributeToAttribute( config ) { * } * } ); * - * downcastMarkerToElement( { + * _downcastMarkerToElement( { * model: 'search', * view: ( markerData, viewWriter ) => { * return viewWriter.createUIElement( 'span', { 'data-marker': 'search', 'data-start': markerData.isOpening } ); @@ -273,6 +276,7 @@ export function downcastAttributeToAttribute( config ) { * * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add a converter to the conversion process. * + * @protected * @param {Object} config Conversion configuration. * @param {String} config.model The name of the model marker (or model marker group) to convert. * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function @@ -280,7 +284,7 @@ export function downcastAttributeToAttribute( config ) { * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. * @returns {Function} Conversion helper. */ -export function downcastMarkerToElement( config ) { +export function _downcastMarkerToElement( config ) { config = cloneDeep( config ); config.view = _normalizeToElementConfig( config.view, 'ui' ); @@ -333,6 +337,7 @@ export function downcastMarkerToElement( config ) { * * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add a converter to the conversion process. * + * @protected * @param {Object} config Conversion configuration. * @param {String} config.model The name of the model marker (or model marker group) to convert. * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} config.view A highlight descriptor @@ -340,7 +345,7 @@ export function downcastMarkerToElement( config ) { * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. * @returns {Function} Conversion helper. */ -export function downcastMarkerToHighlight( config ) { +export function _downcastMarkerToHighlight( config ) { return dispatcher => { dispatcher.on( 'addMarker:' + config.model, highlightText( config.view ), { priority: config.converterPriority || 'normal' } ); dispatcher.on( 'addMarker:' + config.model, highlightElement( config.view ), { priority: config.converterPriority || 'normal' } ); @@ -694,7 +699,7 @@ export function changeAttribute( attributeCreator ) { * by {@link module:engine/conversion/conversion~Conversion#attributeToAttribute `Attribute to Attribute converter`}. * In most cases it is caused by converters misconfiguration when only "generic" converter is defined: * - * editor.conversion.for( 'downcast' ).add( downcastAttributeToAttribute( { + * editor.conversion.for( 'downcast' ).attributeToAttribute( { * model: 'attribute-name', * view: 'attribute-name' * } ) ); @@ -710,7 +715,7 @@ export function changeAttribute( attributeCreator ) { * {@link module:engine/conversion/conversion~Conversion#attributeToElement `Attribute to Element converter`} * with higher {@link module:utils/priorities~PriorityString priority} must also be defined: * - * conversion.for( 'downcast' ).add( downcastAttributeToElement( { + * conversion.for( 'downcast' ).attributeToElement( { * model: { * key: 'attribute-name', * name: '$text' @@ -1144,7 +1149,7 @@ export const helpers = { * as parameters and returns a view container element. */ elementToElement( config ) { - return this.add( downcastElementToElement( config ) ); + return this.add( _downcastElementToElement( config ) ); }, /** @@ -1218,7 +1223,7 @@ export const helpers = { * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. */ attributeToElement( config ) { - return this.add( downcastAttributeToElement( config ) ); + return this.add( _downcastAttributeToElement( config ) ); }, /** @@ -1277,7 +1282,7 @@ export const helpers = { * @returns {Function} Conversion helper. */ attributeToAttribute( config ) { - return this.add( downcastAttributeToAttribute( config ) ); + return this.add( _downcastAttributeToAttribute( config ) ); }, /** @@ -1328,7 +1333,7 @@ export const helpers = { * @returns {Function} Conversion helper. */ markerToElement( config ) { - return this.add( downcastMarkerToElement( config ) ); + return this.add( _downcastMarkerToElement( config ) ); }, /** @@ -1386,6 +1391,6 @@ export const helpers = { * @returns {Function} Conversion helper. */ markerToHighlight( config ) { - return this.add( downcastMarkerToHighlight( config ) ); + return this.add( _downcastMarkerToHighlight( config ) ); } }; diff --git a/tests/controller/datacontroller.js b/tests/controller/datacontroller.js index b2e82969e..7aee21c6e 100644 --- a/tests/controller/datacontroller.js +++ b/tests/controller/datacontroller.js @@ -24,9 +24,9 @@ import { } from '../../src/conversion/upcast-converters'; import { - downcastElementToElement, - downcastAttributeToElement, - downcastMarkerToHighlight + _downcastElementToElement, + _downcastAttributeToElement, + _downcastMarkerToHighlight } from '../../src/conversion/downcast-converters'; describe( 'DataController', () => { @@ -285,7 +285,7 @@ describe( 'DataController', () => { schema.register( 'paragraph', { inheritAllFrom: '$block' } ); setData( model, 'foo' ); - downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher ); + _downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher ); expect( data.get() ).to.equal( '

foo

' ); } ); @@ -294,7 +294,7 @@ describe( 'DataController', () => { schema.register( 'paragraph', { inheritAllFrom: '$block' } ); setData( model, '' ); - downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher ); + _downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher ); expect( data.get() ).to.equal( '

 

' ); } ); @@ -303,7 +303,7 @@ describe( 'DataController', () => { schema.register( 'paragraph', { inheritAllFrom: '$block' } ); setData( model, 'foobar' ); - downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher ); + _downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher ); expect( data.get() ).to.equal( '

foo

bar

' ); } ); @@ -319,7 +319,7 @@ describe( 'DataController', () => { schema.register( 'paragraph', { inheritAllFrom: '$block' } ); setData( model, 'foo<$text bold="true">bar' ); - downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher ); + _downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher ); expect( data.get() ).to.equal( '

foobar

' ); } ); @@ -328,8 +328,8 @@ describe( 'DataController', () => { schema.register( 'paragraph', { inheritAllFrom: '$block' } ); setData( model, 'foo<$text bold="true">bar' ); - downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher ); - downcastAttributeToElement( { model: 'bold', view: 'strong' } )( data.downcastDispatcher ); + _downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher ); + _downcastAttributeToElement( { model: 'bold', view: 'strong' } )( data.downcastDispatcher ); expect( data.get() ).to.equal( '

foobar

' ); } ); @@ -341,8 +341,8 @@ describe( 'DataController', () => { setData( model, 'foo', { rootName: 'main' } ); setData( model, 'Bar', { rootName: 'title' } ); - downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher ); - downcastAttributeToElement( { model: 'bold', view: 'strong' } )( data.downcastDispatcher ); + _downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher ); + _downcastAttributeToElement( { model: 'bold', view: 'strong' } )( data.downcastDispatcher ); expect( data.get() ).to.equal( '

foo

' ); expect( data.get( 'main' ) ).to.equal( '

foo

' ); @@ -358,7 +358,7 @@ describe( 'DataController', () => { schema.extend( '$block', { allowIn: 'div' } ); schema.extend( 'div', { allowIn: '$root' } ); - downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher ); + _downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher ); } ); it( 'should stringify a content of an element', () => { @@ -382,7 +382,7 @@ describe( 'DataController', () => { schema.extend( '$block', { allowIn: 'div' } ); schema.extend( 'div', { allowIn: '$root' } ); - downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher ); + _downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher ); } ); it( 'should convert a content of an element', () => { @@ -403,7 +403,7 @@ describe( 'DataController', () => { const modelElement = parseModel( '
foobar
', schema ); const modelRoot = model.document.getRoot(); - downcastMarkerToHighlight( { model: 'marker:a', view: { classes: 'a' } } )( data.downcastDispatcher ); + _downcastMarkerToHighlight( { model: 'marker:a', view: { classes: 'a' } } )( data.downcastDispatcher ); model.change( writer => { writer.insert( modelElement, modelRoot, 0 ); @@ -421,8 +421,8 @@ describe( 'DataController', () => { const modelElement = parseModel( '
foobar
', schema ); const modelRoot = model.document.getRoot(); - downcastMarkerToHighlight( { model: 'marker:a', view: { classes: 'a' } } )( data.downcastDispatcher ); - downcastMarkerToHighlight( { model: 'marker:b', view: { classes: 'b' } } )( data.downcastDispatcher ); + _downcastMarkerToHighlight( { model: 'marker:a', view: { classes: 'a' } } )( data.downcastDispatcher ); + _downcastMarkerToHighlight( { model: 'marker:b', view: { classes: 'b' } } )( data.downcastDispatcher ); const modelP1 = modelElement.getChild( 0 ); const modelP2 = modelElement.getChild( 1 ); diff --git a/tests/controller/editingcontroller.js b/tests/controller/editingcontroller.js index 8a67ed62a..b274c3602 100644 --- a/tests/controller/editingcontroller.js +++ b/tests/controller/editingcontroller.js @@ -14,8 +14,7 @@ import View from '../../src/view/view'; import Mapper from '../../src/conversion/mapper'; import DowncastDispatcher from '../../src/conversion/downcastdispatcher'; -import { downcastElementToElement, downcastMarkerToHighlight } from '../../src/conversion/downcast-converters'; - +import { _downcastElementToElement, _downcastMarkerToHighlight } from '../../src/conversion/downcast-converters'; import Model from '../../src/model/model'; import ModelPosition from '../../src/model/position'; import ModelRange from '../../src/model/range'; @@ -91,9 +90,9 @@ describe( 'EditingController', () => { model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); model.schema.register( 'div', { inheritAllFrom: '$block' } ); - downcastElementToElement( { model: 'paragraph', view: 'p' } )( editing.downcastDispatcher ); - downcastElementToElement( { model: 'div', view: 'div' } )( editing.downcastDispatcher ); - downcastMarkerToHighlight( { model: 'marker', view: {} } )( editing.downcastDispatcher ); + _downcastElementToElement( { model: 'paragraph', view: 'p' } )( editing.downcastDispatcher ); + _downcastElementToElement( { model: 'div', view: 'div' } )( editing.downcastDispatcher ); + _downcastMarkerToHighlight( { model: 'marker', view: {} } )( editing.downcastDispatcher ); // Note: The below code is highly overcomplicated due to #455. model.change( writer => { diff --git a/tests/conversion/downcast-converters.js b/tests/conversion/downcast-converters.js index bba8208bb..40835e661 100644 --- a/tests/conversion/downcast-converters.js +++ b/tests/conversion/downcast-converters.js @@ -21,7 +21,11 @@ import log from '@ckeditor/ckeditor5-utils/src/log'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { - downcastElementToElement, downcastAttributeToElement, downcastAttributeToAttribute, downcastMarkerToElement, downcastMarkerToHighlight, + _downcastElementToElement, + _downcastAttributeToElement, + _downcastAttributeToAttribute, + _downcastMarkerToElement, + _downcastMarkerToHighlight, insertElement, insertUIElement, changeAttribute, wrap, removeUIElement, highlightElement, highlightText, removeHighlight, createViewElementFromHighlightDescriptor } from '../../src/conversion/downcast-converters'; @@ -48,9 +52,9 @@ describe( 'downcast-helpers', () => { conversion.register( { name: 'downcast', dispatcher: controller.downcastDispatcher } ); } ); - describe( 'downcastElementToElement', () => { + describe( '_downcastElementToElement', () => { it( 'config.view is a string', () => { - const helper = downcastElementToElement( { model: 'paragraph', view: 'p' } ); + const helper = _downcastElementToElement( { model: 'paragraph', view: 'p' } ); conversion.for( 'downcast' ).add( helper ); @@ -62,8 +66,8 @@ describe( 'downcast-helpers', () => { } ); it( 'can be overwritten using converterPriority', () => { - const helperA = downcastElementToElement( { model: 'paragraph', view: 'p' } ); - const helperB = downcastElementToElement( { model: 'paragraph', view: 'foo', converterPriority: 'high' } ); + const helperA = _downcastElementToElement( { model: 'paragraph', view: 'p' } ); + const helperB = _downcastElementToElement( { model: 'paragraph', view: 'foo', converterPriority: 'high' } ); conversion.for( 'downcast' ).add( helperA ).add( helperB ); @@ -75,7 +79,7 @@ describe( 'downcast-helpers', () => { } ); it( 'config.view is a view element definition', () => { - const helper = downcastElementToElement( { + const helper = _downcastElementToElement( { model: 'fancyParagraph', view: { name: 'p', @@ -93,7 +97,7 @@ describe( 'downcast-helpers', () => { } ); it( 'config.view is a function', () => { - const helper = downcastElementToElement( { + const helper = _downcastElementToElement( { model: 'heading', view: ( modelElement, viewWriter ) => viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) ) } ); @@ -108,9 +112,9 @@ describe( 'downcast-helpers', () => { } ); } ); - describe( 'downcastAttributeToElement', () => { + describe( '_downcastAttributeToElement', () => { it( 'config.view is a string', () => { - const helper = downcastAttributeToElement( { model: 'bold', view: 'strong' } ); + const helper = _downcastAttributeToElement( { model: 'bold', view: 'strong' } ); conversion.for( 'downcast' ).add( helper ); @@ -122,8 +126,8 @@ describe( 'downcast-helpers', () => { } ); it( 'can be overwritten using converterPriority', () => { - const helperA = downcastAttributeToElement( { model: 'bold', view: 'strong' } ); - const helperB = downcastAttributeToElement( { model: 'bold', view: 'b', converterPriority: 'high' } ); + const helperA = _downcastAttributeToElement( { model: 'bold', view: 'strong' } ); + const helperB = _downcastAttributeToElement( { model: 'bold', view: 'b', converterPriority: 'high' } ); conversion.for( 'downcast' ).add( helperA ).add( helperB ); @@ -135,7 +139,7 @@ describe( 'downcast-helpers', () => { } ); it( 'config.view is a view element definition', () => { - const helper = downcastAttributeToElement( { + const helper = _downcastAttributeToElement( { model: 'invert', view: { name: 'span', @@ -154,7 +158,7 @@ describe( 'downcast-helpers', () => { } ); it( 'config.view allows specifying the element\'s priority', () => { - const helper = downcastAttributeToElement( { + const helper = _downcastAttributeToElement( { model: 'invert', view: { name: 'span', @@ -172,7 +176,7 @@ describe( 'downcast-helpers', () => { } ); it( 'model attribute value is enum', () => { - const helper = downcastAttributeToElement( { + const helper = _downcastAttributeToElement( { model: { key: 'fontSize', values: [ 'big', 'small' ] @@ -218,7 +222,7 @@ describe( 'downcast-helpers', () => { } ); it( 'config.view is a function', () => { - const helper = downcastAttributeToElement( { + const helper = _downcastAttributeToElement( { model: 'bold', view: ( modelAttributeValue, viewWriter ) => { return viewWriter.createAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } ); @@ -235,7 +239,7 @@ describe( 'downcast-helpers', () => { } ); it( 'config.model.name is given', () => { - const helper = downcastAttributeToElement( { + const helper = _downcastAttributeToElement( { model: { key: 'color', name: '$text' @@ -247,7 +251,7 @@ describe( 'downcast-helpers', () => { conversion.for( 'downcast' ) .add( helper ) - .add( downcastElementToElement( { + .add( _downcastElementToElement( { model: 'smiley', view: ( modelElement, viewWriter ) => { return viewWriter.createEmptyElement( 'img', { @@ -266,15 +270,15 @@ describe( 'downcast-helpers', () => { } ); } ); - describe( 'downcastAttributeToAttribute', () => { + describe( '_downcastAttributeToAttribute', () => { testUtils.createSinonSandbox(); beforeEach( () => { - conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'image', view: 'img' } ) ); + conversion.for( 'downcast' ).add( _downcastElementToElement( { model: 'image', view: 'img' } ) ); } ); it( 'config.view is a string', () => { - const helper = downcastAttributeToAttribute( { model: 'source', view: 'src' } ); + const helper = _downcastAttributeToAttribute( { model: 'source', view: 'src' } ); conversion.for( 'downcast' ).add( helper ); @@ -292,8 +296,8 @@ describe( 'downcast-helpers', () => { } ); it( 'can be overwritten using converterPriority', () => { - const helperA = downcastAttributeToAttribute( { model: 'source', view: 'href' } ); - const helperB = downcastAttributeToAttribute( { model: 'source', view: 'src', converterPriority: 'high' } ); + const helperA = _downcastAttributeToAttribute( { model: 'source', view: 'href' } ); + const helperB = _downcastAttributeToAttribute( { model: 'source', view: 'src', converterPriority: 'high' } ); conversion.for( 'downcast' ).add( helperA ).add( helperB ); @@ -305,9 +309,9 @@ describe( 'downcast-helpers', () => { } ); it( 'model element name specified', () => { - conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'paragraph', view: 'p' } ) ); + conversion.for( 'downcast' ).add( _downcastElementToElement( { model: 'paragraph', view: 'p' } ) ); - const helper = downcastAttributeToAttribute( { + const helper = _downcastAttributeToAttribute( { model: { name: 'image', key: 'source' @@ -331,9 +335,9 @@ describe( 'downcast-helpers', () => { } ); it( 'config.view is an object, model attribute value is enum', () => { - conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'paragraph', view: 'p' } ) ); + conversion.for( 'downcast' ).add( _downcastElementToElement( { model: 'paragraph', view: 'p' } ) ); - const helper = downcastAttributeToAttribute( { + const helper = _downcastAttributeToAttribute( { model: { key: 'styled', values: [ 'dark', 'light' ] @@ -372,9 +376,9 @@ describe( 'downcast-helpers', () => { } ); it( 'config.view is an object, model attribute value is enum, view has style', () => { - conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'paragraph', view: 'p' } ) ); + conversion.for( 'downcast' ).add( _downcastElementToElement( { model: 'paragraph', view: 'p' } ) ); - const helper = downcastAttributeToAttribute( { + const helper = _downcastAttributeToAttribute( { model: { key: 'align', values: [ 'right', 'center' ] @@ -417,9 +421,9 @@ describe( 'downcast-helpers', () => { } ); it( 'config.view is an object, only name and key are provided', () => { - conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'paragraph', view: 'p' } ) ); + conversion.for( 'downcast' ).add( _downcastElementToElement( { model: 'paragraph', view: 'p' } ) ); - const helper = downcastAttributeToAttribute( { + const helper = _downcastAttributeToAttribute( { model: { name: 'paragraph', key: 'class' @@ -452,7 +456,7 @@ describe( 'downcast-helpers', () => { } ); it( 'config.view is a function', () => { - const helper = downcastAttributeToAttribute( { + const helper = _downcastAttributeToAttribute( { model: 'styled', view: attributeValue => ( { key: 'class', value: 'styled-' + attributeValue } ) } ); @@ -470,9 +474,9 @@ describe( 'downcast-helpers', () => { it( 'config.view and config.model as strings in generic conversion (elements only)', () => { const logSpy = testUtils.sinon.spy( log, 'warn' ); - conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'paragraph', view: 'p' } ) ); + conversion.for( 'downcast' ).add( _downcastElementToElement( { model: 'paragraph', view: 'p' } ) ); - conversion.for( 'downcast' ).add( downcastAttributeToAttribute( { model: 'test', view: 'test' } ) ); + conversion.for( 'downcast' ).add( _downcastAttributeToAttribute( { model: 'test', view: 'test' } ) ); model.change( writer => { writer.insertElement( 'paragraph', { test: '1' }, modelRoot, 0 ); @@ -493,9 +497,9 @@ describe( 'downcast-helpers', () => { it( 'config.view and config.model as strings in generic conversion (elements + text)', () => { const logSpy = testUtils.sinon.spy( log, 'warn' ); - conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'paragraph', view: 'p' } ) ); + conversion.for( 'downcast' ).add( _downcastElementToElement( { model: 'paragraph', view: 'p' } ) ); - conversion.for( 'downcast' ).add( downcastAttributeToAttribute( { model: 'test', view: 'test' } ) ); + conversion.for( 'downcast' ).add( _downcastAttributeToAttribute( { model: 'test', view: 'test' } ) ); model.change( writer => { writer.insertElement( 'paragraph', modelRoot, 0 ); @@ -517,9 +521,9 @@ describe( 'downcast-helpers', () => { } ); } ); - describe( 'downcastMarkerToElement', () => { + describe( '_downcastMarkerToElement', () => { it( 'config.view is a string', () => { - const helper = downcastMarkerToElement( { model: 'search', view: 'marker-search' } ); + const helper = _downcastMarkerToElement( { model: 'search', view: 'marker-search' } ); conversion.for( 'downcast' ).add( helper ); @@ -534,8 +538,8 @@ describe( 'downcast-helpers', () => { } ); it( 'can be overwritten using converterPriority', () => { - const helperA = downcastMarkerToElement( { model: 'search', view: 'marker-search' } ); - const helperB = downcastMarkerToElement( { model: 'search', view: 'search', converterPriority: 'high' } ); + const helperA = _downcastMarkerToElement( { model: 'search', view: 'marker-search' } ); + const helperB = _downcastMarkerToElement( { model: 'search', view: 'search', converterPriority: 'high' } ); conversion.for( 'downcast' ).add( helperA ).add( helperB ); @@ -549,7 +553,7 @@ describe( 'downcast-helpers', () => { } ); it( 'config.view is a view element definition', () => { - const helper = downcastMarkerToElement( { + const helper = _downcastMarkerToElement( { model: 'search', view: { name: 'span', @@ -571,7 +575,7 @@ describe( 'downcast-helpers', () => { } ); it( 'config.view is a function', () => { - const helper = downcastMarkerToElement( { + const helper = _downcastMarkerToElement( { model: 'search', view: ( data, viewWriter ) => { return viewWriter.createUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } ); @@ -590,9 +594,9 @@ describe( 'downcast-helpers', () => { } ); } ); - describe( 'downcastMarkerToHighlight', () => { + describe( '_downcastMarkerToHighlight', () => { it( 'config.view is a highlight descriptor', () => { - const helper = downcastMarkerToHighlight( { model: 'comment', view: { classes: 'comment' } } ); + const helper = _downcastMarkerToHighlight( { model: 'comment', view: { classes: 'comment' } } ); conversion.for( 'downcast' ).add( helper ); @@ -606,8 +610,8 @@ describe( 'downcast-helpers', () => { } ); it( 'can be overwritten using converterPriority', () => { - const helperA = downcastMarkerToHighlight( { model: 'comment', view: { classes: 'comment' } } ); - const helperB = downcastMarkerToHighlight( { model: 'comment', view: { classes: 'new-comment' }, converterPriority: 'high' } ); + const helperA = _downcastMarkerToHighlight( { model: 'comment', view: { classes: 'comment' } } ); + const helperB = _downcastMarkerToHighlight( { model: 'comment', view: { classes: 'new-comment' }, converterPriority: 'high' } ); conversion.for( 'downcast' ).add( helperA ).add( helperB ); @@ -621,7 +625,7 @@ describe( 'downcast-helpers', () => { } ); it( 'config.view is a function', () => { - const helper = downcastMarkerToHighlight( { + const helper = _downcastMarkerToHighlight( { model: 'comment', view: data => { const commentType = data.markerName.split( ':' )[ 1 ]; diff --git a/tests/tickets/699.js b/tests/tickets/699.js index 69a3ccb24..5b08f3ffc 100644 --- a/tests/tickets/699.js +++ b/tests/tickets/699.js @@ -12,10 +12,6 @@ import { upcastElementToElement } from '../../src/conversion/upcast-converters'; -import { - downcastElementToElement -} from '../../src/conversion/downcast-converters'; - import { getData as getModelData } from '../../src/dev-utils/model'; import { getData as getViewData } from '../../src/dev-utils/view'; @@ -54,10 +50,10 @@ function WidgetPlugin( editor ) { } ); schema.extend( 'widget', { allowIn: '$root' } ); - editor.conversion.for( 'downcast' ).add( downcastElementToElement( { + editor.conversion.for( 'downcast' ).elementToElement( { model: 'widget', view: 'widget' - } ) ); + } ); editor.conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'widget', From 6478f829180b892c10f01b208f5521ecf5f8a758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 17 Dec 2018 14:42:29 +0100 Subject: [PATCH 37/84] Align code to the changes in conversion API. --- tests/manual/highlight.js | 13 ++++--------- tests/manual/markers.js | 12 ++++-------- tests/manual/nestededitable.js | 12 ++++-------- tests/manual/selection.js | 12 +++++------- tests/manual/tickets/475/1.js | 8 ++------ tests/manual/tickets/ckeditor5-721/1.js | 9 ++++----- 6 files changed, 23 insertions(+), 43 deletions(-) diff --git a/tests/manual/highlight.js b/tests/manual/highlight.js index 0a7137729..1a1c34a63 100644 --- a/tests/manual/highlight.js +++ b/tests/manual/highlight.js @@ -9,11 +9,6 @@ import { upcastElementToElement, } from '../../src/conversion/upcast-converters'; -import { - downcastElementToElement, - downcastMarkerToHighlight -} from '../../src/conversion/downcast-converters'; - import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; import Enter from '@ckeditor/ckeditor5-enter/src/enter'; import Typing from '@ckeditor/ckeditor5-typing/src/typing'; @@ -44,7 +39,7 @@ class FancyWidget extends Plugin { } ); schema.extend( 'fancywidget', { allowIn: '$root' } ); - conversion.for( 'editingDowncast' ).add( downcastElementToElement( { + conversion.for( 'editingDowncast' ).elementToElement( { model: 'fancywidget', view: ( modelItem, viewWriter ) => { const widgetElement = viewWriter.createContainerElement( 'figure', { class: 'fancy-widget' } ); @@ -52,7 +47,7 @@ class FancyWidget extends Plugin { return toWidget( widgetElement, viewWriter ); } - } ) ); + } ); conversion.for( 'upcast' ) .add( upcastElementToElement( { @@ -69,12 +64,12 @@ ClassicEditor.create( global.document.querySelector( '#editor' ), { .then( editor => { window.editor = editor; - editor.conversion.for( 'editingDowncast' ).add( downcastMarkerToHighlight( { + editor.conversion.for( 'editingDowncast' ).markerToHighlight( { model: 'marker', view: data => ( { classes: 'highlight-' + data.markerName.split( ':' )[ 1 ] } ) - } ) ); + } ); document.getElementById( 'add-marker-yellow' ).addEventListener( 'mousedown', evt => { addMarker( editor, 'yellow' ); diff --git a/tests/manual/markers.js b/tests/manual/markers.js index c96bc9428..7b4e7a7f6 100644 --- a/tests/manual/markers.js +++ b/tests/manual/markers.js @@ -15,10 +15,6 @@ import List from '@ckeditor/ckeditor5-list/src/list'; import Heading from '@ckeditor/ckeditor5-heading/src/heading'; import Undo from '@ckeditor/ckeditor5-undo/src/undo'; -import { - downcastMarkerToHighlight -} from '../../src/conversion/downcast-converters'; - import Position from '../../src/model/position'; import Range from '../../src/model/range'; @@ -35,7 +31,7 @@ ClassicEditor window.editor = editor; model = editor.model; - editor.conversion.for( 'editingDowncast' ).add( downcastMarkerToHighlight( { + editor.conversion.for( 'editingDowncast' ).markerToHighlight( { model: 'highlight', view: data => { const color = data.markerName.split( ':' )[ 1 ]; @@ -45,9 +41,9 @@ ClassicEditor priority: 1 }; } - } ) ); + } ); - editor.conversion.for( 'dataDowncast' ).add( downcastMarkerToHighlight( { + editor.conversion.for( 'dataDowncast' ).markerToHighlight( { model: 'highlight', view: data => { const color = data.markerName.split( ':' )[ 1 ]; @@ -57,7 +53,7 @@ ClassicEditor priority: 1 }; } - } ) ); + } ); window.document.getElementById( 'add-yellow' ).addEventListener( 'mousedown', e => { e.preventDefault(); diff --git a/tests/manual/nestededitable.js b/tests/manual/nestededitable.js index 631c1a875..111894485 100644 --- a/tests/manual/nestededitable.js +++ b/tests/manual/nestededitable.js @@ -9,10 +9,6 @@ import { upcastElementToElement } from '../../src/conversion/upcast-converters'; -import { - downcastElementToElement -} from '../../src/conversion/downcast-converters'; - import { getData } from '../../src/dev-utils/model'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; @@ -41,7 +37,7 @@ class NestedEditable extends Plugin { allowIn: [ 'figure', 'figcaption' ] } ); - editor.conversion.for( 'downcast' ).add( downcastElementToElement( { + editor.conversion.for( 'downcast' ).elementToElement( { model: 'figure', view: { name: 'figure', @@ -49,14 +45,14 @@ class NestedEditable extends Plugin { contenteditable: 'false' } } - } ) ); + } ); editor.conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'figure', view: 'figure' } ) ); - editor.conversion.for( 'downcast' ).add( downcastElementToElement( { + editor.conversion.for( 'downcast' ).elementToElement( { model: 'figcaption', view: ( modelItem, viewWriter ) => { const element = viewWriter.createEditableElement( 'figcaption', { contenteditable: 'true' } ); @@ -71,7 +67,7 @@ class NestedEditable extends Plugin { return element; } - } ) ); + } ); editor.conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'figcaption', diff --git a/tests/manual/selection.js b/tests/manual/selection.js index 894b5a6cc..3fb11d78e 100644 --- a/tests/manual/selection.js +++ b/tests/manual/selection.js @@ -5,8 +5,6 @@ /* global console */ -import { downcastElementToElement } from '../../src/conversion/downcast-converters'; - import { getData } from '../../src/dev-utils/model'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; @@ -48,20 +46,20 @@ class SelectionTest extends Plugin { editor.conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableRow', view: 'tr' } ) ); editor.conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) ); - editor.conversion.for( 'downcast' ).add( downcastElementToElement( { + editor.conversion.for( 'downcast' ).elementToElement( { model: 'table', view: ( modelItem, viewWriter ) => { return toWidget( viewWriter.createContainerElement( 'table' ), viewWriter ); } - } ) ); - editor.conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'tableRow', view: 'tr' } ) ); + } ); + editor.conversion.for( 'downcast' ).elementToElement( { model: 'tableRow', view: 'tr' } ); - editor.conversion.for( 'downcast' ).add( downcastElementToElement( { + editor.conversion.for( 'downcast' ).elementToElement( { model: 'tableCell', view: ( modelItem, viewWriter ) => { return toWidgetEditable( viewWriter.createEditableElement( 'td' ), viewWriter ); } - } ) ); + } ); } } diff --git a/tests/manual/tickets/475/1.js b/tests/manual/tickets/475/1.js index ca8f71e5a..c5fc1943c 100644 --- a/tests/manual/tickets/475/1.js +++ b/tests/manual/tickets/475/1.js @@ -15,10 +15,6 @@ import { upcastElementToAttribute } from '../../../../src/conversion/upcast-converters'; -import { - downcastAttributeToElement, -} from '../../../../src/conversion/downcast-converters'; - import Enter from '@ckeditor/ckeditor5-enter/src/enter'; import Typing from '@ckeditor/ckeditor5-typing/src/typing'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; @@ -31,12 +27,12 @@ class Link extends Plugin { // Allow bold attribute on all inline nodes. editor.model.schema.extend( '$text', { allowAttributes: 'link' } ); - editor.conversion.for( 'downcast' ).add( downcastAttributeToElement( { + editor.conversion.for( 'downcast' ).attributeToElement( { model: 'link', view: ( modelAttributeValue, viewWriter ) => { return viewWriter.createAttributeElement( 'a', { href: modelAttributeValue } ); } - } ) ); + } ); editor.conversion.for( 'upcast' ).add( upcastElementToAttribute( { view: 'a', diff --git a/tests/manual/tickets/ckeditor5-721/1.js b/tests/manual/tickets/ckeditor5-721/1.js index 3a8d2feee..a482b595f 100644 --- a/tests/manual/tickets/ckeditor5-721/1.js +++ b/tests/manual/tickets/ckeditor5-721/1.js @@ -13,7 +13,6 @@ import { toWidget } from '@ckeditor/ckeditor5-widget/src/utils'; import Widget from '@ckeditor/ckeditor5-widget/src/widget'; import ViewPosition from '../../../../src/view/position'; -import { downcastElementToElement } from '../../../../src/conversion/downcast-converters'; import { setData } from '../../../../src/dev-utils/model'; ClassicEditor @@ -41,7 +40,7 @@ ClassicEditor } ); editor.conversion.for( 'downcast' ) - .add( downcastElementToElement( { + .elementToElement( { model: 'widget', view: ( modelItem, writer ) => { const b = writer.createAttributeElement( 'b' ); @@ -51,11 +50,11 @@ ClassicEditor return toWidget( div, writer, { label: 'element label' } ); } - } ) ) - .add( downcastElementToElement( { + } ) + .elementToElement( { model: 'nested', view: ( item, writer ) => writer.createEditableElement( 'figcaption', { contenteditable: true } ) - } ) ); + } ); setData( editor.model, 'foo[]' + From 97214957efa885d586a6c2769d24618383f5ee54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 17 Dec 2018 15:53:20 +0100 Subject: [PATCH 38/84] Align code to the changes in upcast conversion API. --- src/conversion/conversion.js | 43 ++++---- src/conversion/upcast-converters.js | 58 +++++------ tests/controller/datacontroller.js | 14 +-- tests/conversion/upcast-converters.js | 110 ++++++++++----------- tests/manual/highlight.js | 13 +-- tests/manual/nestededitable.js | 12 +-- tests/manual/selection.js | 7 +- tests/manual/tickets/475/1.js | 8 +- tests/tickets/699.js | 8 +- tests/utils/bindtwostepcarettoattribute.js | 7 +- 10 files changed, 126 insertions(+), 154 deletions(-) diff --git a/src/conversion/conversion.js b/src/conversion/conversion.js index 0c3875793..22fdf1a52 100644 --- a/src/conversion/conversion.js +++ b/src/conversion/conversion.js @@ -9,12 +9,6 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; -import { - upcastElementToElement, - upcastElementToAttribute, - upcastAttributeToAttribute -} from './upcast-converters'; - /** * A utility class that helps add converters to upcast and downcast dispatchers. * @@ -34,12 +28,12 @@ import { * method: * * // Add a converter to editing downcast and data downcast. - * editor.conversion.for( 'downcast' ).for( 'downcast' ).elementToElement( config ) ); + * editor.conversion.for( 'downcast' ).elementToElement( config ) ); * * // Add a converter to the data pipepline only: - * editor.conversion.for( 'dataDowncast' ).for( 'downcast' ).elementToElement( dataConversionConfig ) ); + * editor.conversion.for( 'dataDowncast' ).elementToElement( dataConversionConfig ) ); * // And a slightly different one for the editing pipeline: - * editor.conversion.for( 'editingDowncast' ).for( 'downcast' ).elementToElement( editingConversionConfig ) ); + * editor.conversion.for( 'editingDowncast' ).elementToElement( editingConversionConfig ) ); * * The functions used in `add()` calls are one-way converters (i.e. you need to remember yourself to add * a converter in the other direction, if your feature requires that). They are also called "conversion helpers". @@ -127,9 +121,9 @@ export default class Conversion { * * For upcast (view-to-model conversion), these are: * - * * {@link module:engine/conversion/upcast-converters~upcastElementToElement Upcast element-to-element converter}, - * * {@link module:engine/conversion/upcast-converters~upcastElementToAttribute Upcast attribute-to-element converter}, - * * {@link module:engine/conversion/upcast-converters~upcastAttributeToAttribute Upcast attribute-to-attribute converter}. + * * {@link module:engine/conversion/upcast-converters~_upcastElementToElement Upcast element-to-element converter}, + * * {@link module:engine/conversion/upcast-converters~_upcastElementToAttribute Upcast attribute-to-element converter}, + * * {@link module:engine/conversion/upcast-converters~_upcastAttributeToAttribute Upcast attribute-to-attribute converter}. * * An example of using conversion helpers to convert the `paragraph` model element to the `p` view element (and back): * @@ -137,8 +131,8 @@ export default class Conversion { * const config = { model: 'paragraph', view: 'p' }; * * // Add converters to proper dispatchers using conversion helpers. - * conversion.for( 'downcast' ).for( 'downcast' ).elementToElement( config ) ); - * conversion.for( 'upcast' ).add( upcastElementToElement( config ) ); + * conversion.for( 'downcast' ).elementToElement( config ) ); + * conversion.for( 'upcast' ).elementToElement( config ) ); * * An example of providing a custom conversion helper that uses a custom converter function: * @@ -241,13 +235,12 @@ export default class Conversion { // Set up upcast converter. for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) { - this.for( 'upcast' ).add( - upcastElementToElement( { + this.for( 'upcast' ) + .elementToElement( { model, view, converterPriority: definition.converterPriority - } ) - ); + } ); } } @@ -414,13 +407,12 @@ export default class Conversion { // Set up upcast converter. for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) { - this.for( 'upcast' ).add( - upcastElementToAttribute( { + this.for( 'upcast' ) + .elementToAttribute( { view, model, converterPriority: definition.priority - } ) - ); + } ); } } @@ -540,12 +532,11 @@ export default class Conversion { // Set up upcast converter. for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) { - this.for( 'upcast' ).add( - upcastAttributeToAttribute( { + this.for( 'upcast' ) + .attributeToAttribute( { view, model - } ) - ); + } ); } } diff --git a/src/conversion/upcast-converters.js b/src/conversion/upcast-converters.js index 720b81cca..6a58c7563 100644 --- a/src/conversion/upcast-converters.js +++ b/src/conversion/upcast-converters.js @@ -23,11 +23,11 @@ import { cloneDeep } from 'lodash-es'; * * Keep in mind that the element will be inserted only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration. * - * upcastElementToElement( { view: 'p', model: 'paragraph' } ); + * _upcastElementToElement( { view: 'p', model: 'paragraph' } ); * - * upcastElementToElement( { view: 'p', model: 'paragraph', converterPriority: 'high' } ); + * _upcastElementToElement( { view: 'p', model: 'paragraph', converterPriority: 'high' } ); * - * upcastElementToElement( { + * _upcastElementToElement( { * view: { * name: 'p', * classes: 'fancy' @@ -35,7 +35,7 @@ import { cloneDeep } from 'lodash-es'; * model: 'fancyParagraph' * } ); * - * upcastElementToElement( { + * _upcastElementToElement( { * view: { * name: 'p', * classes: 'heading' @@ -54,7 +54,7 @@ import { cloneDeep } from 'lodash-es'; * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. * @returns {Function} Conversion helper. */ -export function upcastElementToElement( config ) { +export function _upcastElementToElement( config ) { config = cloneDeep( config ); const converter = _prepareToElementConverter( config ); @@ -83,11 +83,11 @@ export function upcastElementToElement( config ) { * * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration. * - * upcastElementToAttribute( { view: 'strong', model: 'bold' } ); + * _upcastElementToAttribute( { view: 'strong', model: 'bold' } ); * - * upcastElementToAttribute( { view: 'strong', model: 'bold', converterPriority: 'high' } ); + * _upcastElementToAttribute( { view: 'strong', model: 'bold', converterPriority: 'high' } ); * - * upcastElementToAttribute( { + * _upcastElementToAttribute( { * view: { * name: 'span', * classes: 'bold' @@ -95,7 +95,7 @@ export function upcastElementToElement( config ) { * model: 'bold' * } ); * - * upcastElementToAttribute( { + * _upcastElementToAttribute( { * view: { * name: 'span', * classes: [ 'styled', 'styled-dark' ] @@ -106,7 +106,7 @@ export function upcastElementToElement( config ) { * } * } ); * - * upcastElementToAttribute( { + * _upcastElementToAttribute( { * view: { * name: 'span', * styles: { @@ -140,7 +140,7 @@ export function upcastElementToElement( config ) { * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. * @returns {Function} Conversion helper. */ -export function upcastElementToAttribute( config ) { +export function _upcastElementToAttribute( config ) { config = cloneDeep( config ); _normalizeModelAttributeConfig( config ); @@ -176,13 +176,13 @@ export function upcastElementToAttribute( config ) { * * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration. * - * upcastAttributeToAttribute( { view: 'src', model: 'source' } ); + * _upcastAttributeToAttribute( { view: 'src', model: 'source' } ); * - * upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' } ); + * _upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' } ); * - * upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source', converterPriority: 'normal' } ); + * _upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source', converterPriority: 'normal' } ); * - * upcastAttributeToAttribute( { + * _upcastAttributeToAttribute( { * view: { * key: 'data-style', * value: /[\s\S]+/ @@ -190,7 +190,7 @@ export function upcastElementToAttribute( config ) { * model: 'styled' * } ); * - * upcastAttributeToAttribute( { + * _upcastAttributeToAttribute( { * view: { * name: 'img', * key: 'class', @@ -202,7 +202,7 @@ export function upcastElementToAttribute( config ) { * } * } ); * - * upcastAttributeToAttribute( { + * _upcastAttributeToAttribute( { * view: { * key: 'class', * value: /styled-[\S]+/ @@ -232,7 +232,7 @@ export function upcastElementToAttribute( config ) { * @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority. * @returns {Function} Conversion helper. */ -export function upcastAttributeToAttribute( config ) { +export function _upcastAttributeToAttribute( config ) { config = cloneDeep( config ); let viewKey = null; @@ -258,16 +258,16 @@ export function upcastAttributeToAttribute( config ) { * after the conversion is done, the marker will be available in * {@link module:engine/model/model~Model#markers model document markers}. * - * upcastElementToMarker( { view: 'marker-search', model: 'search' } ); + * _upcastElementToMarker( { view: 'marker-search', model: 'search' } ); * - * upcastElementToMarker( { view: 'marker-search', model: 'search', converterPriority: 'high' } ); + * _upcastElementToMarker( { view: 'marker-search', model: 'search', converterPriority: 'high' } ); * - * upcastElementToMarker( { + * _upcastElementToMarker( { * view: 'marker-search', * model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' ) * } ); * - * upcastElementToMarker( { + * _upcastElementToMarker( { * view: { * name: 'span', * attributes: { @@ -286,12 +286,12 @@ export function upcastAttributeToAttribute( config ) { * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. * @returns {Function} Conversion helper. */ -export function upcastElementToMarker( config ) { +export function _upcastElementToMarker( config ) { config = cloneDeep( config ); _normalizeToMarkerConfig( config ); - return upcastElementToElement( config ); + return _upcastElementToElement( config ); } // Helper function for from-view-element conversion. Checks if `config.view` directly specifies converted view element's name @@ -548,7 +548,7 @@ function _setAttributeOn( modelRange, modelAttribute, shallow, conversionApi ) { } // Helper function for upcasting-to-marker conversion. Takes the config in a format requested by `upcastElementToMarker()` -// function and converts it to a format that is supported by `upcastElementToElement()` function. +// function and converts it to a format that is supported by `_upcastElementToElement()` function. // // @param {Object} config Conversion configuration. function _normalizeToMarkerConfig( config ) { @@ -657,7 +657,7 @@ export const helpers = { * @returns {Function} Conversion helper. */ elementToElement( config ) { - return this.add( upcastElementToElement( config ) ); + return this.add( _upcastElementToElement( config ) ); }, /** @@ -734,7 +734,7 @@ export const helpers = { * @returns {module:engine/conversion/conversion~Conversion} Conversion helper. */ elementToAttribute( config ) { - return this.add( upcastElementToAttribute( config ) ); + return this.add( _upcastElementToAttribute( config ) ); }, /** @@ -815,7 +815,7 @@ export const helpers = { * @returns {Function} Conversion helper. */ attributeToAttribute( config ) { - return this.add( upcastAttributeToAttribute( config ) ); + return this.add( _upcastAttributeToAttribute( config ) ); }, /** @@ -855,6 +855,6 @@ export const helpers = { * @returns {Function} Conversion helper. */ elementToMarker( config ) { - return this.add( upcastElementToMarker( config ) ); + return this.add( _upcastElementToMarker( config ) ); } }; diff --git a/tests/controller/datacontroller.js b/tests/controller/datacontroller.js index 7aee21c6e..940225199 100644 --- a/tests/controller/datacontroller.js +++ b/tests/controller/datacontroller.js @@ -19,8 +19,8 @@ import { parse as parseView, stringify as stringifyView } from '../../src/dev-ut import count from '@ckeditor/ckeditor5-utils/src/count'; import { - upcastElementToElement, - upcastElementToAttribute + _upcastElementToElement, + _upcastElementToAttribute } from '../../src/conversion/upcast-converters'; import { @@ -68,7 +68,7 @@ describe( 'DataController', () => { it( 'should set paragraph', () => { schema.register( 'paragraph', { inheritAllFrom: '$block' } ); - upcastElementToElement( { view: 'p', model: 'paragraph' } )( data.upcastDispatcher ); + _upcastElementToElement( { view: 'p', model: 'paragraph' } )( data.upcastDispatcher ); const output = data.parse( '

foobar

' ); @@ -79,7 +79,7 @@ describe( 'DataController', () => { it( 'should set two paragraphs', () => { schema.register( 'paragraph', { inheritAllFrom: '$block' } ); - upcastElementToElement( { view: 'p', model: 'paragraph' } )( data.upcastDispatcher ); + _upcastElementToElement( { view: 'p', model: 'paragraph' } )( data.upcastDispatcher ); const output = data.parse( '

foo

bar

' ); @@ -93,8 +93,8 @@ describe( 'DataController', () => { allowAttributes: [ 'bold' ] } ); - upcastElementToElement( { view: 'p', model: 'paragraph' } )( data.upcastDispatcher ); - upcastElementToAttribute( { view: 'strong', model: 'bold' } )( data.upcastDispatcher ); + _upcastElementToElement( { view: 'p', model: 'paragraph' } )( data.upcastDispatcher ); + _upcastElementToAttribute( { view: 'strong', model: 'bold' } )( data.upcastDispatcher ); const output = data.parse( '

foobar

' ); @@ -119,7 +119,7 @@ describe( 'DataController', () => { beforeEach( () => { schema.register( 'paragraph', { inheritAllFrom: '$block' } ); - upcastElementToElement( { view: 'p', model: 'paragraph' } )( data.upcastDispatcher ); + _upcastElementToElement( { view: 'p', model: 'paragraph' } )( data.upcastDispatcher ); } ); it( 'should convert content of an element #1', () => { diff --git a/tests/conversion/upcast-converters.js b/tests/conversion/upcast-converters.js index 3e70aa83c..6edfb431a 100644 --- a/tests/conversion/upcast-converters.js +++ b/tests/conversion/upcast-converters.js @@ -20,7 +20,7 @@ import ModelRange from '../../src/model/range'; import ModelPosition from '../../src/model/position'; import { - upcastElementToElement, upcastElementToAttribute, upcastAttributeToAttribute, upcastElementToMarker, + _upcastElementToElement, _upcastElementToAttribute, _upcastAttributeToAttribute, _upcastElementToMarker, convertToModelFragment, convertText } from '../../src/conversion/upcast-converters'; @@ -52,9 +52,9 @@ describe( 'upcast-helpers', () => { conversion.register( { name: 'upcast', dispatcher: upcastDispatcher } ); } ); - describe( 'upcastElementToElement', () => { + describe( '_upcastElementToElement', () => { it( 'config.view is a string', () => { - const helper = upcastElementToElement( { view: 'p', model: 'paragraph' } ); + const helper = _upcastElementToElement( { view: 'p', model: 'paragraph' } ); conversion.for( 'upcast' ).add( helper ); @@ -66,8 +66,8 @@ describe( 'upcast-helpers', () => { inheritAllFrom: '$block' } ); - const helperA = upcastElementToElement( { view: 'p', model: 'p' } ); - const helperB = upcastElementToElement( { view: 'p', model: 'paragraph', converterPriority: 'high' } ); + const helperA = _upcastElementToElement( { view: 'p', model: 'p' } ); + const helperB = _upcastElementToElement( { view: 'p', model: 'paragraph', converterPriority: 'high' } ); conversion.for( 'upcast' ).add( helperA ).add( helperB ); @@ -79,7 +79,7 @@ describe( 'upcast-helpers', () => { inheritAllFrom: '$block' } ); - const helperFancy = upcastElementToElement( { + const helperFancy = _upcastElementToElement( { view: { name: 'p', classes: 'fancy' @@ -99,7 +99,7 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'level' ] } ); - const helper = upcastElementToElement( { + const helper = _upcastElementToElement( { view: { name: 'p', classes: 'heading' @@ -116,7 +116,7 @@ describe( 'upcast-helpers', () => { } ); it( 'should fire conversion of the element children', () => { - const helper = upcastElementToElement( { view: 'p', model: 'paragraph' } ); + const helper = _upcastElementToElement( { view: 'p', model: 'paragraph' } ); conversion.for( 'upcast' ).add( helper ); @@ -124,7 +124,7 @@ describe( 'upcast-helpers', () => { } ); it( 'should not insert a model element if it is not allowed by schema', () => { - const helper = upcastElementToElement( { view: 'h2', model: 'heading' } ); + const helper = _upcastElementToElement( { view: 'h2', model: 'heading' } ); conversion.for( 'upcast' ).add( helper ); @@ -136,8 +136,8 @@ describe( 'upcast-helpers', () => { inheritAllFrom: '$block' } ); - const helperParagraph = upcastElementToElement( { view: 'p', model: 'paragraph' } ); - const helperHeading = upcastElementToElement( { view: 'h2', model: 'heading' } ); + const helperParagraph = _upcastElementToElement( { view: 'p', model: 'paragraph' } ); + const helperHeading = _upcastElementToElement( { view: 'h2', model: 'heading' } ); conversion.for( 'upcast' ).add( helperParagraph ).add( helperHeading ); @@ -152,8 +152,8 @@ describe( 'upcast-helpers', () => { } ); it( 'should not do anything if returned model element is null', () => { - const helperA = upcastElementToElement( { view: 'p', model: 'paragraph' } ); - const helperB = upcastElementToElement( { view: 'p', model: () => null, converterPriority: 'high' } ); + const helperA = _upcastElementToElement( { view: 'p', model: 'paragraph' } ); + const helperB = _upcastElementToElement( { view: 'p', model: () => null, converterPriority: 'high' } ); conversion.for( 'upcast' ).add( helperA ).add( helperB ); @@ -161,9 +161,9 @@ describe( 'upcast-helpers', () => { } ); } ); - describe( 'upcastElementToAttribute', () => { + describe( '_upcastElementToAttribute', () => { it( 'config.view is string', () => { - const helper = upcastElementToAttribute( { view: 'strong', model: 'bold' } ); + const helper = _upcastElementToAttribute( { view: 'strong', model: 'bold' } ); conversion.for( 'upcast' ).add( helper ); @@ -174,8 +174,8 @@ describe( 'upcast-helpers', () => { } ); it( 'can be overwritten using converterPriority', () => { - const helperA = upcastElementToAttribute( { view: 'strong', model: 'strong' } ); - const helperB = upcastElementToAttribute( { view: 'strong', model: 'bold', converterPriority: 'high' } ); + const helperA = _upcastElementToAttribute( { view: 'strong', model: 'strong' } ); + const helperB = _upcastElementToAttribute( { view: 'strong', model: 'bold', converterPriority: 'high' } ); conversion.for( 'upcast' ).add( helperA ).add( helperB ); @@ -186,7 +186,7 @@ describe( 'upcast-helpers', () => { } ); it( 'config.view is an object', () => { - const helper = upcastElementToAttribute( { + const helper = _upcastElementToAttribute( { view: { name: 'span', classes: 'bold' @@ -209,7 +209,7 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'styled' ] } ); - const helper = upcastElementToAttribute( { + const helper = _upcastElementToAttribute( { view: { name: 'span', classes: [ 'styled', 'styled-dark' ] @@ -235,7 +235,7 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'fontSize' ] } ); - const helper = upcastElementToAttribute( { + const helper = _upcastElementToAttribute( { view: { name: 'span', styles: { @@ -278,7 +278,7 @@ describe( 'upcast-helpers', () => { } ); it( 'should not set an attribute if it is not allowed by schema', () => { - const helper = upcastElementToAttribute( { view: 'em', model: 'italic' } ); + const helper = _upcastElementToAttribute( { view: 'em', model: 'italic' } ); conversion.for( 'upcast' ).add( helper ); @@ -289,8 +289,8 @@ describe( 'upcast-helpers', () => { } ); it( 'should not do anything if returned model attribute is null', () => { - const helperA = upcastElementToAttribute( { view: 'strong', model: 'bold' } ); - const helperB = upcastElementToAttribute( { + const helperA = _upcastElementToAttribute( { view: 'strong', model: 'bold' } ); + const helperB = _upcastElementToAttribute( { view: 'strong', model: { key: 'bold', @@ -308,12 +308,12 @@ describe( 'upcast-helpers', () => { } ); it( 'should allow two converters to convert attributes on the same element', () => { - const helperA = upcastElementToAttribute( { + const helperA = _upcastElementToAttribute( { model: 'attribA', view: { name: 'span', classes: 'attrib-a' } } ); - const helperB = upcastElementToAttribute( { + const helperB = _upcastElementToAttribute( { model: 'attribB', view: { name: 'span', styles: { color: 'attrib-b' } } } ); @@ -327,17 +327,17 @@ describe( 'upcast-helpers', () => { } ); it( 'should consume element only when only is name specified', () => { - const helperBold = upcastElementToAttribute( { + const helperBold = _upcastElementToAttribute( { model: 'bold', view: { name: 'strong' } } ); - const helperA = upcastElementToAttribute( { + const helperA = _upcastElementToAttribute( { model: 'attribA', view: { name: 'strong' } } ); - const helperB = upcastElementToAttribute( { + const helperB = _upcastElementToAttribute( { model: 'attribB', view: { name: 'strong', classes: 'foo' } } ); @@ -352,12 +352,12 @@ describe( 'upcast-helpers', () => { // #1443. it( 'should set attributes on the element\'s children', () => { - const helperBold = upcastElementToAttribute( { + const helperBold = _upcastElementToAttribute( { model: 'bold', view: { name: 'strong' } } ); - const helperP = upcastElementToElement( { view: 'p', model: 'paragraph' } ); + const helperP = _upcastElementToElement( { view: 'p', model: 'paragraph' } ); conversion.for( 'upcast' ).add( helperP ).add( helperBold ); @@ -372,9 +372,9 @@ describe( 'upcast-helpers', () => { } ); } ); - describe( 'upcastAttributeToAttribute', () => { + describe( '_upcastAttributeToAttribute', () => { beforeEach( () => { - conversion.for( 'upcast' ).add( upcastElementToElement( { view: 'img', model: 'image' } ) ); + conversion.for( 'upcast' ).add( _upcastElementToElement( { view: 'img', model: 'image' } ) ); schema.register( 'image', { inheritAllFrom: '$block' @@ -386,7 +386,7 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'source' ] } ); - const helper = upcastAttributeToAttribute( { view: 'src', model: 'source' } ); + const helper = _upcastAttributeToAttribute( { view: 'src', model: 'source' } ); conversion.for( 'upcast' ).add( helper ); @@ -401,7 +401,7 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'source' ] } ); - const helper = upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' } ); + const helper = _upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' } ); conversion.for( 'upcast' ).add( helper ); @@ -416,7 +416,7 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'source' ] } ); - const helper = upcastAttributeToAttribute( { view: { name: 'img', key: 'src' }, model: { name: 'image', key: 'source' } } ); + const helper = _upcastAttributeToAttribute( { view: { name: 'img', key: 'src' }, model: { name: 'image', key: 'source' } } ); conversion.for( 'upcast' ).add( helper ); @@ -431,8 +431,8 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'src', 'source' ] } ); - const helperA = upcastAttributeToAttribute( { view: { key: 'src' }, model: 'src' } ); - const helperB = upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source', converterPriority: 'normal' } ); + const helperA = _upcastAttributeToAttribute( { view: { key: 'src' }, model: 'src' } ); + const helperB = _upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source', converterPriority: 'normal' } ); conversion.for( 'upcast' ).add( helperA ).add( helperB ); @@ -447,7 +447,7 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'styled' ] } ); - const helper = upcastAttributeToAttribute( { + const helper = _upcastAttributeToAttribute( { view: { key: 'data-style', value: /[\s\S]*/ @@ -468,7 +468,7 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'styled' ] } ); - const helper = upcastAttributeToAttribute( { + const helper = _upcastAttributeToAttribute( { view: { name: 'img', key: 'class', @@ -482,7 +482,7 @@ describe( 'upcast-helpers', () => { conversion.for( 'upcast' ) .add( helper ) - .add( upcastElementToElement( { view: 'p', model: 'paragraph' } ) ); + .add( _upcastElementToElement( { view: 'p', model: 'paragraph' } ) ); expectResult( new ViewContainerElement( 'img', { class: 'styled-dark' } ), @@ -505,7 +505,7 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'styled' ] } ); - const helper = upcastAttributeToAttribute( { + const helper = _upcastAttributeToAttribute( { view: { key: 'class', value: /styled-[\S]+/ @@ -530,7 +530,7 @@ describe( 'upcast-helpers', () => { } ); it( 'should not set an attribute if it is not allowed by schema', () => { - const helper = upcastAttributeToAttribute( { view: 'src', model: 'source' } ); + const helper = _upcastAttributeToAttribute( { view: 'src', model: 'source' } ); conversion.for( 'upcast' ).add( helper ); @@ -545,7 +545,7 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'styled' ] } ); - const helperA = upcastAttributeToAttribute( { + const helperA = _upcastAttributeToAttribute( { view: { key: 'class', value: 'styled' @@ -556,7 +556,7 @@ describe( 'upcast-helpers', () => { } } ); - const helperB = upcastAttributeToAttribute( { + const helperB = _upcastAttributeToAttribute( { view: { key: 'class', value: 'styled' @@ -584,10 +584,10 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'border', 'shade' ] } ); - conversion.for( 'upcast' ).add( upcastElementToElement( { view: 'div', model: 'div' } ) ); + conversion.for( 'upcast' ).add( _upcastElementToElement( { view: 'div', model: 'div' } ) ); - const shadeHelper = upcastAttributeToAttribute( { view: { key: 'class', value: 'shade' }, model: 'shade' } ); - const borderHelper = upcastAttributeToAttribute( { view: { key: 'class', value: 'border' }, model: 'border' } ); + const shadeHelper = _upcastAttributeToAttribute( { view: { key: 'class', value: 'shade' }, model: 'shade' } ); + const borderHelper = _upcastAttributeToAttribute( { view: { key: 'class', value: 'border' }, model: 'border' } ); conversion.for( 'upcast' ).add( shadeHelper ); conversion.for( 'upcast' ).add( borderHelper ); @@ -603,9 +603,9 @@ describe( 'upcast-helpers', () => { } ); } ); - describe( 'upcastElementToMarker', () => { + describe( '_upcastElementToMarker', () => { it( 'config.view is a string', () => { - const helper = upcastElementToMarker( { view: 'marker-search', model: 'search' } ); + const helper = _upcastElementToMarker( { view: 'marker-search', model: 'search' } ); conversion.for( 'upcast' ).add( helper ); @@ -623,8 +623,8 @@ describe( 'upcast-helpers', () => { } ); it( 'can be overwritten using converterPriority', () => { - const helperA = upcastElementToMarker( { view: 'marker-search', model: 'search-result' } ); - const helperB = upcastElementToMarker( { view: 'marker-search', model: 'search', converterPriority: 'high' } ); + const helperA = _upcastElementToMarker( { view: 'marker-search', model: 'search-result' } ); + const helperB = _upcastElementToMarker( { view: 'marker-search', model: 'search', converterPriority: 'high' } ); conversion.for( 'upcast' ).add( helperA ).add( helperB ); @@ -642,7 +642,7 @@ describe( 'upcast-helpers', () => { } ); it( 'config.view is an object', () => { - const helper = upcastElementToMarker( { + const helper = _upcastElementToMarker( { view: { name: 'span', 'data-marker': 'search' @@ -666,7 +666,7 @@ describe( 'upcast-helpers', () => { } ); it( 'config.model is a function', () => { - const helper = upcastElementToMarker( { + const helper = _upcastElementToMarker( { view: 'comment', model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' ) } ); @@ -687,9 +687,9 @@ describe( 'upcast-helpers', () => { } ); it( 'marker is in a block element', () => { - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'paragraph', view: 'p' } ) ); + conversion.for( 'upcast' ).add( _upcastElementToElement( { model: 'paragraph', view: 'p' } ) ); - const helper = upcastElementToMarker( { view: 'marker-search', model: 'search' } ); + const helper = _upcastElementToMarker( { view: 'marker-search', model: 'search' } ); conversion.for( 'upcast' ).add( helper ); diff --git a/tests/manual/highlight.js b/tests/manual/highlight.js index 1a1c34a63..245cfa299 100644 --- a/tests/manual/highlight.js +++ b/tests/manual/highlight.js @@ -5,10 +5,6 @@ /* global console, window, document */ -import { - upcastElementToElement, -} from '../../src/conversion/upcast-converters'; - import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; import Enter from '@ckeditor/ckeditor5-enter/src/enter'; import Typing from '@ckeditor/ckeditor5-typing/src/typing'; @@ -49,11 +45,10 @@ class FancyWidget extends Plugin { } } ); - conversion.for( 'upcast' ) - .add( upcastElementToElement( { - view: 'figure', - model: 'fancywidget' - } ) ); + conversion.for( 'upcast' ).elementToElement( { + view: 'figure', + model: 'fancywidget' + } ); } } diff --git a/tests/manual/nestededitable.js b/tests/manual/nestededitable.js index 111894485..09e6f1370 100644 --- a/tests/manual/nestededitable.js +++ b/tests/manual/nestededitable.js @@ -5,10 +5,6 @@ /* global console */ -import { - upcastElementToElement -} from '../../src/conversion/upcast-converters'; - import { getData } from '../../src/dev-utils/model'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; @@ -47,10 +43,10 @@ class NestedEditable extends Plugin { } } ); - editor.conversion.for( 'upcast' ).add( upcastElementToElement( { + editor.conversion.for( 'upcast' ).elementToElement( { model: 'figure', view: 'figure' - } ) ); + } ); editor.conversion.for( 'downcast' ).elementToElement( { model: 'figcaption', @@ -69,10 +65,10 @@ class NestedEditable extends Plugin { } } ); - editor.conversion.for( 'upcast' ).add( upcastElementToElement( { + editor.conversion.for( 'upcast' ).elementToElement( { model: 'figcaption', view: 'figcaption' - } ) ); + } ); } } diff --git a/tests/manual/selection.js b/tests/manual/selection.js index 3fb11d78e..60bb98fae 100644 --- a/tests/manual/selection.js +++ b/tests/manual/selection.js @@ -14,7 +14,6 @@ import Enter from '@ckeditor/ckeditor5-enter/src/enter'; import Typing from '@ckeditor/ckeditor5-typing/src/typing'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Undo from '@ckeditor/ckeditor5-undo/src/undo'; -import { upcastElementToElement } from '../../src/conversion/upcast-converters'; import './selection.css'; import { toWidget, toWidgetEditable } from '@ckeditor/ckeditor5-widget/src/utils'; @@ -42,9 +41,9 @@ class SelectionTest extends Plugin { isLimit: true } ); - editor.conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'table', view: 'table' } ) ); - editor.conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableRow', view: 'tr' } ) ); - editor.conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) ); + editor.conversion.for( 'upcast' ).elementToElement( { model: 'table', view: 'table' } ); + editor.conversion.for( 'upcast' ).elementToElement( { model: 'tableRow', view: 'tr' } ); + editor.conversion.for( 'upcast' ).elementToElement( { model: 'tableCell', view: 'td' } ); editor.conversion.for( 'downcast' ).elementToElement( { model: 'table', diff --git a/tests/manual/tickets/475/1.js b/tests/manual/tickets/475/1.js index c5fc1943c..c5c0fc608 100644 --- a/tests/manual/tickets/475/1.js +++ b/tests/manual/tickets/475/1.js @@ -11,10 +11,6 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import Range from '../../../../src/model/range'; import LivePosition from '../../../../src/model/liveposition'; -import { - upcastElementToAttribute -} from '../../../../src/conversion/upcast-converters'; - import Enter from '@ckeditor/ckeditor5-enter/src/enter'; import Typing from '@ckeditor/ckeditor5-typing/src/typing'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; @@ -34,13 +30,13 @@ class Link extends Plugin { } } ); - editor.conversion.for( 'upcast' ).add( upcastElementToAttribute( { + editor.conversion.for( 'upcast' ).elementToAttribute( { view: 'a', model: { key: 'link', value: viewElement => viewElement.getAttribute( 'href' ) } - } ) ); + } ); } } diff --git a/tests/tickets/699.js b/tests/tickets/699.js index 5b08f3ffc..73505fe88 100644 --- a/tests/tickets/699.js +++ b/tests/tickets/699.js @@ -8,10 +8,6 @@ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; -import { - upcastElementToElement -} from '../../src/conversion/upcast-converters'; - import { getData as getModelData } from '../../src/dev-utils/model'; import { getData as getViewData } from '../../src/dev-utils/view'; @@ -55,8 +51,8 @@ function WidgetPlugin( editor ) { view: 'widget' } ); - editor.conversion.for( 'upcast' ).add( upcastElementToElement( { + editor.conversion.for( 'upcast' ).elementToElement( { model: 'widget', view: 'widget' - } ) ); + } ); } diff --git a/tests/utils/bindtwostepcarettoattribute.js b/tests/utils/bindtwostepcarettoattribute.js index ee583e50d..c04d3994a 100644 --- a/tests/utils/bindtwostepcarettoattribute.js +++ b/tests/utils/bindtwostepcarettoattribute.js @@ -11,7 +11,6 @@ import DomEventData from '../../src/view/observer/domeventdata'; import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo'; import bindTwoStepCaretToAttribute from '../../src/utils/bindtwostepcarettoattribute'; import Position from '../../src/model/position'; -import { upcastElementToAttribute } from '../../src/conversion/upcast-converters'; import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard'; import { setData } from '../../src/dev-utils/model'; @@ -37,9 +36,9 @@ describe( 'bindTwoStepCaretToAttribute()', () => { } ); model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); - editor.conversion.for( 'upcast' ).add( upcastElementToAttribute( { view: 'a', model: 'a' } ) ); - editor.conversion.for( 'upcast' ).add( upcastElementToAttribute( { view: 'b', model: 'b' } ) ); - editor.conversion.for( 'upcast' ).add( upcastElementToAttribute( { view: 'c', model: 'c' } ) ); + editor.conversion.for( 'upcast' ).elementToAttribute( { view: 'a', model: 'a' } ); + editor.conversion.for( 'upcast' ).elementToAttribute( { view: 'b', model: 'b' } ); + editor.conversion.for( 'upcast' ).elementToAttribute( { view: 'c', model: 'c' } ); editor.conversion.elementToElement( { model: 'paragraph', view: 'p' } ); bindTwoStepCaretToAttribute( editor.editing.view, editor.model, emitter, 'a' ); From 9c0065e579a4e4d27c72ca0eb37ccb98d56df1e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 18 Dec 2018 12:59:32 +0100 Subject: [PATCH 39/84] Update conversion utility helpers documentation. --- src/conversion/conversion.js | 33 +-- src/conversion/downcast-converters.js | 298 ++++++++------------------ src/conversion/upcast-converters.js | 281 ++++++++---------------- 3 files changed, 196 insertions(+), 416 deletions(-) diff --git a/src/conversion/conversion.js b/src/conversion/conversion.js index 22fdf1a52..b77a4d982 100644 --- a/src/conversion/conversion.js +++ b/src/conversion/conversion.js @@ -97,13 +97,16 @@ export default class Conversion { this._dispatchersGroups.set( options.name, group ); } + /* eslint-disable max-len */ /** * Provides chainable API to assign converters to dispatchers registered under a given group name. Converters are added - * by calling the `.add()` method of an object returned by this function. + * by calling the {@link module:engine/conversion/conversion~ConversionHelpers#add `.add()`} method of an + * {@link module:engine/conversion/conversion~ConversionHelpers conversion helpers} returned by this function. * - * conversion.for( 'downcast' ) + * editor.conversion.for( 'downcast' ) * .add( conversionHelperA ) - * .add( conversionHelperB ); + * .add( conversionHelperB ) + * .elementToElement( config ); * * In this example `conversionHelperA` and `conversionHelperB` will be called for all dispatchers from the `'model'` group. * @@ -115,15 +118,17 @@ export default class Conversion { * * For downcast (model-to-view conversion), these are: * - * * {@link module:engine/conversion/downcast-converters~_downcastElementToElement Downcast element-to-element converter}, - * * {@link module:engine/conversion/downcast-converters~_downcastAttributeToElement Downcast attribute-to-element converter}, - * * {@link module:engine/conversion/downcast-converters~_downcastAttributeToAttribute Downcast attribute-to-attribute converter}. + * * {@link module:engine/conversion/downcast-converters~DowncastHelpers#elementToElement Downcast element-to-element converter}, + * * {@link module:engine/conversion/downcast-converters~DowncastHelpers#attributeToElement Downcast attribute-to-element converter}, + * * {@link module:engine/conversion/downcast-converters~DowncastHelpers#attributeToAttribute Downcast attribute-to-attribute converter}. + * * {@link module:engine/conversion/downcast-converters~DowncastHelpers#markerToElement Downcast marker-to-element converter}. + * * {@link module:engine/conversion/downcast-converters~DowncastHelpers#markerToHighlight Downcast marker-to-highlight converter}. * * For upcast (view-to-model conversion), these are: * - * * {@link module:engine/conversion/upcast-converters~_upcastElementToElement Upcast element-to-element converter}, - * * {@link module:engine/conversion/upcast-converters~_upcastElementToAttribute Upcast attribute-to-element converter}, - * * {@link module:engine/conversion/upcast-converters~_upcastAttributeToAttribute Upcast attribute-to-attribute converter}. + * * {@link module:engine/conversion/upcast-converters~UpcastHelpers#elementToElement Upcast element-to-element converter}, + * * {@link module:engine/conversion/upcast-converters~UpcastHelpers#elementToAttribute Upcast attribute-to-element converter}, + * * {@link module:engine/conversion/upcast-converters~UpcastHelpers#attributeToAttribute Upcast attribute-to-attribute converter}. * * An example of using conversion helpers to convert the `paragraph` model element to the `p` view element (and back): * @@ -131,19 +136,15 @@ export default class Conversion { * const config = { model: 'paragraph', view: 'p' }; * * // Add converters to proper dispatchers using conversion helpers. - * conversion.for( 'downcast' ).elementToElement( config ) ); - * conversion.for( 'upcast' ).elementToElement( config ) ); - * - * An example of providing a custom conversion helper that uses a custom converter function: - * - * // Adding a custom `myConverter` converter for 'paragraph' element insertion, with the default priority ('normal'). - * conversion.for( 'downcast' ).add( conversion.customConverter( 'insert:paragraph', myConverter ) ); + * editor.conversion.for( 'downcast' ).elementToElement( config ) ); + * editor.conversion.for( 'upcast' ).elementToElement( config ) ); * * @param {String} groupName The name of dispatchers group to add the converters to. * @returns {module:engine/conversion/conversion~ConversionHelpers|module:engine/conversion/downcast-converters~DowncastHelpers| * module:engine/conversion/upcast-converters~UpcastHelpers} * An object with the `.add()` method, providing a way to add converters. */ + /* eslint-enable max-len */ for( groupName ) { const { dispatchers, helpers } = this._getDispatchersGroup( groupName ); diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index 64f669350..6687a5fc5 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -22,27 +22,13 @@ import { cloneDeep } from 'lodash-es'; /** * Model element to view element conversion helper. * - * This conversion results in creating a view element. For example, model `Foo` becomes `

Foo

` in the view. + * editor.conversion.for( 'downcast' ) + * .add( _downcastElementToElement( { + * model: 'paragraph', + * view: 'p' + * } ) ); * - * _downcastElementToElement( { model: 'paragraph', view: 'p' } ); - * - * _downcastElementToElement( { model: 'paragraph', view: 'div', converterPriority: 'high' } ); - * - * _downcastElementToElement( { - * model: 'fancyParagraph', - * view: { - * name: 'p', - * classes: 'fancy' - * } - * } ); - * - * _downcastElementToElement( { - * model: 'heading', - * view: ( modelElement, viewWriter ) => viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) ) - * } ); - * - * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter - * to the conversion process. + * The method is publicly available as {@link ~DowncastHelpers#elementToElement `.elementToElement()` downcast helper}. * * @protected * @param {Object} config Conversion configuration. @@ -65,61 +51,13 @@ export function _downcastElementToElement( config ) { /** * Model attribute to view element conversion helper. * - * This conversion results in wrapping view nodes with a view attribute element. For example, a model text node with - * `"Foo"` as data and the `bold` attribute becomes `Foo` in the view. - * - * _downcastAttributeToElement( { model: 'bold', view: 'strong' } ); - * - * _downcastAttributeToElement( { model: 'bold', view: 'b', converterPriority: 'high' } ); - * - * _downcastAttributeToElement( { - * model: 'invert', - * view: { - * name: 'span', - * classes: [ 'font-light', 'bg-dark' ] - * } - * } ); - * - * _downcastAttributeToElement( { - * model: { - * key: 'fontSize', - * values: [ 'big', 'small' ] - * }, - * view: { - * big: { - * name: 'span', - * styles: { - * 'font-size': '1.2em' - * } - * }, - * small: { - * name: 'span', - * styles: { - * 'font-size': '0.8em' - * } - * } - * } - * } ); - * - * _downcastAttributeToElement( { - * model: 'bold', - * view: ( modelAttributeValue, viewWriter ) => { - * return viewWriter.createAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } ); - * } - * } ); - * - * _downcastAttributeToElement( { - * model: { - * key: 'color', - * name: '$text' - * }, - * view: ( modelAttributeValue, viewWriter ) => { - * return viewWriter.createAttributeElement( 'span', { style: 'color:' + modelAttributeValue } ); - * } - * } ); + * editor.conversion.for( 'downcast' ) + * .add( _downcastAttributeToElement( { + * model: 'bold', + * view: 'strong' + * } ) ); * - * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter - * to the conversion process. + * The method is publicly available as {@link ~DowncastHelpers#attributeToElement `.attributeToElement()` downcast helper}. * * @protected * @param {Object} config Conversion configuration. @@ -160,45 +98,13 @@ export function _downcastAttributeToElement( config ) { /** * Model attribute to view attribute conversion helper. * - * This conversion results in adding an attribute to a view node, basing on an attribute from a model node. For example, - * `` is converted to ``. - * - * _downcastAttributeToAttribute( { model: 'source', view: 'src' } ); - * - * _downcastAttributeToAttribute( { model: 'source', view: 'href', converterPriority: 'high' } ); - * - * _downcastAttributeToAttribute( { - * model: { - * name: 'image', - * key: 'source' - * }, - * view: 'src' - * } ); + * editor.conversion.for( 'downcast' ) + * .add( _downcastAttributeToAttribute( { + * model: 'source', + * view: 'src' + * } ) ); * - * _downcastAttributeToAttribute( { - * model: { - * name: 'styled', - * values: [ 'dark', 'light' ] - * }, - * view: { - * dark: { - * key: 'class', - * value: [ 'styled', 'styled-dark' ] - * }, - * light: { - * key: 'class', - * value: [ 'styled', 'styled-light' ] - * } - * } - * } ); - * - * _downcastAttributeToAttribute( { - * model: 'styled', - * view: modelAttributeValue => ( { key: 'class', value: 'styled-' + modelAttributeValue } ) - * } ); - * - * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter - * to the conversion process. + * The method is publicly available as {@link ~DowncastHelpers#attributeToAttribute `.attributeToAttribute()` downcast helper}. * * @protected * @param {Object} config Conversion configuration. @@ -240,41 +146,13 @@ export function _downcastAttributeToAttribute( config ) { /** * Model marker to view element conversion helper. * - * This conversion results in creating a view element on the boundaries of the converted marker. If the converted marker - * is collapsed, only one element is created. For example, model marker set like this: `F[oo b]ar` - * becomes `

Foo bar

` in the view. - * - * _downcastMarkerToElement( { model: 'search', view: 'marker-search' } ); - * - * _downcastMarkerToElement( { model: 'search', view: 'search-result', converterPriority: 'high' } ); - * - * _downcastMarkerToElement( { - * model: 'search', - * view: { - * name: 'span', - * attributes: { - * 'data-marker': 'search' - * } - * } - * } ); - * - * _downcastMarkerToElement( { - * model: 'search', - * view: ( markerData, viewWriter ) => { - * return viewWriter.createUIElement( 'span', { 'data-marker': 'search', 'data-start': markerData.isOpening } ); - * } - * } ); - * - * If a function is passed as the `config.view` parameter, it will be used to generate both boundary elements. The function - * receives the `data` object as a parameter and should return an instance of the - * {@link module:engine/view/uielement~UIElement view UI element}. The `data` and `conversionApi` objects are passed from - * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. Additionally, - * the `data.isOpening` parameter is passed, which is set to `true` for the marker start boundary element, and `false` to - * the marker end boundary element. - * - * This kind of conversion is useful for saving data into the database, so it should be used in the data conversion pipeline. + * editor.conversion.for( 'downcast' ) + * .add( _downcastMarkerToElement( { + * model: 'search', + * view: 'marker-search' + * } ) ); * - * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add a converter to the conversion process. + * The method is publicly available as {@link ~DowncastHelpers#markerToElement `.markerToElement()` downcast helper}. * * @protected * @param {Object} config Conversion configuration. @@ -298,44 +176,13 @@ export function _downcastMarkerToElement( config ) { /** * Model marker to highlight conversion helper. * - * This conversion results in creating a highlight on view nodes. For this kind of conversion, - * {@link module:engine/conversion/downcast-converters~HighlightDescriptor} should be provided. - * - * For text nodes, a `` {@link module:engine/view/attributeelement~AttributeElement} is created and it wraps all text nodes - * in the converted marker range. For example, a model marker set like this: `F[oo b]ar` becomes - * `

Foo bar

` in the view. - * - * {@link module:engine/view/containerelement~ContainerElement} may provide a custom way of handling highlight. Most often, - * the element itself is given classes and attributes described in the highlight descriptor (instead of being wrapped in ``). - * For example, a model marker set like this: `[]` becomes `` - * in the view. - * - * For container elements, the conversion is two-step. While the converter processes the highlight descriptor and passes it - * to a container element, it is the container element instance itself that applies values from the highlight descriptor. - * So, in a sense, the converter takes care of stating what should be applied on what, while the element decides how to apply that. + * editor.conversion.for( 'downcast' ) + * .add( _downcastMarkerToHighlight( { + * model: 'comment', + * view: { classes: 'comment' } + * } ) ); * - * downcastMarkerToHighlight( { model: 'comment', view: { classes: 'comment' } } ); - * - * downcastMarkerToHighlight( { model: 'comment', view: { classes: 'new-comment' }, converterPriority: 'high' } ); - * - * downcastMarkerToHighlight( { - * model: 'comment', - * view: data => { - * // Assuming that the marker name is in a form of comment:commentType. - * const commentType = data.markerName.split( ':' )[ 1 ]; - * - * return { - * classes: [ 'comment', 'comment-' + commentType ] - * }; - * } - * } ); - * - * If a function is passed as the `config.view` parameter, it will be used to generate the highlight descriptor. The function - * receives the `data` object as a parameter and should return a - * {@link module:engine/conversion/downcast-converters~HighlightDescriptor highlight descriptor}. - * The `data` object properties are passed from {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. - * - * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add a converter to the conversion process. + * The method is publicly available as {@link ~DowncastHelpers#markerToElement `.markerToElement()` downcast helper}. * * @protected * @param {Object} config Conversion configuration. @@ -715,7 +562,7 @@ export function changeAttribute( attributeCreator ) { * {@link module:engine/conversion/conversion~Conversion#attributeToElement `Attribute to Element converter`} * with higher {@link module:utils/priorities~PriorityString priority} must also be defined: * - * conversion.for( 'downcast' ).attributeToElement( { + * editor.conversion.for( 'downcast' ).attributeToElement( { * model: { * key: 'attribute-name', * name: '$text' @@ -1121,9 +968,16 @@ export const helpers = { * * This conversion results in creating a view element. For example, model `Foo` becomes `

Foo

` in the view. * - * editor.conversion.for( 'downcast' ).elementToElement( { model: 'paragraph', view: 'p' } ); + * editor.conversion.for( 'downcast' ).elementToElement( { + * model: 'paragraph', + * view: 'p' + * } ); * - * editor.conversion.for( 'downcast' ).elementToElement( { model: 'paragraph', view: 'div', converterPriority: 'high' } ); + * editor.conversion.for( 'downcast' ).elementToElement( { + * model: 'paragraph', + * view: 'div', + * converterPriority: 'high' + * } ); * * editor.conversion.for( 'downcast' ).elementToElement( { * model: 'fancyParagraph', @@ -1135,7 +989,9 @@ export const helpers = { * * editor.conversion.for( 'downcast' ).elementToElement( { * model: 'heading', - * view: ( modelElement, viewWriter ) => viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) ) + * view: ( modelElement, viewWriter ) => { + * return viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) ) + * } * } ); * * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter @@ -1159,9 +1015,16 @@ export const helpers = { * This conversion results in wrapping view nodes with a view attribute element. For example, a model text node with * `"Foo"` as data and the `bold` attribute becomes `Foo` in the view. * - * editor.conversion.for( 'downcast' ).attributeToElement( { model: 'bold', view: 'strong' } ); + * editor.conversion.for( 'downcast' ).attributeToElement( { + * model: 'bold', + * view: 'strong' + * } ); * - * editor.conversion.for( 'downcast' ).attributeToElement( { model: 'bold', view: 'b', converterPriority: 'high' } ); + * editor.conversion.for( 'downcast' ).attributeToElement( { + * model: 'bold', + * view: 'b', + * converterPriority: 'high' + * } ); * * editor.conversion.for( 'downcast' ).attributeToElement( { * model: 'invert', @@ -1195,7 +1058,9 @@ export const helpers = { * editor.conversion.for( 'downcast' ).attributeToElement( { * model: 'bold', * view: ( modelAttributeValue, viewWriter ) => { - * return viewWriter.createAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } ); + * return viewWriter.createAttributeElement( 'span', { + * style: 'font-weight:' + modelAttributeValue + * } ); * } * } ); * @@ -1205,7 +1070,9 @@ export const helpers = { * name: '$text' * }, * view: ( modelAttributeValue, viewWriter ) => { - * return viewWriter.createAttributeElement( 'span', { style: 'color:' + modelAttributeValue } ); + * return viewWriter.createAttributeElement( 'span', { + * style: 'color:' + modelAttributeValue + * } ); * } * } ); * @@ -1232,11 +1099,18 @@ export const helpers = { * This conversion results in adding an attribute to a view node, basing on an attribute from a model node. For example, * `` is converted to ``. * - * conversion.for( 'downcast' ).attributeToAttribute( { model: 'source', view: 'src' } ); + * editor.conversion.for( 'downcast' ).attributeToAttribute( { + * model: 'source', + * view: 'src' + * } ); * - * conversion.for( 'downcast' ).attributeToAttribute( { model: 'source', view: 'href', converterPriority: 'high' } ); + * editor.conversion.for( 'downcast' ).attributeToAttribute( { + * model: 'source', + * view: 'href', + * converterPriority: 'high' + * } ); * - * conversion.for( 'downcast' ).attributeToAttribute( { + * editor.conversion.for( 'downcast' ).attributeToAttribute( { * model: { * name: 'image', * key: 'source' @@ -1244,7 +1118,7 @@ export const helpers = { * view: 'src' * } ); * - * conversion.for( 'downcast' ).attributeToAttribute( { + * editor.conversion.for( 'downcast' ).attributeToAttribute( { * model: { * name: 'styled', * values: [ 'dark', 'light' ] @@ -1261,7 +1135,7 @@ export const helpers = { * } * } ); * - * conversion.for( 'downcast' ).attributeToAttribute( { + * editor.conversion.for( 'downcast' ).attributeToAttribute( { * model: 'styled', * view: modelAttributeValue => ( { key: 'class', value: 'styled-' + modelAttributeValue } ) * } ); @@ -1292,11 +1166,18 @@ export const helpers = { * is collapsed, only one element is created. For example, model marker set like this: `F[oo b]ar` * becomes `

Foo bar

` in the view. * - * conversion.for( 'downcast' ).markerToElement( { model: 'search', view: 'marker-search' } ); + * editor.conversion.for( 'downcast' ).markerToElement( { + * model: 'search', + * view: 'marker-search' + * } ); * - * conversion.for( 'downcast' ).markerToElement( { model: 'search', view: 'search-result', converterPriority: 'high' } ); + * editor.conversion.for( 'downcast' ).markerToElement( { + * model: 'search', + * view: 'search-result', + * converterPriority: 'high' + * } ); * - * conversion.for( 'downcast' ).markerToElement( { + * editor.conversion.for( 'downcast' ).markerToElement( { * model: 'search', * view: { * name: 'span', @@ -1306,10 +1187,13 @@ export const helpers = { * } * } ); * - * conversion.for( 'downcast' ).markerToElement( { + * editor.conversion.for( 'downcast' ).markerToElement( { * model: 'search', * view: ( markerData, viewWriter ) => { - * return viewWriter.createUIElement( 'span', { 'data-marker': 'search', 'data-start': markerData.isOpening } ); + * return viewWriter.createUIElement( 'span', { + * 'data-marker': 'search', + * 'data-start': markerData.isOpening + * } ); * } * } ); * @@ -1322,7 +1206,8 @@ export const helpers = { * * This kind of conversion is useful for saving data into the database, so it should be used in the data conversion pipeline. * - * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add a converter to the conversion process. + * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter + * to the conversion process. * * @method #markerToElement * @param {Object} config Conversion configuration. @@ -1355,15 +1240,15 @@ export const helpers = { * to a container element, it is the container element instance itself that applies values from the highlight descriptor. * So, in a sense, the converter takes care of stating what should be applied on what, while the element decides how to apply that. * - * conversion.for( 'downcast' ).markerToHighlight( { model: 'comment', view: { classes: 'comment' } } ); + * editor.conversion.for( 'downcast' ).markerToHighlight( { model: 'comment', view: { classes: 'comment' } } ); * - * conversion.for( 'downcast' ).markerToHighlight( { + * editor.conversion.for( 'downcast' ).markerToHighlight( { * model: 'comment', * view: { classes: 'new-comment' }, * converterPriority: 'high' * } ); * - * conversion.for( 'downcast' ).markerToHighlight( { + * editor.conversion.for( 'downcast' ).markerToHighlight( { * model: 'comment', * view: data => { * // Assuming that the marker name is in a form of comment:commentType. @@ -1380,7 +1265,8 @@ export const helpers = { * {@link module:engine/conversion/downcast-converters~HighlightDescriptor highlight descriptor}. * The `data` object properties are passed from {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. * - * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add a converter to the conversion process. + * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter + * to the conversion process. * * @method #markerToHighlight * @param {Object} config Conversion configuration. diff --git a/src/conversion/upcast-converters.js b/src/conversion/upcast-converters.js index 6a58c7563..dd32246f5 100644 --- a/src/conversion/upcast-converters.js +++ b/src/conversion/upcast-converters.js @@ -19,33 +19,13 @@ import { cloneDeep } from 'lodash-es'; /** * View element to model element conversion helper. * - * This conversion results in creating a model element. For example, view `

Foo

` becomes `Foo` in the model. + * editor.conversion.for( 'upcast' ) + * .add( _upcastElementToElement( { + * view: 'p', + * model: 'paragraph' + * } ) ); * - * Keep in mind that the element will be inserted only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration. - * - * _upcastElementToElement( { view: 'p', model: 'paragraph' } ); - * - * _upcastElementToElement( { view: 'p', model: 'paragraph', converterPriority: 'high' } ); - * - * _upcastElementToElement( { - * view: { - * name: 'p', - * classes: 'fancy' - * }, - * model: 'fancyParagraph' - * } ); - * - * _upcastElementToElement( { - * view: { - * name: 'p', - * classes: 'heading' - * }, - * model: ( viewElement, modelWriter ) => { - * return modelWriter.createElement( 'heading', { level: viewElement.getAttribute( 'data-level' ) } ); - * } - * } ); - * - * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process. + * The method is publicly available as {@link ~UpcastHelpers#elementToElement `.elementToElement()` upcast helper}. * * @param {Object} config Conversion configuration. * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted. @@ -70,67 +50,13 @@ export function _upcastElementToElement( config ) { /** * View element to model attribute conversion helper. * - * This conversion results in setting an attribute on a model node. For example, view `Foo` becomes - * `Foo` {@link module:engine/model/text~Text model text node} with `bold` attribute set to `true`. - * - * This helper is meant to set a model attribute on all the elements that are inside the converted element: - * - * Foo -->

Foo

--> <$text bold="true">Foo - * - * Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step). - * Even though `` is over `

` element, `bold="true"` was added to the text. See - * {@link module:engine/conversion/upcast-converters~upcastAttributeToAttribute} for comparison. - * - * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration. - * - * _upcastElementToAttribute( { view: 'strong', model: 'bold' } ); - * - * _upcastElementToAttribute( { view: 'strong', model: 'bold', converterPriority: 'high' } ); - * - * _upcastElementToAttribute( { - * view: { - * name: 'span', - * classes: 'bold' - * }, - * model: 'bold' - * } ); + * editor.conversion.for( 'upcast' ) + * .add( _upcastElementToAttribute( { + * view: 'strong', + * model: 'bold' + * } ) ); * - * _upcastElementToAttribute( { - * view: { - * name: 'span', - * classes: [ 'styled', 'styled-dark' ] - * }, - * model: { - * key: 'styled', - * value: 'dark' - * } - * } ); - * - * _upcastElementToAttribute( { - * view: { - * name: 'span', - * styles: { - * 'font-size': /[\s\S]+/ - * } - * }, - * model: { - * key: 'fontSize', - * value: viewElement => { - * const fontSize = viewElement.getStyle( 'font-size' ); - * const value = fontSize.substr( 0, fontSize.length - 2 ); - * - * if ( value <= 10 ) { - * return 'small'; - * } else if ( value > 12 ) { - * return 'big'; - * } - * - * return null; - * } - * } - * } ); - * - * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process. + * The method is publicly available as {@link ~UpcastHelpers#elementToAttribute `.elementToAttribute()` upcast helper}. * * @param {Object} config Conversion configuration. * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted. @@ -158,67 +84,13 @@ export function _upcastElementToAttribute( config ) { /** * View attribute to model attribute conversion helper. * - * This conversion results in setting an attribute on a model node. For example, view `` becomes - * `` in the model. - * - * This helper is meant to convert view attributes from view elements which got converted to the model, so the view attribute - * is set only on the corresponding model node: - * - *

foo
-->
foo
- * - * Above, `class="dark"` attribute is added only to the `
` elements that has it. This is in contrary to - * {@link module:engine/conversion/upcast-converters~upcastElementToAttribute} which sets attributes for all the children in the model: - * - * Foo -->

Foo

--> <$text bold="true">Foo - * - * Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step). - * Even though `` is over `

` element, `bold="true"` was added to the text. - * - * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration. - * - * _upcastAttributeToAttribute( { view: 'src', model: 'source' } ); + * editor.conversion.for( 'upcast' ) + * .add( _upcastAttributeToAttribute( { + * view: 'src', + * model: 'source' + * } ) ); * - * _upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' } ); - * - * _upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source', converterPriority: 'normal' } ); - * - * _upcastAttributeToAttribute( { - * view: { - * key: 'data-style', - * value: /[\s\S]+/ - * }, - * model: 'styled' - * } ); - * - * _upcastAttributeToAttribute( { - * view: { - * name: 'img', - * key: 'class', - * value: 'styled-dark' - * }, - * model: { - * key: 'styled', - * value: 'dark' - * } - * } ); - * - * _upcastAttributeToAttribute( { - * view: { - * key: 'class', - * value: /styled-[\S]+/ - * }, - * model: { - * key: 'styled' - * value: viewElement => { - * const regexp = /styled-([\S]+)/; - * const match = viewElement.getAttribute( 'class' ).match( regexp ); - * - * return match[ 1 ]; - * } - * } - * } ); - * - * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process. + * The method is publicly available as {@link ~UpcastHelpers#attributeToAttribute `.attributeToAttribute()` upcast helper}. * * @param {Object} config Conversion configuration. * @param {String|Object} config.view Specifies which view attribute will be converted. If a `String` is passed, @@ -253,31 +125,13 @@ export function _upcastAttributeToAttribute( config ) { /** * View element to model marker conversion helper. * - * This conversion results in creating a model marker. For example, if the marker was stored in a view as an element: - * `

Foo

Bar

`, - * after the conversion is done, the marker will be available in - * {@link module:engine/model/model~Model#markers model document markers}. + * editor.conversion.for( 'upcast' ) + * .add( _upcastElementToMarker( { + * view: 'marker-search', + * model: 'search' + * } ) ); * - * _upcastElementToMarker( { view: 'marker-search', model: 'search' } ); - * - * _upcastElementToMarker( { view: 'marker-search', model: 'search', converterPriority: 'high' } ); - * - * _upcastElementToMarker( { - * view: 'marker-search', - * model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' ) - * } ); - * - * _upcastElementToMarker( { - * view: { - * name: 'span', - * attributes: { - * 'data-marker': 'search' - * } - * }, - * model: 'search' - * } ); - * - * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process. + * The method is publicly available as {@link ~UpcastHelpers#elementToMarker `.elementToMarker()` upcast helper}. * * @param {Object} config Conversion configuration. * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted. @@ -624,11 +478,18 @@ export const helpers = { * Keep in mind that the element will be inserted only if it is allowed * by {@link module:engine/model/schema~Schema schema} configuration. * - * conversion.for( 'upcast' ).elementToElement( { view: 'p', model: 'paragraph' } ); + * editor.conversion.for( 'upcast' ).elementToElement( { + * view: 'p', + * model: 'paragraph' + * } ); * - * conversion.for( 'upcast' ).elementToElement( { view: 'p', model: 'paragraph', converterPriority: 'high' } ); + * editor.conversion.for( 'upcast' ).elementToElement( { + * view: 'p', + * model: 'paragraph', + * converterPriority: 'high' + * } ); * - * conversion.for( 'upcast' ).elementToElement( { + * editor.conversion.for( 'upcast' ).elementToElement( { * view: { * name: 'p', * classes: 'fancy' @@ -636,7 +497,7 @@ export const helpers = { * model: 'fancyParagraph' * } ); * - * conversion.for( 'upcast' ).elementToElement( { + * editor.conversion.for( 'upcast' ).elementToElement( { * view: { * name: 'p', * classes: 'heading' @@ -646,7 +507,8 @@ export const helpers = { * } * } ); * - * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process. + * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter + * to the conversion process. * * @method #elementToElement * @param {Object} config Conversion configuration. @@ -676,11 +538,18 @@ export const helpers = { * * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration. * - * conversion.for( 'upcast' ).elementToAttribute( { view: 'strong', model: 'bold' } ); + * editor.conversion.for( 'upcast' ).elementToAttribute( { + * view: 'strong', + * model: 'bold' + * } ); * - * conversion.for( 'upcast' ).elementToAttribute( { view: 'strong', model: 'bold', converterPriority: 'high' } ); + * editor.conversion.for( 'upcast' ).elementToAttribute( { + * view: 'strong', + * model: 'bold', + * converterPriority: 'high' + * } ); * - * conversion.for( 'upcast' ).elementToAttribute( { + * editor.conversion.for( 'upcast' ).elementToAttribute( { * view: { * name: 'span', * classes: 'bold' @@ -688,7 +557,7 @@ export const helpers = { * model: 'bold' * } ); * - * conversion.for( 'upcast' ).elementToAttribute( { + * editor.conversion.for( 'upcast' ).elementToAttribute( { * view: { * name: 'span', * classes: [ 'styled', 'styled-dark' ] @@ -699,7 +568,7 @@ export const helpers = { * } * } ); * - * conversion.for( 'upcast' ).elementToAttribute( { + * editor.conversion.for( 'upcast' ).elementToAttribute( { * view: { * name: 'span', * styles: { @@ -723,8 +592,10 @@ export const helpers = { * } * } ); * - * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process. + * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter + * to the conversion process. * + * @method #elementToAttribute * @param {Object} config Conversion configuration. * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted. * @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing @@ -749,7 +620,8 @@ export const helpers = { *
foo
-->
foo
* * Above, `class="dark"` attribute is added only to the `
` elements that has it. This is in contrary to - * {@link module:engine/conversion/upcast-converters~upcastElementToAttribute} which sets attributes for all the children in the model: + * {@link module:engine/conversion/upcast-converters~UpcastHelpers#elementToAttribute} which sets attributes for + * all the children in the model: * * Foo -->

Foo

--> <$text bold="true">Foo * @@ -758,13 +630,23 @@ export const helpers = { * * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration. * - * conversion.for( 'upcast' ).attributeToAttribute( { view: 'src', model: 'source' } ); + * editor.conversion.for( 'upcast' ).attributeToAttribute( { + * view: 'src', + * model: 'source' + * } ); * - * conversion.for( 'upcast' ).attributeToAttribute( { view: { key: 'src' }, model: 'source' } ); + * editor.conversion.for( 'upcast' ).attributeToAttribute( { + * view: { key: 'src' }, + * model: 'source' + * } ); * - * conversion.for( 'upcast' ).attributeToAttribute( { view: { key: 'src' }, model: 'source', converterPriority: 'normal' } ); + * editor.conversion.for( 'upcast' ).attributeToAttribute( { + * view: { key: 'src' }, + * model: 'source', + * converterPriority: 'normal' + * } ); * - * conversion.for( 'upcast' ).attributeToAttribute( { + * editor.conversion.for( 'upcast' ).attributeToAttribute( { * view: { * key: 'data-style', * value: /[\s\S]+/ @@ -772,7 +654,7 @@ export const helpers = { * model: 'styled' * } ); * - * conversion.for( 'upcast' ).attributeToAttribute( { + * editor.conversion.for( 'upcast' ).attributeToAttribute( { * view: { * name: 'img', * key: 'class', @@ -784,7 +666,7 @@ export const helpers = { * } * } ); * - * conversion.for( 'upcast' ).attributeToAttribute( { + * editor.conversion.for( 'upcast' ).attributeToAttribute( { * view: { * key: 'class', * value: /styled-[\S]+/ @@ -800,8 +682,10 @@ export const helpers = { * } * } ); * - * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process. + * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter + * to the conversion process. * + * @method #attributeToAttribute * @param {Object} config Conversion configuration. * @param {String|Object} config.view Specifies which view attribute will be converted. If a `String` is passed, * attributes with given key will be converted. If an `Object` is passed, it must have a required `key` property, @@ -826,16 +710,23 @@ export const helpers = { * after the conversion is done, the marker will be available in * {@link module:engine/model/model~Model#markers model document markers}. * - * conversion.for( 'upcast' ).elementToMarker( { view: 'marker-search', model: 'search' } ); + * editor.conversion.for( 'upcast' ).elementToMarker( { + * view: 'marker-search', + * model: 'search' + * } ); * - * conversion.for( 'upcast' ).elementToMarker( { view: 'marker-search', model: 'search', converterPriority: 'high' } ); + * editor.conversion.for( 'upcast' ).elementToMarker( { + * view: 'marker-search', + * model: 'search', + * converterPriority: 'high' + * } ); * - * conversion.for( 'upcast' ).elementToMarker( { + * editor.conversion.for( 'upcast' ).elementToMarker( { * view: 'marker-search', * model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' ) * } ); * - * conversion.for( 'upcast' ).elementToMarker( { + * editor.conversion.for( 'upcast' ).elementToMarker( { * view: { * name: 'span', * attributes: { @@ -845,8 +736,10 @@ export const helpers = { * model: 'search' * } ); * - * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process. + * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter + * to the conversion process. * + * @method #elementToMarker * @param {Object} config Conversion configuration. * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted. * @param {String|Function} config.model Name of the model marker, or a function that takes a view element and returns From b0108d2ae584486ac9fe70ece95dcd672a341333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 18 Dec 2018 13:30:54 +0100 Subject: [PATCH 40/84] Update links in conversion helpers documentation. --- src/conversion/conversion.js | 13 +++++++------ src/conversion/downcast-converters.js | 9 +++++---- src/conversion/upcast-converters.js | 8 ++++---- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/conversion/conversion.js b/src/conversion/conversion.js index b77a4d982..9ba44ad72 100644 --- a/src/conversion/conversion.js +++ b/src/conversion/conversion.js @@ -104,9 +104,9 @@ export default class Conversion { * {@link module:engine/conversion/conversion~ConversionHelpers conversion helpers} returned by this function. * * editor.conversion.for( 'downcast' ) - * .add( conversionHelperA ) - * .add( conversionHelperB ) - * .elementToElement( config ); + * .add( conversionHelperA ) // Adds a custom converter A. + * .add( conversionHelperB ) // Adds a custom converter B. + * .elementToElement( config ); // Adds a custom element-to-element downcast converter. * * In this example `conversionHelperA` and `conversionHelperB` will be called for all dispatchers from the `'model'` group. * @@ -129,6 +129,7 @@ export default class Conversion { * * {@link module:engine/conversion/upcast-converters~UpcastHelpers#elementToElement Upcast element-to-element converter}, * * {@link module:engine/conversion/upcast-converters~UpcastHelpers#elementToAttribute Upcast attribute-to-element converter}, * * {@link module:engine/conversion/upcast-converters~UpcastHelpers#attributeToAttribute Upcast attribute-to-attribute converter}. + * * {@link module:engine/conversion/upcast-converters~UpcastHelpers#elementToMarker Upcast element-to-marker converter}. * * An example of using conversion helpers to convert the `paragraph` model element to the `p` view element (and back): * @@ -142,7 +143,6 @@ export default class Conversion { * @param {String} groupName The name of dispatchers group to add the converters to. * @returns {module:engine/conversion/conversion~ConversionHelpers|module:engine/conversion/downcast-converters~DowncastHelpers| * module:engine/conversion/upcast-converters~UpcastHelpers} - * An object with the `.add()` method, providing a way to add converters. */ /* eslint-enable max-len */ for( groupName ) { @@ -593,7 +593,7 @@ export default class Conversion { /** * Base class for conversion utilises. * - * @interface ConversionHelpers + * @interface module:engine/conversion/conversion~ConversionHelpers */ /** @@ -603,7 +603,8 @@ export default class Conversion { * * @method module:engine/conversion/conversion~ConversionHelpers#add * @param {Function} conversionHelper The function to be called on event. - * @returns {module:engine/conversion/conversion~Conversion} + * @returns {module:engine/conversion/conversion~ConversionHelpers|module:engine/conversion/downcast-converters~DowncastHelpers| + * module:engine/conversion/upcast-converters~UpcastHelpers} */ // Helper function for the `Conversion` `.add()` method. diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index 6687a5fc5..5460dca65 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -1003,6 +1003,7 @@ export const helpers = { * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function * that takes the model element and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer} * as parameters and returns a view container element. + * @returns {module:engine/conversion/downcast-converters~DowncastHelpers} */ elementToElement( config ) { return this.add( _downcastElementToElement( config ) ); @@ -1011,7 +1012,6 @@ export const helpers = { /** * Model attribute to view element conversion helper. * - * * This conversion results in wrapping view nodes with a view attribute element. For example, a model text node with * `"Foo"` as data and the `bold` attribute becomes `Foo` in the view. * @@ -1088,6 +1088,7 @@ export const helpers = { * as parameters and returns a view attribute element. If `config.model.values` is * given, `config.view` should be an object assigning values from `config.model.values` to view element definitions or functions. * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. + * @returns {module:engine/conversion/downcast-converters~DowncastHelpers} */ attributeToElement( config ) { return this.add( _downcastAttributeToElement( config ) ); @@ -1153,7 +1154,7 @@ export const helpers = { * If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to * `{ key, value }` objects or a functions. * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {Function} Conversion helper. + * @returns {module:engine/conversion/downcast-converters~DowncastHelpers} */ attributeToAttribute( config ) { return this.add( _downcastAttributeToAttribute( config ) ); @@ -1215,7 +1216,7 @@ export const helpers = { * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function * that takes the model marker data as a parameter and returns a view UI element. * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {Function} Conversion helper. + * @returns {module:engine/conversion/downcast-converters~DowncastHelpers} */ markerToElement( config ) { return this.add( _downcastMarkerToElement( config ) ); @@ -1274,7 +1275,7 @@ export const helpers = { * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} config.view A highlight descriptor * that will be used for highlighting or a function that takes the model marker data as a parameter and returns a highlight descriptor. * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {Function} Conversion helper. + * @returns {module:engine/conversion/downcast-converters~DowncastHelpers} */ markerToHighlight( config ) { return this.add( _downcastMarkerToHighlight( config ) ); diff --git a/src/conversion/upcast-converters.js b/src/conversion/upcast-converters.js index dd32246f5..9b604150b 100644 --- a/src/conversion/upcast-converters.js +++ b/src/conversion/upcast-converters.js @@ -516,7 +516,7 @@ export const helpers = { * @param {String|module:engine/model/element~Element|Function} config.model Name of the model element, a model element * instance or a function that takes a view element and returns a model element. The model element will be inserted in the model. * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {Function} Conversion helper. + * @returns {module:engine/conversion/upcast-converters~UpcastHelpers} */ elementToElement( config ) { return this.add( _upcastElementToElement( config ) ); @@ -602,7 +602,7 @@ export const helpers = { * the model attribute. `value` property may be set as a function that takes a view element and returns the value. * If `String` is given, the model attribute value will be set to `true`. * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {module:engine/conversion/conversion~Conversion} Conversion helper. + * @returns {module:engine/conversion/upcast-converters~UpcastHelpers} */ elementToAttribute( config ) { return this.add( _upcastElementToAttribute( config ) ); @@ -696,7 +696,7 @@ export const helpers = { * the model attribute. `value` property may be set as a function that takes a view element and returns the value. * If `String` is given, the model attribute value will be same as view attribute value. * @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority. - * @returns {Function} Conversion helper. + * @returns {module:engine/conversion/upcast-converters~UpcastHelpers} */ attributeToAttribute( config ) { return this.add( _upcastAttributeToAttribute( config ) ); @@ -745,7 +745,7 @@ export const helpers = { * @param {String|Function} config.model Name of the model marker, or a function that takes a view element and returns * a model marker name. * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {Function} Conversion helper. + * @returns {module:engine/conversion/upcast-converters~UpcastHelpers} */ elementToMarker( config ) { return this.add( _upcastElementToMarker( config ) ); From 52bc148afba7a02c62b7afd94b93c107d1677cc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 18 Dec 2018 13:34:23 +0100 Subject: [PATCH 41/84] Rename exported helpers to downcastHelpers & upcastHelpers. --- src/conversion/downcast-converters.js | 2 +- src/conversion/upcast-converters.js | 2 +- tests/conversion/conversion.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index 5460dca65..d7717b808 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -962,7 +962,7 @@ export function createViewElementFromHighlightDescriptor( descriptor ) { * @interface module:engine/conversion/downcast-converters~DowncastHelpers * @extends module:engine/conversion/conversion~ConversionHelpers */ -export const helpers = { +export const downcastHelpers = { /** * Model element to view element conversion helper. * diff --git a/src/conversion/upcast-converters.js b/src/conversion/upcast-converters.js index 9b604150b..fb5391b77 100644 --- a/src/conversion/upcast-converters.js +++ b/src/conversion/upcast-converters.js @@ -468,7 +468,7 @@ export function convertText() { * @interface module:engine/conversion/upcast-converters~UpcastHelpers * @extends module:engine/conversion/conversion~ConversionHelpers */ -export const helpers = { +export const upcastHelpers = { /** * View element to model element conversion helper. * diff --git a/tests/conversion/conversion.js b/tests/conversion/conversion.js index 779739e65..c21efac9f 100644 --- a/tests/conversion/conversion.js +++ b/tests/conversion/conversion.js @@ -8,7 +8,8 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import UpcastDispatcher from '../../src/conversion/upcastdispatcher'; -import { helpers as upcastHelpers, convertText, convertToModelFragment } from '../../src/conversion/upcast-converters'; +import { upcastHelpers, convertText, convertToModelFragment } from '../../src/conversion/upcast-converters'; +import { downcastHelpers } from '../../src/conversion/downcast-converters'; import EditingController from '../../src/controller/editingcontroller'; @@ -16,7 +17,6 @@ import Model from '../../src/model/model'; import { parse as viewParse, stringify as viewStringify } from '../../src/dev-utils/view'; import { setData, stringify as modelStringify } from '../../src/dev-utils/model'; -import { helpers as downcastHelpers } from '../../src/conversion/downcast-converters'; import ViewDocumentFragment from '../../src/view/documentfragment'; import ViewText from '../../src/view/text'; import ViewUIElement from '../../src/view/uielement'; From 6c630fd38a634cd46c9433a896606cb814f719b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 18 Dec 2018 13:51:04 +0100 Subject: [PATCH 42/84] Fix code block in conversion documentation. --- src/conversion/conversion.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/conversion/conversion.js b/src/conversion/conversion.js index 9ba44ad72..e6195319e 100644 --- a/src/conversion/conversion.js +++ b/src/conversion/conversion.js @@ -32,6 +32,7 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; * * // Add a converter to the data pipepline only: * editor.conversion.for( 'dataDowncast' ).elementToElement( dataConversionConfig ) ); + * * // And a slightly different one for the editing pipeline: * editor.conversion.for( 'editingDowncast' ).elementToElement( editingConversionConfig ) ); * From a7d9250881408709f3dc25542e5ced565e2786b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 19 Dec 2018 16:25:40 +0100 Subject: [PATCH 43/84] Update eslint-config-ckeditor5 package in package.json. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6dd9479e5..540f9e6c5 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@ckeditor/ckeditor5-undo": "^10.0.4", "@ckeditor/ckeditor5-widget": "^10.3.1", "eslint": "^5.5.0", - "eslint-config-ckeditor5": "^1.0.7", + "eslint-config-ckeditor5": "^1.0.9", "husky": "^0.14.3", "lint-staged": "^7.0.0" }, From 6862a796ed9d7121a621a94970edb0c8bc15e79f Mon Sep 17 00:00:00 2001 From: Szymon Cofalik Date: Thu, 20 Dec 2018 09:58:05 +0100 Subject: [PATCH 44/84] Code style changes in selection-post-fixer. Co-Authored-By: jodator --- src/model/utils/selection-post-fixer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/utils/selection-post-fixer.js b/src/model/utils/selection-post-fixer.js index 6bfa98c0b..129025ef4 100644 --- a/src/model/utils/selection-post-fixer.js +++ b/src/model/utils/selection-post-fixer.js @@ -187,7 +187,7 @@ function tryFixingNonCollapsedRage( range, schema ) { // - [foo] -> [foo] if ( checkSelectionOnNonLimitElements( start, end, schema ) ) { const isStartObject = start.nodeAfter && schema.isObject( start.nodeAfter ); - const fixedStart = isStartObject ? null : schema.getNearestSelectionRange( start, 'forward' ); + const fixedStart = isStartObject ? start : schema.getNearestSelectionRange( start, 'forward' ).start; const isEndObject = end.nodeBefore && schema.isObject( end.nodeBefore ); const fixedEnd = isEndObject ? null : schema.getNearestSelectionRange( end, 'backward' ); From d1a522fd0ba0c0620915cec6a30976d2365c2f1c Mon Sep 17 00:00:00 2001 From: Szymon Cofalik Date: Thu, 20 Dec 2018 09:58:21 +0100 Subject: [PATCH 45/84] Code style changes in selection-post-fixer. Co-Authored-By: jodator --- src/model/utils/selection-post-fixer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/utils/selection-post-fixer.js b/src/model/utils/selection-post-fixer.js index 129025ef4..e29e08498 100644 --- a/src/model/utils/selection-post-fixer.js +++ b/src/model/utils/selection-post-fixer.js @@ -190,7 +190,7 @@ function tryFixingNonCollapsedRage( range, schema ) { const fixedStart = isStartObject ? start : schema.getNearestSelectionRange( start, 'forward' ).start; const isEndObject = end.nodeBefore && schema.isObject( end.nodeBefore ); - const fixedEnd = isEndObject ? null : schema.getNearestSelectionRange( end, 'backward' ); + const fixedEnd = isEndObject ? end : schema.getNearestSelectionRange( end, 'backward' ).end; return new Range( fixedStart ? fixedStart.start : start, fixedEnd ? fixedEnd.start : end ); } From 92f9a9f984e92b003fb6d538b2c3776bce27d8f9 Mon Sep 17 00:00:00 2001 From: Szymon Cofalik Date: Thu, 20 Dec 2018 09:58:32 +0100 Subject: [PATCH 46/84] Code style changes in selection-post-fixer. Co-Authored-By: jodator --- src/model/utils/selection-post-fixer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/utils/selection-post-fixer.js b/src/model/utils/selection-post-fixer.js index e29e08498..5096c3a15 100644 --- a/src/model/utils/selection-post-fixer.js +++ b/src/model/utils/selection-post-fixer.js @@ -192,7 +192,7 @@ function tryFixingNonCollapsedRage( range, schema ) { const isEndObject = end.nodeBefore && schema.isObject( end.nodeBefore ); const fixedEnd = isEndObject ? end : schema.getNearestSelectionRange( end, 'backward' ).end; - return new Range( fixedStart ? fixedStart.start : start, fixedEnd ? fixedEnd.start : end ); + return new Range( fixedStart, fixedEnd ); } } From d1debb956d728d4ad95ca9c8c2dc4b79b77deba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 20 Dec 2018 10:06:37 +0100 Subject: [PATCH 47/84] Fix code style because of Github. --- src/model/utils/selection-post-fixer.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/model/utils/selection-post-fixer.js b/src/model/utils/selection-post-fixer.js index 5096c3a15..1510de54b 100644 --- a/src/model/utils/selection-post-fixer.js +++ b/src/model/utils/selection-post-fixer.js @@ -187,12 +187,12 @@ function tryFixingNonCollapsedRage( range, schema ) { // - [foo] -> [foo] if ( checkSelectionOnNonLimitElements( start, end, schema ) ) { const isStartObject = start.nodeAfter && schema.isObject( start.nodeAfter ); - const fixedStart = isStartObject ? start : schema.getNearestSelectionRange( start, 'forward' ).start; + const fixedStart = isStartObject ? start : schema.getNearestSelectionRange( start, 'forward' ).start; const isEndObject = end.nodeBefore && schema.isObject( end.nodeBefore ); - const fixedEnd = isEndObject ? end : schema.getNearestSelectionRange( end, 'backward' ).end; + const fixedEnd = isEndObject ? end : schema.getNearestSelectionRange( end, 'backward' ).end; - return new Range( fixedStart, fixedEnd ); + return new Range( fixedStart, fixedEnd ); } } From 5a055e17013a91145c71db4d39434cd9f231a8cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 20 Dec 2018 10:08:59 +0100 Subject: [PATCH 48/84] Revert "Code style changes in selection-post-fixer." This reverts commit 92f9a9f9 --- src/model/utils/selection-post-fixer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/utils/selection-post-fixer.js b/src/model/utils/selection-post-fixer.js index 1510de54b..0aaec838f 100644 --- a/src/model/utils/selection-post-fixer.js +++ b/src/model/utils/selection-post-fixer.js @@ -192,7 +192,7 @@ function tryFixingNonCollapsedRage( range, schema ) { const isEndObject = end.nodeBefore && schema.isObject( end.nodeBefore ); const fixedEnd = isEndObject ? end : schema.getNearestSelectionRange( end, 'backward' ).end; - return new Range( fixedStart, fixedEnd ); + return new Range( fixedStart ? fixedStart.start : start, fixedEnd ? fixedEnd.start : end ); } } From b16b0f0e692d910673ef7b52f8f1d8391afc6969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 20 Dec 2018 10:09:15 +0100 Subject: [PATCH 49/84] Revert "Code style changes in selection-post-fixer." This reverts commit d1a522fd --- src/model/utils/selection-post-fixer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/utils/selection-post-fixer.js b/src/model/utils/selection-post-fixer.js index 0aaec838f..4d44267c3 100644 --- a/src/model/utils/selection-post-fixer.js +++ b/src/model/utils/selection-post-fixer.js @@ -190,7 +190,7 @@ function tryFixingNonCollapsedRage( range, schema ) { const fixedStart = isStartObject ? start : schema.getNearestSelectionRange( start, 'forward' ).start; const isEndObject = end.nodeBefore && schema.isObject( end.nodeBefore ); - const fixedEnd = isEndObject ? end : schema.getNearestSelectionRange( end, 'backward' ).end; + const fixedEnd = isEndObject ? null : schema.getNearestSelectionRange( end, 'backward' ); return new Range( fixedStart ? fixedStart.start : start, fixedEnd ? fixedEnd.start : end ); } From d9643757c0f5a9e2e59b7554df1b20227161cad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 20 Dec 2018 10:09:27 +0100 Subject: [PATCH 50/84] Revert "Code style changes in selection-post-fixer." This reverts commit 6862a796 --- src/model/utils/selection-post-fixer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/utils/selection-post-fixer.js b/src/model/utils/selection-post-fixer.js index 4d44267c3..6bfa98c0b 100644 --- a/src/model/utils/selection-post-fixer.js +++ b/src/model/utils/selection-post-fixer.js @@ -187,7 +187,7 @@ function tryFixingNonCollapsedRage( range, schema ) { // - [foo] -> [foo] if ( checkSelectionOnNonLimitElements( start, end, schema ) ) { const isStartObject = start.nodeAfter && schema.isObject( start.nodeAfter ); - const fixedStart = isStartObject ? start : schema.getNearestSelectionRange( start, 'forward' ).start; + const fixedStart = isStartObject ? null : schema.getNearestSelectionRange( start, 'forward' ); const isEndObject = end.nodeBefore && schema.isObject( end.nodeBefore ); const fixedEnd = isEndObject ? null : schema.getNearestSelectionRange( end, 'backward' ); From 9cf3a14650391627bfe15b0a3abe78874f377fb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 20 Dec 2018 10:11:43 +0100 Subject: [PATCH 51/84] Update comment in tryFixingNonCollapsedRage() of selection-post-fixer. --- src/model/utils/selection-post-fixer.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/model/utils/selection-post-fixer.js b/src/model/utils/selection-post-fixer.js index 6bfa98c0b..50810a226 100644 --- a/src/model/utils/selection-post-fixer.js +++ b/src/model/utils/selection-post-fixer.js @@ -192,7 +192,11 @@ function tryFixingNonCollapsedRage( range, schema ) { const isEndObject = end.nodeBefore && schema.isObject( end.nodeBefore ); const fixedEnd = isEndObject ? null : schema.getNearestSelectionRange( end, 'backward' ); - return new Range( fixedStart ? fixedStart.start : start, fixedEnd ? fixedEnd.start : end ); + // The schema.getNearestSelectionRange might return null - if that happens use original position. + const rangeStart = fixedStart ? fixedStart.start : start; + const rangeEnd = fixedEnd ? fixedEnd.start : end; + + return new Range( rangeStart, rangeEnd ); } } From 9172eee6539eed02aac7a65e32819611e67a4020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 20 Dec 2018 10:13:08 +0100 Subject: [PATCH 52/84] Remove empty line in selection-post-fixer. --- src/model/utils/selection-post-fixer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/model/utils/selection-post-fixer.js b/src/model/utils/selection-post-fixer.js index 50810a226..e6e8614cb 100644 --- a/src/model/utils/selection-post-fixer.js +++ b/src/model/utils/selection-post-fixer.js @@ -159,7 +159,6 @@ function tryFixingCollapsedRange( range, schema ) { // // @param {module:engine/model/range~Range} range Expanded range to fix. // @param {module:engine/model/schema~Schema} schema - // @returns {module:engine/model/range~Range|null} Returns fixed range or null if range is valid. function tryFixingNonCollapsedRage( range, schema ) { const start = range.start; From 77d39f26f162d54db33d377df41c8cf8ce725a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 20 Dec 2018 10:17:07 +0100 Subject: [PATCH 53/84] Code style - remove redundant cast to boolean. --- src/model/utils/selection-post-fixer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/utils/selection-post-fixer.js b/src/model/utils/selection-post-fixer.js index e6e8614cb..b50e13971 100644 --- a/src/model/utils/selection-post-fixer.js +++ b/src/model/utils/selection-post-fixer.js @@ -205,7 +205,7 @@ function tryFixingNonCollapsedRage( range, schema ) { // At this point we eliminated valid positions on text nodes so if one of range positions is placed inside a limit element // then the range crossed limit element boundaries and needs to be fixed. if ( isStartInLimit || isEndInLimit ) { - const bothInSameParent = ( !!start.nodeAfter && !!end.nodeBefore ) && start.nodeAfter === end.nodeBefore; + const bothInSameParent = ( start.nodeAfter && end.nodeBefore ) && start.nodeAfter.parent === end.nodeBefore.parent; const expandStart = isStartInLimit && ( !bothInSameParent || !isInObject( start.nodeAfter, schema ) ); const expandEnd = isEndInLimit && ( !bothInSameParent || !isInObject( end.nodeBefore, schema ) ); From da417e0abdfbb4f7568e9101e782fa7fc85ba29f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 20 Dec 2018 10:18:04 +0100 Subject: [PATCH 54/84] Rename findOuterMostIsLimitAncestor to findOutermostLimitAncestor(). --- src/model/utils/selection-post-fixer.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/model/utils/selection-post-fixer.js b/src/model/utils/selection-post-fixer.js index b50e13971..f7f54f808 100644 --- a/src/model/utils/selection-post-fixer.js +++ b/src/model/utils/selection-post-fixer.js @@ -216,11 +216,11 @@ function tryFixingNonCollapsedRage( range, schema ) { let fixedEnd = end; if ( expandStart ) { - fixedStart = Position._createBefore( findOuterMostIsLimitAncestor( startLimitElement, schema ) ); + fixedStart = Position._createBefore( findOutermostLimitAncestor( startLimitElement, schema ) ); } if ( expandEnd ) { - fixedEnd = Position._createAfter( findOuterMostIsLimitAncestor( endLimitElement, schema ) ); + fixedEnd = Position._createAfter( findOutermostLimitAncestor( endLimitElement, schema ) ); } return new Range( fixedStart, fixedEnd ); @@ -236,7 +236,7 @@ function tryFixingNonCollapsedRage( range, schema ) { // @param {module:engine/model/schema~Schema} schema // @param {String} expandToDirection Direction of expansion - either 'start' or 'end' of the range. // @returns {module:engine/model/node~Node} -function findOuterMostIsLimitAncestor( startingNode, schema ) { +function findOutermostLimitAncestor( startingNode, schema ) { let isLimitNode = startingNode; let parent = isLimitNode; From 3398e5d19a2211d1ddddf3fa2e33b7c8176bb243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 20 Dec 2018 10:19:26 +0100 Subject: [PATCH 55/84] Update checkSelectionOnNonLimitElements() description. --- src/model/utils/selection-post-fixer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/utils/selection-post-fixer.js b/src/model/utils/selection-post-fixer.js index f7f54f808..73eca8e49 100644 --- a/src/model/utils/selection-post-fixer.js +++ b/src/model/utils/selection-post-fixer.js @@ -249,7 +249,7 @@ function findOutermostLimitAncestor( startingNode, schema ) { return isLimitNode; } -// Checks whether one of range ends is placed around non-limit elements. +// Checks whether any of range boundaries is placed around non-limit elements. // // @param {module:engine/model/position~Position} start // @param {module:engine/model/position~Position} end From 05bc920655b4552d392fb817604739a51a282789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 20 Dec 2018 13:30:26 +0100 Subject: [PATCH 56/84] Remove superfluous batch from model~pares() function. --- src/dev-utils/model.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/dev-utils/model.js b/src/dev-utils/model.js index 3fc3debea..da73d5ccd 100644 --- a/src/dev-utils/model.js +++ b/src/dev-utils/model.js @@ -278,7 +278,6 @@ export function stringify( node, selectionOrPositionOrRange = null, markers = nu * * @param {String} data HTML-like string to be parsed. * @param {module:engine/model/schema~Schema} schema A schema instance used by converters for element validation. - * @param {module:engine/model/batch~Batch} batch A batch used for conversion. * @param {Object} [options={}] Additional configuration. * @param {Array} [options.selectionAttributes] A list of attributes which will be passed to the selection. * @param {Boolean} [options.lastRangeBackward=false] If set to `true`, the last range will be added as backward. From de4eb631e7a3e1590f73e3886b73129ccd6b50ac Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 20 Dec 2018 14:11:32 +0100 Subject: [PATCH 57/84] Docs: Fixed wrong documentation links. [skip ci] --- docs/api/engine.md | 2 +- src/model/documentselection.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/api/engine.md b/docs/api/engine.md index 03ad8c32f..1cef12f85 100644 --- a/docs/api/engine.md +++ b/docs/api/engine.md @@ -12,7 +12,7 @@ Together with the {@link api/core core editor architecture} and the {@link api/u ## Documentation -See the introduction to the {@link framework/guides/architecture/intro#editing-engine editing engine's architecture}. +See the introduction to the {@link framework/guides/architecture/editing-engine editing engine's architecture}. You can also browse the API documentation of this package by using the module tree on the left. diff --git a/src/model/documentselection.js b/src/model/documentselection.js index dbe4516b3..a81feecdd 100644 --- a/src/model/documentselection.js +++ b/src/model/documentselection.js @@ -696,7 +696,8 @@ class LiveSelection extends Selection { * UID obtained from the {@link module:engine/model/writer~Writer#overrideSelectionGravity} to restore. * * @error document-selection-gravity-wrong-restore - * @param {String} uid The unique identifier returned by {@link #overrideGravity}. + * @param {String} uid The unique identifier returned by + * {@link module:engine/model/documentselection~DocumentSelection#_overrideGravity}. */ throw new CKEditorError( 'document-selection-gravity-wrong-restore: Attempting to restore the selection gravity for an unknown UID.', From 5e1285dce4911bbc0c2a2766eb0bbca37ee7f88f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 20 Dec 2018 15:52:01 +0100 Subject: [PATCH 58/84] Refactor downcastHelpers & upcastHelpers to classes. --- src/conversion/conversion.js | 103 +- src/conversion/downcast-converters.js | 1197 +++++++++++------------ src/conversion/upcast-converters.js | 1119 ++++++++++----------- tests/controller/datacontroller.js | 53 +- tests/controller/editingcontroller.js | 10 +- tests/conversion/downcast-converters.js | 293 +++--- tests/conversion/upcast-converters.js | 181 ++-- 7 files changed, 1377 insertions(+), 1579 deletions(-) diff --git a/src/conversion/conversion.js b/src/conversion/conversion.js index e6195319e..91f675749 100644 --- a/src/conversion/conversion.js +++ b/src/conversion/conversion.js @@ -79,8 +79,8 @@ export default class Conversion { * @param {module:engine/conversion/downcast-converters~DowncastHelpers| * module:engine/conversion/upcast-converters~UpcastHelpers} helpers */ - register( options ) { - if ( this._dispatchersGroups.has( options.name ) ) { + register( name, group ) { + if ( this._dispatchersGroups.has( name ) ) { /** * Trying to register a group name that was already registered. * @@ -89,13 +89,7 @@ export default class Conversion { throw new CKEditorError( 'conversion-register-group-exists: Trying to register a group name that was already registered.' ); } - const group = { - name: options.name, - dispatchers: Array.isArray( options.dispatcher ) ? options.dispatcher : [ options.dispatcher ], - helpers: options.helpers - }; - - this._dispatchersGroups.set( options.name, group ); + this._dispatchersGroups.set( name, group ); } /* eslint-disable max-len */ @@ -147,17 +141,9 @@ export default class Conversion { */ /* eslint-enable max-len */ for( groupName ) { - const { dispatchers, helpers } = this._getDispatchersGroup( groupName ); - - const baseRetVal = { - add( conversionHelper ) { - _addToDispatchers( dispatchers, conversionHelper ); - - return this; - } - }; + const group = this._getDispatchersGroup( groupName ); - return Object.assign( {}, baseRetVal, helpers ); + return group; } /** @@ -591,38 +577,6 @@ export default class Conversion { * @property {module:engine/conversion/downcast-converters~DowncastHelpers|module:engine/conversion/upcast-converters~UpcastHelpers} helpers */ -/** - * Base class for conversion utilises. - * - * @interface module:engine/conversion/conversion~ConversionHelpers - */ - -/** - * Registers a conversion helper. - * - * **Note**: See full usage example in the `{@link module:engine/conversion/conversion~Conversion#for conversion.for()}` method description - * - * @method module:engine/conversion/conversion~ConversionHelpers#add - * @param {Function} conversionHelper The function to be called on event. - * @returns {module:engine/conversion/conversion~ConversionHelpers|module:engine/conversion/downcast-converters~DowncastHelpers| - * module:engine/conversion/upcast-converters~UpcastHelpers} - */ - -// Helper function for the `Conversion` `.add()` method. -// -// Calls `conversionHelper` on each dispatcher from the group specified earlier in the `.for()` call, effectively -// adding converters to all specified dispatchers. -// -// @private -// @param {Array.} dispatchers -// @param {Function} conversionHelper -function _addToDispatchers( dispatchers, conversionHelper ) { - for ( const dispatcher of dispatchers ) { - conversionHelper( dispatcher ); - } -} - // Helper function that creates a joint array out of an item passed in `definition.view` and items passed in // `definition.upcastAlso`. // @@ -653,3 +607,50 @@ function* _getUpcastDefinition( model, view, upcastAlso ) { } } } + +/** + * Base class for conversion utilises. + * + */ +export class ConversionHelpers { + /** + * Creates ConversionHelpers instance. + * + * @param {Array.} dispatcher + */ + constructor( dispatcher ) { + this._dispatchers = Array.isArray( dispatcher ) ? dispatcher : [ dispatcher ]; + } + + /** + * Registers a conversion helper. + * + * **Note**: See full usage example in the `{@link module:engine/conversion/conversion~Conversion#for conversion.for()}` + * method description + * + * @param {Function} conversionHelper The function to be called on event. + * @returns {module:engine/conversion/conversion~ConversionHelpers|module:engine/conversion/downcast-converters~DowncastHelpers| + * module:engine/conversion/upcast-converters~UpcastHelpers} + */ + add( conversionHelper ) { + this._addToDispatchers( conversionHelper ); + + return this; + } + + /** + * Helper function for the `Conversion` `.add()` method. + * + * Calls `conversionHelper` on each dispatcher from the group specified earlier in the `.for()` call, effectively + * adding converters to all specified dispatchers. + * + * @private + * @param {Function} conversionHelper + */ + _addToDispatchers( conversionHelper ) { + for ( const dispatcher of this._dispatchers ) { + conversionHelper( dispatcher ); + } + } +} diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index d7717b808..4820ec54c 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -9,6 +9,7 @@ import ModelElement from '../model/element'; import ViewAttributeElement from '../view/attributeelement'; import DocumentSelection from '../model/documentselection'; +import { ConversionHelpers } from './conversion'; import log from '@ckeditor/ckeditor5-utils/src/log'; import { cloneDeep } from 'lodash-es'; @@ -20,287 +21,327 @@ import { cloneDeep } from 'lodash-es'; */ /** - * Model element to view element conversion helper. - * - * editor.conversion.for( 'downcast' ) - * .add( _downcastElementToElement( { - * model: 'paragraph', - * view: 'p' - * } ) ); - * - * The method is publicly available as {@link ~DowncastHelpers#elementToElement `.elementToElement()` downcast helper}. - * - * @protected - * @param {Object} config Conversion configuration. - * @param {String} config.model The name of the model element to convert. - * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function - * that takes the model element and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer} - * as parameters and returns a view container element. - * @returns {Function} Conversion helper. - */ -export function _downcastElementToElement( config ) { - config = cloneDeep( config ); - - config.view = _normalizeToElementConfig( config.view, 'container' ); - - return dispatcher => { - dispatcher.on( 'insert:' + config.model, insertElement( config.view ), { priority: config.converterPriority || 'normal' } ); - }; -} - -/** - * Model attribute to view element conversion helper. - * - * editor.conversion.for( 'downcast' ) - * .add( _downcastAttributeToElement( { - * model: 'bold', - * view: 'strong' - * } ) ); - * - * The method is publicly available as {@link ~DowncastHelpers#attributeToElement `.attributeToElement()` downcast helper}. + * Downcast conversion helper functions. * - * @protected - * @param {Object} config Conversion configuration. - * @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array - * of `String`s with possible values if the model attribute is an enumerable. - * @param {module:engine/view/elementdefinition~ElementDefinition|Function|Object} config.view A view element definition or a function - * that takes the model attribute value and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer} - * as parameters and returns a view attribute element. If `config.model.values` is - * given, `config.view` should be an object assigning values from `config.model.values` to view element definitions or functions. - * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {Function} Conversion helper. + * @extends module:engine/conversion/conversion~ConversionHelpers */ -export function _downcastAttributeToElement( config ) { - config = cloneDeep( config ); - - const modelKey = config.model.key ? config.model.key : config.model; - let eventName = 'attribute:' + modelKey; - - if ( config.model.name ) { - eventName += ':' + config.model.name; - } - - if ( config.model.values ) { - for ( const modelValue of config.model.values ) { - config.view[ modelValue ] = _normalizeToElementConfig( config.view[ modelValue ], 'attribute' ); - } - } else { - config.view = _normalizeToElementConfig( config.view, 'attribute' ); +export default class DowncastHelpers extends ConversionHelpers { + /** + * Model element to view element conversion helper. + * + * This conversion results in creating a view element. For example, model `Foo` becomes `

Foo

` in the view. + * + * editor.conversion.for( 'downcast' ).elementToElement( { + * model: 'paragraph', + * view: 'p' + * } ); + * + * editor.conversion.for( 'downcast' ).elementToElement( { + * model: 'paragraph', + * view: 'div', + * converterPriority: 'high' + * } ); + * + * editor.conversion.for( 'downcast' ).elementToElement( { + * model: 'fancyParagraph', + * view: { + * name: 'p', + * classes: 'fancy' + * } + * } ); + * + * editor.conversion.for( 'downcast' ).elementToElement( { + * model: 'heading', + * view: ( modelElement, viewWriter ) => { + * return viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) ) + * } + * } ); + * + * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter + * to the conversion process. + * + * @method #elementToElement + * @param {Object} config Conversion configuration. + * @param {String} config.model The name of the model element to convert. + * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function + * that takes the model element and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer} + * as parameters and returns a view container element. + * @returns {module:engine/conversion/downcast-converters~DowncastHelpers} + */ + elementToElement( config ) { + return this.add( _downcastElementToElement( config ) ); } - const elementCreator = _getFromAttributeCreator( config ); - - return dispatcher => { - dispatcher.on( eventName, wrap( elementCreator ), { priority: config.converterPriority || 'normal' } ); - }; -} - -/** - * Model attribute to view attribute conversion helper. - * - * editor.conversion.for( 'downcast' ) - * .add( _downcastAttributeToAttribute( { - * model: 'source', - * view: 'src' - * } ) ); - * - * The method is publicly available as {@link ~DowncastHelpers#attributeToAttribute `.attributeToAttribute()` downcast helper}. - * - * @protected - * @param {Object} config Conversion configuration. - * @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values, [ name ] }` object describing - * the attribute key, possible values and, optionally, an element name to convert from. - * @param {String|Object|Function} config.view A view attribute key, or a `{ key, value }` object or a function that takes - * the model attribute value and returns a `{ key, value }` object. If `key` is `'class'`, `value` can be a `String` or an - * array of `String`s. If `key` is `'style'`, `value` is an object with key-value pairs. In other cases, `value` is a `String`. - * If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to - * `{ key, value }` objects or a functions. - * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {Function} Conversion helper. - */ -export function _downcastAttributeToAttribute( config ) { - config = cloneDeep( config ); - - const modelKey = config.model.key ? config.model.key : config.model; - let eventName = 'attribute:' + modelKey; - - if ( config.model.name ) { - eventName += ':' + config.model.name; + /** + * Model attribute to view element conversion helper. + * + * This conversion results in wrapping view nodes with a view attribute element. For example, a model text node with + * `"Foo"` as data and the `bold` attribute becomes `Foo` in the view. + * + * editor.conversion.for( 'downcast' ).attributeToElement( { + * model: 'bold', + * view: 'strong' + * } ); + * + * editor.conversion.for( 'downcast' ).attributeToElement( { + * model: 'bold', + * view: 'b', + * converterPriority: 'high' + * } ); + * + * editor.conversion.for( 'downcast' ).attributeToElement( { + * model: 'invert', + * view: { + * name: 'span', + * classes: [ 'font-light', 'bg-dark' ] + * } + * } ); + * + * editor.conversion.for( 'downcast' ).attributeToElement( { + * model: { + * key: 'fontSize', + * values: [ 'big', 'small' ] + * }, + * view: { + * big: { + * name: 'span', + * styles: { + * 'font-size': '1.2em' + * } + * }, + * small: { + * name: 'span', + * styles: { + * 'font-size': '0.8em' + * } + * } + * } + * } ); + * + * editor.conversion.for( 'downcast' ).attributeToElement( { + * model: 'bold', + * view: ( modelAttributeValue, viewWriter ) => { + * return viewWriter.createAttributeElement( 'span', { + * style: 'font-weight:' + modelAttributeValue + * } ); + * } + * } ); + * + * editor.conversion.for( 'downcast' ).attributeToElement( { + * model: { + * key: 'color', + * name: '$text' + * }, + * view: ( modelAttributeValue, viewWriter ) => { + * return viewWriter.createAttributeElement( 'span', { + * style: 'color:' + modelAttributeValue + * } ); + * } + * } ); + * + * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter + * to the conversion process. + * + * @method #attributeToElement + * @param {Object} config Conversion configuration. + * @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array + * of `String`s with possible values if the model attribute is an enumerable. + * @param {module:engine/view/elementdefinition~ElementDefinition|Function|Object} config.view A view element definition or a function + * that takes the model attribute value and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer} + * as parameters and returns a view attribute element. If `config.model.values` is + * given, `config.view` should be an object assigning values from `config.model.values` to view element definitions or functions. + * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. + * @returns {module:engine/conversion/downcast-converters~DowncastHelpers} + */ + attributeToElement( config ) { + return this.add( _downcastAttributeToElement( config ) ); } - if ( config.model.values ) { - for ( const modelValue of config.model.values ) { - config.view[ modelValue ] = _normalizeToAttributeConfig( config.view[ modelValue ] ); - } - } else { - config.view = _normalizeToAttributeConfig( config.view ); + /** + * Model attribute to view attribute conversion helper. + * + * This conversion results in adding an attribute to a view node, basing on an attribute from a model node. For example, + * `` is converted to ``. + * + * editor.conversion.for( 'downcast' ).attributeToAttribute( { + * model: 'source', + * view: 'src' + * } ); + * + * editor.conversion.for( 'downcast' ).attributeToAttribute( { + * model: 'source', + * view: 'href', + * converterPriority: 'high' + * } ); + * + * editor.conversion.for( 'downcast' ).attributeToAttribute( { + * model: { + * name: 'image', + * key: 'source' + * }, + * view: 'src' + * } ); + * + * editor.conversion.for( 'downcast' ).attributeToAttribute( { + * model: { + * name: 'styled', + * values: [ 'dark', 'light' ] + * }, + * view: { + * dark: { + * key: 'class', + * value: [ 'styled', 'styled-dark' ] + * }, + * light: { + * key: 'class', + * value: [ 'styled', 'styled-light' ] + * } + * } + * } ); + * + * editor.conversion.for( 'downcast' ).attributeToAttribute( { + * model: 'styled', + * view: modelAttributeValue => ( { key: 'class', value: 'styled-' + modelAttributeValue } ) + * } ); + * + * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter + * to the conversion process. + * + * @method #attributeToAttribute + * @param {Object} config Conversion configuration. + * @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values, [ name ] }` object describing + * the attribute key, possible values and, optionally, an element name to convert from. + * @param {String|Object|Function} config.view A view attribute key, or a `{ key, value }` object or a function that takes + * the model attribute value and returns a `{ key, value }` object. If `key` is `'class'`, `value` can be a `String` or an + * array of `String`s. If `key` is `'style'`, `value` is an object with key-value pairs. In other cases, `value` is a `String`. + * If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to + * `{ key, value }` objects or a functions. + * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. + * @returns {module:engine/conversion/downcast-converters~DowncastHelpers} + */ + attributeToAttribute( config ) { + return this.add( _downcastAttributeToAttribute( config ) ); } - const elementCreator = _getFromAttributeCreator( config ); - - return dispatcher => { - dispatcher.on( eventName, changeAttribute( elementCreator ), { priority: config.converterPriority || 'normal' } ); - }; -} - -/** - * Model marker to view element conversion helper. - * - * editor.conversion.for( 'downcast' ) - * .add( _downcastMarkerToElement( { - * model: 'search', - * view: 'marker-search' - * } ) ); - * - * The method is publicly available as {@link ~DowncastHelpers#markerToElement `.markerToElement()` downcast helper}. - * - * @protected - * @param {Object} config Conversion configuration. - * @param {String} config.model The name of the model marker (or model marker group) to convert. - * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function - * that takes the model marker data as a parameter and returns a view UI element. - * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {Function} Conversion helper. - */ -export function _downcastMarkerToElement( config ) { - config = cloneDeep( config ); - - config.view = _normalizeToElementConfig( config.view, 'ui' ); - - return dispatcher => { - dispatcher.on( 'addMarker:' + config.model, insertUIElement( config.view ), { priority: config.converterPriority || 'normal' } ); - dispatcher.on( 'removeMarker:' + config.model, removeUIElement( config.view ), { priority: config.converterPriority || 'normal' } ); - }; -} - -/** - * Model marker to highlight conversion helper. - * - * editor.conversion.for( 'downcast' ) - * .add( _downcastMarkerToHighlight( { - * model: 'comment', - * view: { classes: 'comment' } - * } ) ); - * - * The method is publicly available as {@link ~DowncastHelpers#markerToElement `.markerToElement()` downcast helper}. - * - * @protected - * @param {Object} config Conversion configuration. - * @param {String} config.model The name of the model marker (or model marker group) to convert. - * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} config.view A highlight descriptor - * that will be used for highlighting or a function that takes the model marker data as a parameter and returns a highlight descriptor. - * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {Function} Conversion helper. - */ -export function _downcastMarkerToHighlight( config ) { - return dispatcher => { - dispatcher.on( 'addMarker:' + config.model, highlightText( config.view ), { priority: config.converterPriority || 'normal' } ); - dispatcher.on( 'addMarker:' + config.model, highlightElement( config.view ), { priority: config.converterPriority || 'normal' } ); - dispatcher.on( 'removeMarker:' + config.model, removeHighlight( config.view ), { priority: config.converterPriority || 'normal' } ); - }; -} - -// Takes `config.view`, and if it is an {@link module:engine/view/elementdefinition~ElementDefinition}, converts it -// to a function (because lower level converters accept only element creator functions). -// -// @param {module:engine/view/elementdefinition~ElementDefinition|Function} view View configuration. -// @param {'container'|'attribute'|'ui'} viewElementType View element type to create. -// @returns {Function} Element creator function to use in lower level converters. -function _normalizeToElementConfig( view, viewElementType ) { - if ( typeof view == 'function' ) { - // If `view` is already a function, don't do anything. - return view; - } - - return ( modelData, viewWriter ) => _createViewElementFromDefinition( view, viewWriter, viewElementType ); -} - -// Creates a view element instance from the provided {@link module:engine/view/elementdefinition~ElementDefinition} and class. -// -// @param {module:engine/view/elementdefinition~ElementDefinition} viewElementDefinition -// @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter -// @param {'container'|'attribute'|'ui'} viewElementType -// @returns {module:engine/view/element~Element} -function _createViewElementFromDefinition( viewElementDefinition, viewWriter, viewElementType ) { - if ( typeof viewElementDefinition == 'string' ) { - // If `viewElementDefinition` is given as a `String`, normalize it to an object with `name` property. - viewElementDefinition = { name: viewElementDefinition }; - } - - let element; - const attributes = Object.assign( {}, viewElementDefinition.attributes ); - - if ( viewElementType == 'container' ) { - element = viewWriter.createContainerElement( viewElementDefinition.name, attributes ); - } else if ( viewElementType == 'attribute' ) { - const options = { - priority: viewElementDefinition.priority || ViewAttributeElement.DEFAULT_PRIORITY - }; - - element = viewWriter.createAttributeElement( viewElementDefinition.name, attributes, options ); - } else { - // 'ui'. - element = viewWriter.createUIElement( viewElementDefinition.name, attributes ); - } - - if ( viewElementDefinition.styles ) { - const keys = Object.keys( viewElementDefinition.styles ); - - for ( const key of keys ) { - viewWriter.setStyle( key, viewElementDefinition.styles[ key ], element ); - } - } - - if ( viewElementDefinition.classes ) { - const classes = viewElementDefinition.classes; - - if ( typeof classes == 'string' ) { - viewWriter.addClass( classes, element ); - } else { - for ( const className of classes ) { - viewWriter.addClass( className, element ); - } - } - } - - return element; -} - -function _getFromAttributeCreator( config ) { - if ( config.model.values ) { - return ( modelAttributeValue, viewWriter ) => { - const view = config.view[ modelAttributeValue ]; - - if ( view ) { - return view( modelAttributeValue, viewWriter ); - } - - return null; - }; - } else { - return config.view; + /** + * Model marker to view element conversion helper. + * + * This conversion results in creating a view element on the boundaries of the converted marker. If the converted marker + * is collapsed, only one element is created. For example, model marker set like this: `F[oo b]ar` + * becomes `

Foo bar

` in the view. + * + * editor.conversion.for( 'downcast' ).markerToElement( { + * model: 'search', + * view: 'marker-search' + * } ); + * + * editor.conversion.for( 'downcast' ).markerToElement( { + * model: 'search', + * view: 'search-result', + * converterPriority: 'high' + * } ); + * + * editor.conversion.for( 'downcast' ).markerToElement( { + * model: 'search', + * view: { + * name: 'span', + * attributes: { + * 'data-marker': 'search' + * } + * } + * } ); + * + * editor.conversion.for( 'downcast' ).markerToElement( { + * model: 'search', + * view: ( markerData, viewWriter ) => { + * return viewWriter.createUIElement( 'span', { + * 'data-marker': 'search', + * 'data-start': markerData.isOpening + * } ); + * } + * } ); + * + * If a function is passed as the `config.view` parameter, it will be used to generate both boundary elements. The function + * receives the `data` object as a parameter and should return an instance of the + * {@link module:engine/view/uielement~UIElement view UI element}. The `data` and `conversionApi` objects are passed from + * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. Additionally, + * the `data.isOpening` parameter is passed, which is set to `true` for the marker start boundary element, and `false` to + * the marker end boundary element. + * + * This kind of conversion is useful for saving data into the database, so it should be used in the data conversion pipeline. + * + * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter + * to the conversion process. + * + * @method #markerToElement + * @param {Object} config Conversion configuration. + * @param {String} config.model The name of the model marker (or model marker group) to convert. + * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function + * that takes the model marker data as a parameter and returns a view UI element. + * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. + * @returns {module:engine/conversion/downcast-converters~DowncastHelpers} + */ + markerToElement( config ) { + return this.add( _downcastMarkerToElement( config ) ); } -} -// Takes the configuration, adds default parameters if they do not exist and normalizes other parameters to be used in downcast converters -// for generating a view attribute. -// -// @param {Object} view View configuration. -function _normalizeToAttributeConfig( view ) { - if ( typeof view == 'string' ) { - return modelAttributeValue => ( { key: view, value: modelAttributeValue } ); - } else if ( typeof view == 'object' ) { - // { key, value, ... } - if ( view.value ) { - return () => view; - } - // { key, ... } - else { - return modelAttributeValue => ( { key: view.key, value: modelAttributeValue } ); - } - } else { - // function. - return view; + /** + * Model marker to highlight conversion helper. + * + * This conversion results in creating a highlight on view nodes. For this kind of conversion, + * {@link module:engine/conversion/downcast-converters~HighlightDescriptor} should be provided. + * + * For text nodes, a `` {@link module:engine/view/attributeelement~AttributeElement} is created and it wraps all text nodes + * in the converted marker range. For example, a model marker set like this: `F[oo b]ar` becomes + * `

Foo bar

` in the view. + * + * {@link module:engine/view/containerelement~ContainerElement} may provide a custom way of handling highlight. Most often, + * the element itself is given classes and attributes described in the highlight descriptor (instead of being wrapped in ``). + * For example, a model marker set like this: `[]` becomes `` + * in the view. + * + * For container elements, the conversion is two-step. While the converter processes the highlight descriptor and passes it + * to a container element, it is the container element instance itself that applies values from the highlight descriptor. + * So, in a sense, the converter takes care of stating what should be applied on what, while the element decides how to apply that. + * + * editor.conversion.for( 'downcast' ).markerToHighlight( { model: 'comment', view: { classes: 'comment' } } ); + * + * editor.conversion.for( 'downcast' ).markerToHighlight( { + * model: 'comment', + * view: { classes: 'new-comment' }, + * converterPriority: 'high' + * } ); + * + * editor.conversion.for( 'downcast' ).markerToHighlight( { + * model: 'comment', + * view: data => { + * // Assuming that the marker name is in a form of comment:commentType. + * const commentType = data.markerName.split( ':' )[ 1 ]; + * + * return { + * classes: [ 'comment', 'comment-' + commentType ] + * }; + * } + * } ); + * + * If a function is passed as the `config.view` parameter, it will be used to generate the highlight descriptor. The function + * receives the `data` object as a parameter and should return a + * {@link module:engine/conversion/downcast-converters~HighlightDescriptor highlight descriptor}. + * The `data` object properties are passed from {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. + * + * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter + * to the conversion process. + * + * @method #markerToHighlight + * @param {Object} config Conversion configuration. + * @param {String} config.model The name of the model marker (or model marker group) to convert. + * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} config.view A highlight descriptor + * that will be used for highlighting or a function that takes the model marker data as a parameter and returns a highlight descriptor. + * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. + * @returns {module:engine/conversion/downcast-converters~DowncastHelpers} + */ + markerToHighlight( config ) { + return this.add( _downcastMarkerToHighlight( config ) ); } } @@ -873,30 +914,6 @@ export function removeHighlight( highlightDescriptor ) { }; } -// Helper function for `highlight`. Prepares the actual descriptor object using value passed to the converter. -function _prepareDescriptor( highlightDescriptor, data, conversionApi ) { - // If passed descriptor is a creator function, call it. If not, just use passed value. - const descriptor = typeof highlightDescriptor == 'function' ? - highlightDescriptor( data, conversionApi ) : - highlightDescriptor; - - if ( !descriptor ) { - return null; - } - - // Apply default descriptor priority. - if ( !descriptor.priority ) { - descriptor.priority = 10; - } - - // Default descriptor id is marker name. - if ( !descriptor.id ) { - descriptor.id = data.markerName; - } - - return descriptor; -} - /** * Creates a `` {@link module:engine/view/attributeelement~AttributeElement view attribute element} from the information * provided by the {@link module:engine/conversion/downcast-converters~HighlightDescriptor highlight descriptor} object. If a priority @@ -921,6 +938,270 @@ export function createViewElementFromHighlightDescriptor( descriptor ) { return viewElement; } +// Model element to view element conversion helper. +// +// See {@link ~DowncastHelpers#elementToElement `.elementToElement()` downcast helper} for examples. +// +// @param {Object} config Conversion configuration. +// @param {String} config.model The name of the model element to convert. +// @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function +// that takes the model element and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer} +// as parameters and returns a view container element. +// @returns {Function} Conversion helper. +function _downcastElementToElement( config ) { + config = cloneDeep( config ); + + config.view = _normalizeToElementConfig( config.view, 'container' ); + + return dispatcher => { + dispatcher.on( 'insert:' + config.model, insertElement( config.view ), { priority: config.converterPriority || 'normal' } ); + }; +} + +// Model attribute to view element conversion helper. +// +// See {@link ~DowncastHelpers#attributeToElement `.attributeToElement()` downcast helper} for examples. +// +// @param {Object} config Conversion configuration. +// @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array +// of `String`s with possible values if the model attribute is an enumerable. +// @param {module:engine/view/elementdefinition~ElementDefinition|Function|Object} config.view A view element definition or a function +// that takes the model attribute value and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer} +// as parameters and returns a view attribute element. If `config.model.values` is +// given, `config.view` should be an object assigning values from `config.model.values` to view element definitions or functions. +// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. +// @returns {Function} Conversion helper. +function _downcastAttributeToElement( config ) { + config = cloneDeep( config ); + + const modelKey = config.model.key ? config.model.key : config.model; + let eventName = 'attribute:' + modelKey; + + if ( config.model.name ) { + eventName += ':' + config.model.name; + } + + if ( config.model.values ) { + for ( const modelValue of config.model.values ) { + config.view[ modelValue ] = _normalizeToElementConfig( config.view[ modelValue ], 'attribute' ); + } + } else { + config.view = _normalizeToElementConfig( config.view, 'attribute' ); + } + + const elementCreator = _getFromAttributeCreator( config ); + + return dispatcher => { + dispatcher.on( eventName, wrap( elementCreator ), { priority: config.converterPriority || 'normal' } ); + }; +} + +// Model attribute to view attribute conversion helper. +// +// See {@link ~DowncastHelpers#attributeToAttribute `.attributeToAttribute()` downcast helper} for examples. +// +// @param {Object} config Conversion configuration. +// @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values, [ name ] }` object describing +// the attribute key, possible values and, optionally, an element name to convert from. +// @param {String|Object|Function} config.view A view attribute key, or a `{ key, value }` object or a function that takes +// the model attribute value and returns a `{ key, value }` object. If `key` is `'class'`, `value` can be a `String` or an +// array of `String`s. If `key` is `'style'`, `value` is an object with key-value pairs. In other cases, `value` is a `String`. +// If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to +// `{ key, value }` objects or a functions. +// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. +// @returns {Function} Conversion helper. +function _downcastAttributeToAttribute( config ) { + config = cloneDeep( config ); + + const modelKey = config.model.key ? config.model.key : config.model; + let eventName = 'attribute:' + modelKey; + + if ( config.model.name ) { + eventName += ':' + config.model.name; + } + + if ( config.model.values ) { + for ( const modelValue of config.model.values ) { + config.view[ modelValue ] = _normalizeToAttributeConfig( config.view[ modelValue ] ); + } + } else { + config.view = _normalizeToAttributeConfig( config.view ); + } + + const elementCreator = _getFromAttributeCreator( config ); + + return dispatcher => { + dispatcher.on( eventName, changeAttribute( elementCreator ), { priority: config.converterPriority || 'normal' } ); + }; +} + +// Model marker to view element conversion helper. +// +// See {@link ~DowncastHelpers#markerToElement `.markerToElement()` downcast helper} for examples. +// +// @param {Object} config Conversion configuration. +// @param {String} config.model The name of the model marker (or model marker group) to convert. +// @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function +// that takes the model marker data as a parameter and returns a view UI element. +// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. +// @returns {Function} Conversion helper. +function _downcastMarkerToElement( config ) { + config = cloneDeep( config ); + + config.view = _normalizeToElementConfig( config.view, 'ui' ); + + return dispatcher => { + dispatcher.on( 'addMarker:' + config.model, insertUIElement( config.view ), { priority: config.converterPriority || 'normal' } ); + dispatcher.on( 'removeMarker:' + config.model, removeUIElement( config.view ), { priority: config.converterPriority || 'normal' } ); + }; +} + +// Model marker to highlight conversion helper. +// +// See {@link ~DowncastHelpers#markerToElement `.markerToElement()` downcast helper} for examples. +// +// @param {Object} config Conversion configuration. +// @param {String} config.model The name of the model marker (or model marker group) to convert. +// @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} config.view A highlight descriptor +// that will be used for highlighting or a function that takes the model marker data as a parameter and returns a highlight descriptor. +// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. +// @returns {Function} Conversion helper. +function _downcastMarkerToHighlight( config ) { + return dispatcher => { + dispatcher.on( 'addMarker:' + config.model, highlightText( config.view ), { priority: config.converterPriority || 'normal' } ); + dispatcher.on( 'addMarker:' + config.model, highlightElement( config.view ), { priority: config.converterPriority || 'normal' } ); + dispatcher.on( 'removeMarker:' + config.model, removeHighlight( config.view ), { priority: config.converterPriority || 'normal' } ); + }; +} + +// Takes `config.view`, and if it is an {@link module:engine/view/elementdefinition~ElementDefinition}, converts it +// to a function (because lower level converters accept only element creator functions). +// +// @param {module:engine/view/elementdefinition~ElementDefinition|Function} view View configuration. +// @param {'container'|'attribute'|'ui'} viewElementType View element type to create. +// @returns {Function} Element creator function to use in lower level converters. +function _normalizeToElementConfig( view, viewElementType ) { + if ( typeof view == 'function' ) { + // If `view` is already a function, don't do anything. + return view; + } + + return ( modelData, viewWriter ) => _createViewElementFromDefinition( view, viewWriter, viewElementType ); +} + +// Creates a view element instance from the provided {@link module:engine/view/elementdefinition~ElementDefinition} and class. +// +// @param {module:engine/view/elementdefinition~ElementDefinition} viewElementDefinition +// @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter +// @param {'container'|'attribute'|'ui'} viewElementType +// @returns {module:engine/view/element~Element} +function _createViewElementFromDefinition( viewElementDefinition, viewWriter, viewElementType ) { + if ( typeof viewElementDefinition == 'string' ) { + // If `viewElementDefinition` is given as a `String`, normalize it to an object with `name` property. + viewElementDefinition = { name: viewElementDefinition }; + } + + let element; + const attributes = Object.assign( {}, viewElementDefinition.attributes ); + + if ( viewElementType == 'container' ) { + element = viewWriter.createContainerElement( viewElementDefinition.name, attributes ); + } else if ( viewElementType == 'attribute' ) { + const options = { + priority: viewElementDefinition.priority || ViewAttributeElement.DEFAULT_PRIORITY + }; + + element = viewWriter.createAttributeElement( viewElementDefinition.name, attributes, options ); + } else { + // 'ui'. + element = viewWriter.createUIElement( viewElementDefinition.name, attributes ); + } + + if ( viewElementDefinition.styles ) { + const keys = Object.keys( viewElementDefinition.styles ); + + for ( const key of keys ) { + viewWriter.setStyle( key, viewElementDefinition.styles[ key ], element ); + } + } + + if ( viewElementDefinition.classes ) { + const classes = viewElementDefinition.classes; + + if ( typeof classes == 'string' ) { + viewWriter.addClass( classes, element ); + } else { + for ( const className of classes ) { + viewWriter.addClass( className, element ); + } + } + } + + return element; +} + +function _getFromAttributeCreator( config ) { + if ( config.model.values ) { + return ( modelAttributeValue, viewWriter ) => { + const view = config.view[ modelAttributeValue ]; + + if ( view ) { + return view( modelAttributeValue, viewWriter ); + } + + return null; + }; + } else { + return config.view; + } +} + +// Takes the configuration, adds default parameters if they do not exist and normalizes other parameters to be used in downcast converters +// for generating a view attribute. +// +// @param {Object} view View configuration. +function _normalizeToAttributeConfig( view ) { + if ( typeof view == 'string' ) { + return modelAttributeValue => ( { key: view, value: modelAttributeValue } ); + } else if ( typeof view == 'object' ) { + // { key, value, ... } + if ( view.value ) { + return () => view; + } + // { key, ... } + else { + return modelAttributeValue => ( { key: view.key, value: modelAttributeValue } ); + } + } else { + // function. + return view; + } +} + +// Helper function for `highlight`. Prepares the actual descriptor object using value passed to the converter. +function _prepareDescriptor( highlightDescriptor, data, conversionApi ) { + // If passed descriptor is a creator function, call it. If not, just use passed value. + const descriptor = typeof highlightDescriptor == 'function' ? + highlightDescriptor( data, conversionApi ) : + highlightDescriptor; + + if ( !descriptor ) { + return null; + } + + // Apply default descriptor priority. + if ( !descriptor.priority ) { + descriptor.priority = 10; + } + + // Default descriptor id is marker name. + if ( !descriptor.id ) { + descriptor.id = data.markerName; + } + + return descriptor; +} + /** * An object describing how the marker highlight should be represented in the view. * @@ -955,329 +1236,3 @@ export function createViewElementFromHighlightDescriptor( descriptor ) { * attribute element. If the descriptor is applied to an element, usually these attributes will be set on that element, however, * this depends on how the element converts the descriptor. */ - -/** - * Downcast conversion helper functions. - * - * @interface module:engine/conversion/downcast-converters~DowncastHelpers - * @extends module:engine/conversion/conversion~ConversionHelpers - */ -export const downcastHelpers = { - /** - * Model element to view element conversion helper. - * - * This conversion results in creating a view element. For example, model `Foo` becomes `

Foo

` in the view. - * - * editor.conversion.for( 'downcast' ).elementToElement( { - * model: 'paragraph', - * view: 'p' - * } ); - * - * editor.conversion.for( 'downcast' ).elementToElement( { - * model: 'paragraph', - * view: 'div', - * converterPriority: 'high' - * } ); - * - * editor.conversion.for( 'downcast' ).elementToElement( { - * model: 'fancyParagraph', - * view: { - * name: 'p', - * classes: 'fancy' - * } - * } ); - * - * editor.conversion.for( 'downcast' ).elementToElement( { - * model: 'heading', - * view: ( modelElement, viewWriter ) => { - * return viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) ) - * } - * } ); - * - * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter - * to the conversion process. - * - * @method #elementToElement - * @param {Object} config Conversion configuration. - * @param {String} config.model The name of the model element to convert. - * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function - * that takes the model element and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer} - * as parameters and returns a view container element. - * @returns {module:engine/conversion/downcast-converters~DowncastHelpers} - */ - elementToElement( config ) { - return this.add( _downcastElementToElement( config ) ); - }, - - /** - * Model attribute to view element conversion helper. - * - * This conversion results in wrapping view nodes with a view attribute element. For example, a model text node with - * `"Foo"` as data and the `bold` attribute becomes `Foo` in the view. - * - * editor.conversion.for( 'downcast' ).attributeToElement( { - * model: 'bold', - * view: 'strong' - * } ); - * - * editor.conversion.for( 'downcast' ).attributeToElement( { - * model: 'bold', - * view: 'b', - * converterPriority: 'high' - * } ); - * - * editor.conversion.for( 'downcast' ).attributeToElement( { - * model: 'invert', - * view: { - * name: 'span', - * classes: [ 'font-light', 'bg-dark' ] - * } - * } ); - * - * editor.conversion.for( 'downcast' ).attributeToElement( { - * model: { - * key: 'fontSize', - * values: [ 'big', 'small' ] - * }, - * view: { - * big: { - * name: 'span', - * styles: { - * 'font-size': '1.2em' - * } - * }, - * small: { - * name: 'span', - * styles: { - * 'font-size': '0.8em' - * } - * } - * } - * } ); - * - * editor.conversion.for( 'downcast' ).attributeToElement( { - * model: 'bold', - * view: ( modelAttributeValue, viewWriter ) => { - * return viewWriter.createAttributeElement( 'span', { - * style: 'font-weight:' + modelAttributeValue - * } ); - * } - * } ); - * - * editor.conversion.for( 'downcast' ).attributeToElement( { - * model: { - * key: 'color', - * name: '$text' - * }, - * view: ( modelAttributeValue, viewWriter ) => { - * return viewWriter.createAttributeElement( 'span', { - * style: 'color:' + modelAttributeValue - * } ); - * } - * } ); - * - * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter - * to the conversion process. - * - * @method #attributeToElement - * @param {Object} config Conversion configuration. - * @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array - * of `String`s with possible values if the model attribute is an enumerable. - * @param {module:engine/view/elementdefinition~ElementDefinition|Function|Object} config.view A view element definition or a function - * that takes the model attribute value and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer} - * as parameters and returns a view attribute element. If `config.model.values` is - * given, `config.view` should be an object assigning values from `config.model.values` to view element definitions or functions. - * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {module:engine/conversion/downcast-converters~DowncastHelpers} - */ - attributeToElement( config ) { - return this.add( _downcastAttributeToElement( config ) ); - }, - - /** - * Model attribute to view attribute conversion helper. - * - * This conversion results in adding an attribute to a view node, basing on an attribute from a model node. For example, - * `` is converted to ``. - * - * editor.conversion.for( 'downcast' ).attributeToAttribute( { - * model: 'source', - * view: 'src' - * } ); - * - * editor.conversion.for( 'downcast' ).attributeToAttribute( { - * model: 'source', - * view: 'href', - * converterPriority: 'high' - * } ); - * - * editor.conversion.for( 'downcast' ).attributeToAttribute( { - * model: { - * name: 'image', - * key: 'source' - * }, - * view: 'src' - * } ); - * - * editor.conversion.for( 'downcast' ).attributeToAttribute( { - * model: { - * name: 'styled', - * values: [ 'dark', 'light' ] - * }, - * view: { - * dark: { - * key: 'class', - * value: [ 'styled', 'styled-dark' ] - * }, - * light: { - * key: 'class', - * value: [ 'styled', 'styled-light' ] - * } - * } - * } ); - * - * editor.conversion.for( 'downcast' ).attributeToAttribute( { - * model: 'styled', - * view: modelAttributeValue => ( { key: 'class', value: 'styled-' + modelAttributeValue } ) - * } ); - * - * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter - * to the conversion process. - * - * @method #attributeToAttribute - * @param {Object} config Conversion configuration. - * @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values, [ name ] }` object describing - * the attribute key, possible values and, optionally, an element name to convert from. - * @param {String|Object|Function} config.view A view attribute key, or a `{ key, value }` object or a function that takes - * the model attribute value and returns a `{ key, value }` object. If `key` is `'class'`, `value` can be a `String` or an - * array of `String`s. If `key` is `'style'`, `value` is an object with key-value pairs. In other cases, `value` is a `String`. - * If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to - * `{ key, value }` objects or a functions. - * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {module:engine/conversion/downcast-converters~DowncastHelpers} - */ - attributeToAttribute( config ) { - return this.add( _downcastAttributeToAttribute( config ) ); - }, - - /** - * Model marker to view element conversion helper. - * - * This conversion results in creating a view element on the boundaries of the converted marker. If the converted marker - * is collapsed, only one element is created. For example, model marker set like this: `F[oo b]ar` - * becomes `

Foo bar

` in the view. - * - * editor.conversion.for( 'downcast' ).markerToElement( { - * model: 'search', - * view: 'marker-search' - * } ); - * - * editor.conversion.for( 'downcast' ).markerToElement( { - * model: 'search', - * view: 'search-result', - * converterPriority: 'high' - * } ); - * - * editor.conversion.for( 'downcast' ).markerToElement( { - * model: 'search', - * view: { - * name: 'span', - * attributes: { - * 'data-marker': 'search' - * } - * } - * } ); - * - * editor.conversion.for( 'downcast' ).markerToElement( { - * model: 'search', - * view: ( markerData, viewWriter ) => { - * return viewWriter.createUIElement( 'span', { - * 'data-marker': 'search', - * 'data-start': markerData.isOpening - * } ); - * } - * } ); - * - * If a function is passed as the `config.view` parameter, it will be used to generate both boundary elements. The function - * receives the `data` object as a parameter and should return an instance of the - * {@link module:engine/view/uielement~UIElement view UI element}. The `data` and `conversionApi` objects are passed from - * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. Additionally, - * the `data.isOpening` parameter is passed, which is set to `true` for the marker start boundary element, and `false` to - * the marker end boundary element. - * - * This kind of conversion is useful for saving data into the database, so it should be used in the data conversion pipeline. - * - * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter - * to the conversion process. - * - * @method #markerToElement - * @param {Object} config Conversion configuration. - * @param {String} config.model The name of the model marker (or model marker group) to convert. - * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function - * that takes the model marker data as a parameter and returns a view UI element. - * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {module:engine/conversion/downcast-converters~DowncastHelpers} - */ - markerToElement( config ) { - return this.add( _downcastMarkerToElement( config ) ); - }, - - /** - * Model marker to highlight conversion helper. - * - * This conversion results in creating a highlight on view nodes. For this kind of conversion, - * {@link module:engine/conversion/downcast-converters~HighlightDescriptor} should be provided. - * - * For text nodes, a `` {@link module:engine/view/attributeelement~AttributeElement} is created and it wraps all text nodes - * in the converted marker range. For example, a model marker set like this: `F[oo b]ar` becomes - * `

Foo bar

` in the view. - * - * {@link module:engine/view/containerelement~ContainerElement} may provide a custom way of handling highlight. Most often, - * the element itself is given classes and attributes described in the highlight descriptor (instead of being wrapped in ``). - * For example, a model marker set like this: `[]` becomes `` - * in the view. - * - * For container elements, the conversion is two-step. While the converter processes the highlight descriptor and passes it - * to a container element, it is the container element instance itself that applies values from the highlight descriptor. - * So, in a sense, the converter takes care of stating what should be applied on what, while the element decides how to apply that. - * - * editor.conversion.for( 'downcast' ).markerToHighlight( { model: 'comment', view: { classes: 'comment' } } ); - * - * editor.conversion.for( 'downcast' ).markerToHighlight( { - * model: 'comment', - * view: { classes: 'new-comment' }, - * converterPriority: 'high' - * } ); - * - * editor.conversion.for( 'downcast' ).markerToHighlight( { - * model: 'comment', - * view: data => { - * // Assuming that the marker name is in a form of comment:commentType. - * const commentType = data.markerName.split( ':' )[ 1 ]; - * - * return { - * classes: [ 'comment', 'comment-' + commentType ] - * }; - * } - * } ); - * - * If a function is passed as the `config.view` parameter, it will be used to generate the highlight descriptor. The function - * receives the `data` object as a parameter and should return a - * {@link module:engine/conversion/downcast-converters~HighlightDescriptor highlight descriptor}. - * The `data` object properties are passed from {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. - * - * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter - * to the conversion process. - * - * @method #markerToHighlight - * @param {Object} config Conversion configuration. - * @param {String} config.model The name of the model marker (or model marker group) to convert. - * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} config.view A highlight descriptor - * that will be used for highlighting or a function that takes the model marker data as a parameter and returns a highlight descriptor. - * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {module:engine/conversion/downcast-converters~DowncastHelpers} - */ - markerToHighlight( config ) { - return this.add( _downcastMarkerToHighlight( config ) ); - } -}; diff --git a/src/conversion/upcast-converters.js b/src/conversion/upcast-converters.js index fb5391b77..cecbabc97 100644 --- a/src/conversion/upcast-converters.js +++ b/src/conversion/upcast-converters.js @@ -4,8 +4,8 @@ */ import Matcher from '../view/matcher'; - import ModelRange from '../model/range'; +import { ConversionHelpers } from './conversion'; import { cloneDeep } from 'lodash-es'; @@ -17,267 +17,571 @@ import { cloneDeep } from 'lodash-es'; */ /** - * View element to model element conversion helper. - * - * editor.conversion.for( 'upcast' ) - * .add( _upcastElementToElement( { - * view: 'p', - * model: 'paragraph' - * } ) ); - * - * The method is publicly available as {@link ~UpcastHelpers#elementToElement `.elementToElement()` upcast helper}. - * - * @param {Object} config Conversion configuration. - * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted. - * @param {String|module:engine/model/element~Element|Function} config.model Name of the model element, a model element - * instance or a function that takes a view element and returns a model element. The model element will be inserted in the model. - * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {Function} Conversion helper. - */ -export function _upcastElementToElement( config ) { - config = cloneDeep( config ); - - const converter = _prepareToElementConverter( config ); - - const elementName = _getViewElementNameFromConfig( config ); - const eventName = elementName ? 'element:' + elementName : 'element'; - - return dispatcher => { - dispatcher.on( eventName, converter, { priority: config.converterPriority || 'normal' } ); - }; -} - -/** - * View element to model attribute conversion helper. - * - * editor.conversion.for( 'upcast' ) - * .add( _upcastElementToAttribute( { - * view: 'strong', - * model: 'bold' - * } ) ); - * - * The method is publicly available as {@link ~UpcastHelpers#elementToAttribute `.elementToAttribute()` upcast helper}. - * - * @param {Object} config Conversion configuration. - * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted. - * @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing - * the model attribute. `value` property may be set as a function that takes a view element and returns the value. - * If `String` is given, the model attribute value will be set to `true`. - * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {Function} Conversion helper. - */ -export function _upcastElementToAttribute( config ) { - config = cloneDeep( config ); - - _normalizeModelAttributeConfig( config ); - - const converter = _prepareToAttributeConverter( config, false ); - - const elementName = _getViewElementNameFromConfig( config ); - const eventName = elementName ? 'element:' + elementName : 'element'; - - return dispatcher => { - dispatcher.on( eventName, converter, { priority: config.converterPriority || 'low' } ); - }; -} - -/** - * View attribute to model attribute conversion helper. - * - * editor.conversion.for( 'upcast' ) - * .add( _upcastAttributeToAttribute( { - * view: 'src', - * model: 'source' - * } ) ); - * - * The method is publicly available as {@link ~UpcastHelpers#attributeToAttribute `.attributeToAttribute()` upcast helper}. - * - * @param {Object} config Conversion configuration. - * @param {String|Object} config.view Specifies which view attribute will be converted. If a `String` is passed, - * attributes with given key will be converted. If an `Object` is passed, it must have a required `key` property, - * specifying view attribute key, and may have an optional `value` property, specifying view attribute value and optional `name` - * property specifying a view element name from/on which the attribute should be converted. `value` can be given as a `String`, - * a `RegExp` or a function callback, that takes view attribute value as the only parameter and returns `Boolean`. - * @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing - * the model attribute. `value` property may be set as a function that takes a view element and returns the value. - * If `String` is given, the model attribute value will be same as view attribute value. - * @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority. - * @returns {Function} Conversion helper. - */ -export function _upcastAttributeToAttribute( config ) { - config = cloneDeep( config ); - - let viewKey = null; - - if ( typeof config.view == 'string' || config.view.key ) { - viewKey = _normalizeViewAttributeKeyValueConfig( config ); - } - - _normalizeModelAttributeConfig( config, viewKey ); - - const converter = _prepareToAttributeConverter( config, true ); - - return dispatcher => { - dispatcher.on( 'element', converter, { priority: config.converterPriority || 'low' } ); - }; -} - -/** - * View element to model marker conversion helper. - * - * editor.conversion.for( 'upcast' ) - * .add( _upcastElementToMarker( { - * view: 'marker-search', - * model: 'search' - * } ) ); - * - * The method is publicly available as {@link ~UpcastHelpers#elementToMarker `.elementToMarker()` upcast helper}. + * Upcast conversion helper functions. * - * @param {Object} config Conversion configuration. - * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted. - * @param {String|Function} config.model Name of the model marker, or a function that takes a view element and returns - * a model marker name. - * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {Function} Conversion helper. + * @extends module:engine/conversion/conversion~ConversionHelpers */ -export function _upcastElementToMarker( config ) { - config = cloneDeep( config ); - - _normalizeToMarkerConfig( config ); - - return _upcastElementToElement( config ); -} - -// Helper function for from-view-element conversion. Checks if `config.view` directly specifies converted view element's name -// and if so, returns it. -// -// @param {Object} config Conversion config. -// @returns {String|null} View element name or `null` if name is not directly set. -function _getViewElementNameFromConfig( config ) { - if ( typeof config.view == 'string' ) { - return config.view; +export default class UpcastHelpers extends ConversionHelpers { + /** + * View element to model element conversion helper. + * + * This conversion results in creating a model element. For example, + * view `

Foo

` becomes `Foo` in the model. + * + * Keep in mind that the element will be inserted only if it is allowed + * by {@link module:engine/model/schema~Schema schema} configuration. + * + * editor.conversion.for( 'upcast' ).elementToElement( { + * view: 'p', + * model: 'paragraph' + * } ); + * + * editor.conversion.for( 'upcast' ).elementToElement( { + * view: 'p', + * model: 'paragraph', + * converterPriority: 'high' + * } ); + * + * editor.conversion.for( 'upcast' ).elementToElement( { + * view: { + * name: 'p', + * classes: 'fancy' + * }, + * model: 'fancyParagraph' + * } ); + * + * editor.conversion.for( 'upcast' ).elementToElement( { + * view: { + * name: 'p', + * classes: 'heading' + * }, + * model: ( viewElement, modelWriter ) => { + * return modelWriter.createElement( 'heading', { level: viewElement.getAttribute( 'data-level' ) } ); + * } + * } ); + * + * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter + * to the conversion process. + * + * @method #elementToElement + * @param {Object} config Conversion configuration. + * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted. + * @param {String|module:engine/model/element~Element|Function} config.model Name of the model element, a model element + * instance or a function that takes a view element and returns a model element. The model element will be inserted in the model. + * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. + * @returns {module:engine/conversion/upcast-converters~UpcastHelpers} + */ + elementToElement( config ) { + return this.add( _upcastElementToElement( config ) ); } - if ( typeof config.view == 'object' && typeof config.view.name == 'string' ) { - return config.view.name; + /** + * View element to model attribute conversion helper. + * + * This conversion results in setting an attribute on a model node. For example, view `Foo` becomes + * `Foo` {@link module:engine/model/text~Text model text node} with `bold` attribute set to `true`. + * + * This helper is meant to set a model attribute on all the elements that are inside the converted element: + * + * Foo -->

Foo

--> <$text bold="true">Foo + * + * Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step). + * Even though `` is over `

` element, `bold="true"` was added to the text. See + * {@link module:engine/conversion/upcast-converters~UpcastHelpers#attributeToAttribute} for comparison. + * + * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration. + * + * editor.conversion.for( 'upcast' ).elementToAttribute( { + * view: 'strong', + * model: 'bold' + * } ); + * + * editor.conversion.for( 'upcast' ).elementToAttribute( { + * view: 'strong', + * model: 'bold', + * converterPriority: 'high' + * } ); + * + * editor.conversion.for( 'upcast' ).elementToAttribute( { + * view: { + * name: 'span', + * classes: 'bold' + * }, + * model: 'bold' + * } ); + * + * editor.conversion.for( 'upcast' ).elementToAttribute( { + * view: { + * name: 'span', + * classes: [ 'styled', 'styled-dark' ] + * }, + * model: { + * key: 'styled', + * value: 'dark' + * } + * } ); + * + * editor.conversion.for( 'upcast' ).elementToAttribute( { + * view: { + * name: 'span', + * styles: { + * 'font-size': /[\s\S]+/ + * } + * }, + * model: { + * key: 'fontSize', + * value: viewElement => { + * const fontSize = viewElement.getStyle( 'font-size' ); + * const value = fontSize.substr( 0, fontSize.length - 2 ); + * + * if ( value <= 10 ) { + * return 'small'; + * } else if ( value > 12 ) { + * return 'big'; + * } + * + * return null; + * } + * } + * } ); + * + * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter + * to the conversion process. + * + * @method #elementToAttribute + * @param {Object} config Conversion configuration. + * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted. + * @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing + * the model attribute. `value` property may be set as a function that takes a view element and returns the value. + * If `String` is given, the model attribute value will be set to `true`. + * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. + * @returns {module:engine/conversion/upcast-converters~UpcastHelpers} + */ + elementToAttribute( config ) { + return this.add( _upcastElementToAttribute( config ) ); } - return null; -} - -// Helper for to-model-element conversion. Takes a config object and returns a proper converter function. -// -// @param {Object} config Conversion configuration. -// @returns {Function} View to model converter. -function _prepareToElementConverter( config ) { - const matcher = new Matcher( config.view ); - - return ( evt, data, conversionApi ) => { - // This will be usually just one pattern but we support matchers with many patterns too. - const match = matcher.match( data.viewItem ); - - // If there is no match, this callback should not do anything. - if ( !match ) { - return; - } - - // Force consuming element's name. - match.match.name = true; - - // Create model element basing on config. - const modelElement = _getModelElement( config.model, data.viewItem, conversionApi.writer ); - - // Do not convert if element building function returned falsy value. - if ( !modelElement ) { - return; - } - - // When element was already consumed then skip it. - if ( !conversionApi.consumable.test( data.viewItem, match.match ) ) { - return; - } - - // Find allowed parent for element that we are going to insert. - // If current parent does not allow to insert element but one of the ancestors does - // then split nodes to allowed parent. - const splitResult = conversionApi.splitToAllowedParent( modelElement, data.modelCursor ); - - // When there is no split result it means that we can't insert element to model tree, so let's skip it. - if ( !splitResult ) { - return; - } + /** + * View attribute to model attribute conversion helper. + * + * This conversion results in setting an attribute on a model node. For example, view `` becomes + * `` in the model. + * + * This helper is meant to convert view attributes from view elements which got converted to the model, so the view attribute + * is set only on the corresponding model node: + * + *

foo
-->
foo
+ * + * Above, `class="dark"` attribute is added only to the `
` elements that has it. This is in contrary to + * {@link module:engine/conversion/upcast-converters~UpcastHelpers#elementToAttribute} which sets attributes for + * all the children in the model: + * + * Foo -->

Foo

--> <$text bold="true">Foo + * + * Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step). + * Even though `` is over `

` element, `bold="true"` was added to the text. + * + * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration. + * + * editor.conversion.for( 'upcast' ).attributeToAttribute( { + * view: 'src', + * model: 'source' + * } ); + * + * editor.conversion.for( 'upcast' ).attributeToAttribute( { + * view: { key: 'src' }, + * model: 'source' + * } ); + * + * editor.conversion.for( 'upcast' ).attributeToAttribute( { + * view: { key: 'src' }, + * model: 'source', + * converterPriority: 'normal' + * } ); + * + * editor.conversion.for( 'upcast' ).attributeToAttribute( { + * view: { + * key: 'data-style', + * value: /[\s\S]+/ + * }, + * model: 'styled' + * } ); + * + * editor.conversion.for( 'upcast' ).attributeToAttribute( { + * view: { + * name: 'img', + * key: 'class', + * value: 'styled-dark' + * }, + * model: { + * key: 'styled', + * value: 'dark' + * } + * } ); + * + * editor.conversion.for( 'upcast' ).attributeToAttribute( { + * view: { + * key: 'class', + * value: /styled-[\S]+/ + * }, + * model: { + * key: 'styled' + * value: viewElement => { + * const regexp = /styled-([\S]+)/; + * const match = viewElement.getAttribute( 'class' ).match( regexp ); + * + * return match[ 1 ]; + * } + * } + * } ); + * + * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter + * to the conversion process. + * + * @method #attributeToAttribute + * @param {Object} config Conversion configuration. + * @param {String|Object} config.view Specifies which view attribute will be converted. If a `String` is passed, + * attributes with given key will be converted. If an `Object` is passed, it must have a required `key` property, + * specifying view attribute key, and may have an optional `value` property, specifying view attribute value and optional `name` + * property specifying a view element name from/on which the attribute should be converted. `value` can be given as a `String`, + * a `RegExp` or a function callback, that takes view attribute value as the only parameter and returns `Boolean`. + * @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing + * the model attribute. `value` property may be set as a function that takes a view element and returns the value. + * If `String` is given, the model attribute value will be same as view attribute value. + * @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority. + * @returns {module:engine/conversion/upcast-converters~UpcastHelpers} + */ + attributeToAttribute( config ) { + return this.add( _upcastAttributeToAttribute( config ) ); + } - // Insert element on allowed position. - conversionApi.writer.insert( modelElement, splitResult.position ); + /** + * View element to model marker conversion helper. + * + * This conversion results in creating a model marker. For example, if the marker was stored in a view as an element: + * `

Foo

Bar

`, + * after the conversion is done, the marker will be available in + * {@link module:engine/model/model~Model#markers model document markers}. + * + * editor.conversion.for( 'upcast' ).elementToMarker( { + * view: 'marker-search', + * model: 'search' + * } ); + * + * editor.conversion.for( 'upcast' ).elementToMarker( { + * view: 'marker-search', + * model: 'search', + * converterPriority: 'high' + * } ); + * + * editor.conversion.for( 'upcast' ).elementToMarker( { + * view: 'marker-search', + * model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' ) + * } ); + * + * editor.conversion.for( 'upcast' ).elementToMarker( { + * view: { + * name: 'span', + * attributes: { + * 'data-marker': 'search' + * } + * }, + * model: 'search' + * } ); + * + * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter + * to the conversion process. + * + * @method #elementToMarker + * @param {Object} config Conversion configuration. + * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted. + * @param {String|Function} config.model Name of the model marker, or a function that takes a view element and returns + * a model marker name. + * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. + * @returns {module:engine/conversion/upcast-converters~UpcastHelpers} + */ + elementToMarker( config ) { + return this.add( _upcastElementToMarker( config ) ); + } +} - // Convert children and insert to element. - const childrenResult = conversionApi.convertChildren( data.viewItem, conversionApi.writer.createPositionAt( modelElement, 0 ) ); +/** + * Function factory, creates a converter that converts {@link module:engine/view/documentfragment~DocumentFragment view document fragment} + * or all children of {@link module:engine/view/element~Element} into + * {@link module:engine/model/documentfragment~DocumentFragment model document fragment}. + * This is the "entry-point" converter for upcast (view to model conversion). This converter starts the conversion of all children + * of passed view document fragment. Those children {@link module:engine/view/node~Node view nodes} are then handled by other converters. + * + * This also a "default", last resort converter for all view elements that has not been converted by other converters. + * When a view element is being converted to the model but it does not have converter specified, that view element + * will be converted to {@link module:engine/model/documentfragment~DocumentFragment model document fragment} and returned. + * + * @returns {Function} Universal converter for view {@link module:engine/view/documentfragment~DocumentFragment fragments} and + * {@link module:engine/view/element~Element elements} that returns + * {@link module:engine/model/documentfragment~DocumentFragment model fragment} with children of converted view item. + */ +export function convertToModelFragment() { + return ( evt, data, conversionApi ) => { + // Second argument in `consumable.consume` is discarded for ViewDocumentFragment but is needed for ViewElement. + if ( !data.modelRange && conversionApi.consumable.consume( data.viewItem, { name: true } ) ) { + const { modelRange, modelCursor } = conversionApi.convertChildren( data.viewItem, data.modelCursor ); - // Consume appropriate value from consumable values list. - conversionApi.consumable.consume( data.viewItem, match.match ); + data.modelRange = modelRange; + data.modelCursor = modelCursor; + } + }; +} - // Set conversion result range. - data.modelRange = new ModelRange( - // Range should start before inserted element - conversionApi.writer.createPositionBefore( modelElement ), - // Should end after but we need to take into consideration that children could split our - // element, so we need to move range after parent of the last converted child. - // before: [] - // after: [] - conversionApi.writer.createPositionAfter( childrenResult.modelCursor.parent ) - ); +/** + * Function factory, creates a converter that converts {@link module:engine/view/text~Text} to {@link module:engine/model/text~Text}. + * + * @returns {Function} {@link module:engine/view/text~Text View text} converter. + */ +export function convertText() { + return ( evt, data, conversionApi ) => { + if ( conversionApi.schema.checkChild( data.modelCursor, '$text' ) ) { + if ( conversionApi.consumable.consume( data.viewItem ) ) { + const text = conversionApi.writer.createText( data.viewItem.data ); - // Now we need to check where the modelCursor should be. - // If we had to split parent to insert our element then we want to continue conversion inside split parent. - // - // before: [] - // after: [] - if ( splitResult.cursorParent ) { - data.modelCursor = conversionApi.writer.createPositionAt( splitResult.cursorParent, 0 ); + conversionApi.writer.insert( text, data.modelCursor ); - // Otherwise just continue after inserted element. - } else { - data.modelCursor = data.modelRange.end; + data.modelRange = ModelRange._createFromPositionAndShift( data.modelCursor, text.offsetSize ); + data.modelCursor = data.modelRange.end; + } } }; } -// Helper function for upcasting-to-element converter. Takes the model configuration, the converted view element -// and a writer instance and returns a model element instance to be inserted in the model. +// View element to model element conversion helper. // -// @param {String|Function|module:engine/model/element~Element} model Model conversion configuration. -// @param {module:engine/view/node~Node} input The converted view node. -// @param {module:engine/model/writer~Writer} writer A writer instance to use to create the model element. -function _getModelElement( model, input, writer ) { - if ( model instanceof Function ) { - return model( input, writer ); - } else { - return writer.createElement( model ); - } +// See {@link ~UpcastHelpers#elementToElement `.elementToElement()` upcast helper} for examples. +// +// @param {Object} config Conversion configuration. +// @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted. +// @param {String|module:engine/model/element~Element|Function} config.model Name of the model element, a model element +// instance or a function that takes a view element and returns a model element. The model element will be inserted in the model. +// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. +// @returns {Function} Conversion helper. +function _upcastElementToElement( config ) { + config = cloneDeep( config ); + + const converter = _prepareToElementConverter( config ); + + const elementName = _getViewElementNameFromConfig( config ); + const eventName = elementName ? 'element:' + elementName : 'element'; + + return dispatcher => { + dispatcher.on( eventName, converter, { priority: config.converterPriority || 'normal' } ); + }; } -// Helper function view-attribute-to-model-attribute helper. Normalizes `config.view` which was set as `String` or -// as an `Object` with `key`, `value` and `name` properties. Normalized `config.view` has is compatible with -// {@link module:engine/view/matcher~MatcherPattern}. +// View element to model attribute conversion helper. // -// @param {Object} config Conversion config. -// @returns {String} Key of the converted view attribute. -function _normalizeViewAttributeKeyValueConfig( config ) { - if ( typeof config.view == 'string' ) { - config.view = { key: config.view }; - } +// See {@link ~UpcastHelpers#elementToAttribute `.elementToAttribute()` upcast helper} for examples. +// +// @param {Object} config Conversion configuration. +// @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted. +// @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing +// the model attribute. `value` property may be set as a function that takes a view element and returns the value. +// If `String` is given, the model attribute value will be set to `true`. +// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. +// @returns {Function} Conversion helper. +function _upcastElementToAttribute( config ) { + config = cloneDeep( config ); - const key = config.view.key; - let normalized; + _normalizeModelAttributeConfig( config ); - if ( key == 'class' || key == 'style' ) { - const keyName = key == 'class' ? 'classes' : 'styles'; + const converter = _prepareToAttributeConverter( config, false ); - normalized = { - [ keyName ]: config.view.value - }; - } else { - const value = typeof config.view.value == 'undefined' ? /[\s\S]*/ : config.view.value; + const elementName = _getViewElementNameFromConfig( config ); + const eventName = elementName ? 'element:' + elementName : 'element'; + + return dispatcher => { + dispatcher.on( eventName, converter, { priority: config.converterPriority || 'low' } ); + }; +} + +// View attribute to model attribute conversion helper. +// +// See {@link ~UpcastHelpers#attributeToAttribute `.attributeToAttribute()` upcast helper} for examples. +// +// @param {Object} config Conversion configuration. +// @param {String|Object} config.view Specifies which view attribute will be converted. If a `String` is passed, +// attributes with given key will be converted. If an `Object` is passed, it must have a required `key` property, +// specifying view attribute key, and may have an optional `value` property, specifying view attribute value and optional `name` +// property specifying a view element name from/on which the attribute should be converted. `value` can be given as a `String`, +// a `RegExp` or a function callback, that takes view attribute value as the only parameter and returns `Boolean`. +// @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing +// the model attribute. `value` property may be set as a function that takes a view element and returns the value. +// If `String` is given, the model attribute value will be same as view attribute value. +// @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority. +// @returns {Function} Conversion helper. +function _upcastAttributeToAttribute( config ) { + config = cloneDeep( config ); + + let viewKey = null; + + if ( typeof config.view == 'string' || config.view.key ) { + viewKey = _normalizeViewAttributeKeyValueConfig( config ); + } + + _normalizeModelAttributeConfig( config, viewKey ); + + const converter = _prepareToAttributeConverter( config, true ); + + return dispatcher => { + dispatcher.on( 'element', converter, { priority: config.converterPriority || 'low' } ); + }; +} + +// View element to model marker conversion helper. +// +// See {@link ~UpcastHelpers#elementToMarker `.elementToMarker()` upcast helper} for examples. +// +// @param {Object} config Conversion configuration. +// @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted. +// @param {String|Function} config.model Name of the model marker, or a function that takes a view element and returns +// a model marker name. +// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. +// @returns {Function} Conversion helper. +function _upcastElementToMarker( config ) { + config = cloneDeep( config ); + + _normalizeToMarkerConfig( config ); + + return _upcastElementToElement( config ); +} + +// Helper function for from-view-element conversion. Checks if `config.view` directly specifies converted view element's name +// and if so, returns it. +// +// @param {Object} config Conversion config. +// @returns {String|null} View element name or `null` if name is not directly set. +function _getViewElementNameFromConfig( config ) { + if ( typeof config.view == 'string' ) { + return config.view; + } + + if ( typeof config.view == 'object' && typeof config.view.name == 'string' ) { + return config.view.name; + } + + return null; +} + +// Helper for to-model-element conversion. Takes a config object and returns a proper converter function. +// +// @param {Object} config Conversion configuration. +// @returns {Function} View to model converter. +function _prepareToElementConverter( config ) { + const matcher = new Matcher( config.view ); + + return ( evt, data, conversionApi ) => { + // This will be usually just one pattern but we support matchers with many patterns too. + const match = matcher.match( data.viewItem ); + + // If there is no match, this callback should not do anything. + if ( !match ) { + return; + } + + // Force consuming element's name. + match.match.name = true; + + // Create model element basing on config. + const modelElement = _getModelElement( config.model, data.viewItem, conversionApi.writer ); + + // Do not convert if element building function returned falsy value. + if ( !modelElement ) { + return; + } + + // When element was already consumed then skip it. + if ( !conversionApi.consumable.test( data.viewItem, match.match ) ) { + return; + } + + // Find allowed parent for element that we are going to insert. + // If current parent does not allow to insert element but one of the ancestors does + // then split nodes to allowed parent. + const splitResult = conversionApi.splitToAllowedParent( modelElement, data.modelCursor ); + + // When there is no split result it means that we can't insert element to model tree, so let's skip it. + if ( !splitResult ) { + return; + } + + // Insert element on allowed position. + conversionApi.writer.insert( modelElement, splitResult.position ); + + // Convert children and insert to element. + const childrenResult = conversionApi.convertChildren( data.viewItem, conversionApi.writer.createPositionAt( modelElement, 0 ) ); + + // Consume appropriate value from consumable values list. + conversionApi.consumable.consume( data.viewItem, match.match ); + + // Set conversion result range. + data.modelRange = new ModelRange( + // Range should start before inserted element + conversionApi.writer.createPositionBefore( modelElement ), + // Should end after but we need to take into consideration that children could split our + // element, so we need to move range after parent of the last converted child. + // before: [] + // after: [] + conversionApi.writer.createPositionAfter( childrenResult.modelCursor.parent ) + ); + + // Now we need to check where the modelCursor should be. + // If we had to split parent to insert our element then we want to continue conversion inside split parent. + // + // before: [] + // after: [] + if ( splitResult.cursorParent ) { + data.modelCursor = conversionApi.writer.createPositionAt( splitResult.cursorParent, 0 ); + + // Otherwise just continue after inserted element. + } else { + data.modelCursor = data.modelRange.end; + } + }; +} + +// Helper function for upcasting-to-element converter. Takes the model configuration, the converted view element +// and a writer instance and returns a model element instance to be inserted in the model. +// +// @param {String|Function|module:engine/model/element~Element} model Model conversion configuration. +// @param {module:engine/view/node~Node} input The converted view node. +// @param {module:engine/model/writer~Writer} writer A writer instance to use to create the model element. +function _getModelElement( model, input, writer ) { + if ( model instanceof Function ) { + return model( input, writer ); + } else { + return writer.createElement( model ); + } +} + +// Helper function view-attribute-to-model-attribute helper. Normalizes `config.view` which was set as `String` or +// as an `Object` with `key`, `value` and `name` properties. Normalized `config.view` has is compatible with +// {@link module:engine/view/matcher~MatcherPattern}. +// +// @param {Object} config Conversion config. +// @returns {String} Key of the converted view attribute. +function _normalizeViewAttributeKeyValueConfig( config ) { + if ( typeof config.view == 'string' ) { + config.view = { key: config.view }; + } + + const key = config.view.key; + let normalized; + + if ( key == 'class' || key == 'style' ) { + const keyName = key == 'class' ? 'classes' : 'styles'; + + normalized = { + [ keyName ]: config.view.value + }; + } else { + const value = typeof config.view.value == 'undefined' ? /[\s\S]*/ : config.view.value; normalized = { attributes: { @@ -414,340 +718,3 @@ function _normalizeToMarkerConfig( config ) { return modelWriter.createElement( '$marker', { 'data-name': markerName } ); }; } - -/** - * Function factory, creates a converter that converts {@link module:engine/view/documentfragment~DocumentFragment view document fragment} - * or all children of {@link module:engine/view/element~Element} into - * {@link module:engine/model/documentfragment~DocumentFragment model document fragment}. - * This is the "entry-point" converter for upcast (view to model conversion). This converter starts the conversion of all children - * of passed view document fragment. Those children {@link module:engine/view/node~Node view nodes} are then handled by other converters. - * - * This also a "default", last resort converter for all view elements that has not been converted by other converters. - * When a view element is being converted to the model but it does not have converter specified, that view element - * will be converted to {@link module:engine/model/documentfragment~DocumentFragment model document fragment} and returned. - * - * @returns {Function} Universal converter for view {@link module:engine/view/documentfragment~DocumentFragment fragments} and - * {@link module:engine/view/element~Element elements} that returns - * {@link module:engine/model/documentfragment~DocumentFragment model fragment} with children of converted view item. - */ -export function convertToModelFragment() { - return ( evt, data, conversionApi ) => { - // Second argument in `consumable.consume` is discarded for ViewDocumentFragment but is needed for ViewElement. - if ( !data.modelRange && conversionApi.consumable.consume( data.viewItem, { name: true } ) ) { - const { modelRange, modelCursor } = conversionApi.convertChildren( data.viewItem, data.modelCursor ); - - data.modelRange = modelRange; - data.modelCursor = modelCursor; - } - }; -} - -/** - * Function factory, creates a converter that converts {@link module:engine/view/text~Text} to {@link module:engine/model/text~Text}. - * - * @returns {Function} {@link module:engine/view/text~Text View text} converter. - */ -export function convertText() { - return ( evt, data, conversionApi ) => { - if ( conversionApi.schema.checkChild( data.modelCursor, '$text' ) ) { - if ( conversionApi.consumable.consume( data.viewItem ) ) { - const text = conversionApi.writer.createText( data.viewItem.data ); - - conversionApi.writer.insert( text, data.modelCursor ); - - data.modelRange = ModelRange._createFromPositionAndShift( data.modelCursor, text.offsetSize ); - data.modelCursor = data.modelRange.end; - } - } - }; -} - -/** - * Upcast conversion helper functions. - * - * @interface module:engine/conversion/upcast-converters~UpcastHelpers - * @extends module:engine/conversion/conversion~ConversionHelpers - */ -export const upcastHelpers = { - /** - * View element to model element conversion helper. - * - * This conversion results in creating a model element. For example, - * view `

Foo

` becomes `Foo` in the model. - * - * Keep in mind that the element will be inserted only if it is allowed - * by {@link module:engine/model/schema~Schema schema} configuration. - * - * editor.conversion.for( 'upcast' ).elementToElement( { - * view: 'p', - * model: 'paragraph' - * } ); - * - * editor.conversion.for( 'upcast' ).elementToElement( { - * view: 'p', - * model: 'paragraph', - * converterPriority: 'high' - * } ); - * - * editor.conversion.for( 'upcast' ).elementToElement( { - * view: { - * name: 'p', - * classes: 'fancy' - * }, - * model: 'fancyParagraph' - * } ); - * - * editor.conversion.for( 'upcast' ).elementToElement( { - * view: { - * name: 'p', - * classes: 'heading' - * }, - * model: ( viewElement, modelWriter ) => { - * return modelWriter.createElement( 'heading', { level: viewElement.getAttribute( 'data-level' ) } ); - * } - * } ); - * - * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter - * to the conversion process. - * - * @method #elementToElement - * @param {Object} config Conversion configuration. - * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted. - * @param {String|module:engine/model/element~Element|Function} config.model Name of the model element, a model element - * instance or a function that takes a view element and returns a model element. The model element will be inserted in the model. - * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {module:engine/conversion/upcast-converters~UpcastHelpers} - */ - elementToElement( config ) { - return this.add( _upcastElementToElement( config ) ); - }, - - /** - * View element to model attribute conversion helper. - * - * This conversion results in setting an attribute on a model node. For example, view `Foo` becomes - * `Foo` {@link module:engine/model/text~Text model text node} with `bold` attribute set to `true`. - * - * This helper is meant to set a model attribute on all the elements that are inside the converted element: - * - * Foo -->

Foo

--> <$text bold="true">Foo - * - * Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step). - * Even though `` is over `

` element, `bold="true"` was added to the text. See - * {@link module:engine/conversion/upcast-converters~UpcastHelpers#attributeToAttribute} for comparison. - * - * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration. - * - * editor.conversion.for( 'upcast' ).elementToAttribute( { - * view: 'strong', - * model: 'bold' - * } ); - * - * editor.conversion.for( 'upcast' ).elementToAttribute( { - * view: 'strong', - * model: 'bold', - * converterPriority: 'high' - * } ); - * - * editor.conversion.for( 'upcast' ).elementToAttribute( { - * view: { - * name: 'span', - * classes: 'bold' - * }, - * model: 'bold' - * } ); - * - * editor.conversion.for( 'upcast' ).elementToAttribute( { - * view: { - * name: 'span', - * classes: [ 'styled', 'styled-dark' ] - * }, - * model: { - * key: 'styled', - * value: 'dark' - * } - * } ); - * - * editor.conversion.for( 'upcast' ).elementToAttribute( { - * view: { - * name: 'span', - * styles: { - * 'font-size': /[\s\S]+/ - * } - * }, - * model: { - * key: 'fontSize', - * value: viewElement => { - * const fontSize = viewElement.getStyle( 'font-size' ); - * const value = fontSize.substr( 0, fontSize.length - 2 ); - * - * if ( value <= 10 ) { - * return 'small'; - * } else if ( value > 12 ) { - * return 'big'; - * } - * - * return null; - * } - * } - * } ); - * - * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter - * to the conversion process. - * - * @method #elementToAttribute - * @param {Object} config Conversion configuration. - * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted. - * @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing - * the model attribute. `value` property may be set as a function that takes a view element and returns the value. - * If `String` is given, the model attribute value will be set to `true`. - * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {module:engine/conversion/upcast-converters~UpcastHelpers} - */ - elementToAttribute( config ) { - return this.add( _upcastElementToAttribute( config ) ); - }, - - /** - * View attribute to model attribute conversion helper. - * - * This conversion results in setting an attribute on a model node. For example, view `` becomes - * `` in the model. - * - * This helper is meant to convert view attributes from view elements which got converted to the model, so the view attribute - * is set only on the corresponding model node: - * - *

foo
-->
foo
- * - * Above, `class="dark"` attribute is added only to the `
` elements that has it. This is in contrary to - * {@link module:engine/conversion/upcast-converters~UpcastHelpers#elementToAttribute} which sets attributes for - * all the children in the model: - * - * Foo -->

Foo

--> <$text bold="true">Foo - * - * Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step). - * Even though `` is over `

` element, `bold="true"` was added to the text. - * - * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration. - * - * editor.conversion.for( 'upcast' ).attributeToAttribute( { - * view: 'src', - * model: 'source' - * } ); - * - * editor.conversion.for( 'upcast' ).attributeToAttribute( { - * view: { key: 'src' }, - * model: 'source' - * } ); - * - * editor.conversion.for( 'upcast' ).attributeToAttribute( { - * view: { key: 'src' }, - * model: 'source', - * converterPriority: 'normal' - * } ); - * - * editor.conversion.for( 'upcast' ).attributeToAttribute( { - * view: { - * key: 'data-style', - * value: /[\s\S]+/ - * }, - * model: 'styled' - * } ); - * - * editor.conversion.for( 'upcast' ).attributeToAttribute( { - * view: { - * name: 'img', - * key: 'class', - * value: 'styled-dark' - * }, - * model: { - * key: 'styled', - * value: 'dark' - * } - * } ); - * - * editor.conversion.for( 'upcast' ).attributeToAttribute( { - * view: { - * key: 'class', - * value: /styled-[\S]+/ - * }, - * model: { - * key: 'styled' - * value: viewElement => { - * const regexp = /styled-([\S]+)/; - * const match = viewElement.getAttribute( 'class' ).match( regexp ); - * - * return match[ 1 ]; - * } - * } - * } ); - * - * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter - * to the conversion process. - * - * @method #attributeToAttribute - * @param {Object} config Conversion configuration. - * @param {String|Object} config.view Specifies which view attribute will be converted. If a `String` is passed, - * attributes with given key will be converted. If an `Object` is passed, it must have a required `key` property, - * specifying view attribute key, and may have an optional `value` property, specifying view attribute value and optional `name` - * property specifying a view element name from/on which the attribute should be converted. `value` can be given as a `String`, - * a `RegExp` or a function callback, that takes view attribute value as the only parameter and returns `Boolean`. - * @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing - * the model attribute. `value` property may be set as a function that takes a view element and returns the value. - * If `String` is given, the model attribute value will be same as view attribute value. - * @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority. - * @returns {module:engine/conversion/upcast-converters~UpcastHelpers} - */ - attributeToAttribute( config ) { - return this.add( _upcastAttributeToAttribute( config ) ); - }, - - /** - * View element to model marker conversion helper. - * - * This conversion results in creating a model marker. For example, if the marker was stored in a view as an element: - * `

Foo

Bar

`, - * after the conversion is done, the marker will be available in - * {@link module:engine/model/model~Model#markers model document markers}. - * - * editor.conversion.for( 'upcast' ).elementToMarker( { - * view: 'marker-search', - * model: 'search' - * } ); - * - * editor.conversion.for( 'upcast' ).elementToMarker( { - * view: 'marker-search', - * model: 'search', - * converterPriority: 'high' - * } ); - * - * editor.conversion.for( 'upcast' ).elementToMarker( { - * view: 'marker-search', - * model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' ) - * } ); - * - * editor.conversion.for( 'upcast' ).elementToMarker( { - * view: { - * name: 'span', - * attributes: { - * 'data-marker': 'search' - * } - * }, - * model: 'search' - * } ); - * - * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter - * to the conversion process. - * - * @method #elementToMarker - * @param {Object} config Conversion configuration. - * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted. - * @param {String|Function} config.model Name of the model marker, or a function that takes a view element and returns - * a model marker name. - * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {module:engine/conversion/upcast-converters~UpcastHelpers} - */ - elementToMarker( config ) { - return this.add( _upcastElementToMarker( config ) ); - } -}; diff --git a/tests/controller/datacontroller.js b/tests/controller/datacontroller.js index 940225199..3af2401fc 100644 --- a/tests/controller/datacontroller.js +++ b/tests/controller/datacontroller.js @@ -18,19 +18,11 @@ import { parse as parseView, stringify as stringifyView } from '../../src/dev-ut import count from '@ckeditor/ckeditor5-utils/src/count'; -import { - _upcastElementToElement, - _upcastElementToAttribute -} from '../../src/conversion/upcast-converters'; - -import { - _downcastElementToElement, - _downcastAttributeToElement, - _downcastMarkerToHighlight -} from '../../src/conversion/downcast-converters'; +import UpcastHelpers from '../../src/conversion/upcast-converters'; +import DowncastHelpers from '../../src/conversion/downcast-converters'; describe( 'DataController', () => { - let model, modelDocument, htmlDataProcessor, data, schema; + let model, modelDocument, htmlDataProcessor, data, schema, upcastHelpers, downcastHelpers; beforeEach( () => { model = new Model(); @@ -46,6 +38,9 @@ describe( 'DataController', () => { htmlDataProcessor = new HtmlDataProcessor(); data = new DataController( model, htmlDataProcessor ); + + upcastHelpers = new UpcastHelpers( data.upcastDispatcher ); + downcastHelpers = new DowncastHelpers( data.downcastDispatcher ); } ); describe( 'constructor()', () => { @@ -68,7 +63,7 @@ describe( 'DataController', () => { it( 'should set paragraph', () => { schema.register( 'paragraph', { inheritAllFrom: '$block' } ); - _upcastElementToElement( { view: 'p', model: 'paragraph' } )( data.upcastDispatcher ); + upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } ); const output = data.parse( '

foobar

' ); @@ -79,7 +74,7 @@ describe( 'DataController', () => { it( 'should set two paragraphs', () => { schema.register( 'paragraph', { inheritAllFrom: '$block' } ); - _upcastElementToElement( { view: 'p', model: 'paragraph' } )( data.upcastDispatcher ); + upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } ); const output = data.parse( '

foo

bar

' ); @@ -93,8 +88,8 @@ describe( 'DataController', () => { allowAttributes: [ 'bold' ] } ); - _upcastElementToElement( { view: 'p', model: 'paragraph' } )( data.upcastDispatcher ); - _upcastElementToAttribute( { view: 'strong', model: 'bold' } )( data.upcastDispatcher ); + upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } ); + upcastHelpers.elementToAttribute( { view: 'strong', model: 'bold' } ); const output = data.parse( '

foobar

' ); @@ -119,7 +114,7 @@ describe( 'DataController', () => { beforeEach( () => { schema.register( 'paragraph', { inheritAllFrom: '$block' } ); - _upcastElementToElement( { view: 'p', model: 'paragraph' } )( data.upcastDispatcher ); + upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } ); } ); it( 'should convert content of an element #1', () => { @@ -285,7 +280,7 @@ describe( 'DataController', () => { schema.register( 'paragraph', { inheritAllFrom: '$block' } ); setData( model, 'foo' ); - _downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher ); + downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); expect( data.get() ).to.equal( '

foo

' ); } ); @@ -294,7 +289,7 @@ describe( 'DataController', () => { schema.register( 'paragraph', { inheritAllFrom: '$block' } ); setData( model, '' ); - _downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher ); + downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); expect( data.get() ).to.equal( '

 

' ); } ); @@ -303,7 +298,7 @@ describe( 'DataController', () => { schema.register( 'paragraph', { inheritAllFrom: '$block' } ); setData( model, 'foobar' ); - _downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher ); + downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); expect( data.get() ).to.equal( '

foo

bar

' ); } ); @@ -319,7 +314,7 @@ describe( 'DataController', () => { schema.register( 'paragraph', { inheritAllFrom: '$block' } ); setData( model, 'foo<$text bold="true">bar' ); - _downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher ); + downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); expect( data.get() ).to.equal( '

foobar

' ); } ); @@ -328,8 +323,8 @@ describe( 'DataController', () => { schema.register( 'paragraph', { inheritAllFrom: '$block' } ); setData( model, 'foo<$text bold="true">bar' ); - _downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher ); - _downcastAttributeToElement( { model: 'bold', view: 'strong' } )( data.downcastDispatcher ); + downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); + downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } ); expect( data.get() ).to.equal( '

foobar

' ); } ); @@ -341,8 +336,8 @@ describe( 'DataController', () => { setData( model, 'foo', { rootName: 'main' } ); setData( model, 'Bar', { rootName: 'title' } ); - _downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher ); - _downcastAttributeToElement( { model: 'bold', view: 'strong' } )( data.downcastDispatcher ); + downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); + downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } ); expect( data.get() ).to.equal( '

foo

' ); expect( data.get( 'main' ) ).to.equal( '

foo

' ); @@ -358,7 +353,7 @@ describe( 'DataController', () => { schema.extend( '$block', { allowIn: 'div' } ); schema.extend( 'div', { allowIn: '$root' } ); - _downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher ); + downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); } ); it( 'should stringify a content of an element', () => { @@ -382,7 +377,7 @@ describe( 'DataController', () => { schema.extend( '$block', { allowIn: 'div' } ); schema.extend( 'div', { allowIn: '$root' } ); - _downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher ); + downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); } ); it( 'should convert a content of an element', () => { @@ -403,7 +398,7 @@ describe( 'DataController', () => { const modelElement = parseModel( '
foobar
', schema ); const modelRoot = model.document.getRoot(); - _downcastMarkerToHighlight( { model: 'marker:a', view: { classes: 'a' } } )( data.downcastDispatcher ); + downcastHelpers.markerToHighlight( { model: 'marker:a', view: { classes: 'a' } } ); model.change( writer => { writer.insert( modelElement, modelRoot, 0 ); @@ -421,8 +416,8 @@ describe( 'DataController', () => { const modelElement = parseModel( '
foobar
', schema ); const modelRoot = model.document.getRoot(); - _downcastMarkerToHighlight( { model: 'marker:a', view: { classes: 'a' } } )( data.downcastDispatcher ); - _downcastMarkerToHighlight( { model: 'marker:b', view: { classes: 'b' } } )( data.downcastDispatcher ); + downcastHelpers.markerToHighlight( { model: 'marker:a', view: { classes: 'a' } } ); + downcastHelpers.markerToHighlight( { model: 'marker:b', view: { classes: 'b' } } ); const modelP1 = modelElement.getChild( 0 ); const modelP2 = modelElement.getChild( 1 ); diff --git a/tests/controller/editingcontroller.js b/tests/controller/editingcontroller.js index b274c3602..a9765a9c3 100644 --- a/tests/controller/editingcontroller.js +++ b/tests/controller/editingcontroller.js @@ -14,7 +14,7 @@ import View from '../../src/view/view'; import Mapper from '../../src/conversion/mapper'; import DowncastDispatcher from '../../src/conversion/downcastdispatcher'; -import { _downcastElementToElement, _downcastMarkerToHighlight } from '../../src/conversion/downcast-converters'; +import DowncastHelpers from '../../src/conversion/downcast-converters'; import Model from '../../src/model/model'; import ModelPosition from '../../src/model/position'; import ModelRange from '../../src/model/range'; @@ -90,9 +90,11 @@ describe( 'EditingController', () => { model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); model.schema.register( 'div', { inheritAllFrom: '$block' } ); - _downcastElementToElement( { model: 'paragraph', view: 'p' } )( editing.downcastDispatcher ); - _downcastElementToElement( { model: 'div', view: 'div' } )( editing.downcastDispatcher ); - _downcastMarkerToHighlight( { model: 'marker', view: {} } )( editing.downcastDispatcher ); + const downcastHelpers = new DowncastHelpers( editing.downcastDispatcher ); + + downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); + downcastHelpers.elementToElement( { model: 'div', view: 'div' } ); + downcastHelpers.markerToHighlight( { model: 'marker', view: {} } ); // Note: The below code is highly overcomplicated due to #455. model.change( writer => { diff --git a/tests/conversion/downcast-converters.js b/tests/conversion/downcast-converters.js index 40835e661..5f984f26c 100644 --- a/tests/conversion/downcast-converters.js +++ b/tests/conversion/downcast-converters.js @@ -20,12 +20,7 @@ import ViewText from '../../src/view/text'; import log from '@ckeditor/ckeditor5-utils/src/log'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; -import { - _downcastElementToElement, - _downcastAttributeToElement, - _downcastAttributeToAttribute, - _downcastMarkerToElement, - _downcastMarkerToHighlight, +import DowncastHelpers, { insertElement, insertUIElement, changeAttribute, wrap, removeUIElement, highlightElement, highlightText, removeHighlight, createViewElementFromHighlightDescriptor } from '../../src/conversion/downcast-converters'; @@ -33,7 +28,7 @@ import { import { stringify } from '../../src/dev-utils/view'; describe( 'downcast-helpers', () => { - let conversion, model, modelRoot, viewRoot; + let conversion, model, modelRoot, viewRoot, downcastHelpers; beforeEach( () => { model = new Model(); @@ -48,15 +43,15 @@ describe( 'downcast-helpers', () => { viewRoot = controller.view.document.getRoot(); + downcastHelpers = new DowncastHelpers( controller.downcastDispatcher ); + conversion = new Conversion(); - conversion.register( { name: 'downcast', dispatcher: controller.downcastDispatcher } ); + conversion.register( 'downcast', downcastHelpers ); } ); describe( '_downcastElementToElement', () => { it( 'config.view is a string', () => { - const helper = _downcastElementToElement( { model: 'paragraph', view: 'p' } ); - - conversion.for( 'downcast' ).add( helper ); + downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); model.change( writer => { writer.insertElement( 'paragraph', modelRoot, 0 ); @@ -66,10 +61,8 @@ describe( 'downcast-helpers', () => { } ); it( 'can be overwritten using converterPriority', () => { - const helperA = _downcastElementToElement( { model: 'paragraph', view: 'p' } ); - const helperB = _downcastElementToElement( { model: 'paragraph', view: 'foo', converterPriority: 'high' } ); - - conversion.for( 'downcast' ).add( helperA ).add( helperB ); + downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); + downcastHelpers.elementToElement( { model: 'paragraph', view: 'foo', converterPriority: 'high' } ); model.change( writer => { writer.insertElement( 'paragraph', modelRoot, 0 ); @@ -79,7 +72,7 @@ describe( 'downcast-helpers', () => { } ); it( 'config.view is a view element definition', () => { - const helper = _downcastElementToElement( { + downcastHelpers.elementToElement( { model: 'fancyParagraph', view: { name: 'p', @@ -87,8 +80,6 @@ describe( 'downcast-helpers', () => { } } ); - conversion.for( 'downcast' ).add( helper ); - model.change( writer => { writer.insertElement( 'fancyParagraph', modelRoot, 0 ); } ); @@ -97,13 +88,11 @@ describe( 'downcast-helpers', () => { } ); it( 'config.view is a function', () => { - const helper = _downcastElementToElement( { + downcastHelpers.elementToElement( { model: 'heading', view: ( modelElement, viewWriter ) => viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) ) } ); - conversion.for( 'downcast' ).add( helper ); - model.change( writer => { writer.insertElement( 'heading', { level: 2 }, modelRoot, 0 ); } ); @@ -114,9 +103,7 @@ describe( 'downcast-helpers', () => { describe( '_downcastAttributeToElement', () => { it( 'config.view is a string', () => { - const helper = _downcastAttributeToElement( { model: 'bold', view: 'strong' } ); - - conversion.for( 'downcast' ).add( helper ); + downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } ); model.change( writer => { writer.insertText( 'foo', { bold: true }, modelRoot, 0 ); @@ -126,10 +113,8 @@ describe( 'downcast-helpers', () => { } ); it( 'can be overwritten using converterPriority', () => { - const helperA = _downcastAttributeToElement( { model: 'bold', view: 'strong' } ); - const helperB = _downcastAttributeToElement( { model: 'bold', view: 'b', converterPriority: 'high' } ); - - conversion.for( 'downcast' ).add( helperA ).add( helperB ); + downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } ); + downcastHelpers.attributeToElement( { model: 'bold', view: 'b', converterPriority: 'high' } ); model.change( writer => { writer.insertText( 'foo', { bold: true }, modelRoot, 0 ); @@ -139,7 +124,7 @@ describe( 'downcast-helpers', () => { } ); it( 'config.view is a view element definition', () => { - const helper = _downcastAttributeToElement( { + downcastHelpers.attributeToElement( { model: 'invert', view: { name: 'span', @@ -147,8 +132,6 @@ describe( 'downcast-helpers', () => { } } ); - conversion.for( 'downcast' ).add( helper ); - model.change( writer => { writer.insertText( 'foo', { invert: true }, modelRoot, 0 ); } ); @@ -158,7 +141,7 @@ describe( 'downcast-helpers', () => { } ); it( 'config.view allows specifying the element\'s priority', () => { - const helper = _downcastAttributeToElement( { + downcastHelpers.attributeToElement( { model: 'invert', view: { name: 'span', @@ -166,8 +149,6 @@ describe( 'downcast-helpers', () => { } } ); - conversion.for( 'downcast' ).add( helper ); - model.change( writer => { writer.insertText( 'foo', { invert: true }, modelRoot, 0 ); } ); @@ -176,7 +157,7 @@ describe( 'downcast-helpers', () => { } ); it( 'model attribute value is enum', () => { - const helper = _downcastAttributeToElement( { + downcastHelpers.attributeToElement( { model: { key: 'fontSize', values: [ 'big', 'small' ] @@ -198,8 +179,6 @@ describe( 'downcast-helpers', () => { } } ); - conversion.for( 'downcast' ).add( helper ); - model.change( writer => { writer.insertText( 'foo', { fontSize: 'big' }, modelRoot, 0 ); } ); @@ -222,15 +201,13 @@ describe( 'downcast-helpers', () => { } ); it( 'config.view is a function', () => { - const helper = _downcastAttributeToElement( { + downcastHelpers.attributeToElement( { model: 'bold', view: ( modelAttributeValue, viewWriter ) => { return viewWriter.createAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } ); } } ); - conversion.for( 'downcast' ).add( helper ); - model.change( writer => { writer.insertText( 'foo', { bold: '500' }, modelRoot, 0 ); } ); @@ -239,7 +216,7 @@ describe( 'downcast-helpers', () => { } ); it( 'config.model.name is given', () => { - const helper = _downcastAttributeToElement( { + downcastHelpers.attributeToElement( { model: { key: 'color', name: '$text' @@ -249,17 +226,15 @@ describe( 'downcast-helpers', () => { } } ); - conversion.for( 'downcast' ) - .add( helper ) - .add( _downcastElementToElement( { - model: 'smiley', - view: ( modelElement, viewWriter ) => { - return viewWriter.createEmptyElement( 'img', { - src: 'smile.jpg', - class: 'smiley' - } ); - } - } ) ); + downcastHelpers.elementToElement( { + model: 'smiley', + view: ( modelElement, viewWriter ) => { + return viewWriter.createEmptyElement( 'img', { + src: 'smile.jpg', + class: 'smiley' + } ); + } + } ); model.change( writer => { writer.insertText( 'foo', { color: '#FF0000' }, modelRoot, 0 ); @@ -274,13 +249,11 @@ describe( 'downcast-helpers', () => { testUtils.createSinonSandbox(); beforeEach( () => { - conversion.for( 'downcast' ).add( _downcastElementToElement( { model: 'image', view: 'img' } ) ); + downcastHelpers.elementToElement( { model: 'image', view: 'img' } ); } ); it( 'config.view is a string', () => { - const helper = _downcastAttributeToAttribute( { model: 'source', view: 'src' } ); - - conversion.for( 'downcast' ).add( helper ); + downcastHelpers.attributeToAttribute( { model: 'source', view: 'src' } ); model.change( writer => { writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 ); @@ -296,10 +269,8 @@ describe( 'downcast-helpers', () => { } ); it( 'can be overwritten using converterPriority', () => { - const helperA = _downcastAttributeToAttribute( { model: 'source', view: 'href' } ); - const helperB = _downcastAttributeToAttribute( { model: 'source', view: 'src', converterPriority: 'high' } ); - - conversion.for( 'downcast' ).add( helperA ).add( helperB ); + downcastHelpers.attributeToAttribute( { model: 'source', view: 'href' } ); + downcastHelpers.attributeToAttribute( { model: 'source', view: 'src', converterPriority: 'high' } ); model.change( writer => { writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 ); @@ -309,9 +280,9 @@ describe( 'downcast-helpers', () => { } ); it( 'model element name specified', () => { - conversion.for( 'downcast' ).add( _downcastElementToElement( { model: 'paragraph', view: 'p' } ) ); + downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); - const helper = _downcastAttributeToAttribute( { + downcastHelpers.attributeToAttribute( { model: { name: 'image', key: 'source' @@ -319,8 +290,6 @@ describe( 'downcast-helpers', () => { view: 'src' } ); - conversion.for( 'downcast' ).add( helper ); - model.change( writer => { writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 ); } ); @@ -335,9 +304,9 @@ describe( 'downcast-helpers', () => { } ); it( 'config.view is an object, model attribute value is enum', () => { - conversion.for( 'downcast' ).add( _downcastElementToElement( { model: 'paragraph', view: 'p' } ) ); + downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); - const helper = _downcastAttributeToAttribute( { + downcastHelpers.attributeToAttribute( { model: { key: 'styled', values: [ 'dark', 'light' ] @@ -354,8 +323,6 @@ describe( 'downcast-helpers', () => { } } ); - conversion.for( 'downcast' ).add( helper ); - model.change( writer => { writer.insertElement( 'paragraph', { styled: 'dark' }, modelRoot, 0 ); } ); @@ -376,9 +343,9 @@ describe( 'downcast-helpers', () => { } ); it( 'config.view is an object, model attribute value is enum, view has style', () => { - conversion.for( 'downcast' ).add( _downcastElementToElement( { model: 'paragraph', view: 'p' } ) ); + downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); - const helper = _downcastAttributeToAttribute( { + downcastHelpers.attributeToAttribute( { model: { key: 'align', values: [ 'right', 'center' ] @@ -399,8 +366,6 @@ describe( 'downcast-helpers', () => { } } ); - conversion.for( 'downcast' ).add( helper ); - model.change( writer => { writer.insertElement( 'paragraph', { align: 'right' }, modelRoot, 0 ); } ); @@ -421,9 +386,9 @@ describe( 'downcast-helpers', () => { } ); it( 'config.view is an object, only name and key are provided', () => { - conversion.for( 'downcast' ).add( _downcastElementToElement( { model: 'paragraph', view: 'p' } ) ); + downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); - const helper = _downcastAttributeToAttribute( { + downcastHelpers.attributeToAttribute( { model: { name: 'paragraph', key: 'class' @@ -434,8 +399,6 @@ describe( 'downcast-helpers', () => { } } ); - conversion.for( 'downcast' ).add( helper ); - model.change( writer => { writer.insertElement( 'paragraph', { class: 'dark' }, modelRoot, 0 ); } ); @@ -456,13 +419,11 @@ describe( 'downcast-helpers', () => { } ); it( 'config.view is a function', () => { - const helper = _downcastAttributeToAttribute( { + downcastHelpers.attributeToAttribute( { model: 'styled', view: attributeValue => ( { key: 'class', value: 'styled-' + attributeValue } ) } ); - conversion.for( 'downcast' ).add( helper ); - model.change( writer => { writer.insertElement( 'image', { styled: 'pull-out' }, modelRoot, 0 ); } ); @@ -474,9 +435,9 @@ describe( 'downcast-helpers', () => { it( 'config.view and config.model as strings in generic conversion (elements only)', () => { const logSpy = testUtils.sinon.spy( log, 'warn' ); - conversion.for( 'downcast' ).add( _downcastElementToElement( { model: 'paragraph', view: 'p' } ) ); + downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); - conversion.for( 'downcast' ).add( _downcastAttributeToAttribute( { model: 'test', view: 'test' } ) ); + downcastHelpers.attributeToAttribute( { model: 'test', view: 'test' } ); model.change( writer => { writer.insertElement( 'paragraph', { test: '1' }, modelRoot, 0 ); @@ -497,9 +458,9 @@ describe( 'downcast-helpers', () => { it( 'config.view and config.model as strings in generic conversion (elements + text)', () => { const logSpy = testUtils.sinon.spy( log, 'warn' ); - conversion.for( 'downcast' ).add( _downcastElementToElement( { model: 'paragraph', view: 'p' } ) ); + downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); - conversion.for( 'downcast' ).add( _downcastAttributeToAttribute( { model: 'test', view: 'test' } ) ); + downcastHelpers.attributeToAttribute( { model: 'test', view: 'test' } ); model.change( writer => { writer.insertElement( 'paragraph', modelRoot, 0 ); @@ -523,9 +484,7 @@ describe( 'downcast-helpers', () => { describe( '_downcastMarkerToElement', () => { it( 'config.view is a string', () => { - const helper = _downcastMarkerToElement( { model: 'search', view: 'marker-search' } ); - - conversion.for( 'downcast' ).add( helper ); + downcastHelpers.markerToElement( { model: 'search', view: 'marker-search' } ); model.change( writer => { writer.insertText( 'foo', modelRoot, 0 ); @@ -538,10 +497,8 @@ describe( 'downcast-helpers', () => { } ); it( 'can be overwritten using converterPriority', () => { - const helperA = _downcastMarkerToElement( { model: 'search', view: 'marker-search' } ); - const helperB = _downcastMarkerToElement( { model: 'search', view: 'search', converterPriority: 'high' } ); - - conversion.for( 'downcast' ).add( helperA ).add( helperB ); + downcastHelpers.markerToElement( { model: 'search', view: 'marker-search' } ); + downcastHelpers.markerToElement( { model: 'search', view: 'search', converterPriority: 'high' } ); model.change( writer => { writer.insertText( 'foo', modelRoot, 0 ); @@ -553,7 +510,7 @@ describe( 'downcast-helpers', () => { } ); it( 'config.view is a view element definition', () => { - const helper = _downcastMarkerToElement( { + downcastHelpers.markerToElement( { model: 'search', view: { name: 'span', @@ -563,8 +520,6 @@ describe( 'downcast-helpers', () => { } } ); - conversion.for( 'downcast' ).add( helper ); - model.change( writer => { writer.insertText( 'foo', modelRoot, 0 ); const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 2 ) ); @@ -575,15 +530,13 @@ describe( 'downcast-helpers', () => { } ); it( 'config.view is a function', () => { - const helper = _downcastMarkerToElement( { + downcastHelpers.markerToElement( { model: 'search', view: ( data, viewWriter ) => { return viewWriter.createUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } ); } } ); - conversion.for( 'downcast' ).add( helper ); - model.change( writer => { writer.insertText( 'foo', modelRoot, 0 ); const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 2 ) ); @@ -596,9 +549,7 @@ describe( 'downcast-helpers', () => { describe( '_downcastMarkerToHighlight', () => { it( 'config.view is a highlight descriptor', () => { - const helper = _downcastMarkerToHighlight( { model: 'comment', view: { classes: 'comment' } } ); - - conversion.for( 'downcast' ).add( helper ); + downcastHelpers.markerToHighlight( { model: 'comment', view: { classes: 'comment' } } ); model.change( writer => { writer.insertText( 'foo', modelRoot, 0 ); @@ -610,10 +561,8 @@ describe( 'downcast-helpers', () => { } ); it( 'can be overwritten using converterPriority', () => { - const helperA = _downcastMarkerToHighlight( { model: 'comment', view: { classes: 'comment' } } ); - const helperB = _downcastMarkerToHighlight( { model: 'comment', view: { classes: 'new-comment' }, converterPriority: 'high' } ); - - conversion.for( 'downcast' ).add( helperA ).add( helperB ); + downcastHelpers.markerToHighlight( { model: 'comment', view: { classes: 'comment' } } ); + downcastHelpers.markerToHighlight( { model: 'comment', view: { classes: 'new-comment' }, converterPriority: 'high' } ); model.change( writer => { writer.insertText( 'foo', modelRoot, 0 ); @@ -625,7 +574,7 @@ describe( 'downcast-helpers', () => { } ); it( 'config.view is a function', () => { - const helper = _downcastMarkerToHighlight( { + downcastHelpers.markerToHighlight( { model: 'comment', view: data => { const commentType = data.markerName.split( ':' )[ 1 ]; @@ -636,8 +585,6 @@ describe( 'downcast-helpers', () => { } } ); - conversion.for( 'downcast' ).add( helper ); - model.change( writer => { writer.insertText( 'foo', modelRoot, 0 ); const range = writer.createRange( writer.createPositionAt( modelRoot, 0 ), writer.createPositionAt( modelRoot, 3 ) ); @@ -1373,12 +1320,12 @@ describe( 'downcast-converters', () => { expect( viewToString( viewRoot ) ).to.equal( '
' + - '

' + - 'foo' + - '

' + - '

' + - 'bar' + - '

' + + '

' + + 'foo' + + '

' + + '

' + + 'bar' + + '

' + '
' ); @@ -1406,12 +1353,12 @@ describe( 'downcast-converters', () => { expect( viewToString( viewRoot ) ).to.equal( '
' + - '

' + - 'foo' + - '

' + - '

' + - 'bar' + - '

' + + '

' + + 'foo' + + '

' + + '

' + + 'bar' + + '

' + '
' ); @@ -1485,10 +1432,10 @@ describe( 'downcast-converters', () => { expect( viewToString( viewRoot ) ).to.equal( '
' + - '

' + - 'foo' + - '

' + - '

bar

' + + '

' + + 'foo' + + '

' + + '

bar

' + '
' ); @@ -1499,16 +1446,16 @@ describe( 'downcast-converters', () => { expect( viewToString( viewRoot ) ).to.equal( '
' + - '

' + - 'f' + - '' + - 'oo' + - '' + - '

' + - '

' + - 'ba' + - 'r' + - '

' + + '

' + + 'f' + + '' + + 'oo' + + '' + + '

' + + '

' + + 'ba' + + 'r' + + '

' + '
' ); @@ -1519,21 +1466,21 @@ describe( 'downcast-converters', () => { expect( viewToString( viewRoot ) ).to.equal( '
' + - '

' + - 'f' + - '' + - '' + - 'o' + - 'o' + - '' + - '' + - '

' + - '

' + - '' + - 'ba' + - '' + - 'r' + - '

' + + '

' + + 'f' + + '' + + '' + + 'o' + + 'o' + + '' + + '' + + '

' + + '

' + + '' + + 'ba' + + '' + + 'r' + + '

' + '
' ); @@ -1543,15 +1490,15 @@ describe( 'downcast-converters', () => { expect( viewToString( viewRoot ) ).to.equal( '
' + - '

' + - '' + - 'fo' + - 'o' + - '' + - '

' + - '

' + - 'bar' + - '

' + + '

' + + '' + + 'fo' + + 'o' + + '' + + '

' + + '

' + + 'bar' + + '

' + '
' ); @@ -1561,13 +1508,13 @@ describe( 'downcast-converters', () => { expect( viewToString( viewRoot ) ).to.equal( '
' + - '

' + - 'fo' + - 'o' + - '

' + - '

' + - 'bar' + - '

' + + '

' + + 'fo' + + 'o' + + '

' + + '

' + + 'bar' + + '

' + '
' ); @@ -1647,9 +1594,9 @@ describe( 'downcast-converters', () => { expect( viewToString( viewRoot ) ).to.equal( '
' + - '
' + - 'foo' + - '
' + + '
' + + 'foo' + + '
' + '
' ); @@ -1673,9 +1620,9 @@ describe( 'downcast-converters', () => { expect( viewToString( viewRoot ) ).to.equal( '
' + - '
' + - 'foo' + - '
' + + '
' + + 'foo' + + '
' + '
' ); @@ -1732,7 +1679,7 @@ describe( 'downcast-converters', () => { const descriptor = { classes: 'foo-class', attributes: { one: '1', two: '2' }, - priority: 7, + priority: 7 }; const element = createViewElementFromHighlightDescriptor( descriptor ); @@ -1750,7 +1697,7 @@ describe( 'downcast-converters', () => { const descriptor = { classes: [ 'foo-class', 'bar-class' ], attributes: { one: '1', two: '2' }, - priority: 7, + priority: 7 }; const element = createViewElementFromHighlightDescriptor( descriptor ); @@ -1768,7 +1715,7 @@ describe( 'downcast-converters', () => { it( 'should create element without class', () => { const descriptor = { attributes: { one: '1', two: '2' }, - priority: 7, + priority: 7 }; const element = createViewElementFromHighlightDescriptor( descriptor ); @@ -1784,7 +1731,7 @@ describe( 'downcast-converters', () => { it( 'should create element without priority', () => { const descriptor = { classes: 'foo-class', - attributes: { one: '1', two: '2' }, + attributes: { one: '1', two: '2' } }; const element = createViewElementFromHighlightDescriptor( descriptor ); diff --git a/tests/conversion/upcast-converters.js b/tests/conversion/upcast-converters.js index 6edfb431a..05173dfdb 100644 --- a/tests/conversion/upcast-converters.js +++ b/tests/conversion/upcast-converters.js @@ -19,15 +19,12 @@ import ModelText from '../../src/model/text'; import ModelRange from '../../src/model/range'; import ModelPosition from '../../src/model/position'; -import { - _upcastElementToElement, _upcastElementToAttribute, _upcastAttributeToAttribute, _upcastElementToMarker, - convertToModelFragment, convertText -} from '../../src/conversion/upcast-converters'; +import UpcastHelpers, { convertToModelFragment, convertText } from '../../src/conversion/upcast-converters'; import { stringify } from '../../src/dev-utils/model'; -describe( 'upcast-helpers', () => { - let upcastDispatcher, model, schema, conversion; +describe( 'UpcastHelpers', () => { + let upcastDispatcher, model, schema, conversion, upcastHelpers; beforeEach( () => { model = new Model(); @@ -49,14 +46,13 @@ describe( 'upcast-helpers', () => { upcastDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } ); conversion = new Conversion(); - conversion.register( { name: 'upcast', dispatcher: upcastDispatcher } ); + upcastHelpers = new UpcastHelpers( upcastDispatcher ); + conversion.register( 'upcast', upcastHelpers ); } ); describe( '_upcastElementToElement', () => { it( 'config.view is a string', () => { - const helper = _upcastElementToElement( { view: 'p', model: 'paragraph' } ); - - conversion.for( 'upcast' ).add( helper ); + upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } ); expectResult( new ViewContainerElement( 'p' ), '' ); } ); @@ -66,10 +62,8 @@ describe( 'upcast-helpers', () => { inheritAllFrom: '$block' } ); - const helperA = _upcastElementToElement( { view: 'p', model: 'p' } ); - const helperB = _upcastElementToElement( { view: 'p', model: 'paragraph', converterPriority: 'high' } ); - - conversion.for( 'upcast' ).add( helperA ).add( helperB ); + upcastHelpers.elementToElement( { view: 'p', model: 'p' } ); + upcastHelpers.elementToElement( { view: 'p', model: 'paragraph', converterPriority: 'high' } ); expectResult( new ViewContainerElement( 'p' ), '' ); } ); @@ -79,16 +73,14 @@ describe( 'upcast-helpers', () => { inheritAllFrom: '$block' } ); - const helperFancy = _upcastElementToElement( { + upcastHelpers.elementToElement( { view: { name: 'p', classes: 'fancy' }, - model: 'fancyParagraph', + model: 'fancyParagraph' } ); - conversion.for( 'upcast' ).add( helperFancy ); - expectResult( new ViewContainerElement( 'p', { class: 'fancy' } ), '' ); expectResult( new ViewContainerElement( 'p' ), '' ); } ); @@ -99,7 +91,7 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'level' ] } ); - const helper = _upcastElementToElement( { + upcastHelpers.elementToElement( { view: { name: 'p', classes: 'heading' @@ -109,24 +101,18 @@ describe( 'upcast-helpers', () => { } } ); - conversion.for( 'upcast' ).add( helper ); - expectResult( new ViewContainerElement( 'p', { class: 'heading', 'data-level': 2 } ), '' ); expectResult( new ViewContainerElement( 'p', { 'data-level': 2 } ), '' ); } ); it( 'should fire conversion of the element children', () => { - const helper = _upcastElementToElement( { view: 'p', model: 'paragraph' } ); - - conversion.for( 'upcast' ).add( helper ); + upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } ); expectResult( new ViewContainerElement( 'p', null, new ViewText( 'foo' ) ), 'foo' ); } ); it( 'should not insert a model element if it is not allowed by schema', () => { - const helper = _upcastElementToElement( { view: 'h2', model: 'heading' } ); - - conversion.for( 'upcast' ).add( helper ); + upcastHelpers.elementToElement( { view: 'h2', model: 'heading' } ); expectResult( new ViewContainerElement( 'h2' ), '' ); } ); @@ -136,10 +122,8 @@ describe( 'upcast-helpers', () => { inheritAllFrom: '$block' } ); - const helperParagraph = _upcastElementToElement( { view: 'p', model: 'paragraph' } ); - const helperHeading = _upcastElementToElement( { view: 'h2', model: 'heading' } ); - - conversion.for( 'upcast' ).add( helperParagraph ).add( helperHeading ); + upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } ); + upcastHelpers.elementToElement( { view: 'h2', model: 'heading' } ); expectResult( new ViewContainerElement( 'p', null, [ @@ -152,10 +136,8 @@ describe( 'upcast-helpers', () => { } ); it( 'should not do anything if returned model element is null', () => { - const helperA = _upcastElementToElement( { view: 'p', model: 'paragraph' } ); - const helperB = _upcastElementToElement( { view: 'p', model: () => null, converterPriority: 'high' } ); - - conversion.for( 'upcast' ).add( helperA ).add( helperB ); + upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } ); + upcastHelpers.elementToElement( { view: 'p', model: () => null, converterPriority: 'high' } ); expectResult( new ViewContainerElement( 'p' ), '' ); } ); @@ -163,9 +145,7 @@ describe( 'upcast-helpers', () => { describe( '_upcastElementToAttribute', () => { it( 'config.view is string', () => { - const helper = _upcastElementToAttribute( { view: 'strong', model: 'bold' } ); - - conversion.for( 'upcast' ).add( helper ); + upcastHelpers.elementToAttribute( { view: 'strong', model: 'bold' } ); expectResult( new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), @@ -174,10 +154,8 @@ describe( 'upcast-helpers', () => { } ); it( 'can be overwritten using converterPriority', () => { - const helperA = _upcastElementToAttribute( { view: 'strong', model: 'strong' } ); - const helperB = _upcastElementToAttribute( { view: 'strong', model: 'bold', converterPriority: 'high' } ); - - conversion.for( 'upcast' ).add( helperA ).add( helperB ); + upcastHelpers.elementToAttribute( { view: 'strong', model: 'strong' } ); + upcastHelpers.elementToAttribute( { view: 'strong', model: 'bold', converterPriority: 'high' } ); expectResult( new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), @@ -186,7 +164,7 @@ describe( 'upcast-helpers', () => { } ); it( 'config.view is an object', () => { - const helper = _upcastElementToAttribute( { + upcastHelpers.elementToAttribute( { view: { name: 'span', classes: 'bold' @@ -194,8 +172,6 @@ describe( 'upcast-helpers', () => { model: 'bold' } ); - conversion.for( 'upcast' ).add( helper ); - expectResult( new ViewAttributeElement( 'span', { class: 'bold' }, new ViewText( 'foo' ) ), '<$text bold="true">foo' @@ -209,7 +185,7 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'styled' ] } ); - const helper = _upcastElementToAttribute( { + upcastHelpers.elementToAttribute( { view: { name: 'span', classes: [ 'styled', 'styled-dark' ] @@ -220,8 +196,6 @@ describe( 'upcast-helpers', () => { } } ); - conversion.for( 'upcast' ).add( helper ); - expectResult( new ViewAttributeElement( 'span', { class: 'styled styled-dark' }, new ViewText( 'foo' ) ), '<$text styled="dark">foo' @@ -235,7 +209,7 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'fontSize' ] } ); - const helper = _upcastElementToAttribute( { + upcastHelpers.elementToAttribute( { view: { name: 'span', styles: { @@ -259,8 +233,6 @@ describe( 'upcast-helpers', () => { } } ); - conversion.for( 'upcast' ).add( helper ); - expectResult( new ViewAttributeElement( 'span', { style: 'font-size:9px' }, new ViewText( 'foo' ) ), '<$text fontSize="small">foo' @@ -278,9 +250,7 @@ describe( 'upcast-helpers', () => { } ); it( 'should not set an attribute if it is not allowed by schema', () => { - const helper = _upcastElementToAttribute( { view: 'em', model: 'italic' } ); - - conversion.for( 'upcast' ).add( helper ); + upcastHelpers.elementToAttribute( { view: 'em', model: 'italic' } ); expectResult( new ViewAttributeElement( 'em', null, new ViewText( 'foo' ) ), @@ -289,8 +259,8 @@ describe( 'upcast-helpers', () => { } ); it( 'should not do anything if returned model attribute is null', () => { - const helperA = _upcastElementToAttribute( { view: 'strong', model: 'bold' } ); - const helperB = _upcastElementToAttribute( { + upcastHelpers.elementToAttribute( { view: 'strong', model: 'bold' } ); + upcastHelpers.elementToAttribute( { view: 'strong', model: { key: 'bold', @@ -299,8 +269,6 @@ describe( 'upcast-helpers', () => { converterPriority: 'high' } ); - conversion.for( 'upcast' ).add( helperA ).add( helperB ); - expectResult( new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), '<$text bold="true">foo' @@ -308,18 +276,16 @@ describe( 'upcast-helpers', () => { } ); it( 'should allow two converters to convert attributes on the same element', () => { - const helperA = _upcastElementToAttribute( { + upcastHelpers.elementToAttribute( { model: 'attribA', view: { name: 'span', classes: 'attrib-a' } } ); - const helperB = _upcastElementToAttribute( { + upcastHelpers.elementToAttribute( { model: 'attribB', view: { name: 'span', styles: { color: 'attrib-b' } } } ); - conversion.for( 'upcast' ).add( helperA ).add( helperB ); - expectResult( new ViewAttributeElement( 'span', { class: 'attrib-a', style: 'color:attrib-b;' }, new ViewText( 'foo' ) ), '<$text attribA="true" attribB="true">foo' @@ -327,23 +293,21 @@ describe( 'upcast-helpers', () => { } ); it( 'should consume element only when only is name specified', () => { - const helperBold = _upcastElementToAttribute( { + upcastHelpers.elementToAttribute( { model: 'bold', view: { name: 'strong' } } ); - const helperA = _upcastElementToAttribute( { + upcastHelpers.elementToAttribute( { model: 'attribA', view: { name: 'strong' } } ); - const helperB = _upcastElementToAttribute( { + upcastHelpers.elementToAttribute( { model: 'attribB', view: { name: 'strong', classes: 'foo' } } ); - conversion.for( 'upcast' ).add( helperBold ).add( helperA ).add( helperB ); - expectResult( new ViewAttributeElement( 'strong', { class: 'foo' }, new ViewText( 'foo' ) ), '<$text attribB="true" bold="true">foo' @@ -352,14 +316,12 @@ describe( 'upcast-helpers', () => { // #1443. it( 'should set attributes on the element\'s children', () => { - const helperBold = _upcastElementToAttribute( { + upcastHelpers.elementToAttribute( { model: 'bold', view: { name: 'strong' } } ); - const helperP = _upcastElementToElement( { view: 'p', model: 'paragraph' } ); - - conversion.for( 'upcast' ).add( helperP ).add( helperBold ); + upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } ); expectResult( new ViewAttributeElement( @@ -374,7 +336,7 @@ describe( 'upcast-helpers', () => { describe( '_upcastAttributeToAttribute', () => { beforeEach( () => { - conversion.for( 'upcast' ).add( _upcastElementToElement( { view: 'img', model: 'image' } ) ); + upcastHelpers.elementToElement( { view: 'img', model: 'image' } ); schema.register( 'image', { inheritAllFrom: '$block' @@ -386,9 +348,7 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'source' ] } ); - const helper = _upcastAttributeToAttribute( { view: 'src', model: 'source' } ); - - conversion.for( 'upcast' ).add( helper ); + upcastHelpers.attributeToAttribute( { view: 'src', model: 'source' } ); expectResult( new ViewAttributeElement( 'img', { src: 'foo.jpg' } ), @@ -401,9 +361,7 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'source' ] } ); - const helper = _upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' } ); - - conversion.for( 'upcast' ).add( helper ); + upcastHelpers.attributeToAttribute( { view: { key: 'src' }, model: 'source' } ); expectResult( new ViewAttributeElement( 'img', { src: 'foo.jpg' } ), @@ -416,9 +374,7 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'source' ] } ); - const helper = _upcastAttributeToAttribute( { view: { name: 'img', key: 'src' }, model: { name: 'image', key: 'source' } } ); - - conversion.for( 'upcast' ).add( helper ); + upcastHelpers.attributeToAttribute( { view: { name: 'img', key: 'src' }, model: { name: 'image', key: 'source' } } ); expectResult( new ViewAttributeElement( 'img', { src: 'foo.jpg' } ), @@ -431,10 +387,8 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'src', 'source' ] } ); - const helperA = _upcastAttributeToAttribute( { view: { key: 'src' }, model: 'src' } ); - const helperB = _upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source', converterPriority: 'normal' } ); - - conversion.for( 'upcast' ).add( helperA ).add( helperB ); + upcastHelpers.attributeToAttribute( { view: { key: 'src' }, model: 'src' } ); + upcastHelpers.attributeToAttribute( { view: { key: 'src' }, model: 'source', converterPriority: 'normal' } ); expectResult( new ViewAttributeElement( 'img', { src: 'foo.jpg' } ), @@ -447,7 +401,7 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'styled' ] } ); - const helper = _upcastAttributeToAttribute( { + upcastHelpers.attributeToAttribute( { view: { key: 'data-style', value: /[\s\S]*/ @@ -455,8 +409,6 @@ describe( 'upcast-helpers', () => { model: 'styled' } ); - conversion.for( 'upcast' ).add( helper ); - expectResult( new ViewAttributeElement( 'img', { 'data-style': 'dark' } ), '' @@ -468,7 +420,7 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'styled' ] } ); - const helper = _upcastAttributeToAttribute( { + upcastHelpers.attributeToAttribute( { view: { name: 'img', key: 'class', @@ -480,9 +432,7 @@ describe( 'upcast-helpers', () => { } } ); - conversion.for( 'upcast' ) - .add( helper ) - .add( _upcastElementToElement( { view: 'p', model: 'paragraph' } ) ); + upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } ); expectResult( new ViewContainerElement( 'img', { class: 'styled-dark' } ), @@ -505,7 +455,7 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'styled' ] } ); - const helper = _upcastAttributeToAttribute( { + upcastHelpers.attributeToAttribute( { view: { key: 'class', value: /styled-[\S]+/ @@ -521,8 +471,6 @@ describe( 'upcast-helpers', () => { } } ); - conversion.for( 'upcast' ).add( helper ); - expectResult( new ViewAttributeElement( 'img', { 'class': 'styled-dark' } ), '' @@ -530,9 +478,7 @@ describe( 'upcast-helpers', () => { } ); it( 'should not set an attribute if it is not allowed by schema', () => { - const helper = _upcastAttributeToAttribute( { view: 'src', model: 'source' } ); - - conversion.for( 'upcast' ).add( helper ); + upcastHelpers.attributeToAttribute( { view: 'src', model: 'source' } ); expectResult( new ViewAttributeElement( 'img', { src: 'foo.jpg' } ), @@ -545,7 +491,7 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'styled' ] } ); - const helperA = _upcastAttributeToAttribute( { + upcastHelpers.attributeToAttribute( { view: { key: 'class', value: 'styled' @@ -556,7 +502,7 @@ describe( 'upcast-helpers', () => { } } ); - const helperB = _upcastAttributeToAttribute( { + upcastHelpers.attributeToAttribute( { view: { key: 'class', value: 'styled' @@ -567,8 +513,6 @@ describe( 'upcast-helpers', () => { } } ); - conversion.for( 'upcast' ).add( helperA ).add( helperB ); - expectResult( new ViewAttributeElement( 'img', { class: 'styled' } ), '' @@ -584,13 +528,10 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'border', 'shade' ] } ); - conversion.for( 'upcast' ).add( _upcastElementToElement( { view: 'div', model: 'div' } ) ); + upcastHelpers.elementToElement( { view: 'div', model: 'div' } ); - const shadeHelper = _upcastAttributeToAttribute( { view: { key: 'class', value: 'shade' }, model: 'shade' } ); - const borderHelper = _upcastAttributeToAttribute( { view: { key: 'class', value: 'border' }, model: 'border' } ); - - conversion.for( 'upcast' ).add( shadeHelper ); - conversion.for( 'upcast' ).add( borderHelper ); + upcastHelpers.attributeToAttribute( { view: { key: 'class', value: 'shade' }, model: 'shade' } ); + upcastHelpers.attributeToAttribute( { view: { key: 'class', value: 'border' }, model: 'border' } ); expectResult( new ViewContainerElement( @@ -605,9 +546,7 @@ describe( 'upcast-helpers', () => { describe( '_upcastElementToMarker', () => { it( 'config.view is a string', () => { - const helper = _upcastElementToMarker( { view: 'marker-search', model: 'search' } ); - - conversion.for( 'upcast' ).add( helper ); + upcastHelpers.elementToMarker( { view: 'marker-search', model: 'search' } ); const frag = new ViewDocumentFragment( [ new ViewText( 'fo' ), @@ -623,10 +562,8 @@ describe( 'upcast-helpers', () => { } ); it( 'can be overwritten using converterPriority', () => { - const helperA = _upcastElementToMarker( { view: 'marker-search', model: 'search-result' } ); - const helperB = _upcastElementToMarker( { view: 'marker-search', model: 'search', converterPriority: 'high' } ); - - conversion.for( 'upcast' ).add( helperA ).add( helperB ); + upcastHelpers.elementToMarker( { view: 'marker-search', model: 'search-result' } ); + upcastHelpers.elementToMarker( { view: 'marker-search', model: 'search', converterPriority: 'high' } ); const frag = new ViewDocumentFragment( [ new ViewText( 'fo' ), @@ -642,7 +579,7 @@ describe( 'upcast-helpers', () => { } ); it( 'config.view is an object', () => { - const helper = _upcastElementToMarker( { + upcastHelpers.elementToMarker( { view: { name: 'span', 'data-marker': 'search' @@ -650,8 +587,6 @@ describe( 'upcast-helpers', () => { model: 'search' } ); - conversion.for( 'upcast' ).add( helper ); - const frag = new ViewDocumentFragment( [ new ViewText( 'f' ), new ViewUIElement( 'span', { 'data-marker': 'search' } ), @@ -666,13 +601,11 @@ describe( 'upcast-helpers', () => { } ); it( 'config.model is a function', () => { - const helper = _upcastElementToMarker( { + upcastHelpers.elementToMarker( { view: 'comment', model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' ) } ); - conversion.for( 'upcast' ).add( helper ); - const frag = new ViewDocumentFragment( [ new ViewText( 'foo' ), new ViewUIElement( 'comment', { 'data-comment-id': 4 } ), @@ -687,11 +620,9 @@ describe( 'upcast-helpers', () => { } ); it( 'marker is in a block element', () => { - conversion.for( 'upcast' ).add( _upcastElementToElement( { model: 'paragraph', view: 'p' } ) ); - - const helper = _upcastElementToMarker( { view: 'marker-search', model: 'search' } ); + upcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); - conversion.for( 'upcast' ).add( helper ); + upcastHelpers.elementToMarker( { view: 'marker-search', model: 'search' } ); const element = new ViewContainerElement( 'p', null, [ new ViewText( 'fo' ), From 3bcf6515caea6e4d6050103ca3dd722ff3cd3ffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 20 Dec 2018 16:15:23 +0100 Subject: [PATCH 59/84] Rename downcast-converters from module:engine/conversion to downcasthelpers. --- src/controller/datacontroller.js | 4 +- src/controller/editingcontroller.js | 2 +- src/conversion/conversion.js | 39 ++++++++----------- ...ncast-converters.js => downcasthelpers.js} | 36 ++++++++--------- ...{upcast-converters.js => upcasthelpers.js} | 14 +++---- src/dev-utils/model.js | 2 +- src/model/markercollection.js | 4 +- tests/controller/datacontroller.js | 4 +- tests/controller/editingcontroller.js | 2 +- tests/conversion/conversion.js | 16 ++++---- .../downcast-selection-converters.js | 2 +- ...ncast-converters.js => downcasthelpers.js} | 2 +- ...{upcast-converters.js => upcasthelpers.js} | 2 +- 13 files changed, 62 insertions(+), 67 deletions(-) rename src/conversion/{downcast-converters.js => downcasthelpers.js} (96%) rename src/conversion/{upcast-converters.js => upcasthelpers.js} (98%) rename tests/conversion/{downcast-converters.js => downcasthelpers.js} (99%) rename tests/conversion/{upcast-converters.js => upcasthelpers.js} (99%) diff --git a/src/controller/datacontroller.js b/src/controller/datacontroller.js index f8d519573..e183dc74b 100644 --- a/src/controller/datacontroller.js +++ b/src/controller/datacontroller.js @@ -14,10 +14,10 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import Mapper from '../conversion/mapper'; import DowncastDispatcher from '../conversion/downcastdispatcher'; -import { insertText } from '../conversion/downcast-converters'; +import { insertText } from '../conversion/downcasthelpers'; import UpcastDispatcher from '../conversion/upcastdispatcher'; -import { convertText, convertToModelFragment } from '../conversion/upcast-converters'; +import { convertText, convertToModelFragment } from '../conversion/upcasthelpers'; import ViewDocumentFragment from '../view/documentfragment'; import ViewDocument from '../view/document'; diff --git a/src/controller/editingcontroller.js b/src/controller/editingcontroller.js index 1ae8b14e9..b504ff4b5 100644 --- a/src/controller/editingcontroller.js +++ b/src/controller/editingcontroller.js @@ -11,7 +11,7 @@ import RootEditableElement from '../view/rooteditableelement'; import View from '../view/view'; import Mapper from '../conversion/mapper'; import DowncastDispatcher from '../conversion/downcastdispatcher'; -import { insertText, remove } from '../conversion/downcast-converters'; +import { insertText, remove } from '../conversion/downcasthelpers'; import { convertSelectionChange } from '../conversion/upcast-selection-converters'; import { clearAttributes, convertCollapsedSelection, convertRangeSelection } from '../conversion/downcast-selection-converters'; diff --git a/src/conversion/conversion.js b/src/conversion/conversion.js index 91f675749..a66752596 100644 --- a/src/conversion/conversion.js +++ b/src/conversion/conversion.js @@ -38,8 +38,8 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; * * The functions used in `add()` calls are one-way converters (i.e. you need to remember yourself to add * a converter in the other direction, if your feature requires that). They are also called "conversion helpers". - * You can find a set of them in the {@link module:engine/conversion/downcast-converters} and - * {@link module:engine/conversion/upcast-converters} modules. + * You can find a set of them in the {@link module:engine/conversion/downcasthelpers} and + * {@link module:engine/conversion/upcasthelpers} modules. * * Besides allowing to register converters to specific dispatchers, you can also use methods available in this * class to add two-way converters (upcast and downcast): @@ -76,8 +76,8 @@ export default class Conversion { * module:engine/conversion/upcastdispatcher~UpcastDispatcher|Array.} options.dispatcher Dispatcher or array of dispatchers to register * under the given name. - * @param {module:engine/conversion/downcast-converters~DowncastHelpers| - * module:engine/conversion/upcast-converters~UpcastHelpers} helpers + * @param {module:engine/conversion/downcasthelpers~DowncastHelpers| + * module:engine/conversion/upcasthelpers~UpcastHelpers} helpers */ register( name, group ) { if ( this._dispatchersGroups.has( name ) ) { @@ -92,7 +92,6 @@ export default class Conversion { this._dispatchersGroups.set( name, group ); } - /* eslint-disable max-len */ /** * Provides chainable API to assign converters to dispatchers registered under a given group name. Converters are added * by calling the {@link module:engine/conversion/conversion~ConversionHelpers#add `.add()`} method of an @@ -113,18 +112,18 @@ export default class Conversion { * * For downcast (model-to-view conversion), these are: * - * * {@link module:engine/conversion/downcast-converters~DowncastHelpers#elementToElement Downcast element-to-element converter}, - * * {@link module:engine/conversion/downcast-converters~DowncastHelpers#attributeToElement Downcast attribute-to-element converter}, - * * {@link module:engine/conversion/downcast-converters~DowncastHelpers#attributeToAttribute Downcast attribute-to-attribute converter}. - * * {@link module:engine/conversion/downcast-converters~DowncastHelpers#markerToElement Downcast marker-to-element converter}. - * * {@link module:engine/conversion/downcast-converters~DowncastHelpers#markerToHighlight Downcast marker-to-highlight converter}. + * * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement Downcast element-to-element converter}, + * * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement Downcast attribute-to-element converter}, + * * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToAttribute Downcast attribute-to-attribute converter}. + * * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToElement Downcast marker-to-element converter}. + * * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToHighlight Downcast marker-to-highlight converter}. * * For upcast (view-to-model conversion), these are: * - * * {@link module:engine/conversion/upcast-converters~UpcastHelpers#elementToElement Upcast element-to-element converter}, - * * {@link module:engine/conversion/upcast-converters~UpcastHelpers#elementToAttribute Upcast attribute-to-element converter}, - * * {@link module:engine/conversion/upcast-converters~UpcastHelpers#attributeToAttribute Upcast attribute-to-attribute converter}. - * * {@link module:engine/conversion/upcast-converters~UpcastHelpers#elementToMarker Upcast element-to-marker converter}. + * * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToElement Upcast element-to-element converter}, + * * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToAttribute Upcast attribute-to-element converter}, + * * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#attributeToAttribute Upcast attribute-to-attribute converter}. + * * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToMarker Upcast element-to-marker converter}. * * An example of using conversion helpers to convert the `paragraph` model element to the `p` view element (and back): * @@ -136,10 +135,8 @@ export default class Conversion { * editor.conversion.for( 'upcast' ).elementToElement( config ) ); * * @param {String} groupName The name of dispatchers group to add the converters to. - * @returns {module:engine/conversion/conversion~ConversionHelpers|module:engine/conversion/downcast-converters~DowncastHelpers| - * module:engine/conversion/upcast-converters~UpcastHelpers} + * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers| module:engine/conversion/upcasthelpers~UpcastHelpers} */ - /* eslint-enable max-len */ for( groupName ) { const group = this._getDispatchersGroup( groupName ); @@ -574,7 +571,7 @@ export default class Conversion { * @property {String} name Group name * @property {Array.} dispatchers - * @property {module:engine/conversion/downcast-converters~DowncastHelpers|module:engine/conversion/upcast-converters~UpcastHelpers} helpers + * @property {module:engine/conversion/downcasthelpers~DowncastHelpers|module:engine/conversion/upcasthelpers~UpcastHelpers} helpers */ // Helper function that creates a joint array out of an item passed in `definition.view` and items passed in @@ -609,8 +606,7 @@ function* _getUpcastDefinition( model, view, upcastAlso ) { } /** - * Base class for conversion utilises. - * + * Base class for conversion helpers. */ export class ConversionHelpers { /** @@ -630,8 +626,7 @@ export class ConversionHelpers { * method description * * @param {Function} conversionHelper The function to be called on event. - * @returns {module:engine/conversion/conversion~ConversionHelpers|module:engine/conversion/downcast-converters~DowncastHelpers| - * module:engine/conversion/upcast-converters~UpcastHelpers} + * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers| module:engine/conversion/upcasthelpers~UpcastHelpers} */ add( conversionHelper ) { this._addToDispatchers( conversionHelper ); diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcasthelpers.js similarity index 96% rename from src/conversion/downcast-converters.js rename to src/conversion/downcasthelpers.js index 4820ec54c..eb9f1c7a0 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcasthelpers.js @@ -17,7 +17,7 @@ import { cloneDeep } from 'lodash-es'; /** * Contains downcast (model-to-view) converters for {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}. * - * @module engine/conversion/downcast-converters + * @module engine/conversion/downcasthelpers */ /** @@ -66,7 +66,7 @@ export default class DowncastHelpers extends ConversionHelpers { * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function * that takes the model element and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer} * as parameters and returns a view container element. - * @returns {module:engine/conversion/downcast-converters~DowncastHelpers} + * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers} */ elementToElement( config ) { return this.add( _downcastElementToElement( config ) ); @@ -151,7 +151,7 @@ export default class DowncastHelpers extends ConversionHelpers { * as parameters and returns a view attribute element. If `config.model.values` is * given, `config.view` should be an object assigning values from `config.model.values` to view element definitions or functions. * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {module:engine/conversion/downcast-converters~DowncastHelpers} + * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers} */ attributeToElement( config ) { return this.add( _downcastAttributeToElement( config ) ); @@ -217,7 +217,7 @@ export default class DowncastHelpers extends ConversionHelpers { * If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to * `{ key, value }` objects or a functions. * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {module:engine/conversion/downcast-converters~DowncastHelpers} + * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers} */ attributeToAttribute( config ) { return this.add( _downcastAttributeToAttribute( config ) ); @@ -279,7 +279,7 @@ export default class DowncastHelpers extends ConversionHelpers { * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function * that takes the model marker data as a parameter and returns a view UI element. * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {module:engine/conversion/downcast-converters~DowncastHelpers} + * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers} */ markerToElement( config ) { return this.add( _downcastMarkerToElement( config ) ); @@ -289,7 +289,7 @@ export default class DowncastHelpers extends ConversionHelpers { * Model marker to highlight conversion helper. * * This conversion results in creating a highlight on view nodes. For this kind of conversion, - * {@link module:engine/conversion/downcast-converters~HighlightDescriptor} should be provided. + * {@link module:engine/conversion/downcasthelpers~HighlightDescriptor} should be provided. * * For text nodes, a `` {@link module:engine/view/attributeelement~AttributeElement} is created and it wraps all text nodes * in the converted marker range. For example, a model marker set like this: `F[oo b]ar` becomes @@ -326,7 +326,7 @@ export default class DowncastHelpers extends ConversionHelpers { * * If a function is passed as the `config.view` parameter, it will be used to generate the highlight descriptor. The function * receives the `data` object as a parameter and should return a - * {@link module:engine/conversion/downcast-converters~HighlightDescriptor highlight descriptor}. + * {@link module:engine/conversion/downcasthelpers~HighlightDescriptor highlight descriptor}. * The `data` object properties are passed from {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. * * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter @@ -335,10 +335,10 @@ export default class DowncastHelpers extends ConversionHelpers { * @method #markerToHighlight * @param {Object} config Conversion configuration. * @param {String} config.model The name of the model marker (or model marker group) to convert. - * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} config.view A highlight descriptor + * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} config.view A highlight descriptor * that will be used for highlighting or a function that takes the model marker data as a parameter and returns a highlight descriptor. * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {module:engine/conversion/downcast-converters~DowncastHelpers} + * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers} */ markerToHighlight( config ) { return this.add( _downcastMarkerToHighlight( config ) ); @@ -734,7 +734,7 @@ export function wrap( elementCreator ) { /** * Function factory that creates a converter which converts the text inside marker's range. The converter wraps the text with * {@link module:engine/view/attributeelement~AttributeElement} created from the provided descriptor. - * See {link module:engine/conversion/downcast-converters~createViewElementFromHighlightDescriptor}. + * See {link module:engine/conversion/downcasthelpers~createViewElementFromHighlightDescriptor}. * * It can also be used to convert the selection that is inside a marker. In that case, an empty attribute element will be * created and the selection will be put inside it. @@ -746,7 +746,7 @@ export function wrap( elementCreator ) { * This converter binds the created {@link module:engine/view/attributeelement~AttributeElement attribute elemens} with the marker name * using the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker} method. * - * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} highlightDescriptor + * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor * @returns {Function} */ export function highlightText( highlightDescriptor ) { @@ -809,7 +809,7 @@ export function highlightText( highlightDescriptor ) { * This converter binds altered {@link module:engine/view/containerelement~ContainerElement container elements} with the marker name using * the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker} method. * - * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} highlightDescriptor + * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor * @returns {Function} */ export function highlightElement( highlightDescriptor ) { @@ -856,7 +856,7 @@ export function highlightElement( highlightDescriptor ) { * Both text nodes and elements are handled by this converter but they are handled a bit differently. * * Text nodes are unwrapped using the {@link module:engine/view/attributeelement~AttributeElement attribute element} created from the - * provided highlight descriptor. See {link module:engine/conversion/downcast-converters~HighlightDescriptor}. + * provided highlight descriptor. See {link module:engine/conversion/downcasthelpers~HighlightDescriptor}. * * For elements, the converter checks if an element has the `removeHighlight` function stored as a * {@link module:engine/view/element~Element#_setCustomProperty custom property}. If so, it uses it to remove the highlight. @@ -871,7 +871,7 @@ export function highlightElement( highlightDescriptor ) { * * This converter unbinds elements from the marker name. * - * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} highlightDescriptor + * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor * @returns {Function} */ export function removeHighlight( highlightDescriptor ) { @@ -916,10 +916,10 @@ export function removeHighlight( highlightDescriptor ) { /** * Creates a `` {@link module:engine/view/attributeelement~AttributeElement view attribute element} from the information - * provided by the {@link module:engine/conversion/downcast-converters~HighlightDescriptor highlight descriptor} object. If a priority + * provided by the {@link module:engine/conversion/downcasthelpers~HighlightDescriptor highlight descriptor} object. If a priority * is not provided in the descriptor, the default priority will be used. * - * @param {module:engine/conversion/downcast-converters~HighlightDescriptor} descriptor + * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} descriptor * @returns {module:engine/view/attributeelement~AttributeElement} */ export function createViewElementFromHighlightDescriptor( descriptor ) { @@ -1062,7 +1062,7 @@ function _downcastMarkerToElement( config ) { // // @param {Object} config Conversion configuration. // @param {String} config.model The name of the model marker (or model marker group) to convert. -// @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} config.view A highlight descriptor +// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} config.view A highlight descriptor // that will be used for highlighting or a function that takes the model marker data as a parameter and returns a highlight descriptor. // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. // @returns {Function} Conversion helper. @@ -1217,7 +1217,7 @@ function _prepareDescriptor( highlightDescriptor, data, conversionApi ) { * * The descriptor `id` is passed to the `removeHighlight` function upon conversion and should be used to remove the highlight with the * given ID from the element. * - * @typedef {Object} module:engine/conversion/downcast-converters~HighlightDescriptor + * @typedef {Object} module:engine/conversion/downcasthelpers~HighlightDescriptor * * @property {String|Array.} classes A CSS class or an array of classes to set. If the descriptor is used to * create an {@link module:engine/view/attributeelement~AttributeElement attribute element} over text nodes, these classes will be set diff --git a/src/conversion/upcast-converters.js b/src/conversion/upcasthelpers.js similarity index 98% rename from src/conversion/upcast-converters.js rename to src/conversion/upcasthelpers.js index cecbabc97..91a790384 100644 --- a/src/conversion/upcast-converters.js +++ b/src/conversion/upcasthelpers.js @@ -13,7 +13,7 @@ import { cloneDeep } from 'lodash-es'; * Contains {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for * {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}. * - * @module engine/conversion/upcast-converters + * @module engine/conversion/upcasthelpers */ /** @@ -69,7 +69,7 @@ export default class UpcastHelpers extends ConversionHelpers { * @param {String|module:engine/model/element~Element|Function} config.model Name of the model element, a model element * instance or a function that takes a view element and returns a model element. The model element will be inserted in the model. * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {module:engine/conversion/upcast-converters~UpcastHelpers} + * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers} */ elementToElement( config ) { return this.add( _upcastElementToElement( config ) ); @@ -87,7 +87,7 @@ export default class UpcastHelpers extends ConversionHelpers { * * Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step). * Even though `` is over `

` element, `bold="true"` was added to the text. See - * {@link module:engine/conversion/upcast-converters~UpcastHelpers#attributeToAttribute} for comparison. + * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#attributeToAttribute} for comparison. * * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration. * @@ -155,7 +155,7 @@ export default class UpcastHelpers extends ConversionHelpers { * the model attribute. `value` property may be set as a function that takes a view element and returns the value. * If `String` is given, the model attribute value will be set to `true`. * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {module:engine/conversion/upcast-converters~UpcastHelpers} + * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers} */ elementToAttribute( config ) { return this.add( _upcastElementToAttribute( config ) ); @@ -173,7 +173,7 @@ export default class UpcastHelpers extends ConversionHelpers { *

foo
-->
foo
* * Above, `class="dark"` attribute is added only to the `
` elements that has it. This is in contrary to - * {@link module:engine/conversion/upcast-converters~UpcastHelpers#elementToAttribute} which sets attributes for + * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToAttribute} which sets attributes for * all the children in the model: * * Foo -->

Foo

--> <$text bold="true">Foo @@ -249,7 +249,7 @@ export default class UpcastHelpers extends ConversionHelpers { * the model attribute. `value` property may be set as a function that takes a view element and returns the value. * If `String` is given, the model attribute value will be same as view attribute value. * @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority. - * @returns {module:engine/conversion/upcast-converters~UpcastHelpers} + * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers} */ attributeToAttribute( config ) { return this.add( _upcastAttributeToAttribute( config ) ); @@ -298,7 +298,7 @@ export default class UpcastHelpers extends ConversionHelpers { * @param {String|Function} config.model Name of the model marker, or a function that takes a view element and returns * a model marker name. * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. - * @returns {module:engine/conversion/upcast-converters~UpcastHelpers} + * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers} */ elementToMarker( config ) { return this.add( _upcastElementToMarker( config ) ); diff --git a/src/dev-utils/model.js b/src/dev-utils/model.js index da73d5ccd..e5b224ad9 100644 --- a/src/dev-utils/model.js +++ b/src/dev-utils/model.js @@ -32,7 +32,7 @@ import { convertRangeSelection, convertCollapsedSelection, } from '../conversion/downcast-selection-converters'; -import { insertText, insertElement, wrap, insertUIElement } from '../conversion/downcast-converters'; +import { insertText, insertElement, wrap, insertUIElement } from '../conversion/downcasthelpers'; import { isPlainObject } from 'lodash-es'; import toMap from '@ckeditor/ckeditor5-utils/src/tomap'; diff --git a/src/model/markercollection.js b/src/model/markercollection.js index 63411115d..f266f0f7f 100644 --- a/src/model/markercollection.js +++ b/src/model/markercollection.js @@ -300,9 +300,9 @@ mix( MarkerCollection, EmitterMixin ); * * Markers downcast happens on {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker} and * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:removeMarker} events. - * Use {@link module:engine/conversion/downcast-converters downcast converters} or attach a custom converter to mentioned events. + * Use {@link module:engine/conversion/downcasthelpers downcast converters} or attach a custom converter to mentioned events. * For {@link module:engine/controller/datacontroller~DataController data pipeline}, marker should be downcasted to an element. - * Then, it can be upcasted back to a marker. Again, use {@link module:engine/conversion/upcast-converters upcast converters} or + * Then, it can be upcasted back to a marker. Again, use {@link module:engine/conversion/upcasthelpers upcast converters} or * attach a custom converter to {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element}. * * Another upside of markers is that finding marked part of document is fast and easy. Using attributes to mark some nodes diff --git a/tests/controller/datacontroller.js b/tests/controller/datacontroller.js index 3af2401fc..d9a78391a 100644 --- a/tests/controller/datacontroller.js +++ b/tests/controller/datacontroller.js @@ -18,8 +18,8 @@ import { parse as parseView, stringify as stringifyView } from '../../src/dev-ut import count from '@ckeditor/ckeditor5-utils/src/count'; -import UpcastHelpers from '../../src/conversion/upcast-converters'; -import DowncastHelpers from '../../src/conversion/downcast-converters'; +import UpcastHelpers from '../../src/conversion/upcasthelpers'; +import DowncastHelpers from '../../src/conversion/downcasthelpers'; describe( 'DataController', () => { let model, modelDocument, htmlDataProcessor, data, schema, upcastHelpers, downcastHelpers; diff --git a/tests/controller/editingcontroller.js b/tests/controller/editingcontroller.js index a9765a9c3..e87add671 100644 --- a/tests/controller/editingcontroller.js +++ b/tests/controller/editingcontroller.js @@ -14,7 +14,7 @@ import View from '../../src/view/view'; import Mapper from '../../src/conversion/mapper'; import DowncastDispatcher from '../../src/conversion/downcastdispatcher'; -import DowncastHelpers from '../../src/conversion/downcast-converters'; +import DowncastHelpers from '../../src/conversion/downcasthelpers'; import Model from '../../src/model/model'; import ModelPosition from '../../src/model/position'; import ModelRange from '../../src/model/range'; diff --git a/tests/conversion/conversion.js b/tests/conversion/conversion.js index c21efac9f..fbd337ce6 100644 --- a/tests/conversion/conversion.js +++ b/tests/conversion/conversion.js @@ -8,8 +8,8 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import UpcastDispatcher from '../../src/conversion/upcastdispatcher'; -import { upcastHelpers, convertText, convertToModelFragment } from '../../src/conversion/upcast-converters'; -import { downcastHelpers } from '../../src/conversion/downcast-converters'; +import UpcastHelpers, { convertText, convertToModelFragment } from '../../src/conversion/upcasthelpers'; +import DowncastHelpers from '../../src/conversion/downcasthelpers'; import EditingController from '../../src/controller/editingcontroller'; @@ -31,15 +31,15 @@ describe( 'Conversion', () => { dispA = Symbol( 'dispA' ); dispB = Symbol( 'dispB' ); - conversion.register( { name: 'ab', dispatcher: [ dispA, dispB ] } ); - conversion.register( { name: 'a', dispatcher: dispA } ); - conversion.register( { name: 'b', dispatcher: dispB } ); + conversion.register( 'ab', new UpcastHelpers( [ dispA, dispB ] ) ); + conversion.register( 'a', new UpcastHelpers( dispA ) ); + conversion.register( 'b', new UpcastHelpers( dispB ) ); } ); describe( 'register()', () => { it( 'should throw when trying to use same group name twice', () => { expect( () => { - conversion.register( { name: 'ab' } ); + conversion.register( 'ab' ); } ).to.throw( CKEditorError, /conversion-register-group-exists/ ); } ); } ); @@ -118,8 +118,8 @@ describe( 'Conversion', () => { viewDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } ); conversion = new Conversion(); - conversion.register( { name: 'upcast', dispatcher: [ viewDispatcher ], helpers: upcastHelpers } ); - conversion.register( { name: 'downcast', dispatcher: [ controller.downcastDispatcher ], helpers: downcastHelpers } ); + conversion.register( 'upcast', new UpcastHelpers( [ viewDispatcher ] ) ); + conversion.register( 'downcast', new DowncastHelpers( controller.downcastDispatcher ) ); } ); describe( 'elementToElement', () => { diff --git a/tests/conversion/downcast-selection-converters.js b/tests/conversion/downcast-selection-converters.js index 1dc9ee05d..f1545c355 100644 --- a/tests/conversion/downcast-selection-converters.js +++ b/tests/conversion/downcast-selection-converters.js @@ -23,7 +23,7 @@ import { highlightElement, highlightText, removeHighlight -} from '../../src/conversion/downcast-converters'; +} from '../../src/conversion/downcasthelpers'; import createViewRoot from '../view/_utils/createroot'; import { stringify as stringifyView } from '../../src/dev-utils/view'; diff --git a/tests/conversion/downcast-converters.js b/tests/conversion/downcasthelpers.js similarity index 99% rename from tests/conversion/downcast-converters.js rename to tests/conversion/downcasthelpers.js index 5f984f26c..16c993dec 100644 --- a/tests/conversion/downcast-converters.js +++ b/tests/conversion/downcasthelpers.js @@ -23,7 +23,7 @@ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import DowncastHelpers, { insertElement, insertUIElement, changeAttribute, wrap, removeUIElement, highlightElement, highlightText, removeHighlight, createViewElementFromHighlightDescriptor -} from '../../src/conversion/downcast-converters'; +} from '../../src/conversion/downcasthelpers'; import { stringify } from '../../src/dev-utils/view'; diff --git a/tests/conversion/upcast-converters.js b/tests/conversion/upcasthelpers.js similarity index 99% rename from tests/conversion/upcast-converters.js rename to tests/conversion/upcasthelpers.js index 05173dfdb..95ffddb46 100644 --- a/tests/conversion/upcast-converters.js +++ b/tests/conversion/upcasthelpers.js @@ -19,7 +19,7 @@ import ModelText from '../../src/model/text'; import ModelRange from '../../src/model/range'; import ModelPosition from '../../src/model/position'; -import UpcastHelpers, { convertToModelFragment, convertText } from '../../src/conversion/upcast-converters'; +import UpcastHelpers, { convertToModelFragment, convertText } from '../../src/conversion/upcasthelpers'; import { stringify } from '../../src/dev-utils/model'; From 00dc3ded504b9dbb2d2225e2f8d131b95b571446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 20 Dec 2018 16:46:20 +0100 Subject: [PATCH 60/84] Add tests for new conversion helpers API. --- tests/conversion/conversion.js | 206 ++++++---------------------- tests/conversion/downcasthelpers.js | 32 ++++- tests/conversion/upcasthelpers.js | 24 +++- 3 files changed, 90 insertions(+), 172 deletions(-) diff --git a/tests/conversion/conversion.js b/tests/conversion/conversion.js index fbd337ce6..c66a09e62 100644 --- a/tests/conversion/conversion.js +++ b/tests/conversion/conversion.js @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md. */ -import Conversion from '../../src/conversion/conversion'; +import Conversion, { ConversionHelpers } from '../../src/conversion/conversion'; import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import UpcastDispatcher from '../../src/conversion/upcastdispatcher'; @@ -16,13 +16,10 @@ import EditingController from '../../src/controller/editingcontroller'; import Model from '../../src/model/model'; import { parse as viewParse, stringify as viewStringify } from '../../src/dev-utils/view'; -import { setData, stringify as modelStringify } from '../../src/dev-utils/model'; -import ViewDocumentFragment from '../../src/view/documentfragment'; -import ViewText from '../../src/view/text'; -import ViewUIElement from '../../src/view/uielement'; +import { stringify as modelStringify } from '../../src/dev-utils/model'; describe( 'Conversion', () => { - let conversion, dispA, dispB; + let conversion, dispA, dispB, dispC; beforeEach( () => { conversion = new Conversion(); @@ -30,10 +27,12 @@ describe( 'Conversion', () => { // Placeholders. Will be used only to see if their were given as attribute for a spy function. dispA = Symbol( 'dispA' ); dispB = Symbol( 'dispB' ); + dispC = Symbol( 'dispC' ); conversion.register( 'ab', new UpcastHelpers( [ dispA, dispB ] ) ); conversion.register( 'a', new UpcastHelpers( dispA ) ); conversion.register( 'b', new UpcastHelpers( dispB ) ); + conversion.register( 'c', new DowncastHelpers( dispC ) ); } ); describe( 'register()', () => { @@ -45,10 +44,8 @@ describe( 'Conversion', () => { } ); describe( 'for()', () => { - it( 'should return object with .add() method', () => { - const forResult = conversion.for( 'ab' ); - - expect( forResult.add ).to.be.instanceof( Function ); + it( 'should return ConversionHelpers', () => { + expect( conversion.for( 'ab' ) ).to.be.instanceof( ConversionHelpers ); } ); it( 'should throw if non-existing group name has been used', () => { @@ -56,6 +53,13 @@ describe( 'Conversion', () => { conversion.for( 'foo' ); } ).to.throw( CKEditorError, /conversion-for-unknown-group/ ); } ); + + it( 'should return proper helpers for group', () => { + expect( conversion.for( 'ab' ) ).to.be.instanceof( UpcastHelpers ); + expect( conversion.for( 'a' ) ).to.be.instanceof( UpcastHelpers ); + expect( conversion.for( 'b' ) ).to.be.instanceof( UpcastHelpers ); + expect( conversion.for( 'c' ) ).to.be.instanceof( DowncastHelpers ); + } ); } ); describe( 'add()', () => { @@ -632,148 +636,6 @@ describe( 'Conversion', () => { } ); } ); - describe( 'for( \'downcast\' )', () => { - describe( 'elementToElement()', () => { - it( 'adds downcast converter', () => { - conversion.for( 'downcast' ).elementToElement( { model: 'paragraph', view: 'p' } ); - - testDowncast( 'foo', '

foo

' ); - } ); - } ); - - describe( 'attributeToElement()', () => { - it( 'adds downcast converter', () => { - conversion.for( 'downcast' ).elementToElement( { model: 'paragraph', view: 'p' } ); - conversion.for( 'downcast' ).attributeToElement( { model: 'bold', view: 'strong' } ); - - testDowncast( '<$text bold="true">Foo bar', '

Foo bar

' ); - } ); - } ); - - describe( 'attributeToAttribute()', () => { - it( 'adds downcast converter', () => { - schema.register( 'image', { - inheritAllFrom: '$block', - allowAttributes: [ 'source' ] - } ); - - conversion.for( 'downcast' ).elementToElement( { model: 'image', view: 'img' } ); - conversion.for( 'downcast' ).attributeToAttribute( { model: 'source', view: 'src' } ); - - testDowncast( '', '' ); - } ); - } ); - - describe( 'markerToElement()', () => { - it( 'adds downcast converter', () => { - conversion.for( 'downcast' ).markerToElement( { model: 'search', view: 'marker-search' } ); - - model.change( writer => { - writer.insertText( 'foo', modelRoot, 0 ); - - const range = writer.createRange( - writer.createPositionAt( modelRoot, 1 ), - writer.createPositionAt( modelRoot, 2 ) - ); - writer.addMarker( 'search', { range, usingOperation: false } ); - } ); - - expect( viewStringify( viewRoot, null, { ignoreRoot: true } ) ) - .to.equal( 'foo' ); - } ); - } ); - - describe( 'markerToHighlight()', () => { - it( 'adds downcast converter', () => { - conversion.for( 'downcast' ).markerToHighlight( { model: 'comment', view: { classes: 'comment' } } ); - - model.change( writer => { - writer.insertText( 'foo', modelRoot, 0 ); - const range = writer.createRange( - writer.createPositionAt( modelRoot, 0 ), - writer.createPositionAt( modelRoot, 3 ) - ); - writer.addMarker( 'comment', { range, usingOperation: false } ); - } ); - - expect( viewStringify( viewRoot, null, { ignoreRoot: true } ) ) - .to.equal( 'foo' ); - } ); - } ); - } ); - - describe( 'for( \'upcast\' )', () => { - describe( 'elementToElement()', () => { - it( 'adds upcast converter', () => { - conversion.for( 'upcast' ).elementToElement( { model: 'paragraph', view: 'p' } ); - // TODO this shouldn't be required - conversion.for( 'downcast' ).elementToElement( { model: 'paragraph', view: 'p' } ); - - testUpcast( '

foo

', 'foo' ); - } ); - } ); - - describe( 'elementToAttribute()', () => { - it( 'adds upcast converter', () => { - conversion.for( 'upcast' ).elementToElement( { model: 'paragraph', view: 'p' } ); - conversion.for( 'downcast' ).elementToElement( { model: 'paragraph', view: 'p' } ); - - conversion.for( 'upcast' ).elementToAttribute( { model: 'bold', view: 'strong' } ); - conversion.for( 'downcast' ).attributeToElement( { model: 'bold', view: 'strong' } ); - - testUpcast( '

Foo bar

', '<$text bold="true">Foo bar' ); - } ); - } ); - - describe( 'attributeToAttribute()', () => { - it( 'adds upcast converter', () => { - schema.register( 'image', { - inheritAllFrom: '$block', - allowAttributes: [ 'source' ] - } ); - - conversion.for( 'downcast' ).elementToElement( { model: 'image', view: 'img' } ); - conversion.for( 'downcast' ).attributeToAttribute( { model: 'source', view: 'src' } ); - - conversion.for( 'upcast' ).elementToElement( { model: 'image', view: 'img' } ); - conversion.for( 'upcast' ).attributeToAttribute( { model: 'source', view: 'src' } ); - - testUpcast( '', '' ); - } ); - } ); - - describe( 'markerToElement()', () => { - it( 'adds upcast converter', () => { - conversion.for( 'upcast' ).elementToMarker( { model: 'search', view: 'marker-search' } ); - conversion.for( 'downcast' ).markerToElement( { model: 'search', view: 'marker-search' } ); - - const frag = new ViewDocumentFragment( [ - new ViewText( 'fo' ), - new ViewUIElement( 'marker-search' ), - new ViewText( 'oba' ), - new ViewUIElement( 'marker-search' ), - new ViewText( 'r' ) - ] ); - - const marker = { name: 'search', start: [ 2 ], end: [ 5 ] }; - - testUpcastMarker( frag, 'foobar', marker ); - } ); - } ); - } ); - - function testDowncast( input, expectedView ) { - setData( model, input ); - - expect( viewStringify( viewRoot, null, { ignoreRoot: true } ) ).to.equal( expectedView ); - } - - function testUpcast( input, expectedModel ) { - loadData( input ); - - expect( modelStringify( model.document.getRoot() ) ).to.equal( expectedModel ); - } - function test( input, expectedModel, expectedView = null ) { loadData( input ); @@ -797,20 +659,40 @@ describe( 'Conversion', () => { writer.insert( convertedModel, modelRoot, 0 ); } ); } + } ); +} ); - function testUpcastMarker( viewToConvert, modelString, marker ) { - const conversionResult = model.change( writer => viewDispatcher.convert( viewToConvert, writer ) ); +describe( 'ConversionHelpers', () => { + describe( 'add()', () => { + const dispA = Symbol( 'dispA' ); + const dispB = Symbol( 'dispB' ); - if ( marker ) { - expect( conversionResult.markers.has( marker.name ) ).to.be.true; + it( 'should call a helper for one defined dispatcher', () => { + const spy = sinon.spy(); + const helpers = new ConversionHelpers( dispA ); - const convertedMarker = conversionResult.markers.get( marker.name ); + helpers.add( spy ); - expect( convertedMarker.start.path ).to.deep.equal( marker.start ); - expect( convertedMarker.end.path ).to.deep.equal( marker.end ); - } + sinon.assert.calledOnce( spy ); + sinon.assert.calledWithExactly( spy, dispA ); + } ); - expect( viewStringify( conversionResult ) ).to.equal( modelString ); - } + it( 'should call helper for all defined dispatcherers', () => { + const spy = sinon.spy(); + const helpers = new ConversionHelpers( [ dispA, dispB ] ); + + helpers.add( spy ); + + sinon.assert.calledTwice( spy ); + sinon.assert.calledWithExactly( spy, dispA ); + sinon.assert.calledWithExactly( spy, dispB ); + } ); + + it( 'should be chainable', () => { + const spy = sinon.spy(); + const helpers = new ConversionHelpers( dispA ); + + expect( helpers.add( spy ) ).to.equal( helpers ); + } ); } ); } ); diff --git a/tests/conversion/downcasthelpers.js b/tests/conversion/downcasthelpers.js index 16c993dec..c8582a38d 100644 --- a/tests/conversion/downcasthelpers.js +++ b/tests/conversion/downcasthelpers.js @@ -27,7 +27,7 @@ import DowncastHelpers, { import { stringify } from '../../src/dev-utils/view'; -describe( 'downcast-helpers', () => { +describe( 'DowncastHelpers', () => { let conversion, model, modelRoot, viewRoot, downcastHelpers; beforeEach( () => { @@ -49,7 +49,11 @@ describe( 'downcast-helpers', () => { conversion.register( 'downcast', downcastHelpers ); } ); - describe( '_downcastElementToElement', () => { + describe( 'elementToElement()', () => { + it( 'should be chainable', () => { + expect( downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ) ).to.equal( downcastHelpers ); + } ); + it( 'config.view is a string', () => { downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); @@ -101,7 +105,11 @@ describe( 'downcast-helpers', () => { } ); } ); - describe( '_downcastAttributeToElement', () => { + describe( 'attributeToElement()', () => { + it( 'should be chainable', () => { + expect( downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } ) ).to.equal( downcastHelpers ); + } ); + it( 'config.view is a string', () => { downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } ); @@ -245,13 +253,17 @@ describe( 'downcast-helpers', () => { } ); } ); - describe( '_downcastAttributeToAttribute', () => { + describe( 'attributeToAttribute()', () => { testUtils.createSinonSandbox(); beforeEach( () => { downcastHelpers.elementToElement( { model: 'image', view: 'img' } ); } ); + it( 'should be chainable', () => { + expect( downcastHelpers.attributeToAttribute( { model: 'source', view: 'src' } ) ).to.equal( downcastHelpers ); + } ); + it( 'config.view is a string', () => { downcastHelpers.attributeToAttribute( { model: 'source', view: 'src' } ); @@ -482,7 +494,11 @@ describe( 'downcast-helpers', () => { } ); } ); - describe( '_downcastMarkerToElement', () => { + describe( 'markerToElement()', () => { + it( 'should be chainable', () => { + expect( downcastHelpers.markerToElement( { model: 'search', view: 'marker-search' } ) ).to.equal( downcastHelpers ); + } ); + it( 'config.view is a string', () => { downcastHelpers.markerToElement( { model: 'search', view: 'marker-search' } ); @@ -547,7 +563,11 @@ describe( 'downcast-helpers', () => { } ); } ); - describe( '_downcastMarkerToHighlight', () => { + describe( 'markerToHighlight()', () => { + it( 'should be chainable', () => { + expect( downcastHelpers.markerToHighlight( { model: 'comment', view: { classes: 'comment' } } ) ).to.equal( downcastHelpers ); + } ); + it( 'config.view is a highlight descriptor', () => { downcastHelpers.markerToHighlight( { model: 'comment', view: { classes: 'comment' } } ); diff --git a/tests/conversion/upcasthelpers.js b/tests/conversion/upcasthelpers.js index 95ffddb46..c7951e41f 100644 --- a/tests/conversion/upcasthelpers.js +++ b/tests/conversion/upcasthelpers.js @@ -50,7 +50,11 @@ describe( 'UpcastHelpers', () => { conversion.register( 'upcast', upcastHelpers ); } ); - describe( '_upcastElementToElement', () => { + describe( '.elementToElement()', () => { + it( 'should be chainable', () => { + expect( upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } ) ).to.equal( upcastHelpers ); + } ); + it( 'config.view is a string', () => { upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } ); @@ -143,7 +147,11 @@ describe( 'UpcastHelpers', () => { } ); } ); - describe( '_upcastElementToAttribute', () => { + describe( '.elementToAttribute()', () => { + it( 'should be chainable', () => { + expect( upcastHelpers.elementToAttribute( { view: 'strong', model: 'bold' } ) ).to.equal( upcastHelpers ); + } ); + it( 'config.view is string', () => { upcastHelpers.elementToAttribute( { view: 'strong', model: 'bold' } ); @@ -334,7 +342,7 @@ describe( 'UpcastHelpers', () => { } ); } ); - describe( '_upcastAttributeToAttribute', () => { + describe( '.attributeToAttribute()', () => { beforeEach( () => { upcastHelpers.elementToElement( { view: 'img', model: 'image' } ); @@ -343,6 +351,10 @@ describe( 'UpcastHelpers', () => { } ); } ); + it( 'should be chainable', () => { + expect( upcastHelpers.attributeToAttribute( { view: 'src', model: 'source' } ) ).to.equal( upcastHelpers ); + } ); + it( 'config.view is a string', () => { schema.extend( 'image', { allowAttributes: [ 'source' ] @@ -544,7 +556,11 @@ describe( 'UpcastHelpers', () => { } ); } ); - describe( '_upcastElementToMarker', () => { + describe( '.elementToMarker()', () => { + it( 'should be chainable', () => { + expect( upcastHelpers.elementToMarker( { view: 'marker-search', model: 'search' } ) ).to.equal( upcastHelpers ); + } ); + it( 'config.view is a string', () => { upcastHelpers.elementToMarker( { view: 'marker-search', model: 'search' } ); From a49fd54dbbe917746d1065ec36b172f1c4d7c876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 20 Dec 2018 16:53:27 +0100 Subject: [PATCH 61/84] Fix a typo in docs of conversion.for() method. --- src/conversion/conversion.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/conversion/conversion.js b/src/conversion/conversion.js index a66752596..59f8d0b81 100644 --- a/src/conversion/conversion.js +++ b/src/conversion/conversion.js @@ -135,7 +135,7 @@ export default class Conversion { * editor.conversion.for( 'upcast' ).elementToElement( config ) ); * * @param {String} groupName The name of dispatchers group to add the converters to. - * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers| module:engine/conversion/upcasthelpers~UpcastHelpers} + * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers|module:engine/conversion/upcasthelpers~UpcastHelpers} */ for( groupName ) { const group = this._getDispatchersGroup( groupName ); @@ -626,7 +626,7 @@ export class ConversionHelpers { * method description * * @param {Function} conversionHelper The function to be called on event. - * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers| module:engine/conversion/upcasthelpers~UpcastHelpers} + * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers|module:engine/conversion/upcasthelpers~UpcastHelpers} */ add( conversionHelper ) { this._addToDispatchers( conversionHelper ); From 22d9ae9e4f0b674ddb3548c8d8192c07b9598f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 21 Dec 2018 12:35:18 +0100 Subject: [PATCH 62/84] Remove removeUIElement(), changeAttribute(), highlightText(), highlightElement() and removeHighlight() from API. --- src/conversion/downcasthelpers.js | 286 ++-- .../downcast-selection-converters.js | 28 +- tests/conversion/downcasthelpers.js | 1281 ++++++++--------- 3 files changed, 797 insertions(+), 798 deletions(-) diff --git a/src/conversion/downcasthelpers.js b/src/conversion/downcasthelpers.js index eb9f1c7a0..abe2b4912 100644 --- a/src/conversion/downcasthelpers.js +++ b/src/conversion/downcasthelpers.js @@ -345,50 +345,6 @@ export default class DowncastHelpers extends ConversionHelpers { } } -/** - * Function factory that creates a converter which converts node insertion changes from the model to the view. - * The function passed will be provided with all the parameters of the dispatcher's - * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert `insert` event}. - * It is expected that the function returns an {@link module:engine/view/element~Element}. - * The result of the function will be inserted into the view. - * - * The converter automatically consumes the corresponding value from the consumables list, stops the event (see - * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}) and binds the model and view elements. - * - * downcastDispatcher.on( - * 'insert:myElem', - * insertElement( ( modelItem, viewWriter ) => { - * const text = viewWriter.createText( 'myText' ); - * const myElem = viewWriter.createElement( 'myElem', { myAttr: 'my-' + modelItem.getAttribute( 'myAttr' ) }, text ); - * - * // Do something fancy with `myElem` using `modelItem` or other parameters. - * - * return myElem; - * } - * ) ); - * - * @param {Function} elementCreator Function returning a view element, which will be inserted. - * @returns {Function} Insert element event converter. - */ -export function insertElement( elementCreator ) { - return ( evt, data, conversionApi ) => { - const viewElement = elementCreator( data.item, conversionApi.writer ); - - if ( !viewElement ) { - return; - } - - if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) { - return; - } - - const viewPosition = conversionApi.mapper.toViewPosition( data.range.start ); - - conversionApi.mapper.bindElements( data.item, viewElement ); - conversionApi.writer.insert( viewPosition, viewElement ); - }; -} - /** * Function factory that creates a default downcast converter for text insertion changes. * @@ -441,6 +397,145 @@ export function remove() { }; } +/** + * Creates a `` {@link module:engine/view/attributeelement~AttributeElement view attribute element} from the information + * provided by the {@link module:engine/conversion/downcasthelpers~HighlightDescriptor highlight descriptor} object. If a priority + * is not provided in the descriptor, the default priority will be used. + * + * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} descriptor + * @returns {module:engine/view/attributeelement~AttributeElement} + */ +export function createViewElementFromHighlightDescriptor( descriptor ) { + const viewElement = new ViewAttributeElement( 'span', descriptor.attributes ); + + if ( descriptor.classes ) { + viewElement._addClass( descriptor.classes ); + } + + if ( descriptor.priority ) { + viewElement._priority = descriptor.priority; + } + + viewElement._id = descriptor.id; + + return viewElement; +} + +// only: insertText, insertElement, wrap, insertUIElement + +/** + * Function factory that creates a converter which converts set/change/remove attribute changes from the model to the view. + * It can also be used to convert selection attributes. In that case, an empty attribute element will be created and the + * selection will be put inside it. + * + * Attributes from the model are converted to a view element that will be wrapping these view nodes that are bound to + * model elements having the given attribute. This is useful for attributes like `bold` that may be set on text nodes in the model + * but are represented as an element in the view: + * + * [paragraph] MODEL ====> VIEW

+ * |- a {bold: true} |- + * |- b {bold: true} | |- ab + * |- c |- c + * + * Passed `Function` will be provided with the attribute value and then all the parameters of the + * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute `attribute` event}. + * It is expected that the function returns an {@link module:engine/view/element~Element}. + * The result of the function will be the wrapping element. + * When the provided `Function` does not return any element, no conversion will take place. + * + * The converter automatically consumes the corresponding value from the consumables list and stops the event (see + * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}). + * + * modelDispatcher.on( 'attribute:bold', wrapItem( ( modelAttributeValue, viewWriter ) => { + * return viewWriter.createAttributeElement( 'strong' ); + * } ); + * + * @param {Function} elementCreator Function returning a view element that will be used for wrapping. + * @returns {Function} Set/change attribute converter. + */ +export function wrap( elementCreator ) { + return ( evt, data, conversionApi ) => { + // Recreate current wrapping node. It will be used to unwrap view range if the attribute value has changed + // or the attribute was removed. + const oldViewElement = elementCreator( data.attributeOldValue, conversionApi.writer ); + + // Create node to wrap with. + const newViewElement = elementCreator( data.attributeNewValue, conversionApi.writer ); + + if ( !oldViewElement && !newViewElement ) { + return; + } + + if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { + return; + } + + const viewWriter = conversionApi.writer; + const viewSelection = viewWriter.document.selection; + + if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) { + // Selection attribute conversion. + viewWriter.wrap( viewSelection.getFirstRange(), newViewElement ); + } else { + // Node attribute conversion. + let viewRange = conversionApi.mapper.toViewRange( data.range ); + + // First, unwrap the range from current wrapper. + if ( data.attributeOldValue !== null && oldViewElement ) { + viewRange = viewWriter.unwrap( viewRange, oldViewElement ); + } + + if ( data.attributeNewValue !== null && newViewElement ) { + viewWriter.wrap( viewRange, newViewElement ); + } + } + }; +} + +/** + * Function factory that creates a converter which converts node insertion changes from the model to the view. + * The function passed will be provided with all the parameters of the dispatcher's + * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert `insert` event}. + * It is expected that the function returns an {@link module:engine/view/element~Element}. + * The result of the function will be inserted into the view. + * + * The converter automatically consumes the corresponding value from the consumables list, stops the event (see + * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}) and binds the model and view elements. + * + * downcastDispatcher.on( + * 'insert:myElem', + * insertElement( ( modelItem, viewWriter ) => { + * const text = viewWriter.createText( 'myText' ); + * const myElem = viewWriter.createElement( 'myElem', { myAttr: 'my-' + modelItem.getAttribute( 'myAttr' ) }, text ); + * + * // Do something fancy with `myElem` using `modelItem` or other parameters. + * + * return myElem; + * } + * ) ); + * + * @param {Function} elementCreator Function returning a view element, which will be inserted. + * @returns {Function} Insert element event converter. + */ +export function insertElement( elementCreator ) { + return ( evt, data, conversionApi ) => { + const viewElement = elementCreator( data.item, conversionApi.writer ); + + if ( !viewElement ) { + return; + } + + if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) { + return; + } + + const viewPosition = conversionApi.mapper.toViewPosition( data.range.start ); + + conversionApi.mapper.bindElements( data.item, viewElement ); + conversionApi.writer.insert( viewPosition, viewElement ); + }; +} + /** * Function factory that creates a converter which converts marker adding change to the * {@link module:engine/view/uielement~UIElement view UI element}. @@ -510,7 +605,7 @@ export function insertUIElement( elementCreator ) { * * @returns {Function} Removed UI element converter. */ -export function removeUIElement() { +function removeUIElement() { return ( evt, data, conversionApi ) => { const elements = conversionApi.mapper.markerNameToElements( data.markerName ); @@ -561,7 +656,7 @@ export function removeUIElement() { * The function is passed the model attribute value as the first parameter and additional data about the change as the second parameter. * @returns {Function} Set/change attribute converter. */ -export function changeAttribute( attributeCreator ) { +function changeAttribute( attributeCreator ) { attributeCreator = attributeCreator || ( ( value, data ) => ( { value, key: data.attributeKey } ) ); return ( evt, data, conversionApi ) => { @@ -662,75 +757,6 @@ export function changeAttribute( attributeCreator ) { }; } -/** - * Function factory that creates a converter which converts set/change/remove attribute changes from the model to the view. - * It can also be used to convert selection attributes. In that case, an empty attribute element will be created and the - * selection will be put inside it. - * - * Attributes from the model are converted to a view element that will be wrapping these view nodes that are bound to - * model elements having the given attribute. This is useful for attributes like `bold` that may be set on text nodes in the model - * but are represented as an element in the view: - * - * [paragraph] MODEL ====> VIEW

- * |- a {bold: true} |- - * |- b {bold: true} | |- ab - * |- c |- c - * - * Passed `Function` will be provided with the attribute value and then all the parameters of the - * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute `attribute` event}. - * It is expected that the function returns an {@link module:engine/view/element~Element}. - * The result of the function will be the wrapping element. - * When the provided `Function` does not return any element, no conversion will take place. - * - * The converter automatically consumes the corresponding value from the consumables list and stops the event (see - * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}). - * - * modelDispatcher.on( 'attribute:bold', wrapItem( ( modelAttributeValue, viewWriter ) => { - * return viewWriter.createAttributeElement( 'strong' ); - * } ); - * - * @param {Function} elementCreator Function returning a view element that will be used for wrapping. - * @returns {Function} Set/change attribute converter. - */ -export function wrap( elementCreator ) { - return ( evt, data, conversionApi ) => { - // Recreate current wrapping node. It will be used to unwrap view range if the attribute value has changed - // or the attribute was removed. - const oldViewElement = elementCreator( data.attributeOldValue, conversionApi.writer ); - - // Create node to wrap with. - const newViewElement = elementCreator( data.attributeNewValue, conversionApi.writer ); - - if ( !oldViewElement && !newViewElement ) { - return; - } - - if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { - return; - } - - const viewWriter = conversionApi.writer; - const viewSelection = viewWriter.document.selection; - - if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) { - // Selection attribute conversion. - viewWriter.wrap( viewSelection.getFirstRange(), newViewElement ); - } else { - // Node attribute conversion. - let viewRange = conversionApi.mapper.toViewRange( data.range ); - - // First, unwrap the range from current wrapper. - if ( data.attributeOldValue !== null && oldViewElement ) { - viewRange = viewWriter.unwrap( viewRange, oldViewElement ); - } - - if ( data.attributeNewValue !== null && newViewElement ) { - viewWriter.wrap( viewRange, newViewElement ); - } - } - }; -} - /** * Function factory that creates a converter which converts the text inside marker's range. The converter wraps the text with * {@link module:engine/view/attributeelement~AttributeElement} created from the provided descriptor. @@ -749,7 +775,7 @@ export function wrap( elementCreator ) { * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor * @returns {Function} */ -export function highlightText( highlightDescriptor ) { +function highlightText( highlightDescriptor ) { return ( evt, data, conversionApi ) => { if ( data.markerRange.isCollapsed ) { return; @@ -812,7 +838,7 @@ export function highlightText( highlightDescriptor ) { * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor * @returns {Function} */ -export function highlightElement( highlightDescriptor ) { +function highlightElement( highlightDescriptor ) { return ( evt, data, conversionApi ) => { if ( data.markerRange.isCollapsed ) { return; @@ -874,7 +900,7 @@ export function highlightElement( highlightDescriptor ) { * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor * @returns {Function} */ -export function removeHighlight( highlightDescriptor ) { +function removeHighlight( highlightDescriptor ) { return ( evt, data, conversionApi ) => { // This conversion makes sense only for non-collapsed range. if ( data.markerRange.isCollapsed ) { @@ -914,30 +940,6 @@ export function removeHighlight( highlightDescriptor ) { }; } -/** - * Creates a `` {@link module:engine/view/attributeelement~AttributeElement view attribute element} from the information - * provided by the {@link module:engine/conversion/downcasthelpers~HighlightDescriptor highlight descriptor} object. If a priority - * is not provided in the descriptor, the default priority will be used. - * - * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} descriptor - * @returns {module:engine/view/attributeelement~AttributeElement} - */ -export function createViewElementFromHighlightDescriptor( descriptor ) { - const viewElement = new ViewAttributeElement( 'span', descriptor.attributes ); - - if ( descriptor.classes ) { - viewElement._addClass( descriptor.classes ); - } - - if ( descriptor.priority ) { - viewElement._priority = descriptor.priority; - } - - viewElement._id = descriptor.id; - - return viewElement; -} - // Model element to view element conversion helper. // // See {@link ~DowncastHelpers#elementToElement `.elementToElement()` downcast helper} for examples. diff --git a/tests/conversion/downcast-selection-converters.js b/tests/conversion/downcast-selection-converters.js index f1545c355..d342918a0 100644 --- a/tests/conversion/downcast-selection-converters.js +++ b/tests/conversion/downcast-selection-converters.js @@ -16,13 +16,10 @@ import { clearAttributes, } from '../../src/conversion/downcast-selection-converters'; -import { +import DowncastHelpers, { insertElement, insertText, - wrap, - highlightElement, - highlightText, - removeHighlight + wrap } from '../../src/conversion/downcasthelpers'; import createViewRoot from '../view/_utils/createroot'; @@ -30,7 +27,7 @@ import { stringify as stringifyView } from '../../src/dev-utils/view'; import { setData as setModelData } from '../../src/dev-utils/model'; describe( 'downcast-selection-converters', () => { - let dispatcher, mapper, model, view, modelDoc, modelRoot, docSelection, viewDoc, viewRoot, viewSelection, highlightDescriptor; + let dispatcher, mapper, model, view, modelDoc, modelRoot, docSelection, viewDoc, viewRoot, viewSelection, downcastHelpers; beforeEach( () => { model = new Model(); @@ -48,8 +45,6 @@ describe( 'downcast-selection-converters', () => { mapper = new Mapper(); mapper.bindElements( modelRoot, viewRoot ); - highlightDescriptor = { classes: 'marker', priority: 1 }; - dispatcher = new DowncastDispatcher( { mapper, viewSelection } ); dispatcher.on( 'insert:$text', insertText() ); @@ -57,9 +52,8 @@ describe( 'downcast-selection-converters', () => { const strongCreator = ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'strong' ); dispatcher.on( 'attribute:bold', wrap( strongCreator ) ); - dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) ); - dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) ); - dispatcher.on( 'removeMarker:marker', removeHighlight( highlightDescriptor ) ); + downcastHelpers = new DowncastHelpers( dispatcher ); + downcastHelpers.markerToHighlight( { model: 'marker', view: { classes: 'marker' }, converterPriority: 1 } ); // Default selection converters. dispatcher.on( 'selection', clearAttributes(), { priority: 'low' } ); @@ -241,9 +235,10 @@ describe( 'downcast-selection-converters', () => { } ); it( 'in marker - using highlight descriptor creator', () => { - dispatcher.on( 'addMarker:marker2', highlightText( - data => ( { classes: data.markerName } ) - ) ); + downcastHelpers.markerToHighlight( { + model: 'marker2', + view: data => ( { classes: data.markerName } ) + } ); setModelData( model, 'foobar' ); @@ -269,7 +264,10 @@ describe( 'downcast-selection-converters', () => { } ); it( 'should do nothing if creator return null', () => { - dispatcher.on( 'addMarker:marker3', highlightText( () => null ) ); + downcastHelpers.markerToHighlight( { + model: 'marker3', + view: () => null + } ); setModelData( model, 'foobar' ); diff --git a/tests/conversion/downcasthelpers.js b/tests/conversion/downcasthelpers.js index c8582a38d..b0b8c895d 100644 --- a/tests/conversion/downcasthelpers.js +++ b/tests/conversion/downcasthelpers.js @@ -21,8 +21,7 @@ import log from '@ckeditor/ckeditor5-utils/src/log'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import DowncastHelpers, { - insertElement, insertUIElement, changeAttribute, wrap, removeUIElement, - highlightElement, highlightText, removeHighlight, createViewElementFromHighlightDescriptor + insertElement, wrap, createViewElementFromHighlightDescriptor } from '../../src/conversion/downcasthelpers'; import { stringify } from '../../src/dev-utils/view'; @@ -644,7 +643,7 @@ describe( 'downcast-converters', () => { ) ); - dispatcher.on( 'attribute:class', changeAttribute() ); + // dispatcher.on( 'attribute:class', changeAttribute() ); modelRootStart = model.createPositionAt( modelRoot, 0 ); } ); @@ -735,95 +734,95 @@ describe( 'downcast-converters', () => { expect( viewToString( viewRoot ) ).to.equal( '

' ); } ); } ); - - describe( 'changeAttribute', () => { - it( 'should convert attribute insert/change/remove on a model node', () => { - const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) ); - - model.change( writer => { - writer.insert( modelElement, modelRootStart ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - - model.change( writer => { - writer.setAttribute( 'class', 'bar', modelElement ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - - model.change( writer => { - writer.removeAttribute( 'class', modelElement ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - } ); - - it( 'should convert insert/change/remove with attribute generating function as a parameter', () => { - const themeConverter = ( value, data ) => { - if ( data.item instanceof ModelElement && data.item.childCount > 0 ) { - value += ' fix-content'; - } - - return { key: 'class', value }; - }; - - dispatcher.on( 'insert:div', insertElement( ( modelElement, viewWriter ) => viewWriter.createContainerElement( 'div' ) ) ); - dispatcher.on( 'attribute:theme', changeAttribute( themeConverter ) ); - - const modelParagraph = new ModelElement( 'paragraph', { theme: 'nice' }, new ModelText( 'foobar' ) ); - const modelDiv = new ModelElement( 'div', { theme: 'nice' } ); - - model.change( writer => { - writer.insert( [ modelParagraph, modelDiv ], modelRootStart ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - - model.change( writer => { - writer.setAttribute( 'theme', 'awesome', modelParagraph ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - - model.change( writer => { - writer.removeAttribute( 'theme', modelParagraph ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - } ); - - it( 'should be possible to override setAttribute', () => { - const modelElement = new ModelElement( 'paragraph', { classes: 'foo' }, new ModelText( 'foobar' ) ); - - dispatcher.on( 'attribute:class', ( evt, data, conversionApi ) => { - conversionApi.consumable.consume( data.item, 'attribute:class' ); - }, { converterPriority: 'high' } ); - - model.change( writer => { - writer.insert( modelElement, modelRootStart ); - } ); - - // No attribute set. - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - } ); - - it( 'should not convert or consume if element creator returned null', () => { - const callback = sinon.stub().returns( null ); - - dispatcher.on( 'attribute:class', changeAttribute( callback ) ); - - const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) ); - - model.change( writer => { - writer.insert( modelElement, modelRootStart ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - - sinon.assert.called( callback ); - } ); - } ); + // + // describe( 'changeAttribute', () => { + // it( 'should convert attribute insert/change/remove on a model node', () => { + // const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) ); + // + // model.change( writer => { + // writer.insert( modelElement, modelRootStart ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + // + // model.change( writer => { + // writer.setAttribute( 'class', 'bar', modelElement ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + // + // model.change( writer => { + // writer.removeAttribute( 'class', modelElement ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + // } ); + // + // it( 'should convert insert/change/remove with attribute generating function as a parameter', () => { + // const themeConverter = ( value, data ) => { + // if ( data.item instanceof ModelElement && data.item.childCount > 0 ) { + // value += ' fix-content'; + // } + // + // return { key: 'class', value }; + // }; + // + // dispatcher.on( 'insert:div', insertElement( ( modelElement, viewWriter ) => viewWriter.createContainerElement( 'div' ) ) ); + // dispatcher.on( 'attribute:theme', changeAttribute( themeConverter ) ); + // + // const modelParagraph = new ModelElement( 'paragraph', { theme: 'nice' }, new ModelText( 'foobar' ) ); + // const modelDiv = new ModelElement( 'div', { theme: 'nice' } ); + // + // model.change( writer => { + // writer.insert( [ modelParagraph, modelDiv ], modelRootStart ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + // + // model.change( writer => { + // writer.setAttribute( 'theme', 'awesome', modelParagraph ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + // + // model.change( writer => { + // writer.removeAttribute( 'theme', modelParagraph ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + // } ); + // + // it( 'should be possible to override setAttribute', () => { + // const modelElement = new ModelElement( 'paragraph', { classes: 'foo' }, new ModelText( 'foobar' ) ); + // + // dispatcher.on( 'attribute:class', ( evt, data, conversionApi ) => { + // conversionApi.consumable.consume( data.item, 'attribute:class' ); + // }, { converterPriority: 'high' } ); + // + // model.change( writer => { + // writer.insert( modelElement, modelRootStart ); + // } ); + // + // // No attribute set. + // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + // } ); + // + // it( 'should not convert or consume if element creator returned null', () => { + // const callback = sinon.stub().returns( null ); + // + // dispatcher.on( 'attribute:class', changeAttribute( callback ) ); + // + // const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) ); + // + // model.change( writer => { + // writer.insert( modelElement, modelRootStart ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + // + // sinon.assert.called( callback ); + // } ); + // } ); describe( 'wrap', () => { it( 'should convert insert/change/remove of attribute in model into wrapping element in a view', () => { @@ -955,170 +954,170 @@ describe( 'downcast-converters', () => { } ); } ); - describe( 'insertUIElement/removeUIElement', () => { - let modelText, modelElement, range; - - beforeEach( () => { - modelText = new ModelText( 'foobar' ); - modelElement = new ModelElement( 'paragraph', null, modelText ); - - model.change( writer => { - writer.insert( modelElement, modelRootStart ); - } ); - } ); - - describe( 'collapsed range', () => { - beforeEach( () => { - range = model.createRange( model.createPositionAt( modelElement, 3 ), model.createPositionAt( modelElement, 3 ) ); - } ); - - it( 'should insert and remove ui element', () => { - const creator = ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } ); - - dispatcher.on( 'addMarker:marker', insertUIElement( creator ) ); - dispatcher.on( 'removeMarker:marker', removeUIElement( creator ) ); - - model.change( writer => { - writer.addMarker( 'marker', { range, usingOperation: false } ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - - model.change( writer => { - writer.removeMarker( 'marker' ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - } ); - - it( 'should not convert if consumable was consumed', () => { - sinon.spy( dispatcher, 'fire' ); - - dispatcher.on( 'addMarker:marker', insertUIElement( - ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } ) ) - ); - - dispatcher.on( 'addMarker:marker', ( evt, data, conversionApi ) => { - conversionApi.consumable.consume( data.markerRange, 'addMarker:marker' ); - }, { priority: 'high' } ); - - model.change( writer => { - writer.addMarker( 'marker', { range, usingOperation: false } ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - expect( dispatcher.fire.calledWith( 'addMarker:marker' ) ); - } ); - - it( 'should not convert if creator returned null', () => { - dispatcher.on( 'addMarker:marker', insertUIElement( () => null ) ); - dispatcher.on( 'removeMarker:marker', removeUIElement( () => null ) ); - - model.change( writer => { - writer.addMarker( 'marker', { range, usingOperation: false } ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - - model.change( writer => { - writer.removeMarker( 'marker' ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - } ); - } ); - - describe( 'non-collapsed range', () => { - beforeEach( () => { - range = model.createRange( model.createPositionAt( modelElement, 2 ), model.createPositionAt( modelElement, 5 ) ); - } ); - - it( 'should insert and remove ui element - element as a creator', () => { - const creator = ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } ); - - dispatcher.on( 'addMarker:marker', insertUIElement( creator ) ); - dispatcher.on( 'removeMarker:marker', removeUIElement( creator ) ); - - model.change( writer => { - writer.addMarker( 'marker', { range, usingOperation: false } ); - } ); - - expect( viewToString( viewRoot ) ) - .to.equal( '

foobar

' ); - - model.change( writer => { - writer.removeMarker( 'marker' ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - } ); - - it( 'should insert and remove ui element - function as a creator', () => { - const creator = ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': data.markerName } ); - - dispatcher.on( 'addMarker:marker', insertUIElement( creator ) ); - dispatcher.on( 'removeMarker:marker', removeUIElement( creator ) ); - - model.change( writer => { - writer.addMarker( 'marker', { range, usingOperation: false } ); - } ); - - expect( viewToString( viewRoot ) ) - .to.equal( '

foobar

' ); - - model.change( writer => { - writer.removeMarker( 'marker' ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - } ); - - it( 'should insert and remove different opening and ending element', () => { - function creator( data, viewWriter ) { - if ( data.isOpening ) { - return viewWriter.createUIElement( 'span', { 'class': data.markerName, 'data-start': true } ); - } - - return viewWriter.createUIElement( 'span', { 'class': data.markerName, 'data-end': true } ); - } - - dispatcher.on( 'addMarker:marker', insertUIElement( creator ) ); - dispatcher.on( 'removeMarker:marker', removeUIElement( creator ) ); - - model.change( writer => { - writer.addMarker( 'marker', { range, usingOperation: false } ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( - '

foobar

' - ); - - model.change( writer => { - writer.removeMarker( 'marker' ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - } ); - - it( 'should not convert if consumable was consumed', () => { - const creator = ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } ); - - sinon.spy( dispatcher, 'fire' ); - - dispatcher.on( 'addMarker:marker', insertUIElement( creator ) ); - dispatcher.on( 'addMarker:marker', ( evt, data, conversionApi ) => { - conversionApi.consumable.consume( data.item, 'addMarker:marker' ); - }, { priority: 'high' } ); - - model.change( writer => { - writer.addMarker( 'marker', { range, usingOperation: false } ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - expect( dispatcher.fire.calledWith( 'addMarker:marker' ) ); - } ); - } ); - } ); + // describe( 'insertUIElement/removeUIElement', () => { + // let modelText, modelElement, range; + // + // beforeEach( () => { + // modelText = new ModelText( 'foobar' ); + // modelElement = new ModelElement( 'paragraph', null, modelText ); + // + // model.change( writer => { + // writer.insert( modelElement, modelRootStart ); + // } ); + // } ); + // + // describe( 'collapsed range', () => { + // beforeEach( () => { + // range = model.createRange( model.createPositionAt( modelElement, 3 ), model.createPositionAt( modelElement, 3 ) ); + // } ); + // + // it( 'should insert and remove ui element', () => { + // const creator = ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } ); + // + // dispatcher.on( 'addMarker:marker', insertUIElement( creator ) ); + // dispatcher.on( 'removeMarker:marker', removeUIElement( creator ) ); + // + // model.change( writer => { + // writer.addMarker( 'marker', { range, usingOperation: false } ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + // + // model.change( writer => { + // writer.removeMarker( 'marker' ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + // } ); + // + // it( 'should not convert if consumable was consumed', () => { + // sinon.spy( dispatcher, 'fire' ); + // + // dispatcher.on( 'addMarker:marker', insertUIElement( + // ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } ) ) + // ); + // + // dispatcher.on( 'addMarker:marker', ( evt, data, conversionApi ) => { + // conversionApi.consumable.consume( data.markerRange, 'addMarker:marker' ); + // }, { priority: 'high' } ); + // + // model.change( writer => { + // writer.addMarker( 'marker', { range, usingOperation: false } ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + // expect( dispatcher.fire.calledWith( 'addMarker:marker' ) ); + // } ); + // + // it( 'should not convert if creator returned null', () => { + // dispatcher.on( 'addMarker:marker', insertUIElement( () => null ) ); + // dispatcher.on( 'removeMarker:marker', removeUIElement( () => null ) ); + // + // model.change( writer => { + // writer.addMarker( 'marker', { range, usingOperation: false } ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + // + // model.change( writer => { + // writer.removeMarker( 'marker' ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + // } ); + // } ); + // + // describe( 'non-collapsed range', () => { + // beforeEach( () => { + // range = model.createRange( model.createPositionAt( modelElement, 2 ), model.createPositionAt( modelElement, 5 ) ); + // } ); + // + // it( 'should insert and remove ui element - element as a creator', () => { + // const creator = ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } ); + // + // dispatcher.on( 'addMarker:marker', insertUIElement( creator ) ); + // dispatcher.on( 'removeMarker:marker', removeUIElement( creator ) ); + // + // model.change( writer => { + // writer.addMarker( 'marker', { range, usingOperation: false } ); + // } ); + // + // expect( viewToString( viewRoot ) ) + // .to.equal( '

foobar

' ); + // + // model.change( writer => { + // writer.removeMarker( 'marker' ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + // } ); + // + // it( 'should insert and remove ui element - function as a creator', () => { + // const creator = ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': data.markerName } ); + // + // dispatcher.on( 'addMarker:marker', insertUIElement( creator ) ); + // dispatcher.on( 'removeMarker:marker', removeUIElement( creator ) ); + // + // model.change( writer => { + // writer.addMarker( 'marker', { range, usingOperation: false } ); + // } ); + // + // expect( viewToString( viewRoot ) ) + // .to.equal( '

foobar

' ); + // + // model.change( writer => { + // writer.removeMarker( 'marker' ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + // } ); + // + // it( 'should insert and remove different opening and ending element', () => { + // function creator( data, viewWriter ) { + // if ( data.isOpening ) { + // return viewWriter.createUIElement( 'span', { 'class': data.markerName, 'data-start': true } ); + // } + // + // return viewWriter.createUIElement( 'span', { 'class': data.markerName, 'data-end': true } ); + // } + // + // dispatcher.on( 'addMarker:marker', insertUIElement( creator ) ); + // dispatcher.on( 'removeMarker:marker', removeUIElement( creator ) ); + // + // model.change( writer => { + // writer.addMarker( 'marker', { range, usingOperation: false } ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( + // '

foobar

' + // ); + // + // model.change( writer => { + // writer.removeMarker( 'marker' ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + // } ); + // + // it( 'should not convert if consumable was consumed', () => { + // const creator = ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } ); + // + // sinon.spy( dispatcher, 'fire' ); + // + // dispatcher.on( 'addMarker:marker', insertUIElement( creator ) ); + // dispatcher.on( 'addMarker:marker', ( evt, data, conversionApi ) => { + // conversionApi.consumable.consume( data.item, 'addMarker:marker' ); + // }, { priority: 'high' } ); + // + // model.change( writer => { + // writer.addMarker( 'marker', { range, usingOperation: false } ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + // expect( dispatcher.fire.calledWith( 'addMarker:marker' ) ); + // } ); + // } ); + // } ); // Remove converter is by default already added in `EditingController` instance. describe( 'remove', () => { @@ -1308,391 +1307,391 @@ describe( 'downcast-converters', () => { } ); } ); - describe( 'highlight', () => { - describe( 'on text', () => { - const highlightDescriptor = { - classes: 'highlight-class', - priority: 7, - attributes: { title: 'title' } - }; - - let markerRange; - - beforeEach( () => { - const modelElement1 = new ModelElement( 'paragraph', null, new ModelText( 'foo' ) ); - const modelElement2 = new ModelElement( 'paragraph', null, new ModelText( 'bar' ) ); - - model.change( writer => { - writer.insert( [ modelElement1, modelElement2 ], modelRootStart ); - } ); - - markerRange = model.createRangeIn( modelRoot ); - } ); - - it( 'should wrap and unwrap text nodes', () => { - dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) ); - dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) ); - dispatcher.on( 'removeMarker:marker', removeHighlight( highlightDescriptor ) ); - - model.change( writer => { - writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( - '
' + - '

' + - 'foo' + - '

' + - '

' + - 'bar' + - '

' + - '
' - ); - - model.change( writer => { - writer.removeMarker( 'marker' ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); - } ); - - it( 'should be possible to overwrite', () => { - dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) ); - dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) ); - dispatcher.on( 'removeMarker:marker', removeHighlight( highlightDescriptor ) ); - - const newDescriptor = { classes: 'override-class' }; - - dispatcher.on( 'addMarker:marker', highlightText( newDescriptor ), { priority: 'high' } ); - dispatcher.on( 'addMarker:marker', highlightElement( newDescriptor ), { priority: 'high' } ); - dispatcher.on( 'removeMarker:marker', removeHighlight( newDescriptor ), { priority: 'high' } ); - - model.change( writer => { - writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( - '
' + - '

' + - 'foo' + - '

' + - '

' + - 'bar' + - '

' + - '
' - ); - - model.change( writer => { - writer.removeMarker( 'marker' ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); - } ); - - it( 'should do nothing if descriptor is not provided or generating function returns null', () => { - dispatcher.on( 'addMarker:marker', highlightText( () => null ), { priority: 'high' } ); - dispatcher.on( 'addMarker:marker', highlightElement( () => null ), { priority: 'high' } ); - dispatcher.on( 'removeMarker:marker', removeHighlight( () => null ), { priority: 'high' } ); - - model.change( writer => { - writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); - - model.change( writer => { - writer.removeMarker( 'marker' ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); - } ); - - it( 'should do nothing if collapsed marker is converted', () => { - const descriptor = { classes: 'foo' }; - - dispatcher.on( 'addMarker:marker', highlightText( descriptor ), { priority: 'high' } ); - dispatcher.on( 'addMarker:marker', highlightElement( descriptor ), { priority: 'high' } ); - dispatcher.on( 'removeMarker:marker', removeHighlight( descriptor ), { priority: 'high' } ); - - markerRange = model.createRange( model.createPositionAt( modelRoot, 0 ), model.createPositionAt( modelRoot, 0 ) ); - - model.change( writer => { - writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); - - model.change( () => { - model.markers._remove( 'marker' ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); - } ); - - it( 'should correctly wrap and unwrap multiple, intersecting markers', () => { - const descriptorFoo = { classes: 'foo' }; - const descriptorBar = { classes: 'bar' }; - const descriptorXyz = { classes: 'xyz' }; - - dispatcher.on( 'addMarker:markerFoo', highlightText( descriptorFoo ) ); - dispatcher.on( 'addMarker:markerBar', highlightText( descriptorBar ) ); - dispatcher.on( 'addMarker:markerXyz', highlightText( descriptorXyz ) ); - - dispatcher.on( 'removeMarker:markerFoo', removeHighlight( descriptorFoo ) ); - dispatcher.on( 'removeMarker:markerBar', removeHighlight( descriptorBar ) ); - dispatcher.on( 'removeMarker:markerXyz', removeHighlight( descriptorXyz ) ); - - const p1 = modelRoot.getChild( 0 ); - const p2 = modelRoot.getChild( 1 ); - - model.change( writer => { - const range = writer.createRange( writer.createPositionAt( p1, 0 ), writer.createPositionAt( p1, 3 ) ); - writer.addMarker( 'markerFoo', { range, usingOperation: false } ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( - '
' + - '

' + - 'foo' + - '

' + - '

bar

' + - '
' - ); - - model.change( writer => { - const range = writer.createRange( writer.createPositionAt( p1, 1 ), writer.createPositionAt( p2, 2 ) ); - writer.addMarker( 'markerBar', { range, usingOperation: false } ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( - '
' + - '

' + - 'f' + - '' + - 'oo' + - '' + - '

' + - '

' + - 'ba' + - 'r' + - '

' + - '
' - ); - - model.change( writer => { - const range = writer.createRange( writer.createPositionAt( p1, 2 ), writer.createPositionAt( p2, 3 ) ); - writer.addMarker( 'markerXyz', { range, usingOperation: false } ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( - '
' + - '

' + - 'f' + - '' + - '' + - 'o' + - 'o' + - '' + - '' + - '

' + - '

' + - '' + - 'ba' + - '' + - 'r' + - '

' + - '
' - ); - - model.change( writer => { - writer.removeMarker( 'markerBar' ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( - '
' + - '

' + - '' + - 'fo' + - 'o' + - '' + - '

' + - '

' + - 'bar' + - '

' + - '
' - ); - - model.change( writer => { - writer.removeMarker( 'markerFoo' ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( - '
' + - '

' + - 'fo' + - 'o' + - '

' + - '

' + - 'bar' + - '

' + - '
' - ); - - model.change( writer => { - writer.removeMarker( 'markerXyz' ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); - } ); - - it( 'should do nothing if marker is applied and removed on empty-ish range', () => { - dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) ); - dispatcher.on( 'removeMarker:marker', removeHighlight( highlightDescriptor ) ); - - const p1 = modelRoot.getChild( 0 ); - const p2 = modelRoot.getChild( 1 ); - - const markerRange = model.createRange( model.createPositionAt( p1, 3 ), model.createPositionAt( p2, 0 ) ); - - model.change( writer => { - writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); - - model.change( writer => { - writer.removeMarker( 'marker', { range: markerRange, usingOperation: false } ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); - } ); - } ); - - describe( 'on element', () => { - const highlightDescriptor = { - classes: 'highlight-class', - priority: 7, - attributes: { title: 'title' }, - id: 'customId' - }; - - let markerRange; - - beforeEach( () => { - // Provide converter for div element. View div element will have custom highlight handling. - dispatcher.on( 'insert:div', insertElement( () => { - const viewContainer = new ViewContainerElement( 'div' ); - - viewContainer._setCustomProperty( 'addHighlight', ( element, descriptor, writer ) => { - writer.addClass( descriptor.classes, element ); - } ); - - viewContainer._setCustomProperty( 'removeHighlight', ( element, id, writer ) => { - writer.setAttribute( 'class', '', element ); - } ); - - return viewContainer; - } ) ); - - const modelElement = new ModelElement( 'div', null, new ModelText( 'foo' ) ); - - model.change( writer => { - writer.insert( modelElement, modelRootStart ); - } ); - - markerRange = model.createRangeOn( modelElement ); - - dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) ); - dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) ); - dispatcher.on( 'removeMarker:marker', removeHighlight( highlightDescriptor ) ); - } ); - - it( 'should use addHighlight and removeHighlight on elements and not convert children nodes', () => { - model.change( writer => { - writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( - '
' + - '
' + - 'foo' + - '
' + - '
' - ); - - model.change( writer => { - writer.removeMarker( 'marker' ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '
foo
' ); - } ); - - it( 'should be possible to override', () => { - const newDescriptor = { classes: 'override-class' }; - - dispatcher.on( 'addMarker:marker', highlightText( newDescriptor ), { priority: 'high' } ); - dispatcher.on( 'addMarker:marker', highlightElement( newDescriptor ), { priority: 'high' } ); - dispatcher.on( 'removeMarker:marker', removeHighlight( newDescriptor ), { priority: 'high' } ); - - model.change( writer => { - writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( - '
' + - '
' + - 'foo' + - '
' + - '
' - ); - - model.change( writer => { - writer.removeMarker( 'marker' ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '
foo
' ); - } ); - - it( 'should use default priority and id if not provided', () => { - const viewDiv = viewRoot.getChild( 0 ); - - dispatcher.on( 'addMarker:marker2', highlightText( () => null ) ); - dispatcher.on( 'addMarker:marker2', highlightElement( () => null ) ); - dispatcher.on( 'removeMarker:marker2', removeHighlight( () => null ) ); - - viewDiv._setCustomProperty( 'addHighlight', ( element, descriptor ) => { - expect( descriptor.priority ).to.equal( ViewAttributeElement.DEFAULT_PRIORITY ); - expect( descriptor.id ).to.equal( 'marker:foo-bar-baz' ); - } ); - - viewDiv._setCustomProperty( 'removeHighlight', ( element, id ) => { - expect( id ).to.equal( 'marker:foo-bar-baz' ); - } ); - - model.change( writer => { - writer.addMarker( 'marker2', { range: markerRange, usingOperation: false } ); - } ); - } ); - - it( 'should do nothing if descriptor is not provided', () => { - dispatcher.on( 'addMarker:marker2', highlightText( () => null ) ); - dispatcher.on( 'addMarker:marker2', highlightElement( () => null ) ); - dispatcher.on( 'removeMarker:marker2', removeHighlight( () => null ) ); - - model.change( writer => { - writer.addMarker( 'marker2', { range: markerRange, usingOperation: false } ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '
foo
' ); - - model.change( writer => { - writer.removeMarker( 'marker2' ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '
foo
' ); - } ); - } ); - } ); + // describe( 'highlight', () => { + // describe( 'on text', () => { + // const highlightDescriptor = { + // classes: 'highlight-class', + // priority: 7, + // attributes: { title: 'title' } + // }; + // + // let markerRange; + // + // beforeEach( () => { + // const modelElement1 = new ModelElement( 'paragraph', null, new ModelText( 'foo' ) ); + // const modelElement2 = new ModelElement( 'paragraph', null, new ModelText( 'bar' ) ); + // + // model.change( writer => { + // writer.insert( [ modelElement1, modelElement2 ], modelRootStart ); + // } ); + // + // markerRange = model.createRangeIn( modelRoot ); + // } ); + // + // it( 'should wrap and unwrap text nodes', () => { + // dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) ); + // dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) ); + // dispatcher.on( 'removeMarker:marker', removeHighlight( highlightDescriptor ) ); + // + // model.change( writer => { + // writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( + // '
' + + // '

' + + // 'foo' + + // '

' + + // '

' + + // 'bar' + + // '

' + + // '
' + // ); + // + // model.change( writer => { + // writer.removeMarker( 'marker' ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); + // } ); + // + // it( 'should be possible to overwrite', () => { + // dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) ); + // dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) ); + // dispatcher.on( 'removeMarker:marker', removeHighlight( highlightDescriptor ) ); + // + // const newDescriptor = { classes: 'override-class' }; + // + // dispatcher.on( 'addMarker:marker', highlightText( newDescriptor ), { priority: 'high' } ); + // dispatcher.on( 'addMarker:marker', highlightElement( newDescriptor ), { priority: 'high' } ); + // dispatcher.on( 'removeMarker:marker', removeHighlight( newDescriptor ), { priority: 'high' } ); + // + // model.change( writer => { + // writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( + // '
' + + // '

' + + // 'foo' + + // '

' + + // '

' + + // 'bar' + + // '

' + + // '
' + // ); + // + // model.change( writer => { + // writer.removeMarker( 'marker' ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); + // } ); + // + // it( 'should do nothing if descriptor is not provided or generating function returns null', () => { + // dispatcher.on( 'addMarker:marker', highlightText( () => null ), { priority: 'high' } ); + // dispatcher.on( 'addMarker:marker', highlightElement( () => null ), { priority: 'high' } ); + // dispatcher.on( 'removeMarker:marker', removeHighlight( () => null ), { priority: 'high' } ); + // + // model.change( writer => { + // writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); + // + // model.change( writer => { + // writer.removeMarker( 'marker' ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); + // } ); + // + // it( 'should do nothing if collapsed marker is converted', () => { + // const descriptor = { classes: 'foo' }; + // + // dispatcher.on( 'addMarker:marker', highlightText( descriptor ), { priority: 'high' } ); + // dispatcher.on( 'addMarker:marker', highlightElement( descriptor ), { priority: 'high' } ); + // dispatcher.on( 'removeMarker:marker', removeHighlight( descriptor ), { priority: 'high' } ); + // + // markerRange = model.createRange( model.createPositionAt( modelRoot, 0 ), model.createPositionAt( modelRoot, 0 ) ); + // + // model.change( writer => { + // writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); + // + // model.change( () => { + // model.markers._remove( 'marker' ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); + // } ); + // + // it( 'should correctly wrap and unwrap multiple, intersecting markers', () => { + // const descriptorFoo = { classes: 'foo' }; + // const descriptorBar = { classes: 'bar' }; + // const descriptorXyz = { classes: 'xyz' }; + // + // dispatcher.on( 'addMarker:markerFoo', highlightText( descriptorFoo ) ); + // dispatcher.on( 'addMarker:markerBar', highlightText( descriptorBar ) ); + // dispatcher.on( 'addMarker:markerXyz', highlightText( descriptorXyz ) ); + // + // dispatcher.on( 'removeMarker:markerFoo', removeHighlight( descriptorFoo ) ); + // dispatcher.on( 'removeMarker:markerBar', removeHighlight( descriptorBar ) ); + // dispatcher.on( 'removeMarker:markerXyz', removeHighlight( descriptorXyz ) ); + // + // const p1 = modelRoot.getChild( 0 ); + // const p2 = modelRoot.getChild( 1 ); + // + // model.change( writer => { + // const range = writer.createRange( writer.createPositionAt( p1, 0 ), writer.createPositionAt( p1, 3 ) ); + // writer.addMarker( 'markerFoo', { range, usingOperation: false } ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( + // '
' + + // '

' + + // 'foo' + + // '

' + + // '

bar

' + + // '
' + // ); + // + // model.change( writer => { + // const range = writer.createRange( writer.createPositionAt( p1, 1 ), writer.createPositionAt( p2, 2 ) ); + // writer.addMarker( 'markerBar', { range, usingOperation: false } ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( + // '
' + + // '

' + + // 'f' + + // '' + + // 'oo' + + // '' + + // '

' + + // '

' + + // 'ba' + + // 'r' + + // '

' + + // '
' + // ); + // + // model.change( writer => { + // const range = writer.createRange( writer.createPositionAt( p1, 2 ), writer.createPositionAt( p2, 3 ) ); + // writer.addMarker( 'markerXyz', { range, usingOperation: false } ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( + // '
' + + // '

' + + // 'f' + + // '' + + // '' + + // 'o' + + // 'o' + + // '' + + // '' + + // '

' + + // '

' + + // '' + + // 'ba' + + // '' + + // 'r' + + // '

' + + // '
' + // ); + // + // model.change( writer => { + // writer.removeMarker( 'markerBar' ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( + // '
' + + // '

' + + // '' + + // 'fo' + + // 'o' + + // '' + + // '

' + + // '

' + + // 'bar' + + // '

' + + // '
' + // ); + // + // model.change( writer => { + // writer.removeMarker( 'markerFoo' ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( + // '
' + + // '

' + + // 'fo' + + // 'o' + + // '

' + + // '

' + + // 'bar' + + // '

' + + // '
' + // ); + // + // model.change( writer => { + // writer.removeMarker( 'markerXyz' ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); + // } ); + // + // it( 'should do nothing if marker is applied and removed on empty-ish range', () => { + // dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) ); + // dispatcher.on( 'removeMarker:marker', removeHighlight( highlightDescriptor ) ); + // + // const p1 = modelRoot.getChild( 0 ); + // const p2 = modelRoot.getChild( 1 ); + // + // const markerRange = model.createRange( model.createPositionAt( p1, 3 ), model.createPositionAt( p2, 0 ) ); + // + // model.change( writer => { + // writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); + // + // model.change( writer => { + // writer.removeMarker( 'marker', { range: markerRange, usingOperation: false } ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); + // } ); + // } ); + // + // describe( 'on element', () => { + // const highlightDescriptor = { + // classes: 'highlight-class', + // priority: 7, + // attributes: { title: 'title' }, + // id: 'customId' + // }; + // + // let markerRange; + // + // beforeEach( () => { + // // Provide converter for div element. View div element will have custom highlight handling. + // dispatcher.on( 'insert:div', insertElement( () => { + // const viewContainer = new ViewContainerElement( 'div' ); + // + // viewContainer._setCustomProperty( 'addHighlight', ( element, descriptor, writer ) => { + // writer.addClass( descriptor.classes, element ); + // } ); + // + // viewContainer._setCustomProperty( 'removeHighlight', ( element, id, writer ) => { + // writer.setAttribute( 'class', '', element ); + // } ); + // + // return viewContainer; + // } ) ); + // + // const modelElement = new ModelElement( 'div', null, new ModelText( 'foo' ) ); + // + // model.change( writer => { + // writer.insert( modelElement, modelRootStart ); + // } ); + // + // markerRange = model.createRangeOn( modelElement ); + // + // dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) ); + // dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) ); + // dispatcher.on( 'removeMarker:marker', removeHighlight( highlightDescriptor ) ); + // } ); + // + // it( 'should use addHighlight and removeHighlight on elements and not convert children nodes', () => { + // model.change( writer => { + // writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( + // '
' + + // '
' + + // 'foo' + + // '
' + + // '
' + // ); + // + // model.change( writer => { + // writer.removeMarker( 'marker' ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '
foo
' ); + // } ); + // + // it( 'should be possible to override', () => { + // const newDescriptor = { classes: 'override-class' }; + // + // dispatcher.on( 'addMarker:marker', highlightText( newDescriptor ), { priority: 'high' } ); + // dispatcher.on( 'addMarker:marker', highlightElement( newDescriptor ), { priority: 'high' } ); + // dispatcher.on( 'removeMarker:marker', removeHighlight( newDescriptor ), { priority: 'high' } ); + // + // model.change( writer => { + // writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( + // '
' + + // '
' + + // 'foo' + + // '
' + + // '
' + // ); + // + // model.change( writer => { + // writer.removeMarker( 'marker' ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '
foo
' ); + // } ); + // + // it( 'should use default priority and id if not provided', () => { + // const viewDiv = viewRoot.getChild( 0 ); + // + // dispatcher.on( 'addMarker:marker2', highlightText( () => null ) ); + // dispatcher.on( 'addMarker:marker2', highlightElement( () => null ) ); + // dispatcher.on( 'removeMarker:marker2', removeHighlight( () => null ) ); + // + // viewDiv._setCustomProperty( 'addHighlight', ( element, descriptor ) => { + // expect( descriptor.priority ).to.equal( ViewAttributeElement.DEFAULT_PRIORITY ); + // expect( descriptor.id ).to.equal( 'marker:foo-bar-baz' ); + // } ); + // + // viewDiv._setCustomProperty( 'removeHighlight', ( element, id ) => { + // expect( id ).to.equal( 'marker:foo-bar-baz' ); + // } ); + // + // model.change( writer => { + // writer.addMarker( 'marker2', { range: markerRange, usingOperation: false } ); + // } ); + // } ); + // + // it( 'should do nothing if descriptor is not provided', () => { + // dispatcher.on( 'addMarker:marker2', highlightText( () => null ) ); + // dispatcher.on( 'addMarker:marker2', highlightElement( () => null ) ); + // dispatcher.on( 'removeMarker:marker2', removeHighlight( () => null ) ); + // + // model.change( writer => { + // writer.addMarker( 'marker2', { range: markerRange, usingOperation: false } ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '
foo
' ); + // + // model.change( writer => { + // writer.removeMarker( 'marker2' ); + // } ); + // + // expect( viewToString( viewRoot ) ).to.equal( '
foo
' ); + // } ); + // } ); + // } ); describe( 'createViewElementFromHighlightDescriptor()', () => { it( 'should return attribute element from descriptor object', () => { From 5612d437ee933dab17da105deadf71666929287e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 21 Dec 2018 12:59:04 +0100 Subject: [PATCH 63/84] Restore highlight tests. --- tests/conversion/downcasthelpers.js | 820 +++++++++++++++------------- 1 file changed, 432 insertions(+), 388 deletions(-) diff --git a/tests/conversion/downcasthelpers.js b/tests/conversion/downcasthelpers.js index b0b8c895d..771f7db47 100644 --- a/tests/conversion/downcasthelpers.js +++ b/tests/conversion/downcasthelpers.js @@ -27,14 +27,16 @@ import DowncastHelpers, { import { stringify } from '../../src/dev-utils/view'; describe( 'DowncastHelpers', () => { - let conversion, model, modelRoot, viewRoot, downcastHelpers; + let conversion, model, modelRoot, viewRoot, downcastHelpers, controller; + + let modelRootStart; beforeEach( () => { model = new Model(); const modelDoc = model.document; modelRoot = modelDoc.createRoot(); - const controller = new EditingController( model ); + controller = new EditingController( model ); // Set name of view root the same as dom root. // This is a mock of attaching view root to dom root. @@ -46,6 +48,8 @@ describe( 'DowncastHelpers', () => { conversion = new Conversion(); conversion.register( 'downcast', downcastHelpers ); + + modelRootStart = model.createPositionAt( modelRoot, 0 ); } ); describe( 'elementToElement()', () => { @@ -612,11 +616,437 @@ describe( 'DowncastHelpers', () => { expectResult( 'foo' ); } ); + + describe( 'highlight', () => { + const highlightConfig = { + model: 'marker', + view: { + classes: 'highlight-class', + attributes: { title: 'title' } + }, + converterPriority: 7 + }; + + describe( 'on text', () => { + let markerRange; + + beforeEach( () => { + downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); + + const modelElement1 = new ModelElement( 'paragraph', null, new ModelText( 'foo' ) ); + const modelElement2 = new ModelElement( 'paragraph', null, new ModelText( 'bar' ) ); + + model.change( writer => { + writer.insert( [ modelElement1, modelElement2 ], modelRootStart ); + } ); + + markerRange = model.createRangeIn( modelRoot ); + } ); + + it( 'should wrap and unwrap text nodes', () => { + downcastHelpers.markerToHighlight( highlightConfig ); + + model.change( writer => { + writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( + '
' + + '

' + + 'foo' + + '

' + + '

' + + 'bar' + + '

' + + '
' + ); + + model.change( writer => { + writer.removeMarker( 'marker' ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); + } ); + + it( 'should be possible to overwrite', () => { + downcastHelpers.markerToHighlight( highlightConfig ); + downcastHelpers.markerToHighlight( { + model: 'marker', + view: { classes: 'override-class' }, + converterPriority: 'high' + } ); + + model.change( writer => { + writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( + '
' + + '

' + + 'foo' + + '

' + + '

' + + 'bar' + + '

' + + '
' + ); + + model.change( writer => { + writer.removeMarker( 'marker' ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); + } ); + + it( 'should do nothing if descriptor is not provided or generating function returns null', () => { + downcastHelpers.markerToHighlight( { + model: 'marker', + view: () => null, + converterPriority: 'high' + } ); + + model.change( writer => { + writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); + + model.change( writer => { + writer.removeMarker( 'marker' ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); + } ); + + it( 'should do nothing if collapsed marker is converted', () => { + downcastHelpers.markerToHighlight( { + model: 'marker', + view: { classes: 'foo' }, + converterPriority: 'high' + } ); + + markerRange = model.createRange( model.createPositionAt( modelRoot, 0 ), model.createPositionAt( modelRoot, 0 ) ); + + model.change( writer => { + writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); + + model.change( () => { + model.markers._remove( 'marker' ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); + } ); + + it( 'should correctly wrap and unwrap multiple, intersecting markers', () => { + downcastHelpers.markerToHighlight( { + model: 'markerFoo', + view: { classes: 'foo' } + } ); + downcastHelpers.markerToHighlight( { + model: 'markerBar', + view: { classes: 'bar' } + } ); + downcastHelpers.markerToHighlight( { + model: 'markerXyz', + view: { classes: 'xyz' } + } ); + + const p1 = modelRoot.getChild( 0 ); + const p2 = modelRoot.getChild( 1 ); + + model.change( writer => { + const range = writer.createRange( writer.createPositionAt( p1, 0 ), writer.createPositionAt( p1, 3 ) ); + writer.addMarker( 'markerFoo', { range, usingOperation: false } ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( + '
' + + '

' + + 'foo' + + '

' + + '

bar

' + + '
' + ); + + model.change( writer => { + const range = writer.createRange( writer.createPositionAt( p1, 1 ), writer.createPositionAt( p2, 2 ) ); + writer.addMarker( 'markerBar', { range, usingOperation: false } ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( + '
' + + '

' + + 'f' + + '' + + 'oo' + + '' + + '

' + + '

' + + 'ba' + + 'r' + + '

' + + '
' + ); + + model.change( writer => { + const range = writer.createRange( writer.createPositionAt( p1, 2 ), writer.createPositionAt( p2, 3 ) ); + writer.addMarker( 'markerXyz', { range, usingOperation: false } ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( + '
' + + '

' + + 'f' + + '' + + '' + + 'o' + + 'o' + + '' + + '' + + '

' + + '

' + + '' + + 'ba' + + '' + + 'r' + + '

' + + '
' + ); + + model.change( writer => { + writer.removeMarker( 'markerBar' ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( + '
' + + '

' + + '' + + 'fo' + + 'o' + + '' + + '

' + + '

' + + 'bar' + + '

' + + '
' + ); + + model.change( writer => { + writer.removeMarker( 'markerFoo' ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( + '
' + + '

' + + 'fo' + + 'o' + + '

' + + '

' + + 'bar' + + '

' + + '
' + ); + + model.change( writer => { + writer.removeMarker( 'markerXyz' ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); + } ); + + it( 'should do nothing if marker is applied and removed on empty-ish range', () => { + downcastHelpers.markerToHighlight( highlightConfig ); + + const p1 = modelRoot.getChild( 0 ); + const p2 = modelRoot.getChild( 1 ); + + const markerRange = model.createRange( model.createPositionAt( p1, 3 ), model.createPositionAt( p2, 0 ) ); + + model.change( writer => { + writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); + + model.change( writer => { + writer.removeMarker( 'marker', { range: markerRange, usingOperation: false } ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); + } ); + } ); + + describe( 'on element', () => { + const highlightConfig = { + model: 'marker', + view: { + classes: 'highlight-class', + attributes: { title: 'title' }, + id: 'customId' + }, + converterPriority: 7 + }; + + let markerRange; + + beforeEach( () => { + downcastHelpers.elementToElement( { + model: 'div', + view: () => { + const viewContainer = new ViewContainerElement( 'div' ); + + viewContainer._setCustomProperty( 'addHighlight', ( element, descriptor, writer ) => { + writer.addClass( descriptor.classes, element ); + } ); + + viewContainer._setCustomProperty( 'removeHighlight', ( element, id, writer ) => { + writer.setAttribute( 'class', '', element ); + } ); + + return viewContainer; + } + } ); + + const modelElement = new ModelElement( 'div', null, new ModelText( 'foo' ) ); + + model.change( writer => { + writer.insert( modelElement, modelRootStart ); + } ); + + markerRange = model.createRangeOn( modelElement ); + + downcastHelpers.markerToHighlight( highlightConfig ); + } ); + + it( 'should use addHighlight and removeHighlight on elements and not convert children nodes', () => { + model.change( writer => { + writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( + '
' + + '
' + + 'foo' + + '
' + + '
' + ); + + model.change( writer => { + writer.removeMarker( 'marker' ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '
foo
' ); + } ); + + it( 'should be possible to override', () => { + downcastHelpers.markerToHighlight( { + model: 'marker', + view: { classes: 'override-class' }, + converterPriority: 'high' + } ); + + model.change( writer => { + writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( + '
' + + '
' + + 'foo' + + '
' + + '
' + ); + + model.change( writer => { + writer.removeMarker( 'marker' ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '
foo
' ); + } ); + + it( 'should use default priority and id if not provided', () => { + const viewDiv = viewRoot.getChild( 0 ); + downcastHelpers.markerToHighlight( { + model: 'marker2', + view: () => null, + converterPriority: 'high' + } ); + + viewDiv._setCustomProperty( 'addHighlight', ( element, descriptor ) => { + expect( descriptor.priority ).to.equal( ViewAttributeElement.DEFAULT_PRIORITY ); + expect( descriptor.id ).to.equal( 'marker:foo-bar-baz' ); + } ); + + viewDiv._setCustomProperty( 'removeHighlight', ( element, id ) => { + expect( id ).to.equal( 'marker:foo-bar-baz' ); + } ); + + model.change( writer => { + writer.addMarker( 'marker2', { range: markerRange, usingOperation: false } ); + } ); + } ); + + it( 'should do nothing if descriptor is not provided', () => { + downcastHelpers.markerToHighlight( { + model: 'marker2', + view: () => null + } ); + + model.change( writer => { + writer.addMarker( 'marker2', { range: markerRange, usingOperation: false } ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '
foo
' ); + + model.change( writer => { + writer.removeMarker( 'marker2' ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '
foo
' ); + } ); + } ); + } ); } ); function expectResult( string ) { expect( stringify( viewRoot, null, { ignoreRoot: true } ) ).to.equal( string ); } + + function viewAttributesToString( item ) { + let result = ''; + + for ( const key of item.getAttributeKeys() ) { + const value = item.getAttribute( key ); + + if ( value ) { + result += ' ' + key + '="' + value + '"'; + } + } + + return result; + } + + function viewToString( item ) { + let result = ''; + + if ( item instanceof ViewText ) { + result = item.data; + } else { + // ViewElement or ViewDocumentFragment. + for ( const child of item.getChildren() ) { + result += viewToString( child ); + } + + if ( item instanceof ViewElement ) { + result = '<' + item.name + viewAttributesToString( item ) + '>' + result + ''; + } + } + + return result; + } } ); describe( 'downcast-converters', () => { @@ -1307,392 +1737,6 @@ describe( 'downcast-converters', () => { } ); } ); - // describe( 'highlight', () => { - // describe( 'on text', () => { - // const highlightDescriptor = { - // classes: 'highlight-class', - // priority: 7, - // attributes: { title: 'title' } - // }; - // - // let markerRange; - // - // beforeEach( () => { - // const modelElement1 = new ModelElement( 'paragraph', null, new ModelText( 'foo' ) ); - // const modelElement2 = new ModelElement( 'paragraph', null, new ModelText( 'bar' ) ); - // - // model.change( writer => { - // writer.insert( [ modelElement1, modelElement2 ], modelRootStart ); - // } ); - // - // markerRange = model.createRangeIn( modelRoot ); - // } ); - // - // it( 'should wrap and unwrap text nodes', () => { - // dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) ); - // dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) ); - // dispatcher.on( 'removeMarker:marker', removeHighlight( highlightDescriptor ) ); - // - // model.change( writer => { - // writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( - // '
' + - // '

' + - // 'foo' + - // '

' + - // '

' + - // 'bar' + - // '

' + - // '
' - // ); - // - // model.change( writer => { - // writer.removeMarker( 'marker' ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); - // } ); - // - // it( 'should be possible to overwrite', () => { - // dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) ); - // dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) ); - // dispatcher.on( 'removeMarker:marker', removeHighlight( highlightDescriptor ) ); - // - // const newDescriptor = { classes: 'override-class' }; - // - // dispatcher.on( 'addMarker:marker', highlightText( newDescriptor ), { priority: 'high' } ); - // dispatcher.on( 'addMarker:marker', highlightElement( newDescriptor ), { priority: 'high' } ); - // dispatcher.on( 'removeMarker:marker', removeHighlight( newDescriptor ), { priority: 'high' } ); - // - // model.change( writer => { - // writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( - // '
' + - // '

' + - // 'foo' + - // '

' + - // '

' + - // 'bar' + - // '

' + - // '
' - // ); - // - // model.change( writer => { - // writer.removeMarker( 'marker' ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); - // } ); - // - // it( 'should do nothing if descriptor is not provided or generating function returns null', () => { - // dispatcher.on( 'addMarker:marker', highlightText( () => null ), { priority: 'high' } ); - // dispatcher.on( 'addMarker:marker', highlightElement( () => null ), { priority: 'high' } ); - // dispatcher.on( 'removeMarker:marker', removeHighlight( () => null ), { priority: 'high' } ); - // - // model.change( writer => { - // writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); - // - // model.change( writer => { - // writer.removeMarker( 'marker' ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); - // } ); - // - // it( 'should do nothing if collapsed marker is converted', () => { - // const descriptor = { classes: 'foo' }; - // - // dispatcher.on( 'addMarker:marker', highlightText( descriptor ), { priority: 'high' } ); - // dispatcher.on( 'addMarker:marker', highlightElement( descriptor ), { priority: 'high' } ); - // dispatcher.on( 'removeMarker:marker', removeHighlight( descriptor ), { priority: 'high' } ); - // - // markerRange = model.createRange( model.createPositionAt( modelRoot, 0 ), model.createPositionAt( modelRoot, 0 ) ); - // - // model.change( writer => { - // writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); - // - // model.change( () => { - // model.markers._remove( 'marker' ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); - // } ); - // - // it( 'should correctly wrap and unwrap multiple, intersecting markers', () => { - // const descriptorFoo = { classes: 'foo' }; - // const descriptorBar = { classes: 'bar' }; - // const descriptorXyz = { classes: 'xyz' }; - // - // dispatcher.on( 'addMarker:markerFoo', highlightText( descriptorFoo ) ); - // dispatcher.on( 'addMarker:markerBar', highlightText( descriptorBar ) ); - // dispatcher.on( 'addMarker:markerXyz', highlightText( descriptorXyz ) ); - // - // dispatcher.on( 'removeMarker:markerFoo', removeHighlight( descriptorFoo ) ); - // dispatcher.on( 'removeMarker:markerBar', removeHighlight( descriptorBar ) ); - // dispatcher.on( 'removeMarker:markerXyz', removeHighlight( descriptorXyz ) ); - // - // const p1 = modelRoot.getChild( 0 ); - // const p2 = modelRoot.getChild( 1 ); - // - // model.change( writer => { - // const range = writer.createRange( writer.createPositionAt( p1, 0 ), writer.createPositionAt( p1, 3 ) ); - // writer.addMarker( 'markerFoo', { range, usingOperation: false } ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( - // '
' + - // '

' + - // 'foo' + - // '

' + - // '

bar

' + - // '
' - // ); - // - // model.change( writer => { - // const range = writer.createRange( writer.createPositionAt( p1, 1 ), writer.createPositionAt( p2, 2 ) ); - // writer.addMarker( 'markerBar', { range, usingOperation: false } ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( - // '
' + - // '

' + - // 'f' + - // '' + - // 'oo' + - // '' + - // '

' + - // '

' + - // 'ba' + - // 'r' + - // '

' + - // '
' - // ); - // - // model.change( writer => { - // const range = writer.createRange( writer.createPositionAt( p1, 2 ), writer.createPositionAt( p2, 3 ) ); - // writer.addMarker( 'markerXyz', { range, usingOperation: false } ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( - // '
' + - // '

' + - // 'f' + - // '' + - // '' + - // 'o' + - // 'o' + - // '' + - // '' + - // '

' + - // '

' + - // '' + - // 'ba' + - // '' + - // 'r' + - // '

' + - // '
' - // ); - // - // model.change( writer => { - // writer.removeMarker( 'markerBar' ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( - // '
' + - // '

' + - // '' + - // 'fo' + - // 'o' + - // '' + - // '

' + - // '

' + - // 'bar' + - // '

' + - // '
' - // ); - // - // model.change( writer => { - // writer.removeMarker( 'markerFoo' ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( - // '
' + - // '

' + - // 'fo' + - // 'o' + - // '

' + - // '

' + - // 'bar' + - // '

' + - // '
' - // ); - // - // model.change( writer => { - // writer.removeMarker( 'markerXyz' ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); - // } ); - // - // it( 'should do nothing if marker is applied and removed on empty-ish range', () => { - // dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) ); - // dispatcher.on( 'removeMarker:marker', removeHighlight( highlightDescriptor ) ); - // - // const p1 = modelRoot.getChild( 0 ); - // const p2 = modelRoot.getChild( 1 ); - // - // const markerRange = model.createRange( model.createPositionAt( p1, 3 ), model.createPositionAt( p2, 0 ) ); - // - // model.change( writer => { - // writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); - // - // model.change( writer => { - // writer.removeMarker( 'marker', { range: markerRange, usingOperation: false } ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foo

bar

' ); - // } ); - // } ); - // - // describe( 'on element', () => { - // const highlightDescriptor = { - // classes: 'highlight-class', - // priority: 7, - // attributes: { title: 'title' }, - // id: 'customId' - // }; - // - // let markerRange; - // - // beforeEach( () => { - // // Provide converter for div element. View div element will have custom highlight handling. - // dispatcher.on( 'insert:div', insertElement( () => { - // const viewContainer = new ViewContainerElement( 'div' ); - // - // viewContainer._setCustomProperty( 'addHighlight', ( element, descriptor, writer ) => { - // writer.addClass( descriptor.classes, element ); - // } ); - // - // viewContainer._setCustomProperty( 'removeHighlight', ( element, id, writer ) => { - // writer.setAttribute( 'class', '', element ); - // } ); - // - // return viewContainer; - // } ) ); - // - // const modelElement = new ModelElement( 'div', null, new ModelText( 'foo' ) ); - // - // model.change( writer => { - // writer.insert( modelElement, modelRootStart ); - // } ); - // - // markerRange = model.createRangeOn( modelElement ); - // - // dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) ); - // dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) ); - // dispatcher.on( 'removeMarker:marker', removeHighlight( highlightDescriptor ) ); - // } ); - // - // it( 'should use addHighlight and removeHighlight on elements and not convert children nodes', () => { - // model.change( writer => { - // writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( - // '
' + - // '
' + - // 'foo' + - // '
' + - // '
' - // ); - // - // model.change( writer => { - // writer.removeMarker( 'marker' ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '
foo
' ); - // } ); - // - // it( 'should be possible to override', () => { - // const newDescriptor = { classes: 'override-class' }; - // - // dispatcher.on( 'addMarker:marker', highlightText( newDescriptor ), { priority: 'high' } ); - // dispatcher.on( 'addMarker:marker', highlightElement( newDescriptor ), { priority: 'high' } ); - // dispatcher.on( 'removeMarker:marker', removeHighlight( newDescriptor ), { priority: 'high' } ); - // - // model.change( writer => { - // writer.addMarker( 'marker', { range: markerRange, usingOperation: false } ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( - // '
' + - // '
' + - // 'foo' + - // '
' + - // '
' - // ); - // - // model.change( writer => { - // writer.removeMarker( 'marker' ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '
foo
' ); - // } ); - // - // it( 'should use default priority and id if not provided', () => { - // const viewDiv = viewRoot.getChild( 0 ); - // - // dispatcher.on( 'addMarker:marker2', highlightText( () => null ) ); - // dispatcher.on( 'addMarker:marker2', highlightElement( () => null ) ); - // dispatcher.on( 'removeMarker:marker2', removeHighlight( () => null ) ); - // - // viewDiv._setCustomProperty( 'addHighlight', ( element, descriptor ) => { - // expect( descriptor.priority ).to.equal( ViewAttributeElement.DEFAULT_PRIORITY ); - // expect( descriptor.id ).to.equal( 'marker:foo-bar-baz' ); - // } ); - // - // viewDiv._setCustomProperty( 'removeHighlight', ( element, id ) => { - // expect( id ).to.equal( 'marker:foo-bar-baz' ); - // } ); - // - // model.change( writer => { - // writer.addMarker( 'marker2', { range: markerRange, usingOperation: false } ); - // } ); - // } ); - // - // it( 'should do nothing if descriptor is not provided', () => { - // dispatcher.on( 'addMarker:marker2', highlightText( () => null ) ); - // dispatcher.on( 'addMarker:marker2', highlightElement( () => null ) ); - // dispatcher.on( 'removeMarker:marker2', removeHighlight( () => null ) ); - // - // model.change( writer => { - // writer.addMarker( 'marker2', { range: markerRange, usingOperation: false } ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '
foo
' ); - // - // model.change( writer => { - // writer.removeMarker( 'marker2' ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '
foo
' ); - // } ); - // } ); - // } ); - describe( 'createViewElementFromHighlightDescriptor()', () => { it( 'should return attribute element from descriptor object', () => { const descriptor = { From b78c4a812a1741030146f662da88e00f48219377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 21 Dec 2018 13:20:58 +0100 Subject: [PATCH 64/84] Restore markerToElement tests. --- tests/conversion/downcasthelpers.js | 345 ++++++++++++++-------------- 1 file changed, 177 insertions(+), 168 deletions(-) diff --git a/tests/conversion/downcasthelpers.js b/tests/conversion/downcasthelpers.js index 771f7db47..1f9e19541 100644 --- a/tests/conversion/downcasthelpers.js +++ b/tests/conversion/downcasthelpers.js @@ -20,9 +20,7 @@ import ViewText from '../../src/view/text'; import log from '@ckeditor/ckeditor5-utils/src/log'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; -import DowncastHelpers, { - insertElement, wrap, createViewElementFromHighlightDescriptor -} from '../../src/conversion/downcasthelpers'; +import DowncastHelpers, { createViewElementFromHighlightDescriptor, insertElement, wrap } from '../../src/conversion/downcasthelpers'; import { stringify } from '../../src/dev-utils/view'; @@ -498,6 +496,8 @@ describe( 'DowncastHelpers', () => { } ); describe( 'markerToElement()', () => { + let modelText, modelElement, range; + it( 'should be chainable', () => { expect( downcastHelpers.markerToElement( { model: 'search', view: 'marker-search' } ) ).to.equal( downcastHelpers ); } ); @@ -564,6 +564,180 @@ describe( 'DowncastHelpers', () => { expectResult( 'foo' ); } ); + + describe( 'collapsed range', () => { + beforeEach( () => { + modelText = new ModelText( 'foobar' ); + modelElement = new ModelElement( 'paragraph', null, modelText ); + + downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); + + model.change( writer => { + writer.insert( modelElement, modelRootStart ); + } ); + + range = model.createRange( model.createPositionAt( modelElement, 3 ), model.createPositionAt( modelElement, 3 ) ); + } ); + + it( 'should insert and remove ui element', () => { + downcastHelpers.markerToElement( { + model: 'marker', + view: ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } ) + } ); + + model.change( writer => { + writer.addMarker( 'marker', { range, usingOperation: false } ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + + model.change( writer => { + writer.removeMarker( 'marker' ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + } ); + + it( 'should not convert if consumable was consumed', () => { + sinon.spy( controller.downcastDispatcher, 'fire' ); + + downcastHelpers.markerToElement( { + model: 'marker', + view: ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } ) + } ); + + controller.downcastDispatcher.on( 'addMarker:marker', ( evt, data, conversionApi ) => { + conversionApi.consumable.consume( data.markerRange, 'addMarker:marker' ); + }, { priority: 'high' } ); + + model.change( writer => { + writer.addMarker( 'marker', { range, usingOperation: false } ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + expect( controller.downcastDispatcher.fire.calledWith( 'addMarker:marker' ) ); + } ); + + it( 'should not convert if creator returned null', () => { + downcastHelpers.markerToElement( { + model: 'marker', + view: () => null + } ); + + model.change( writer => { + writer.addMarker( 'marker', { range, usingOperation: false } ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + + model.change( writer => { + writer.removeMarker( 'marker' ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + } ); + } ); + + describe( 'non-collapsed range', () => { + beforeEach( () => { + modelText = new ModelText( 'foobar' ); + modelElement = new ModelElement( 'paragraph', null, modelText ); + + downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); + + model.change( writer => { + writer.insert( modelElement, modelRootStart ); + } ); + + range = model.createRange( model.createPositionAt( modelElement, 2 ), model.createPositionAt( modelElement, 5 ) ); + } ); + + it( 'should insert and remove ui element - element as a creator', () => { + downcastHelpers.markerToElement( { + model: 'marker', + view: ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } ) + } ); + + model.change( writer => { + writer.addMarker( 'marker', { range, usingOperation: false } ); + } ); + + expect( viewToString( viewRoot ) ) + .to.equal( '

foobar

' ); + + model.change( writer => { + writer.removeMarker( 'marker' ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + } ); + + it( 'should insert and remove ui element - function as a creator', () => { + downcastHelpers.markerToElement( { + model: 'marker', + view: ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': data.markerName } ) + } ); + + model.change( writer => { + writer.addMarker( 'marker', { range, usingOperation: false } ); + } ); + + expect( viewToString( viewRoot ) ) + .to.equal( '

foobar

' ); + + model.change( writer => { + writer.removeMarker( 'marker' ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + } ); + + it( 'should insert and remove different opening and ending element', () => { + downcastHelpers.markerToElement( { + model: 'marker', + view: ( data, viewWriter ) => { + if ( data.isOpening ) { + return viewWriter.createUIElement( 'span', { 'class': data.markerName, 'data-start': true } ); + } + + return viewWriter.createUIElement( 'span', { 'class': data.markerName, 'data-end': true } ); + } + } ); + + model.change( writer => { + writer.addMarker( 'marker', { range, usingOperation: false } ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( + '

foobar

' + ); + + model.change( writer => { + writer.removeMarker( 'marker' ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + } ); + + it( 'should not convert if consumable was consumed', () => { + sinon.spy( controller.downcastDispatcher, 'fire' ); + + downcastHelpers.markerToElement( { + model: 'marker', + view: ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } ) + } ); + controller.downcastDispatcher.on( 'addMarker:marker', ( evt, data, conversionApi ) => { + conversionApi.consumable.consume( data.item, 'addMarker:marker' ); + }, { priority: 'high' } ); + + model.change( writer => { + writer.addMarker( 'marker', { range, usingOperation: false } ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + expect( controller.downcastDispatcher.fire.calledWith( 'addMarker:marker' ) ); + } ); + } ); } ); describe( 'markerToHighlight()', () => { @@ -1384,171 +1558,6 @@ describe( 'downcast-converters', () => { } ); } ); - // describe( 'insertUIElement/removeUIElement', () => { - // let modelText, modelElement, range; - // - // beforeEach( () => { - // modelText = new ModelText( 'foobar' ); - // modelElement = new ModelElement( 'paragraph', null, modelText ); - // - // model.change( writer => { - // writer.insert( modelElement, modelRootStart ); - // } ); - // } ); - // - // describe( 'collapsed range', () => { - // beforeEach( () => { - // range = model.createRange( model.createPositionAt( modelElement, 3 ), model.createPositionAt( modelElement, 3 ) ); - // } ); - // - // it( 'should insert and remove ui element', () => { - // const creator = ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } ); - // - // dispatcher.on( 'addMarker:marker', insertUIElement( creator ) ); - // dispatcher.on( 'removeMarker:marker', removeUIElement( creator ) ); - // - // model.change( writer => { - // writer.addMarker( 'marker', { range, usingOperation: false } ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - // - // model.change( writer => { - // writer.removeMarker( 'marker' ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - // } ); - // - // it( 'should not convert if consumable was consumed', () => { - // sinon.spy( dispatcher, 'fire' ); - // - // dispatcher.on( 'addMarker:marker', insertUIElement( - // ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } ) ) - // ); - // - // dispatcher.on( 'addMarker:marker', ( evt, data, conversionApi ) => { - // conversionApi.consumable.consume( data.markerRange, 'addMarker:marker' ); - // }, { priority: 'high' } ); - // - // model.change( writer => { - // writer.addMarker( 'marker', { range, usingOperation: false } ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - // expect( dispatcher.fire.calledWith( 'addMarker:marker' ) ); - // } ); - // - // it( 'should not convert if creator returned null', () => { - // dispatcher.on( 'addMarker:marker', insertUIElement( () => null ) ); - // dispatcher.on( 'removeMarker:marker', removeUIElement( () => null ) ); - // - // model.change( writer => { - // writer.addMarker( 'marker', { range, usingOperation: false } ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - // - // model.change( writer => { - // writer.removeMarker( 'marker' ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - // } ); - // } ); - // - // describe( 'non-collapsed range', () => { - // beforeEach( () => { - // range = model.createRange( model.createPositionAt( modelElement, 2 ), model.createPositionAt( modelElement, 5 ) ); - // } ); - // - // it( 'should insert and remove ui element - element as a creator', () => { - // const creator = ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } ); - // - // dispatcher.on( 'addMarker:marker', insertUIElement( creator ) ); - // dispatcher.on( 'removeMarker:marker', removeUIElement( creator ) ); - // - // model.change( writer => { - // writer.addMarker( 'marker', { range, usingOperation: false } ); - // } ); - // - // expect( viewToString( viewRoot ) ) - // .to.equal( '

foobar

' ); - // - // model.change( writer => { - // writer.removeMarker( 'marker' ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - // } ); - // - // it( 'should insert and remove ui element - function as a creator', () => { - // const creator = ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': data.markerName } ); - // - // dispatcher.on( 'addMarker:marker', insertUIElement( creator ) ); - // dispatcher.on( 'removeMarker:marker', removeUIElement( creator ) ); - // - // model.change( writer => { - // writer.addMarker( 'marker', { range, usingOperation: false } ); - // } ); - // - // expect( viewToString( viewRoot ) ) - // .to.equal( '

foobar

' ); - // - // model.change( writer => { - // writer.removeMarker( 'marker' ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - // } ); - // - // it( 'should insert and remove different opening and ending element', () => { - // function creator( data, viewWriter ) { - // if ( data.isOpening ) { - // return viewWriter.createUIElement( 'span', { 'class': data.markerName, 'data-start': true } ); - // } - // - // return viewWriter.createUIElement( 'span', { 'class': data.markerName, 'data-end': true } ); - // } - // - // dispatcher.on( 'addMarker:marker', insertUIElement( creator ) ); - // dispatcher.on( 'removeMarker:marker', removeUIElement( creator ) ); - // - // model.change( writer => { - // writer.addMarker( 'marker', { range, usingOperation: false } ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( - // '

foobar

' - // ); - // - // model.change( writer => { - // writer.removeMarker( 'marker' ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - // } ); - // - // it( 'should not convert if consumable was consumed', () => { - // const creator = ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } ); - // - // sinon.spy( dispatcher, 'fire' ); - // - // dispatcher.on( 'addMarker:marker', insertUIElement( creator ) ); - // dispatcher.on( 'addMarker:marker', ( evt, data, conversionApi ) => { - // conversionApi.consumable.consume( data.item, 'addMarker:marker' ); - // }, { priority: 'high' } ); - // - // model.change( writer => { - // writer.addMarker( 'marker', { range, usingOperation: false } ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - // expect( dispatcher.fire.calledWith( 'addMarker:marker' ) ); - // } ); - // } ); - // } ); - // Remove converter is by default already added in `EditingController` instance. describe( 'remove', () => { it( 'should remove items from view accordingly to changes in model #1', () => { From 7b604cf07cd7b36fc73fa12b1f1c6666ba05eb12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 21 Dec 2018 14:05:18 +0100 Subject: [PATCH 65/84] Restore attributeToAttribute tests. --- tests/conversion/downcasthelpers.js | 193 +++++++++++++++------------- 1 file changed, 104 insertions(+), 89 deletions(-) diff --git a/tests/conversion/downcasthelpers.js b/tests/conversion/downcasthelpers.js index 1f9e19541..8685c114e 100644 --- a/tests/conversion/downcasthelpers.js +++ b/tests/conversion/downcasthelpers.js @@ -259,6 +259,15 @@ describe( 'DowncastHelpers', () => { beforeEach( () => { downcastHelpers.elementToElement( { model: 'image', view: 'img' } ); + downcastHelpers.elementToElement( { + model: 'paragraph', + view: ( modelItem, viewWriter ) => viewWriter.createContainerElement( 'p' ) + } ); + + downcastHelpers.attributeToAttribute( { + model: 'class', + view: 'class' + } ); } ); it( 'should be chainable', () => { @@ -493,6 +502,101 @@ describe( 'DowncastHelpers', () => { expectResult( '

Foo

Bar

' ); } ); + + it( 'should convert attribute insert/change/remove on a model node', () => { + const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) ); + + model.change( writer => { + writer.insert( modelElement, modelRootStart ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + + model.change( writer => { + writer.setAttribute( 'class', 'bar', modelElement ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + + model.change( writer => { + writer.removeAttribute( 'class', modelElement ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + } ); + + it( 'should convert insert/change/remove with attribute generating function as a parameter', () => { + downcastHelpers.elementToElement( { model: 'div', view: 'div' } ); + downcastHelpers.attributeToAttribute( { + model: 'theme', + view: ( value, data ) => { + if ( data.item instanceof ModelElement && data.item.childCount > 0 ) { + value += ' fix-content'; + } + + return { key: 'class', value }; + } + } ); + + const modelParagraph = new ModelElement( 'paragraph', { theme: 'nice' }, new ModelText( 'foobar' ) ); + const modelDiv = new ModelElement( 'div', { theme: 'nice' } ); + + model.change( writer => { + writer.insert( [ modelParagraph, modelDiv ], modelRootStart ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + + model.change( writer => { + writer.setAttribute( 'theme', 'awesome', modelParagraph ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + + model.change( writer => { + writer.removeAttribute( 'theme', modelParagraph ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + } ); + + it( 'should be possible to override setAttribute', () => { + downcastHelpers.attributeToAttribute( { + model: 'class', + view: ( evt, data, conversionApi ) => { + conversionApi.consumable.consume( data.item, 'attribute:class' ); + }, + converterPriority: 'high' + } ); + + model.change( writer => { + const modelElement = new ModelElement( 'paragraph', { classes: 'foo' }, new ModelText( 'foobar' ) ); + writer.insert( modelElement, modelRootStart ); + } ); + + // No attribute set. + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + } ); + + it( 'should not convert or consume if element creator returned null', () => { + const callback = sinon.stub().returns( null ); + + downcastHelpers.attributeToAttribute( { + model: 'class', + view: callback, + converterPriority: 'high' + } ); + + const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) ); + + model.change( writer => { + writer.insert( modelElement, modelRootStart ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + + sinon.assert.called( callback ); + } ); } ); describe( 'markerToElement()', () => { @@ -1338,95 +1442,6 @@ describe( 'downcast-converters', () => { expect( viewToString( viewRoot ) ).to.equal( '
' ); } ); } ); - // - // describe( 'changeAttribute', () => { - // it( 'should convert attribute insert/change/remove on a model node', () => { - // const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) ); - // - // model.change( writer => { - // writer.insert( modelElement, modelRootStart ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - // - // model.change( writer => { - // writer.setAttribute( 'class', 'bar', modelElement ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - // - // model.change( writer => { - // writer.removeAttribute( 'class', modelElement ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - // } ); - // - // it( 'should convert insert/change/remove with attribute generating function as a parameter', () => { - // const themeConverter = ( value, data ) => { - // if ( data.item instanceof ModelElement && data.item.childCount > 0 ) { - // value += ' fix-content'; - // } - // - // return { key: 'class', value }; - // }; - // - // dispatcher.on( 'insert:div', insertElement( ( modelElement, viewWriter ) => viewWriter.createContainerElement( 'div' ) ) ); - // dispatcher.on( 'attribute:theme', changeAttribute( themeConverter ) ); - // - // const modelParagraph = new ModelElement( 'paragraph', { theme: 'nice' }, new ModelText( 'foobar' ) ); - // const modelDiv = new ModelElement( 'div', { theme: 'nice' } ); - // - // model.change( writer => { - // writer.insert( [ modelParagraph, modelDiv ], modelRootStart ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - // - // model.change( writer => { - // writer.setAttribute( 'theme', 'awesome', modelParagraph ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - // - // model.change( writer => { - // writer.removeAttribute( 'theme', modelParagraph ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - // } ); - // - // it( 'should be possible to override setAttribute', () => { - // const modelElement = new ModelElement( 'paragraph', { classes: 'foo' }, new ModelText( 'foobar' ) ); - // - // dispatcher.on( 'attribute:class', ( evt, data, conversionApi ) => { - // conversionApi.consumable.consume( data.item, 'attribute:class' ); - // }, { converterPriority: 'high' } ); - // - // model.change( writer => { - // writer.insert( modelElement, modelRootStart ); - // } ); - // - // // No attribute set. - // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - // } ); - // - // it( 'should not convert or consume if element creator returned null', () => { - // const callback = sinon.stub().returns( null ); - // - // dispatcher.on( 'attribute:class', changeAttribute( callback ) ); - // - // const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) ); - // - // model.change( writer => { - // writer.insert( modelElement, modelRootStart ); - // } ); - // - // expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - // - // sinon.assert.called( callback ); - // } ); - // } ); describe( 'wrap', () => { it( 'should convert insert/change/remove of attribute in model into wrapping element in a view', () => { From 5ed6ee952ee70cd8f744ddf046b2829571c7a5d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 21 Dec 2018 14:30:08 +0100 Subject: [PATCH 66/84] Remove insertElement(), insertUIElement() from public API. --- src/conversion/downcasthelpers.js | 6 +- src/dev-utils/model.js | 73 ++++++++++++++++--- .../downcast-selection-converters.js | 16 ++-- tests/conversion/downcasthelpers.js | 54 +++++--------- 4 files changed, 88 insertions(+), 61 deletions(-) diff --git a/src/conversion/downcasthelpers.js b/src/conversion/downcasthelpers.js index abe2b4912..de240594c 100644 --- a/src/conversion/downcasthelpers.js +++ b/src/conversion/downcasthelpers.js @@ -421,8 +421,6 @@ export function createViewElementFromHighlightDescriptor( descriptor ) { return viewElement; } -// only: insertText, insertElement, wrap, insertUIElement - /** * Function factory that creates a converter which converts set/change/remove attribute changes from the model to the view. * It can also be used to convert selection attributes. In that case, an empty attribute element will be created and the @@ -517,7 +515,7 @@ export function wrap( elementCreator ) { * @param {Function} elementCreator Function returning a view element, which will be inserted. * @returns {Function} Insert element event converter. */ -export function insertElement( elementCreator ) { +function insertElement( elementCreator ) { return ( evt, data, conversionApi ) => { const viewElement = elementCreator( data.item, conversionApi.writer ); @@ -550,7 +548,7 @@ export function insertElement( elementCreator ) { * that will be inserted. * @returns {Function} Insert element event converter. */ -export function insertUIElement( elementCreator ) { +function insertUIElement( elementCreator ) { return ( evt, data, conversionApi ) => { // Create two view elements. One will be inserted at the beginning of marker, one at the end. // If marker is collapsed, only "opening" element will be inserted. diff --git a/src/dev-utils/model.js b/src/dev-utils/model.js index e5b224ad9..22bca0225 100644 --- a/src/dev-utils/model.js +++ b/src/dev-utils/model.js @@ -32,7 +32,7 @@ import { convertRangeSelection, convertCollapsedSelection, } from '../conversion/downcast-selection-converters'; -import { insertText, insertElement, wrap, insertUIElement } from '../conversion/downcasthelpers'; +import { insertText, wrap } from '../conversion/downcasthelpers'; import { isPlainObject } from 'lodash-es'; import toMap from '@ckeditor/ckeditor5-utils/src/tomap'; @@ -225,19 +225,72 @@ export function stringify( node, selectionOrPositionOrRange = null, markers = nu converter( evt, data, conversionApi ); } } ); - downcastDispatcher.on( 'insert', insertElement( modelItem => { - // Stringify object types values for properly display as an output string. - const attributes = convertAttributes( modelItem.getAttributes(), stringifyAttributeValue ); + downcastDispatcher.on( 'insert', ( evt, data, conversionApi ) => { + const attributes = convertAttributes( data.item.getAttributes(), stringifyAttributeValue ); + const viewElement = new ViewContainerElement( data.item.name, attributes ); + + if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) { + return; + } + + const viewPosition = conversionApi.mapper.toViewPosition( data.range.start ); + + conversionApi.mapper.bindElements( data.item, viewElement ); + conversionApi.writer.insert( viewPosition, viewElement ); + } ); - return new ViewContainerElement( modelItem.name, attributes ); - } ) ); downcastDispatcher.on( 'selection', convertRangeSelection() ); downcastDispatcher.on( 'selection', convertCollapsedSelection() ); - downcastDispatcher.on( 'addMarker', insertUIElement( ( data, writer ) => { - const name = data.markerName + ':' + ( data.isOpening ? 'start' : 'end' ); + downcastDispatcher.on( 'addMarker', ( evt, data, conversionApi ) => { + const elementCreator = ( data, writer ) => { + const name = data.markerName + ':' + ( data.isOpening ? 'start' : 'end' ); + + return writer.createUIElement( name ); + }; + + // Create two view elements. One will be inserted at the beginning of marker, one at the end. + // If marker is collapsed, only "opening" element will be inserted. + data.isOpening = true; + const viewStartElement = elementCreator( data, conversionApi.writer ); + + data.isOpening = false; + const viewEndElement = elementCreator( data, conversionApi.writer ); + + if ( !viewStartElement || !viewEndElement ) { + return; + } + + const markerRange = data.markerRange; - return writer.createUIElement( name ); - } ) ); + // Marker that is collapsed has consumable build differently that non-collapsed one. + // For more information see `addMarker` event description. + // If marker's range is collapsed - check if it can be consumed. + if ( markerRange.isCollapsed && !conversionApi.consumable.consume( markerRange, evt.name ) ) { + return; + } + + // If marker's range is not collapsed - consume all items inside. + for ( const value of markerRange ) { + if ( !conversionApi.consumable.consume( value.item, evt.name ) ) { + return; + } + } + + const mapper = conversionApi.mapper; + const viewWriter = conversionApi.writer; + + // Add "opening" element. + viewWriter.insert( mapper.toViewPosition( markerRange.start ), viewStartElement ); + conversionApi.mapper.bindElementToMarker( viewStartElement, data.markerName ); + + // Add "closing" element only if range is not collapsed. + if ( !markerRange.isCollapsed ) { + viewWriter.insert( mapper.toViewPosition( markerRange.end ), viewEndElement ); + conversionApi.mapper.bindElementToMarker( viewEndElement, data.markerName ); + } + + evt.stop(); + } ); // Convert model to view. const writer = view._writer; diff --git a/tests/conversion/downcast-selection-converters.js b/tests/conversion/downcast-selection-converters.js index d342918a0..719066914 100644 --- a/tests/conversion/downcast-selection-converters.js +++ b/tests/conversion/downcast-selection-converters.js @@ -16,11 +16,7 @@ import { clearAttributes, } from '../../src/conversion/downcast-selection-converters'; -import DowncastHelpers, { - insertElement, - insertText, - wrap -} from '../../src/conversion/downcasthelpers'; +import DowncastHelpers, { insertText, wrap } from '../../src/conversion/downcasthelpers'; import createViewRoot from '../view/_utils/createroot'; import { stringify as stringifyView } from '../../src/dev-utils/view'; @@ -498,12 +494,12 @@ describe( 'downcast-selection-converters', () => { model.schema.extend( 'td', { allowIn: 'tr' } ); model.schema.extend( '$text', { allowIn: 'td' } ); + const downcastHelpers = new DowncastHelpers( dispatcher ); + // "Universal" converter to convert table structure. - const containerCreator = ( modelElement, viewWriter ) => viewWriter.createContainerElement( modelElement.name ); - const tableConverter = insertElement( containerCreator ); - dispatcher.on( 'insert:table', tableConverter ); - dispatcher.on( 'insert:tr', tableConverter ); - dispatcher.on( 'insert:td', tableConverter ); + downcastHelpers.elementToElement( { model: 'table', view: 'table' } ); + downcastHelpers.elementToElement( { model: 'tr', view: 'tr' } ); + downcastHelpers.elementToElement( { model: 'td', view: 'td' } ); // Special converter for table cells. dispatcher.on( 'selection', ( evt, data, conversionApi ) => { diff --git a/tests/conversion/downcasthelpers.js b/tests/conversion/downcasthelpers.js index 8685c114e..5912139a3 100644 --- a/tests/conversion/downcasthelpers.js +++ b/tests/conversion/downcasthelpers.js @@ -20,7 +20,7 @@ import ViewText from '../../src/view/text'; import log from '@ckeditor/ckeditor5-utils/src/log'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; -import DowncastHelpers, { createViewElementFromHighlightDescriptor, insertElement, wrap } from '../../src/conversion/downcasthelpers'; +import DowncastHelpers, { createViewElementFromHighlightDescriptor, wrap } from '../../src/conversion/downcasthelpers'; import { stringify } from '../../src/dev-utils/view'; @@ -252,6 +252,18 @@ describe( 'DowncastHelpers', () => { expectResult( 'foo' ); } ); + + it( 'should not convert if creator returned null', () => { + downcastHelpers.elementToElement( { model: 'div', view: () => null } ); + + const modelElement = new ModelElement( 'div' ); + + model.change( writer => { + writer.insert( modelElement, modelRootStart ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '
' ); + } ); } ); describe( 'attributeToAttribute()', () => { @@ -1343,15 +1355,8 @@ describe( 'downcast-converters', () => { controller.view.document.getRoot()._name = 'div'; dispatcher = controller.downcastDispatcher; - - dispatcher.on( - 'insert:paragraph', - insertElement( - ( modelItem, viewWriter ) => viewWriter.createContainerElement( 'p' ) - ) - ); - - // dispatcher.on( 'attribute:class', changeAttribute() ); + const downcastHelpers = new DowncastHelpers( dispatcher ); + downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); modelRootStart = model.createPositionAt( modelRoot, 0 ); } ); @@ -1419,30 +1424,6 @@ describe( 'downcast-converters', () => { } ); } ); - describe( 'insertElement', () => { - it( 'should convert element insertion in model', () => { - const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar' ) ); - - model.change( writer => { - writer.insert( modelElement, modelRootStart ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - } ); - - it( 'should not convert if creator returned null', () => { - dispatcher.on( 'insert:div', insertElement( () => null ) ); - - const modelElement = new ModelElement( 'div' ); - - model.change( writer => { - writer.insert( modelElement, modelRootStart ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '
' ); - } ); - } ); - describe( 'wrap', () => { it( 'should convert insert/change/remove of attribute in model into wrapping element in a view', () => { const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) ); @@ -1693,9 +1674,8 @@ describe( 'downcast-converters', () => { const modelP = new ModelElement( 'paragraph', null, new ModelText( 'foo' ) ); const modelWidget = new ModelElement( 'widget', null, modelP ); - dispatcher.on( 'insert:widget', insertElement( - ( modelElement, viewWriter ) => viewWriter.createContainerElement( 'widget' ) ) - ); + const downcastHelpers = new DowncastHelpers( dispatcher ); + downcastHelpers.elementToElement( { model: 'widget', view: 'widget' } ); model.change( writer => { writer.insert( modelWidget, modelRootStart ); From 374c6ce3af757450c53e443cea7926be778ceeec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 21 Dec 2018 14:33:28 +0100 Subject: [PATCH 67/84] Remove redundant code. --- src/conversion/downcasthelpers.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/conversion/downcasthelpers.js b/src/conversion/downcasthelpers.js index de240594c..0527355b0 100644 --- a/src/conversion/downcasthelpers.js +++ b/src/conversion/downcasthelpers.js @@ -655,8 +655,6 @@ function removeUIElement() { * @returns {Function} Set/change attribute converter. */ function changeAttribute( attributeCreator ) { - attributeCreator = attributeCreator || ( ( value, data ) => ( { value, key: data.attributeKey } ) ); - return ( evt, data, conversionApi ) => { const oldAttribute = attributeCreator( data.attributeOldValue, data ); const newAttribute = attributeCreator( data.attributeNewValue, data ); From f08e1ba305b121bd74838aa1f344c18a077a80f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 21 Dec 2018 14:35:31 +0100 Subject: [PATCH 68/84] Remove redundant code from dev-utils/model. --- src/dev-utils/model.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/dev-utils/model.js b/src/dev-utils/model.js index 22bca0225..36ebd0aa2 100644 --- a/src/dev-utils/model.js +++ b/src/dev-utils/model.js @@ -256,10 +256,6 @@ export function stringify( node, selectionOrPositionOrRange = null, markers = nu data.isOpening = false; const viewEndElement = elementCreator( data, conversionApi.writer ); - if ( !viewStartElement || !viewEndElement ) { - return; - } - const markerRange = data.markerRange; // Marker that is collapsed has consumable build differently that non-collapsed one. From 276b61b09fb8204308d81dd9f879ea1017bfdf86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 21 Dec 2018 15:07:31 +0100 Subject: [PATCH 69/84] Add tests for getData() markers in dev-utils/model.js. --- src/dev-utils/model.js | 11 +---------- tests/dev-utils/model.js | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/dev-utils/model.js b/src/dev-utils/model.js index 36ebd0aa2..2289c34d8 100644 --- a/src/dev-utils/model.js +++ b/src/dev-utils/model.js @@ -258,18 +258,9 @@ export function stringify( node, selectionOrPositionOrRange = null, markers = nu const markerRange = data.markerRange; - // Marker that is collapsed has consumable build differently that non-collapsed one. - // For more information see `addMarker` event description. - // If marker's range is collapsed - check if it can be consumed. - if ( markerRange.isCollapsed && !conversionApi.consumable.consume( markerRange, evt.name ) ) { - return; - } - // If marker's range is not collapsed - consume all items inside. for ( const value of markerRange ) { - if ( !conversionApi.consumable.consume( value.item, evt.name ) ) { - return; - } + conversionApi.consumable.consume( value.item, evt.name ); } const mapper = conversionApi.mapper; diff --git a/tests/dev-utils/model.js b/tests/dev-utils/model.js index b99b0f307..95a38ebb0 100644 --- a/tests/dev-utils/model.js +++ b/tests/dev-utils/model.js @@ -79,6 +79,28 @@ describe( 'model test utils', () => { getData( { invalid: 'document' } ); } ).to.throw( TypeError, 'Model needs to be an instance of module:engine/model/model~Model.' ); } ); + + describe( 'markers', () => { + it( 'should stringify collapsed marker', () => { + setData( model, 'bar' ); + + model.markers._set( 'foo', new Range( Position._createAt( document.getRoot(), 0 ) ) ); + + expect( getData( model, { convertMarkers: true, withoutSelection: true } ) ) + .to.equal( 'bar' ); + } ); + + it( 'should stringify non-collapsed marker', () => { + setData( model, 'bar' ); + + const markerRange = new Range( Position._createAt( document.getRoot(), 0 ), Position._createAt( document.getRoot(), 1 ) ); + + model.markers._set( 'foo', markerRange ); + + expect( getData( model, { convertMarkers: true, withoutSelection: true } ) ) + .to.equal( 'bar' ); + } ); + } ); } ); describe( 'setData', () => { From 5b46d260bff6131ed770c63ba610c4162cf9f0ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 21 Dec 2018 15:14:18 +0100 Subject: [PATCH 70/84] Remove underscore from private functions. --- src/conversion/downcasthelpers.js | 54 +++++++++++++-------------- src/conversion/upcasthelpers.js | 62 +++++++++++++++---------------- 2 files changed, 58 insertions(+), 58 deletions(-) diff --git a/src/conversion/downcasthelpers.js b/src/conversion/downcasthelpers.js index 0527355b0..1d2a23d6e 100644 --- a/src/conversion/downcasthelpers.js +++ b/src/conversion/downcasthelpers.js @@ -69,7 +69,7 @@ export default class DowncastHelpers extends ConversionHelpers { * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers} */ elementToElement( config ) { - return this.add( _downcastElementToElement( config ) ); + return this.add( downcastElementToElement( config ) ); } /** @@ -154,7 +154,7 @@ export default class DowncastHelpers extends ConversionHelpers { * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers} */ attributeToElement( config ) { - return this.add( _downcastAttributeToElement( config ) ); + return this.add( downcastAttributeToElement( config ) ); } /** @@ -220,7 +220,7 @@ export default class DowncastHelpers extends ConversionHelpers { * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers} */ attributeToAttribute( config ) { - return this.add( _downcastAttributeToAttribute( config ) ); + return this.add( downcastAttributeToAttribute( config ) ); } /** @@ -282,7 +282,7 @@ export default class DowncastHelpers extends ConversionHelpers { * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers} */ markerToElement( config ) { - return this.add( _downcastMarkerToElement( config ) ); + return this.add( downcastMarkerToElement( config ) ); } /** @@ -341,7 +341,7 @@ export default class DowncastHelpers extends ConversionHelpers { * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers} */ markerToHighlight( config ) { - return this.add( _downcastMarkerToHighlight( config ) ); + return this.add( downcastMarkerToHighlight( config ) ); } } @@ -781,7 +781,7 @@ function highlightText( highlightDescriptor ) { return; } - const descriptor = _prepareDescriptor( highlightDescriptor, data, conversionApi ); + const descriptor = prepareDescriptor( highlightDescriptor, data, conversionApi ); if ( !descriptor ) { return; @@ -844,7 +844,7 @@ function highlightElement( highlightDescriptor ) { return; } - const descriptor = _prepareDescriptor( highlightDescriptor, data, conversionApi ); + const descriptor = prepareDescriptor( highlightDescriptor, data, conversionApi ); if ( !descriptor ) { return; @@ -903,7 +903,7 @@ function removeHighlight( highlightDescriptor ) { return; } - const descriptor = _prepareDescriptor( highlightDescriptor, data, conversionApi ); + const descriptor = prepareDescriptor( highlightDescriptor, data, conversionApi ); if ( !descriptor ) { return; @@ -946,10 +946,10 @@ function removeHighlight( highlightDescriptor ) { // that takes the model element and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer} // as parameters and returns a view container element. // @returns {Function} Conversion helper. -function _downcastElementToElement( config ) { +function downcastElementToElement( config ) { config = cloneDeep( config ); - config.view = _normalizeToElementConfig( config.view, 'container' ); + config.view = normalizeToElementConfig( config.view, 'container' ); return dispatcher => { dispatcher.on( 'insert:' + config.model, insertElement( config.view ), { priority: config.converterPriority || 'normal' } ); @@ -969,7 +969,7 @@ function _downcastElementToElement( config ) { // given, `config.view` should be an object assigning values from `config.model.values` to view element definitions or functions. // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. // @returns {Function} Conversion helper. -function _downcastAttributeToElement( config ) { +function downcastAttributeToElement( config ) { config = cloneDeep( config ); const modelKey = config.model.key ? config.model.key : config.model; @@ -981,13 +981,13 @@ function _downcastAttributeToElement( config ) { if ( config.model.values ) { for ( const modelValue of config.model.values ) { - config.view[ modelValue ] = _normalizeToElementConfig( config.view[ modelValue ], 'attribute' ); + config.view[ modelValue ] = normalizeToElementConfig( config.view[ modelValue ], 'attribute' ); } } else { - config.view = _normalizeToElementConfig( config.view, 'attribute' ); + config.view = normalizeToElementConfig( config.view, 'attribute' ); } - const elementCreator = _getFromAttributeCreator( config ); + const elementCreator = getFromAttributeCreator( config ); return dispatcher => { dispatcher.on( eventName, wrap( elementCreator ), { priority: config.converterPriority || 'normal' } ); @@ -1008,7 +1008,7 @@ function _downcastAttributeToElement( config ) { // `{ key, value }` objects or a functions. // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. // @returns {Function} Conversion helper. -function _downcastAttributeToAttribute( config ) { +function downcastAttributeToAttribute( config ) { config = cloneDeep( config ); const modelKey = config.model.key ? config.model.key : config.model; @@ -1020,13 +1020,13 @@ function _downcastAttributeToAttribute( config ) { if ( config.model.values ) { for ( const modelValue of config.model.values ) { - config.view[ modelValue ] = _normalizeToAttributeConfig( config.view[ modelValue ] ); + config.view[ modelValue ] = normalizeToAttributeConfig( config.view[ modelValue ] ); } } else { - config.view = _normalizeToAttributeConfig( config.view ); + config.view = normalizeToAttributeConfig( config.view ); } - const elementCreator = _getFromAttributeCreator( config ); + const elementCreator = getFromAttributeCreator( config ); return dispatcher => { dispatcher.on( eventName, changeAttribute( elementCreator ), { priority: config.converterPriority || 'normal' } ); @@ -1043,10 +1043,10 @@ function _downcastAttributeToAttribute( config ) { // that takes the model marker data as a parameter and returns a view UI element. // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. // @returns {Function} Conversion helper. -function _downcastMarkerToElement( config ) { +function downcastMarkerToElement( config ) { config = cloneDeep( config ); - config.view = _normalizeToElementConfig( config.view, 'ui' ); + config.view = normalizeToElementConfig( config.view, 'ui' ); return dispatcher => { dispatcher.on( 'addMarker:' + config.model, insertUIElement( config.view ), { priority: config.converterPriority || 'normal' } ); @@ -1064,7 +1064,7 @@ function _downcastMarkerToElement( config ) { // that will be used for highlighting or a function that takes the model marker data as a parameter and returns a highlight descriptor. // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. // @returns {Function} Conversion helper. -function _downcastMarkerToHighlight( config ) { +function downcastMarkerToHighlight( config ) { return dispatcher => { dispatcher.on( 'addMarker:' + config.model, highlightText( config.view ), { priority: config.converterPriority || 'normal' } ); dispatcher.on( 'addMarker:' + config.model, highlightElement( config.view ), { priority: config.converterPriority || 'normal' } ); @@ -1078,13 +1078,13 @@ function _downcastMarkerToHighlight( config ) { // @param {module:engine/view/elementdefinition~ElementDefinition|Function} view View configuration. // @param {'container'|'attribute'|'ui'} viewElementType View element type to create. // @returns {Function} Element creator function to use in lower level converters. -function _normalizeToElementConfig( view, viewElementType ) { +function normalizeToElementConfig( view, viewElementType ) { if ( typeof view == 'function' ) { // If `view` is already a function, don't do anything. return view; } - return ( modelData, viewWriter ) => _createViewElementFromDefinition( view, viewWriter, viewElementType ); + return ( modelData, viewWriter ) => createViewElementFromDefinition( view, viewWriter, viewElementType ); } // Creates a view element instance from the provided {@link module:engine/view/elementdefinition~ElementDefinition} and class. @@ -1093,7 +1093,7 @@ function _normalizeToElementConfig( view, viewElementType ) { // @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter // @param {'container'|'attribute'|'ui'} viewElementType // @returns {module:engine/view/element~Element} -function _createViewElementFromDefinition( viewElementDefinition, viewWriter, viewElementType ) { +function createViewElementFromDefinition( viewElementDefinition, viewWriter, viewElementType ) { if ( typeof viewElementDefinition == 'string' ) { // If `viewElementDefinition` is given as a `String`, normalize it to an object with `name` property. viewElementDefinition = { name: viewElementDefinition }; @@ -1138,7 +1138,7 @@ function _createViewElementFromDefinition( viewElementDefinition, viewWriter, vi return element; } -function _getFromAttributeCreator( config ) { +function getFromAttributeCreator( config ) { if ( config.model.values ) { return ( modelAttributeValue, viewWriter ) => { const view = config.view[ modelAttributeValue ]; @@ -1158,7 +1158,7 @@ function _getFromAttributeCreator( config ) { // for generating a view attribute. // // @param {Object} view View configuration. -function _normalizeToAttributeConfig( view ) { +function normalizeToAttributeConfig( view ) { if ( typeof view == 'string' ) { return modelAttributeValue => ( { key: view, value: modelAttributeValue } ); } else if ( typeof view == 'object' ) { @@ -1177,7 +1177,7 @@ function _normalizeToAttributeConfig( view ) { } // Helper function for `highlight`. Prepares the actual descriptor object using value passed to the converter. -function _prepareDescriptor( highlightDescriptor, data, conversionApi ) { +function prepareDescriptor( highlightDescriptor, data, conversionApi ) { // If passed descriptor is a creator function, call it. If not, just use passed value. const descriptor = typeof highlightDescriptor == 'function' ? highlightDescriptor( data, conversionApi ) : diff --git a/src/conversion/upcasthelpers.js b/src/conversion/upcasthelpers.js index 91a790384..5c7f38ce1 100644 --- a/src/conversion/upcasthelpers.js +++ b/src/conversion/upcasthelpers.js @@ -72,7 +72,7 @@ export default class UpcastHelpers extends ConversionHelpers { * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers} */ elementToElement( config ) { - return this.add( _upcastElementToElement( config ) ); + return this.add( upcastElementToElement( config ) ); } /** @@ -158,7 +158,7 @@ export default class UpcastHelpers extends ConversionHelpers { * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers} */ elementToAttribute( config ) { - return this.add( _upcastElementToAttribute( config ) ); + return this.add( upcastElementToAttribute( config ) ); } /** @@ -252,7 +252,7 @@ export default class UpcastHelpers extends ConversionHelpers { * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers} */ attributeToAttribute( config ) { - return this.add( _upcastAttributeToAttribute( config ) ); + return this.add( upcastAttributeToAttribute( config ) ); } /** @@ -301,7 +301,7 @@ export default class UpcastHelpers extends ConversionHelpers { * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers} */ elementToMarker( config ) { - return this.add( _upcastElementToMarker( config ) ); + return this.add( upcastElementToMarker( config ) ); } } @@ -362,12 +362,12 @@ export function convertText() { // instance or a function that takes a view element and returns a model element. The model element will be inserted in the model. // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. // @returns {Function} Conversion helper. -function _upcastElementToElement( config ) { +function upcastElementToElement( config ) { config = cloneDeep( config ); - const converter = _prepareToElementConverter( config ); + const converter = prepareToElementConverter( config ); - const elementName = _getViewElementNameFromConfig( config ); + const elementName = getViewElementNameFromConfig( config ); const eventName = elementName ? 'element:' + elementName : 'element'; return dispatcher => { @@ -386,14 +386,14 @@ function _upcastElementToElement( config ) { // If `String` is given, the model attribute value will be set to `true`. // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. // @returns {Function} Conversion helper. -function _upcastElementToAttribute( config ) { +function upcastElementToAttribute( config ) { config = cloneDeep( config ); - _normalizeModelAttributeConfig( config ); + normalizeModelAttributeConfig( config ); - const converter = _prepareToAttributeConverter( config, false ); + const converter = prepareToAttributeConverter( config, false ); - const elementName = _getViewElementNameFromConfig( config ); + const elementName = getViewElementNameFromConfig( config ); const eventName = elementName ? 'element:' + elementName : 'element'; return dispatcher => { @@ -416,18 +416,18 @@ function _upcastElementToAttribute( config ) { // If `String` is given, the model attribute value will be same as view attribute value. // @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority. // @returns {Function} Conversion helper. -function _upcastAttributeToAttribute( config ) { +function upcastAttributeToAttribute( config ) { config = cloneDeep( config ); let viewKey = null; if ( typeof config.view == 'string' || config.view.key ) { - viewKey = _normalizeViewAttributeKeyValueConfig( config ); + viewKey = normalizeViewAttributeKeyValueConfig( config ); } - _normalizeModelAttributeConfig( config, viewKey ); + normalizeModelAttributeConfig( config, viewKey ); - const converter = _prepareToAttributeConverter( config, true ); + const converter = prepareToAttributeConverter( config, true ); return dispatcher => { dispatcher.on( 'element', converter, { priority: config.converterPriority || 'low' } ); @@ -444,12 +444,12 @@ function _upcastAttributeToAttribute( config ) { // a model marker name. // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. // @returns {Function} Conversion helper. -function _upcastElementToMarker( config ) { +function upcastElementToMarker( config ) { config = cloneDeep( config ); - _normalizeToMarkerConfig( config ); + normalizeToMarkerConfig( config ); - return _upcastElementToElement( config ); + return upcastElementToElement( config ); } // Helper function for from-view-element conversion. Checks if `config.view` directly specifies converted view element's name @@ -457,7 +457,7 @@ function _upcastElementToMarker( config ) { // // @param {Object} config Conversion config. // @returns {String|null} View element name or `null` if name is not directly set. -function _getViewElementNameFromConfig( config ) { +function getViewElementNameFromConfig( config ) { if ( typeof config.view == 'string' ) { return config.view; } @@ -473,7 +473,7 @@ function _getViewElementNameFromConfig( config ) { // // @param {Object} config Conversion configuration. // @returns {Function} View to model converter. -function _prepareToElementConverter( config ) { +function prepareToElementConverter( config ) { const matcher = new Matcher( config.view ); return ( evt, data, conversionApi ) => { @@ -489,7 +489,7 @@ function _prepareToElementConverter( config ) { match.match.name = true; // Create model element basing on config. - const modelElement = _getModelElement( config.model, data.viewItem, conversionApi.writer ); + const modelElement = getModelElement( config.model, data.viewItem, conversionApi.writer ); // Do not convert if element building function returned falsy value. if ( !modelElement ) { @@ -552,7 +552,7 @@ function _prepareToElementConverter( config ) { // @param {String|Function|module:engine/model/element~Element} model Model conversion configuration. // @param {module:engine/view/node~Node} input The converted view node. // @param {module:engine/model/writer~Writer} writer A writer instance to use to create the model element. -function _getModelElement( model, input, writer ) { +function getModelElement( model, input, writer ) { if ( model instanceof Function ) { return model( input, writer ); } else { @@ -566,7 +566,7 @@ function _getModelElement( model, input, writer ) { // // @param {Object} config Conversion config. // @returns {String} Key of the converted view attribute. -function _normalizeViewAttributeKeyValueConfig( config ) { +function normalizeViewAttributeKeyValueConfig( config ) { if ( typeof config.view == 'string' ) { config.view = { key: config.view }; } @@ -606,7 +606,7 @@ function _normalizeViewAttributeKeyValueConfig( config ) { // @param {Object} config Conversion config. // @param {String} viewAttributeKeyToCopy Key of the converted view attribute. If it is set, model attribute value // will be equal to view attribute value. -function _normalizeModelAttributeConfig( config, viewAttributeKeyToCopy = null ) { +function normalizeModelAttributeConfig( config, viewAttributeKeyToCopy = null ) { const defaultModelValue = viewAttributeKeyToCopy === null ? true : viewElement => viewElement.getAttribute( viewAttributeKeyToCopy ); const key = typeof config.model != 'object' ? config.model : config.model.key; @@ -622,7 +622,7 @@ function _normalizeModelAttributeConfig( config, viewAttributeKeyToCopy = null ) // @param {Object|Array.} config Conversion configuration. It is possible to provide multiple configurations in an array. // @param {Boolean} shallow If set to `true` the attribute will be set only on top-level nodes. Otherwise, it will be set // on all elements in the range. -function _prepareToAttributeConverter( config, shallow ) { +function prepareToAttributeConverter( config, shallow ) { const matcher = new Matcher( config.view ); return ( evt, data, conversionApi ) => { @@ -641,7 +641,7 @@ function _prepareToAttributeConverter( config, shallow ) { return; } - if ( _onlyViewNameIsDefined( config ) ) { + if ( onlyViewNameIsDefined( config ) ) { match.match.name = true; } else { // Do not test or consume `name` consumable. @@ -661,7 +661,7 @@ function _prepareToAttributeConverter( config, shallow ) { } // Set attribute on current `output`. `Schema` is checked inside this helper function. - const attributeWasSet = _setAttributeOn( data.modelRange, { key: modelKey, value: modelValue }, shallow, conversionApi ); + const attributeWasSet = setAttributeOn( data.modelRange, { key: modelKey, value: modelValue }, shallow, conversionApi ); if ( attributeWasSet ) { conversionApi.consumable.consume( data.viewItem, match.match ); @@ -673,8 +673,8 @@ function _prepareToAttributeConverter( config, shallow ) { // // @param {Object} config Conversion config. // @returns {Boolean} -function _onlyViewNameIsDefined( config ) { - if ( typeof config.view == 'object' && !_getViewElementNameFromConfig( config ) ) { +function onlyViewNameIsDefined( config ) { + if ( typeof config.view == 'object' && !getViewElementNameFromConfig( config ) ) { return false; } @@ -690,7 +690,7 @@ function _onlyViewNameIsDefined( config ) { // @param {Boolean} shallow If set to `true` the attribute will be set only on top-level nodes. Otherwise, it will be set // on all elements in the range. // @returns {Boolean} `true` if attribute was set on at least one node from given `modelRange`. -function _setAttributeOn( modelRange, modelAttribute, shallow, conversionApi ) { +function setAttributeOn( modelRange, modelAttribute, shallow, conversionApi ) { let result = false; // Set attribute on each item in range according to Schema. @@ -709,7 +709,7 @@ function _setAttributeOn( modelRange, modelAttribute, shallow, conversionApi ) { // function and converts it to a format that is supported by `_upcastElementToElement()` function. // // @param {Object} config Conversion configuration. -function _normalizeToMarkerConfig( config ) { +function normalizeToMarkerConfig( config ) { const oldModel = config.model; config.model = ( viewElement, modelWriter ) => { From 2f41916122b32ec9c06c5464ad2e615f7a1dc5b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 21 Dec 2018 15:20:39 +0100 Subject: [PATCH 71/84] Remove public documentation of private functions in downcasthelpers.js. --- src/conversion/downcasthelpers.js | 266 ++++++++++++++---------------- 1 file changed, 126 insertions(+), 140 deletions(-) diff --git a/src/conversion/downcasthelpers.js b/src/conversion/downcasthelpers.js index 1d2a23d6e..90e97a7c4 100644 --- a/src/conversion/downcasthelpers.js +++ b/src/conversion/downcasthelpers.js @@ -490,31 +490,29 @@ export function wrap( elementCreator ) { }; } -/** - * Function factory that creates a converter which converts node insertion changes from the model to the view. - * The function passed will be provided with all the parameters of the dispatcher's - * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert `insert` event}. - * It is expected that the function returns an {@link module:engine/view/element~Element}. - * The result of the function will be inserted into the view. - * - * The converter automatically consumes the corresponding value from the consumables list, stops the event (see - * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}) and binds the model and view elements. - * - * downcastDispatcher.on( - * 'insert:myElem', - * insertElement( ( modelItem, viewWriter ) => { - * const text = viewWriter.createText( 'myText' ); - * const myElem = viewWriter.createElement( 'myElem', { myAttr: 'my-' + modelItem.getAttribute( 'myAttr' ) }, text ); - * - * // Do something fancy with `myElem` using `modelItem` or other parameters. - * - * return myElem; - * } - * ) ); - * - * @param {Function} elementCreator Function returning a view element, which will be inserted. - * @returns {Function} Insert element event converter. - */ +// Function factory that creates a converter which converts node insertion changes from the model to the view. +// The function passed will be provided with all the parameters of the dispatcher's +// {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert `insert` event}. +// It is expected that the function returns an {@link module:engine/view/element~Element}. +// The result of the function will be inserted into the view. +// +// The converter automatically consumes the corresponding value from the consumables list, stops the event (see +// {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}) and binds the model and view elements. +// +// downcastDispatcher.on( +// 'insert:myElem', +// insertElement( ( modelItem, viewWriter ) => { +// const text = viewWriter.createText( 'myText' ); +// const myElem = viewWriter.createElement( 'myElem', { myAttr: 'my-' + modelItem.getAttribute( 'myAttr' ) }, text ); +// +// // Do something fancy with `myElem` using `modelItem` or other parameters. +// +// return myElem; +// } +// ) ); +// +// @param {Function} elementCreator Function returning a view element, which will be inserted. +// @returns {Function} Insert element event converter. function insertElement( elementCreator ) { return ( evt, data, conversionApi ) => { const viewElement = elementCreator( data.item, conversionApi.writer ); @@ -534,20 +532,18 @@ function insertElement( elementCreator ) { }; } -/** - * Function factory that creates a converter which converts marker adding change to the - * {@link module:engine/view/uielement~UIElement view UI element}. - * - * The view UI element that will be added to the view depends on the passed parameter. See {@link ~insertElement}. - * In case of a non-collapsed range, the UI element will not wrap nodes but separate elements will be placed at the beginning - * and at the end of the range. - * - * This converter binds created UI elements with the marker name using {@link module:engine/conversion/mapper~Mapper#bindElementToMarker}. - * - * @param {module:engine/view/uielement~UIElement|Function} elementCreator A view UI element or a function returning the view element - * that will be inserted. - * @returns {Function} Insert element event converter. - */ +// Function factory that creates a converter which converts marker adding change to the +// {@link module:engine/view/uielement~UIElement view UI element}. +// +// The view UI element that will be added to the view depends on the passed parameter. See {@link ~insertElement}. +// In case of a non-collapsed range, the UI element will not wrap nodes but separate elements will be placed at the beginning +// and at the end of the range. +// +// This converter binds created UI elements with the marker name using {@link module:engine/conversion/mapper~Mapper#bindElementToMarker}. +// +// @param {module:engine/view/uielement~UIElement|Function} elementCreator A view UI element or a function returning the view element +// that will be inserted. +// @returns {Function} Insert element event converter. function insertUIElement( elementCreator ) { return ( evt, data, conversionApi ) => { // Create two view elements. One will be inserted at the beginning of marker, one at the end. @@ -595,14 +591,12 @@ function insertUIElement( elementCreator ) { }; } -/** - * Function factory that returns a default downcast converter for removing a {@link module:engine/view/uielement~UIElement UI element} - * basing on marker remove change. - * - * This converter unbinds elements from the marker name. - * - * @returns {Function} Removed UI element converter. - */ +// Function factory that returns a default downcast converter for removing a {@link module:engine/view/uielement~UIElement UI element} +// basing on marker remove change. +// +// This converter unbinds elements from the marker name. +// +// @returns {Function} Removed UI element converter. function removeUIElement() { return ( evt, data, conversionApi ) => { const elements = conversionApi.mapper.markerNameToElements( data.markerName ); @@ -623,37 +617,35 @@ function removeUIElement() { }; } -/** - * Function factory that creates a converter which converts set/change/remove attribute changes from the model to the view. - * - * Attributes from the model are converted to the view element attributes in the view. You may provide a custom function to generate - * a key-value attribute pair to add/change/remove. If not provided, model attributes will be converted to view element - * attributes on a one-to-one basis. - * - * **Note:** The provided attribute creator should always return the same `key` for a given attribute from the model. - * - * The converter automatically consumes the corresponding value from the consumables list and stops the event (see - * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}). - * - * modelDispatcher.on( 'attribute:customAttr:myElem', changeAttribute( ( value, data ) => { - * // Change attribute key from `customAttr` to `class` in the view. - * const key = 'class'; - * let value = data.attributeNewValue; - * - * // Force attribute value to 'empty' if the model element is empty. - * if ( data.item.childCount === 0 ) { - * value = 'empty'; - * } - * - * // Return the key-value pair. - * return { key, value }; - * } ) ); - * - * @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which - * represent the attribute key and attribute value to be set on a {@link module:engine/view/element~Element view element}. - * The function is passed the model attribute value as the first parameter and additional data about the change as the second parameter. - * @returns {Function} Set/change attribute converter. - */ +// Function factory that creates a converter which converts set/change/remove attribute changes from the model to the view. +// +// Attributes from the model are converted to the view element attributes in the view. You may provide a custom function to generate +// a key-value attribute pair to add/change/remove. If not provided, model attributes will be converted to view element +// attributes on a one-to-one basis. +// +// *Note:** The provided attribute creator should always return the same `key` for a given attribute from the model. +// +// The converter automatically consumes the corresponding value from the consumables list and stops the event (see +// {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}). +// +// modelDispatcher.on( 'attribute:customAttr:myElem', changeAttribute( ( value, data ) => { +// // Change attribute key from `customAttr` to `class` in the view. +// const key = 'class'; +// let value = data.attributeNewValue; +// +// // Force attribute value to 'empty' if the model element is empty. +// if ( data.item.childCount === 0 ) { +// value = 'empty'; +// } +// +// // Return the key-value pair. +// return { key, value }; +// } ) ); +// +// @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which +// represent the attribute key and attribute value to be set on a {@link module:engine/view/element~Element view element}. +// The function is passed the model attribute value as the first parameter and additional data about the change as the second parameter. +// @returns {Function} Set/change attribute converter. function changeAttribute( attributeCreator ) { return ( evt, data, conversionApi ) => { const oldAttribute = attributeCreator( data.attributeOldValue, data ); @@ -753,24 +745,22 @@ function changeAttribute( attributeCreator ) { }; } -/** - * Function factory that creates a converter which converts the text inside marker's range. The converter wraps the text with - * {@link module:engine/view/attributeelement~AttributeElement} created from the provided descriptor. - * See {link module:engine/conversion/downcasthelpers~createViewElementFromHighlightDescriptor}. - * - * It can also be used to convert the selection that is inside a marker. In that case, an empty attribute element will be - * created and the selection will be put inside it. - * - * If the highlight descriptor does not provide the `priority` property, `10` will be used. - * - * If the highlight descriptor does not provide the `id` property, the name of the marker will be used. - * - * This converter binds the created {@link module:engine/view/attributeelement~AttributeElement attribute elemens} with the marker name - * using the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker} method. - * - * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor - * @returns {Function} - */ +// Function factory that creates a converter which converts the text inside marker's range. The converter wraps the text with +// {@link module:engine/view/attributeelement~AttributeElement} created from the provided descriptor. +// See {link module:engine/conversion/downcasthelpers~createViewElementFromHighlightDescriptor}. +// +// It can also be used to convert the selection that is inside a marker. In that case, an empty attribute element will be +// created and the selection will be put inside it. +// +// If the highlight descriptor does not provide the `priority` property, `10` will be used. +// +// If the highlight descriptor does not provide the `id` property, the name of the marker will be used. +// +// This converter binds the created {@link module:engine/view/attributeelement~AttributeElement attribute elemens} with the marker name +// using the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker} method. +// +// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor +// @returns {Function} function highlightText( highlightDescriptor ) { return ( evt, data, conversionApi ) => { if ( data.markerRange.isCollapsed ) { @@ -814,26 +804,24 @@ function highlightText( highlightDescriptor ) { }; } -/** - * Converter function factory. It creates a function which applies the marker's highlight to an element inside the marker's range. - * - * The converter checks if an element has the `addHighlight` function stored as a - * {@link module:engine/view/element~Element#_setCustomProperty custom property} and, if so, uses it to apply the highlight. - * In such case the converter will consume all element's children, assuming that they were handled by the element itself. - * - * When the `addHighlight` custom property is not present, the element is not converted in any special way. - * This means that converters will proceed to convert the element's child nodes. - * - * If the highlight descriptor does not provide the `priority` property, `10` will be used. - * - * If the highlight descriptor does not provide the `id` property, the name of the marker will be used. - * - * This converter binds altered {@link module:engine/view/containerelement~ContainerElement container elements} with the marker name using - * the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker} method. - * - * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor - * @returns {Function} - */ +// Converter function factory. It creates a function which applies the marker's highlight to an element inside the marker's range. +// +// The converter checks if an element has the `addHighlight` function stored as a +// {@link module:engine/view/element~Element#_setCustomProperty custom property} and, if so, uses it to apply the highlight. +// In such case the converter will consume all element's children, assuming that they were handled by the element itself. +// +// When the `addHighlight` custom property is not present, the element is not converted in any special way. +// This means that converters will proceed to convert the element's child nodes. +// +// If the highlight descriptor does not provide the `priority` property, `10` will be used. +// +// If the highlight descriptor does not provide the `id` property, the name of the marker will be used. +// +// This converter binds altered {@link module:engine/view/containerelement~ContainerElement container elements} with the marker name using +// the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker} method. +// +// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor +// @returns {Function} function highlightElement( highlightDescriptor ) { return ( evt, data, conversionApi ) => { if ( data.markerRange.isCollapsed ) { @@ -872,30 +860,28 @@ function highlightElement( highlightDescriptor ) { }; } -/** - * Function factory that creates a converter which converts the removing model marker to the view. - * - * Both text nodes and elements are handled by this converter but they are handled a bit differently. - * - * Text nodes are unwrapped using the {@link module:engine/view/attributeelement~AttributeElement attribute element} created from the - * provided highlight descriptor. See {link module:engine/conversion/downcasthelpers~HighlightDescriptor}. - * - * For elements, the converter checks if an element has the `removeHighlight` function stored as a - * {@link module:engine/view/element~Element#_setCustomProperty custom property}. If so, it uses it to remove the highlight. - * In such case, the children of that element will not be converted. - * - * When `removeHighlight` is not present, the element is not converted in any special way. - * The converter will proceed to convert the element's child nodes instead. - * - * If the highlight descriptor does not provide the `priority` property, `10` will be used. - * - * If the highlight descriptor does not provide the `id` property, the name of the marker will be used. - * - * This converter unbinds elements from the marker name. - * - * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor - * @returns {Function} - */ +// Function factory that creates a converter which converts the removing model marker to the view. +// +// Both text nodes and elements are handled by this converter but they are handled a bit differently. +// +// Text nodes are unwrapped using the {@link module:engine/view/attributeelement~AttributeElement attribute element} created from the +// provided highlight descriptor. See {link module:engine/conversion/downcasthelpers~HighlightDescriptor}. +// +// For elements, the converter checks if an element has the `removeHighlight` function stored as a +// {@link module:engine/view/element~Element#_setCustomProperty custom property}. If so, it uses it to remove the highlight. +// In such case, the children of that element will not be converted. +// +// When `removeHighlight` is not present, the element is not converted in any special way. +// The converter will proceed to convert the element's child nodes instead. +// +// If the highlight descriptor does not provide the `priority` property, `10` will be used. +// +// If the highlight descriptor does not provide the `id` property, the name of the marker will be used. +// +// This converter unbinds elements from the marker name. +// +// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor +// @returns {Function} function removeHighlight( highlightDescriptor ) { return ( evt, data, conversionApi ) => { // This conversion makes sense only for non-collapsed range. From 6b2541ae2a7d0e395c54e990eefce63d4a736a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 2 Jan 2019 12:39:35 +0100 Subject: [PATCH 72/84] Expose `insertElement()` conversion helper as a protected export. --- src/conversion/downcasthelpers.js | 51 ++++++++++++++++--------------- src/dev-utils/model.js | 19 ++++-------- 2 files changed, 33 insertions(+), 37 deletions(-) diff --git a/src/conversion/downcasthelpers.js b/src/conversion/downcasthelpers.js index 2f76e8069..a838f29e3 100644 --- a/src/conversion/downcasthelpers.js +++ b/src/conversion/downcasthelpers.js @@ -490,30 +490,33 @@ export function wrap( elementCreator ) { }; } -// Function factory that creates a converter which converts node insertion changes from the model to the view. -// The function passed will be provided with all the parameters of the dispatcher's -// {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert `insert` event}. -// It is expected that the function returns an {@link module:engine/view/element~Element}. -// The result of the function will be inserted into the view. -// -// The converter automatically consumes the corresponding value from the consumables list, stops the event (see -// {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}) and binds the model and view elements. -// -// downcastDispatcher.on( -// 'insert:myElem', -// insertElement( ( modelItem, viewWriter ) => { -// const text = viewWriter.createText( 'myText' ); -// const myElem = viewWriter.createElement( 'myElem', { myAttr: 'my-' + modelItem.getAttribute( 'myAttr' ) }, text ); -// -// // Do something fancy with `myElem` using `modelItem` or other parameters. -// -// return myElem; -// } -// ) ); -// -// @param {Function} elementCreator Function returning a view element, which will be inserted. -// @returns {Function} Insert element event converter. -function insertElement( elementCreator ) { +/** + * Function factory that creates a converter which converts node insertion changes from the model to the view. + * The function passed will be provided with all the parameters of the dispatcher's + * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert `insert` event}. + * It is expected that the function returns an {@link module:engine/view/element~Element}. + * The result of the function will be inserted into the view. + * + * The converter automatically consumes the corresponding value from the consumables list, stops the event (see + * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}) and binds the model and view elements. + * + * downcastDispatcher.on( + * 'insert:myElem', + * insertElement( ( modelItem, viewWriter ) => { + * const text = viewWriter.createText( 'myText' ); + * const myElem = viewWriter.createElement( 'myElem', { myAttr: 'my-' + modelItem.getAttribute( 'myAttr' ) }, text ); + * + * // Do something fancy with `myElem` using `modelItem` or other parameters. + * + * return myElem; + * } + * ) ); + * + * @protected + * @param {Function} elementCreator Function returning a view element, which will be inserted. + * @returns {Function} Insert element event converter. + */ +export function insertElement( elementCreator ) { return ( evt, data, conversionApi ) => { const viewElement = elementCreator( data.item, conversionApi.writer ); diff --git a/src/dev-utils/model.js b/src/dev-utils/model.js index 2289c34d8..4638f7c02 100644 --- a/src/dev-utils/model.js +++ b/src/dev-utils/model.js @@ -32,7 +32,7 @@ import { convertRangeSelection, convertCollapsedSelection, } from '../conversion/downcast-selection-converters'; -import { insertText, wrap } from '../conversion/downcasthelpers'; +import { insertElement, insertText, wrap } from '../conversion/downcasthelpers'; import { isPlainObject } from 'lodash-es'; import toMap from '@ckeditor/ckeditor5-utils/src/tomap'; @@ -225,19 +225,12 @@ export function stringify( node, selectionOrPositionOrRange = null, markers = nu converter( evt, data, conversionApi ); } } ); - downcastDispatcher.on( 'insert', ( evt, data, conversionApi ) => { - const attributes = convertAttributes( data.item.getAttributes(), stringifyAttributeValue ); - const viewElement = new ViewContainerElement( data.item.name, attributes ); + downcastDispatcher.on( 'insert', insertElement( modelItem => { + // Stringify object types values for properly display as an output string. + const attributes = convertAttributes( modelItem.getAttributes(), stringifyAttributeValue ); - if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) { - return; - } - - const viewPosition = conversionApi.mapper.toViewPosition( data.range.start ); - - conversionApi.mapper.bindElements( data.item, viewElement ); - conversionApi.writer.insert( viewPosition, viewElement ); - } ); + return new ViewContainerElement( modelItem.name, attributes ); + } ) ); downcastDispatcher.on( 'selection', convertRangeSelection() ); downcastDispatcher.on( 'selection', convertCollapsedSelection() ); From ed98b60e0ec61ed53f76849a62724cbc214a7c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 2 Jan 2019 12:42:35 +0100 Subject: [PATCH 73/84] Expose `insertUIElement()` conversion helper as a protected export. --- src/conversion/downcasthelpers.js | 28 +++++++++++---------- src/dev-utils/model.js | 42 ++++--------------------------- 2 files changed, 20 insertions(+), 50 deletions(-) diff --git a/src/conversion/downcasthelpers.js b/src/conversion/downcasthelpers.js index a838f29e3..9b5248cf3 100644 --- a/src/conversion/downcasthelpers.js +++ b/src/conversion/downcasthelpers.js @@ -535,19 +535,21 @@ export function insertElement( elementCreator ) { }; } -// Function factory that creates a converter which converts marker adding change to the -// {@link module:engine/view/uielement~UIElement view UI element}. -// -// The view UI element that will be added to the view depends on the passed parameter. See {@link ~insertElement}. -// In case of a non-collapsed range, the UI element will not wrap nodes but separate elements will be placed at the beginning -// and at the end of the range. -// -// This converter binds created UI elements with the marker name using {@link module:engine/conversion/mapper~Mapper#bindElementToMarker}. -// -// @param {module:engine/view/uielement~UIElement|Function} elementCreator A view UI element or a function returning the view element -// that will be inserted. -// @returns {Function} Insert element event converter. -function insertUIElement( elementCreator ) { +/** + * Function factory that creates a converter which converts marker adding change to the + * {@link module:engine/view/uielement~UIElement view UI element}. + * + * The view UI element that will be added to the view depends on the passed parameter. See {@link ~insertElement}. + * In case of a non-collapsed range, the UI element will not wrap nodes but separate elements will be placed at the beginning + * and at the end of the range. + * + * This converter binds created UI elements with the marker name using {@link module:engine/conversion/mapper~Mapper#bindElementToMarker}. + * + * @param {module:engine/view/uielement~UIElement|Function} elementCreator A view UI element or a function returning the view element + * that will be inserted. + * @returns {Function} Insert element event converter. + */ +export function insertUIElement( elementCreator ) { return ( evt, data, conversionApi ) => { // Create two view elements. One will be inserted at the beginning of marker, one at the end. // If marker is collapsed, only "opening" element will be inserted. diff --git a/src/dev-utils/model.js b/src/dev-utils/model.js index 4638f7c02..4427c4097 100644 --- a/src/dev-utils/model.js +++ b/src/dev-utils/model.js @@ -32,7 +32,7 @@ import { convertRangeSelection, convertCollapsedSelection, } from '../conversion/downcast-selection-converters'; -import { insertElement, insertText, wrap } from '../conversion/downcasthelpers'; +import { insertElement, insertText, insertUIElement, wrap } from '../conversion/downcasthelpers'; import { isPlainObject } from 'lodash-es'; import toMap from '@ckeditor/ckeditor5-utils/src/tomap'; @@ -234,43 +234,11 @@ export function stringify( node, selectionOrPositionOrRange = null, markers = nu downcastDispatcher.on( 'selection', convertRangeSelection() ); downcastDispatcher.on( 'selection', convertCollapsedSelection() ); - downcastDispatcher.on( 'addMarker', ( evt, data, conversionApi ) => { - const elementCreator = ( data, writer ) => { - const name = data.markerName + ':' + ( data.isOpening ? 'start' : 'end' ); + downcastDispatcher.on( 'addMarker', insertUIElement( ( data, writer ) => { + const name = data.markerName + ':' + ( data.isOpening ? 'start' : 'end' ); - return writer.createUIElement( name ); - }; - - // Create two view elements. One will be inserted at the beginning of marker, one at the end. - // If marker is collapsed, only "opening" element will be inserted. - data.isOpening = true; - const viewStartElement = elementCreator( data, conversionApi.writer ); - - data.isOpening = false; - const viewEndElement = elementCreator( data, conversionApi.writer ); - - const markerRange = data.markerRange; - - // If marker's range is not collapsed - consume all items inside. - for ( const value of markerRange ) { - conversionApi.consumable.consume( value.item, evt.name ); - } - - const mapper = conversionApi.mapper; - const viewWriter = conversionApi.writer; - - // Add "opening" element. - viewWriter.insert( mapper.toViewPosition( markerRange.start ), viewStartElement ); - conversionApi.mapper.bindElementToMarker( viewStartElement, data.markerName ); - - // Add "closing" element only if range is not collapsed. - if ( !markerRange.isCollapsed ) { - viewWriter.insert( mapper.toViewPosition( markerRange.end ), viewEndElement ); - conversionApi.mapper.bindElementToMarker( viewEndElement, data.markerName ); - } - - evt.stop(); - } ); + return writer.createUIElement( name ); + } ) ); // Convert model to view. const writer = view._writer; From e4460dbd7853ef0c0f14d771805ca41b120caafa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 2 Jan 2019 12:55:48 +0100 Subject: [PATCH 74/84] Add missing @protected annotation to insertUIElement function. --- src/conversion/downcasthelpers.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/conversion/downcasthelpers.js b/src/conversion/downcasthelpers.js index 9b5248cf3..97dc6e241 100644 --- a/src/conversion/downcasthelpers.js +++ b/src/conversion/downcasthelpers.js @@ -545,6 +545,7 @@ export function insertElement( elementCreator ) { * * This converter binds created UI elements with the marker name using {@link module:engine/conversion/mapper~Mapper#bindElementToMarker}. * + * @protected * @param {module:engine/view/uielement~UIElement|Function} elementCreator A view UI element or a function returning the view element * that will be inserted. * @returns {Function} Insert element event converter. From e361bf2833516b2d17cdb567e5c0cfb3f87c7e0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 2 Jan 2019 13:41:03 +0100 Subject: [PATCH 75/84] Use DowncastHelpers#attributeToElement() instead of wrap() directly in tests. --- tests/conversion/downcast-selection-converters.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/conversion/downcast-selection-converters.js b/tests/conversion/downcast-selection-converters.js index 719066914..fffc3e5d5 100644 --- a/tests/conversion/downcast-selection-converters.js +++ b/tests/conversion/downcast-selection-converters.js @@ -16,7 +16,7 @@ import { clearAttributes, } from '../../src/conversion/downcast-selection-converters'; -import DowncastHelpers, { insertText, wrap } from '../../src/conversion/downcasthelpers'; +import DowncastHelpers, { insertText } from '../../src/conversion/downcasthelpers'; import createViewRoot from '../view/_utils/createroot'; import { stringify as stringifyView } from '../../src/dev-utils/view'; @@ -45,10 +45,8 @@ describe( 'downcast-selection-converters', () => { dispatcher.on( 'insert:$text', insertText() ); - const strongCreator = ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'strong' ); - dispatcher.on( 'attribute:bold', wrap( strongCreator ) ); - downcastHelpers = new DowncastHelpers( dispatcher ); + downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } ); downcastHelpers.markerToHighlight( { model: 'marker', view: { classes: 'marker' }, converterPriority: 1 } ); // Default selection converters. From fc11ecdde6be568979117f805aec7c187dd7d9cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 2 Jan 2019 14:14:33 +0100 Subject: [PATCH 76/84] Remove wrap() from public API. --- src/conversion/downcasthelpers.js | 3 +- tests/conversion/downcasthelpers.js | 273 +++++++++++++++------------- 2 files changed, 144 insertions(+), 132 deletions(-) diff --git a/src/conversion/downcasthelpers.js b/src/conversion/downcasthelpers.js index 2f76e8069..00a6a6947 100644 --- a/src/conversion/downcasthelpers.js +++ b/src/conversion/downcasthelpers.js @@ -444,10 +444,11 @@ export function createViewElementFromHighlightDescriptor( descriptor ) { * The converter automatically consumes the corresponding value from the consumables list and stops the event (see * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}). * - * modelDispatcher.on( 'attribute:bold', wrapItem( ( modelAttributeValue, viewWriter ) => { + * modelDispatcher.on( 'attribute:bold', wrap( ( modelAttributeValue, viewWriter ) => { * return viewWriter.createAttributeElement( 'strong' ); * } ); * + * @protected * @param {Function} elementCreator Function returning a view element that will be used for wrapping. * @returns {Function} Set/change attribute converter. */ diff --git a/tests/conversion/downcasthelpers.js b/tests/conversion/downcasthelpers.js index 5912139a3..8be542608 100644 --- a/tests/conversion/downcasthelpers.js +++ b/tests/conversion/downcasthelpers.js @@ -20,7 +20,7 @@ import ViewText from '../../src/view/text'; import log from '@ckeditor/ckeditor5-utils/src/log'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; -import DowncastHelpers, { createViewElementFromHighlightDescriptor, wrap } from '../../src/conversion/downcasthelpers'; +import DowncastHelpers, { createViewElementFromHighlightDescriptor } from '../../src/conversion/downcasthelpers'; import { stringify } from '../../src/dev-utils/view'; @@ -107,6 +107,10 @@ describe( 'DowncastHelpers', () => { } ); describe( 'attributeToElement()', () => { + beforeEach( () => { + downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); + } ); + it( 'should be chainable', () => { expect( downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } ) ).to.equal( downcastHelpers ); } ); @@ -264,6 +268,143 @@ describe( 'DowncastHelpers', () => { expect( viewToString( viewRoot ) ).to.equal( '
' ); } ); + + it( 'should convert insert/change/remove of attribute in model into wrapping element in a view', () => { + const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) ); + + downcastHelpers.attributeToElement( { + model: 'bold', + view: ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' ) + } ); + + model.change( writer => { + writer.insert( modelElement, modelRootStart ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + + model.change( writer => { + writer.removeAttribute( 'bold', writer.createRangeIn( modelElement ) ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + } ); + + it( 'should convert insert/remove of attribute in model with wrapping element generating function as a parameter', () => { + const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { style: 'bold' } ) ); + + downcastHelpers.attributeToElement( { + model: 'style', + view: ( modelAttributeValue, viewWriter ) => { + if ( modelAttributeValue == 'bold' ) { + return viewWriter.createAttributeElement( 'b' ); + } + } + } ); + + model.change( writer => { + writer.insert( modelElement, modelRootStart ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + + model.change( writer => { + writer.removeAttribute( 'style', writer.createRangeIn( modelElement ) ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + } ); + + it( 'should update range on re-wrapping attribute (#475)', () => { + const modelElement = new ModelElement( 'paragraph', null, [ + new ModelText( 'x' ), + new ModelText( 'foo', { link: 'http://foo.com' } ), + new ModelText( 'x' ) + ] ); + + downcastHelpers.attributeToElement( { + model: 'link', + view: ( modelAttributeValue, viewWriter ) => { + return viewWriter.createAttributeElement( 'a', { href: modelAttributeValue } ); + } + } ); + + model.change( writer => { + writer.insert( modelElement, modelRootStart ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

xfoox

' ); + + // Set new attribute on old link but also on non-linked characters. + model.change( writer => { + writer.setAttribute( 'link', 'http://foobar.com', writer.createRangeIn( modelElement ) ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '' ); + } ); + + it( 'should support unicode', () => { + const modelElement = new ModelElement( 'paragraph', null, [ 'நி', new ModelText( 'லைக்', { bold: true } ), 'கு' ] ); + + downcastHelpers.attributeToElement( { + model: 'bold', + view: ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' ) + } ); + + model.change( writer => { + writer.insert( modelElement, modelRootStart ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

நிலைக்கு

' ); + + model.change( writer => { + writer.removeAttribute( 'bold', writer.createRangeIn( modelElement ) ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

நிலைக்கு

' ); + } ); + + it( 'should be possible to override ', () => { + const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) ); + + downcastHelpers.attributeToElement( { + model: 'bold', + view: ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' ) + } ); + downcastHelpers.attributeToElement( { + model: 'bold', + view: ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'strong' ), + converterPriority: 'high' + } ); + + model.change( writer => { + writer.insert( modelElement, modelRootStart ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + } ); + + it( 'should not convert and not consume if creator function returned null', () => { + sinon.spy( controller.downcastDispatcher, 'fire' ); + + const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { italic: true } ) ); + + downcastHelpers.attributeToElement( { + model: 'italic', + view: () => null + } ); + + const spy = sinon.spy(); + controller.downcastDispatcher.on( 'attribute:italic', spy ); + + model.change( writer => { + writer.insert( modelElement, modelRootStart ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + expect( controller.downcastDispatcher.fire.calledWith( 'attribute:italic:$text' ) ).to.be.true; + expect( spy.called ).to.be.true; + } ); } ); describe( 'attributeToAttribute()', () => { @@ -1424,136 +1565,6 @@ describe( 'downcast-converters', () => { } ); } ); - describe( 'wrap', () => { - it( 'should convert insert/change/remove of attribute in model into wrapping element in a view', () => { - const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) ); - const creator = ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' ); - - dispatcher.on( 'attribute:bold', wrap( creator ) ); - - model.change( writer => { - writer.insert( modelElement, modelRootStart ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - - model.change( writer => { - writer.removeAttribute( 'bold', writer.createRangeIn( modelElement ) ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - } ); - - it( 'should convert insert/remove of attribute in model with wrapping element generating function as a parameter', () => { - const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { style: 'bold' } ) ); - - const elementGenerator = ( modelAttributeValue, viewWriter ) => { - if ( modelAttributeValue == 'bold' ) { - return viewWriter.createAttributeElement( 'b' ); - } - }; - - dispatcher.on( 'attribute:style', wrap( elementGenerator ) ); - - model.change( writer => { - writer.insert( modelElement, modelRootStart ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - - model.change( writer => { - writer.removeAttribute( 'style', writer.createRangeIn( modelElement ) ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - } ); - - it( 'should update range on re-wrapping attribute (#475)', () => { - const modelElement = new ModelElement( 'paragraph', null, [ - new ModelText( 'x' ), - new ModelText( 'foo', { link: 'http://foo.com' } ), - new ModelText( 'x' ) - ] ); - - const elementGenerator = ( modelAttributeValue, viewWriter ) => { - return viewWriter.createAttributeElement( 'a', { href: modelAttributeValue } ); - }; - - dispatcher.on( 'attribute:link', wrap( elementGenerator ) ); - - model.change( writer => { - writer.insert( modelElement, modelRootStart ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

xfoox

' ); - - // Set new attribute on old link but also on non-linked characters. - model.change( writer => { - writer.setAttribute( 'link', 'http://foobar.com', writer.createRangeIn( modelElement ) ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '' ); - } ); - - it( 'should support unicode', () => { - const modelElement = new ModelElement( 'paragraph', null, [ 'நி', new ModelText( 'லைக்', { bold: true } ), 'கு' ] ); - const creator = ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' ); - - dispatcher.on( 'attribute:bold', wrap( creator ) ); - - model.change( writer => { - writer.insert( modelElement, modelRootStart ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

நிலைக்கு

' ); - - model.change( writer => { - writer.removeAttribute( 'bold', writer.createRangeIn( modelElement ) ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

நிலைக்கு

' ); - } ); - - it( 'should be possible to override wrap', () => { - const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) ); - - dispatcher.on( 'attribute:bold', wrap( ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' ) ) ); - - dispatcher.on( - 'attribute:bold', - wrap( ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'strong' ) ), - { priority: 'high' } - ); - - model.change( writer => { - writer.insert( modelElement, modelRootStart ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - } ); - - it( 'should not convert and not consume if creator function returned null', () => { - const elementGenerator = () => null; - - sinon.spy( dispatcher, 'fire' ); - - const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { italic: true } ) ); - - dispatcher.on( 'attribute:italic', wrap( elementGenerator ) ); - - const spy = sinon.spy(); - dispatcher.on( 'attribute:italic', spy ); - - model.change( writer => { - writer.insert( modelElement, modelRootStart ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - expect( dispatcher.fire.calledWith( 'attribute:italic:$text' ) ).to.be.true; - expect( spy.called ).to.be.true; - } ); - } ); - // Remove converter is by default already added in `EditingController` instance. describe( 'remove', () => { it( 'should remove items from view accordingly to changes in model #1', () => { From c4cb398befc302466d4c5fce4bc1d896c77b4571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 2 Jan 2019 15:13:37 +0100 Subject: [PATCH 77/84] Move downcast conversion helpers for selection to downcasthelpers.js. --- src/controller/editingcontroller.js | 3 +- .../downcast-selection-converters.js | 117 ------------------ src/conversion/downcasthelpers.js | 117 ++++++++++++++++++ src/dev-utils/model.js | 9 +- .../downcast-selection-converters.js | 12 +- 5 files changed, 130 insertions(+), 128 deletions(-) diff --git a/src/controller/editingcontroller.js b/src/controller/editingcontroller.js index b504ff4b5..d7a7ed732 100644 --- a/src/controller/editingcontroller.js +++ b/src/controller/editingcontroller.js @@ -11,9 +11,8 @@ import RootEditableElement from '../view/rooteditableelement'; import View from '../view/view'; import Mapper from '../conversion/mapper'; import DowncastDispatcher from '../conversion/downcastdispatcher'; -import { insertText, remove } from '../conversion/downcasthelpers'; +import { clearAttributes, convertCollapsedSelection, convertRangeSelection, insertText, remove } from '../conversion/downcasthelpers'; import { convertSelectionChange } from '../conversion/upcast-selection-converters'; -import { clearAttributes, convertCollapsedSelection, convertRangeSelection } from '../conversion/downcast-selection-converters'; import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; diff --git a/src/conversion/downcast-selection-converters.js b/src/conversion/downcast-selection-converters.js index 9024ec87d..59a907dcf 100644 --- a/src/conversion/downcast-selection-converters.js +++ b/src/conversion/downcast-selection-converters.js @@ -10,120 +10,3 @@ * * @module engine/conversion/downcast-selection-converters */ - -/** - * Function factory that creates a converter which converts a non-collapsed {@link module:engine/model/selection~Selection model selection} - * to a {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate - * value from the `consumable` object and maps model positions from the selection to view positions. - * - * modelDispatcher.on( 'selection', convertRangeSelection() ); - * - * @returns {Function} Selection converter. - */ -export function convertRangeSelection() { - return ( evt, data, conversionApi ) => { - const selection = data.selection; - - if ( selection.isCollapsed ) { - return; - } - - if ( !conversionApi.consumable.consume( selection, 'selection' ) ) { - return; - } - - const viewRanges = []; - - for ( const range of selection.getRanges() ) { - const viewRange = conversionApi.mapper.toViewRange( range ); - viewRanges.push( viewRange ); - } - - conversionApi.writer.setSelection( viewRanges, { backward: selection.isBackward } ); - }; -} - -/** - * Function factory that creates a converter which converts a collapsed {@link module:engine/model/selection~Selection model selection} to - * a {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate - * value from the `consumable` object, maps the model selection position to the view position and breaks - * {@link module:engine/view/attributeelement~AttributeElement attribute elements} at the selection position. - * - * modelDispatcher.on( 'selection', convertCollapsedSelection() ); - * - * An example of the view state before and after converting the collapsed selection: - * - *

f^oobar

- * ->

f^oobar

- * - * By breaking attribute elements like ``, the selection is in a correct element. Then, when the selection attribute is - * converted, broken attributes might be merged again, or the position where the selection is may be wrapped - * with different, appropriate attribute elements. - * - * See also {@link module:engine/conversion/downcast-selection-converters~clearAttributes} which does a clean-up - * by merging attributes. - * - * @returns {Function} Selection converter. - */ -export function convertCollapsedSelection() { - return ( evt, data, conversionApi ) => { - const selection = data.selection; - - if ( !selection.isCollapsed ) { - return; - } - - if ( !conversionApi.consumable.consume( selection, 'selection' ) ) { - return; - } - - const viewWriter = conversionApi.writer; - const modelPosition = selection.getFirstPosition(); - const viewPosition = conversionApi.mapper.toViewPosition( modelPosition ); - const brokenPosition = viewWriter.breakAttributes( viewPosition ); - - viewWriter.setSelection( brokenPosition ); - }; -} - -/** - * Function factory that creates a converter which clears artifacts after the previous - * {@link module:engine/model/selection~Selection model selection} conversion. It removes all empty - * {@link module:engine/view/attributeelement~AttributeElement view attribute elements} and merges sibling attributes at all start and end - * positions of all ranges. - * - *

^

- * ->

^

- * - *

foo^barbar

- * ->

foo^barbar

- * - *

foo^barbar

- * ->

foo^barbar

- * - * This listener should be assigned before any converter for the new selection: - * - * modelDispatcher.on( 'selection', clearAttributes() ); - * - * See {@link module:engine/conversion/downcast-selection-converters~convertCollapsedSelection} - * which does the opposite by breaking attributes in the selection position. - * - * @returns {Function} Selection converter. - */ -export function clearAttributes() { - return ( evt, data, conversionApi ) => { - const viewWriter = conversionApi.writer; - const viewSelection = viewWriter.document.selection; - - for ( const range of viewSelection.getRanges() ) { - // Not collapsed selection should not have artifacts. - if ( range.isCollapsed ) { - // Position might be in the node removed by the view writer. - if ( range.end.parent.document ) { - conversionApi.writer.mergeAttributes( range.start ); - } - } - } - viewWriter.setSelection( null ); - }; -} diff --git a/src/conversion/downcasthelpers.js b/src/conversion/downcasthelpers.js index e8cc47832..734486938 100644 --- a/src/conversion/downcasthelpers.js +++ b/src/conversion/downcasthelpers.js @@ -421,6 +421,123 @@ export function createViewElementFromHighlightDescriptor( descriptor ) { return viewElement; } +/** + * Function factory that creates a converter which converts a non-collapsed {@link module:engine/model/selection~Selection model selection} + * to a {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate + * value from the `consumable` object and maps model positions from the selection to view positions. + * + * modelDispatcher.on( 'selection', convertRangeSelection() ); + * + * @returns {Function} Selection converter. + */ +export function convertRangeSelection() { + return ( evt, data, conversionApi ) => { + const selection = data.selection; + + if ( selection.isCollapsed ) { + return; + } + + if ( !conversionApi.consumable.consume( selection, 'selection' ) ) { + return; + } + + const viewRanges = []; + + for ( const range of selection.getRanges() ) { + const viewRange = conversionApi.mapper.toViewRange( range ); + viewRanges.push( viewRange ); + } + + conversionApi.writer.setSelection( viewRanges, { backward: selection.isBackward } ); + }; +} + +/** + * Function factory that creates a converter which converts a collapsed {@link module:engine/model/selection~Selection model selection} to + * a {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate + * value from the `consumable` object, maps the model selection position to the view position and breaks + * {@link module:engine/view/attributeelement~AttributeElement attribute elements} at the selection position. + * + * modelDispatcher.on( 'selection', convertCollapsedSelection() ); + * + * An example of the view state before and after converting the collapsed selection: + * + *

f^oobar

+ * ->

f^oobar

+ * + * By breaking attribute elements like ``, the selection is in a correct element. Then, when the selection attribute is + * converted, broken attributes might be merged again, or the position where the selection is may be wrapped + * with different, appropriate attribute elements. + * + * See also {@link module:engine/conversion/downcast-selection-converters~clearAttributes} which does a clean-up + * by merging attributes. + * + * @returns {Function} Selection converter. + */ +export function convertCollapsedSelection() { + return ( evt, data, conversionApi ) => { + const selection = data.selection; + + if ( !selection.isCollapsed ) { + return; + } + + if ( !conversionApi.consumable.consume( selection, 'selection' ) ) { + return; + } + + const viewWriter = conversionApi.writer; + const modelPosition = selection.getFirstPosition(); + const viewPosition = conversionApi.mapper.toViewPosition( modelPosition ); + const brokenPosition = viewWriter.breakAttributes( viewPosition ); + + viewWriter.setSelection( brokenPosition ); + }; +} + +/** + * Function factory that creates a converter which clears artifacts after the previous + * {@link module:engine/model/selection~Selection model selection} conversion. It removes all empty + * {@link module:engine/view/attributeelement~AttributeElement view attribute elements} and merges sibling attributes at all start and end + * positions of all ranges. + * + *

^

+ * ->

^

+ * + *

foo^barbar

+ * ->

foo^barbar

+ * + *

foo^barbar

+ * ->

foo^barbar

+ * + * This listener should be assigned before any converter for the new selection: + * + * modelDispatcher.on( 'selection', clearAttributes() ); + * + * See {@link module:engine/conversion/downcast-selection-converters~convertCollapsedSelection} + * which does the opposite by breaking attributes in the selection position. + * + * @returns {Function} Selection converter. + */ +export function clearAttributes() { + return ( evt, data, conversionApi ) => { + const viewWriter = conversionApi.writer; + const viewSelection = viewWriter.document.selection; + + for ( const range of viewSelection.getRanges() ) { + // Not collapsed selection should not have artifacts. + if ( range.isCollapsed ) { + // Position might be in the node removed by the view writer. + if ( range.end.parent.document ) { + conversionApi.writer.mergeAttributes( range.start ); + } + } + } + viewWriter.setSelection( null ); + }; +} + /** * Function factory that creates a converter which converts set/change/remove attribute changes from the model to the view. * It can also be used to convert selection attributes. In that case, an empty attribute element will be created and the diff --git a/src/dev-utils/model.js b/src/dev-utils/model.js index 4427c4097..7ec10940e 100644 --- a/src/dev-utils/model.js +++ b/src/dev-utils/model.js @@ -29,10 +29,13 @@ import DowncastDispatcher from '../conversion/downcastdispatcher'; import UpcastDispatcher from '../conversion/upcastdispatcher'; import Mapper from '../conversion/mapper'; import { - convertRangeSelection, convertCollapsedSelection, -} from '../conversion/downcast-selection-converters'; -import { insertElement, insertText, insertUIElement, wrap } from '../conversion/downcasthelpers'; + convertRangeSelection, + insertElement, + insertText, + insertUIElement, + wrap +} from '../conversion/downcasthelpers'; import { isPlainObject } from 'lodash-es'; import toMap from '@ckeditor/ckeditor5-utils/src/tomap'; diff --git a/tests/conversion/downcast-selection-converters.js b/tests/conversion/downcast-selection-converters.js index fffc3e5d5..92b956137 100644 --- a/tests/conversion/downcast-selection-converters.js +++ b/tests/conversion/downcast-selection-converters.js @@ -10,13 +10,13 @@ import ViewUIElement from '../../src/view/uielement'; import Mapper from '../../src/conversion/mapper'; import DowncastDispatcher from '../../src/conversion/downcastdispatcher'; -import { - convertRangeSelection, - convertCollapsedSelection, - clearAttributes, -} from '../../src/conversion/downcast-selection-converters'; -import DowncastHelpers, { insertText } from '../../src/conversion/downcasthelpers'; +import DowncastHelpers, { + clearAttributes, + convertCollapsedSelection, + convertRangeSelection, + insertText +} from '../../src/conversion/downcasthelpers'; import createViewRoot from '../view/_utils/createroot'; import { stringify as stringifyView } from '../../src/dev-utils/view'; From 0520e31336e1aa04b14a3c3b665e9c3b171e7dd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 2 Jan 2019 15:50:35 +0100 Subject: [PATCH 78/84] Remove downcast-selection-converters.js. --- .../downcast-selection-converters.js | 12 - .../downcast-selection-converters.js | 594 ------------------ tests/conversion/downcasthelpers.js | 589 ++++++++++++++++- 3 files changed, 585 insertions(+), 610 deletions(-) delete mode 100644 src/conversion/downcast-selection-converters.js delete mode 100644 tests/conversion/downcast-selection-converters.js diff --git a/src/conversion/downcast-selection-converters.js b/src/conversion/downcast-selection-converters.js deleted file mode 100644 index 59a907dcf..000000000 --- a/src/conversion/downcast-selection-converters.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -/** - * Contains {@link module:engine/model/selection~Selection model selection} to - * {@link module:engine/view/documentselection~DocumentSelection view selection} converters for - * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher downcast dispatcher}. - * - * @module engine/conversion/downcast-selection-converters - */ diff --git a/tests/conversion/downcast-selection-converters.js b/tests/conversion/downcast-selection-converters.js deleted file mode 100644 index 92b956137..000000000 --- a/tests/conversion/downcast-selection-converters.js +++ /dev/null @@ -1,594 +0,0 @@ -/** - * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -import Model from '../../src/model/model'; - -import View from '../../src/view/view'; -import ViewUIElement from '../../src/view/uielement'; - -import Mapper from '../../src/conversion/mapper'; -import DowncastDispatcher from '../../src/conversion/downcastdispatcher'; - -import DowncastHelpers, { - clearAttributes, - convertCollapsedSelection, - convertRangeSelection, - insertText -} from '../../src/conversion/downcasthelpers'; - -import createViewRoot from '../view/_utils/createroot'; -import { stringify as stringifyView } from '../../src/dev-utils/view'; -import { setData as setModelData } from '../../src/dev-utils/model'; - -describe( 'downcast-selection-converters', () => { - let dispatcher, mapper, model, view, modelDoc, modelRoot, docSelection, viewDoc, viewRoot, viewSelection, downcastHelpers; - - beforeEach( () => { - model = new Model(); - modelDoc = model.document; - modelRoot = modelDoc.createRoot(); - docSelection = modelDoc.selection; - - model.schema.extend( '$text', { allowIn: '$root' } ); - - view = new View(); - viewDoc = view.document; - viewRoot = createViewRoot( viewDoc ); - viewSelection = viewDoc.selection; - - mapper = new Mapper(); - mapper.bindElements( modelRoot, viewRoot ); - - dispatcher = new DowncastDispatcher( { mapper, viewSelection } ); - - dispatcher.on( 'insert:$text', insertText() ); - - downcastHelpers = new DowncastHelpers( dispatcher ); - downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } ); - downcastHelpers.markerToHighlight( { model: 'marker', view: { classes: 'marker' }, converterPriority: 1 } ); - - // Default selection converters. - dispatcher.on( 'selection', clearAttributes(), { priority: 'low' } ); - dispatcher.on( 'selection', convertRangeSelection(), { priority: 'low' } ); - dispatcher.on( 'selection', convertCollapsedSelection(), { priority: 'low' } ); - } ); - - afterEach( () => { - view.destroy(); - } ); - - describe( 'default converters', () => { - describe( 'range selection', () => { - it( 'in same container', () => { - test( - [ 1, 4 ], - 'foobar', - 'f{oob}ar' - ); - } ); - - it( 'in same container with unicode characters', () => { - test( - [ 2, 6 ], - 'நிலைக்கு', - 'நி{லைக்}கு' - ); - } ); - - it( 'in same container, over attribute', () => { - test( - [ 1, 5 ], - 'fo<$text bold="true">obar', - 'f{ooba}r' - ); - } ); - - it( 'in same container, next to attribute', () => { - test( - [ 1, 2 ], - 'fo<$text bold="true">obar', - 'f{o}obar' - ); - } ); - - it( 'in same attribute', () => { - test( - [ 2, 4 ], - 'f<$text bold="true">oobar', - 'fo{ob}ar' - ); - } ); - - it( 'in same attribute, selection same as attribute', () => { - test( - [ 2, 4 ], - 'fo<$text bold="true">obar', - 'fo{ob}ar' - ); - } ); - - it( 'starts in text node, ends in attribute #1', () => { - test( - [ 1, 3 ], - 'fo<$text bold="true">obar', - 'f{oo}bar' - ); - } ); - - it( 'starts in text node, ends in attribute #2', () => { - test( - [ 1, 4 ], - 'fo<$text bold="true">obar', - 'f{oob}ar' - ); - } ); - - it( 'starts in attribute, ends in text node', () => { - test( - [ 3, 5 ], - 'fo<$text bold="true">obar', - 'foo{ba}r' - ); - } ); - - it( 'consumes consumable values properly', () => { - // Add callback that will fire before default ones. - // This should prevent default callback doing anything. - dispatcher.on( 'selection', ( evt, data, conversionApi ) => { - expect( conversionApi.consumable.consume( data.selection, 'selection' ) ).to.be.true; - }, { priority: 'high' } ); - - // Similar test case as the first in this suite. - test( - [ 1, 4 ], - 'foobar', - 'foobar' // No selection in view. - ); - } ); - - it( 'should convert backward selection', () => { - test( - [ 1, 3, 'backward' ], - 'foobar', - 'f{oo}bar' - ); - - expect( viewSelection.focus.offset ).to.equal( 1 ); - } ); - } ); - - describe( 'collapsed selection', () => { - let marker; - - it( 'in container', () => { - test( - [ 1, 1 ], - 'foobar', - 'f{}oobar' - ); - } ); - - it( 'in attribute', () => { - test( - [ 3, 3 ], - 'f<$text bold="true">oobar', - 'foo{}bar' - ); - } ); - - it( 'in attribute and marker', () => { - setModelData( model, 'fo<$text bold="true">obar' ); - - model.change( writer => { - const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) ); - marker = writer.addMarker( 'marker', { range, usingOperation: false } ); - writer.setSelection( modelRoot, 3 ); - } ); - - // Remove view children manually (without firing additional conversion). - viewRoot._removeChildren( 0, viewRoot.childCount ); - - // Convert model to view. - view.change( writer => { - dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); - dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); - dispatcher.convertSelection( docSelection, model.markers, writer ); - } ); - - // Stringify view and check if it is same as expected. - expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal( - '
foo{}bar
' - ); - } ); - - it( 'in attribute and marker - no attribute', () => { - setModelData( model, 'fo<$text bold="true">obar' ); - - model.change( writer => { - const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) ); - marker = writer.addMarker( 'marker', { range, usingOperation: false } ); - writer.setSelection( modelRoot, 3 ); - writer.removeSelectionAttribute( 'bold' ); - } ); - - // Remove view children manually (without firing additional conversion). - viewRoot._removeChildren( 0, viewRoot.childCount ); - - // Convert model to view. - view.change( writer => { - dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); - dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); - dispatcher.convertSelection( docSelection, model.markers, writer ); - } ); - - // Stringify view and check if it is same as expected. - expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) - .to.equal( '
foo[]bar
' ); - } ); - - it( 'in marker - using highlight descriptor creator', () => { - downcastHelpers.markerToHighlight( { - model: 'marker2', - view: data => ( { classes: data.markerName } ) - } ); - - setModelData( model, 'foobar' ); - - model.change( writer => { - const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) ); - marker = writer.addMarker( 'marker2', { range, usingOperation: false } ); - writer.setSelection( modelRoot, 3 ); - } ); - - // Remove view children manually (without firing additional conversion). - viewRoot._removeChildren( 0, viewRoot.childCount ); - - // Convert model to view. - view.change( writer => { - dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); - dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); - dispatcher.convertSelection( docSelection, model.markers, writer ); - } ); - - // Stringify view and check if it is same as expected. - expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) - .to.equal( '
foo{}bar
' ); - } ); - - it( 'should do nothing if creator return null', () => { - downcastHelpers.markerToHighlight( { - model: 'marker3', - view: () => null - } ); - - setModelData( model, 'foobar' ); - - model.change( writer => { - const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) ); - marker = writer.addMarker( 'marker3', { range, usingOperation: false } ); - writer.setSelection( modelRoot, 3 ); - } ); - - // Remove view children manually (without firing additional conversion). - viewRoot._removeChildren( 0, viewRoot.childCount ); - - // Convert model to view. - view.change( writer => { - dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); - dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); - dispatcher.convertSelection( docSelection, model.markers, writer ); - } ); - - // Stringify view and check if it is same as expected. - expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) - .to.equal( '
foo{}bar
' ); - } ); - - // #1072 - if the container has only ui elements, collapsed selection attribute should be rendered after those ui elements. - it( 'selection with attribute before ui element - no non-ui children', () => { - setModelData( model, '' ); - - // Add two ui elements to view. - viewRoot._appendChild( [ - new ViewUIElement( 'span' ), - new ViewUIElement( 'span' ) - ] ); - - model.change( writer => { - writer.setSelection( writer.createRange( writer.createPositionFromPath( modelRoot, [ 0 ] ) ) ); - writer.setSelectionAttribute( 'bold', true ); - } ); - - // Convert model to view. - view.change( writer => { - dispatcher.convertSelection( docSelection, model.markers, writer ); - } ); - - // Stringify view and check if it is same as expected. - expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) - .to.equal( '
[]
' ); - } ); - - // #1072. - it( 'selection with attribute before ui element - has non-ui children #1', () => { - setModelData( model, 'x' ); - - model.change( writer => { - writer.setSelection( writer.createRange( writer.createPositionFromPath( modelRoot, [ 1 ] ) ) ); - writer.setSelectionAttribute( 'bold', true ); - } ); - - // Convert model to view. - view.change( writer => { - dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); - - // Add ui element to view. - const uiElement = new ViewUIElement( 'span' ); - viewRoot._insertChild( 1, uiElement ); - - dispatcher.convertSelection( docSelection, model.markers, writer ); - } ); - - // Stringify view and check if it is same as expected. - expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) - .to.equal( '
x[]
' ); - } ); - - // #1072. - it( 'selection with attribute before ui element - has non-ui children #2', () => { - setModelData( model, '<$text bold="true">xy' ); - - model.change( writer => { - writer.setSelection( writer.createRange( writer.createPositionFromPath( modelRoot, [ 1 ] ) ) ); - writer.setSelectionAttribute( 'bold', true ); - } ); - - // Convert model to view. - view.change( writer => { - dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); - - // Add ui element to view. - const uiElement = new ViewUIElement( 'span' ); - viewRoot._insertChild( 1, uiElement, writer ); - dispatcher.convertSelection( docSelection, model.markers, writer ); - } ); - - // Stringify view and check if it is same as expected. - expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) - .to.equal( '
x{}y
' ); - } ); - - it( 'consumes consumable values properly', () => { - // Add callbacks that will fire before default ones. - // This should prevent default callbacks doing anything. - dispatcher.on( 'selection', ( evt, data, conversionApi ) => { - expect( conversionApi.consumable.consume( data.selection, 'selection' ) ).to.be.true; - }, { priority: 'high' } ); - - dispatcher.on( 'attribute:bold', ( evt, data, conversionApi ) => { - expect( conversionApi.consumable.consume( data.item, 'attribute:bold' ) ).to.be.true; - }, { priority: 'high' } ); - - // Similar test case as above. - test( - [ 3, 3 ], - 'f<$text bold="true">oobar', - 'foobar' // No selection in view and no attribute. - ); - } ); - } ); - } ); - - describe( 'clean-up', () => { - describe( 'convertRangeSelection', () => { - it( 'should remove all ranges before adding new range', () => { - test( - [ 0, 2 ], - 'foobar', - '{fo}obar' - ); - - test( - [ 3, 5 ], - 'foobar', - 'foo{ba}r' - ); - - expect( viewSelection.rangeCount ).to.equal( 1 ); - } ); - } ); - - describe( 'convertCollapsedSelection', () => { - it( 'should remove all ranges before adding new range', () => { - test( - [ 2, 2 ], - 'foobar', - 'fo{}obar' - ); - - test( - [ 3, 3 ], - 'foobar', - 'foo{}bar' - ); - - expect( viewSelection.rangeCount ).to.equal( 1 ); - } ); - } ); - - describe( 'clearAttributes', () => { - it( 'should remove all ranges before adding new range', () => { - test( - [ 3, 3 ], - 'foobar', - 'foo[]bar', - { bold: 'true' } - ); - - view.change( writer => { - const modelRange = model.createRange( model.createPositionAt( modelRoot, 1 ), model.createPositionAt( modelRoot, 1 ) ); - model.change( writer => { - writer.setSelection( modelRange ); - } ); - - dispatcher.convertSelection( modelDoc.selection, model.markers, writer ); - } ); - - expect( viewSelection.rangeCount ).to.equal( 1 ); - - const viewString = stringifyView( viewRoot, viewSelection, { showType: false } ); - expect( viewString ).to.equal( '
f{}oobar
' ); - } ); - - it( 'should do nothing if the attribute element had been already removed', () => { - test( - [ 3, 3 ], - 'foobar', - 'foo[]bar', - { bold: 'true' } - ); - - view.change( writer => { - // Remove manually. - writer.mergeAttributes( viewSelection.getFirstPosition() ); - - const modelRange = model.createRange( model.createPositionAt( modelRoot, 1 ), model.createPositionAt( modelRoot, 1 ) ); - model.change( writer => { - writer.setSelection( modelRange ); - } ); - - dispatcher.convertSelection( modelDoc.selection, model.markers, writer ); - } ); - - expect( viewSelection.rangeCount ).to.equal( 1 ); - - const viewString = stringifyView( viewRoot, viewSelection, { showType: false } ); - expect( viewString ).to.equal( '
f{}oobar
' ); - } ); - - it( 'should clear fake selection', () => { - const modelRange = model.createRange( model.createPositionAt( modelRoot, 1 ), model.createPositionAt( modelRoot, 1 ) ); - - view.change( writer => { - writer.setSelection( modelRange, { fake: true } ); - - dispatcher.convertSelection( docSelection, model.markers, writer ); - } ); - expect( viewSelection.isFake ).to.be.false; - } ); - } ); - } ); - - describe( 'table cell selection converter', () => { - beforeEach( () => { - model.schema.register( 'table', { isLimit: true } ); - model.schema.register( 'tr', { isLimit: true } ); - model.schema.register( 'td', { isLimit: true } ); - - model.schema.extend( 'table', { allowIn: '$root' } ); - model.schema.extend( 'tr', { allowIn: 'table' } ); - model.schema.extend( 'td', { allowIn: 'tr' } ); - model.schema.extend( '$text', { allowIn: 'td' } ); - - const downcastHelpers = new DowncastHelpers( dispatcher ); - - // "Universal" converter to convert table structure. - downcastHelpers.elementToElement( { model: 'table', view: 'table' } ); - downcastHelpers.elementToElement( { model: 'tr', view: 'tr' } ); - downcastHelpers.elementToElement( { model: 'td', view: 'td' } ); - - // Special converter for table cells. - dispatcher.on( 'selection', ( evt, data, conversionApi ) => { - const selection = data.selection; - - if ( !conversionApi.consumable.test( selection, 'selection' ) || selection.isCollapsed ) { - return; - } - - for ( const range of selection.getRanges() ) { - const node = range.start.parent; - - if ( !!node && node.is( 'td' ) ) { - conversionApi.consumable.consume( selection, 'selection' ); - - const viewNode = conversionApi.mapper.toViewElement( node ); - conversionApi.writer.addClass( 'selected', viewNode ); - } - } - }, { priority: 'high' } ); - } ); - - it( 'should not be used to convert selection that is not on table cell', () => { - test( - [ 1, 5 ], - 'f{o<$text bold="true">oba}r', - 'f{ooba}r' - ); - } ); - - it( 'should add a class to the selected table cell', () => { - test( - // table tr#0 td#0 [foo, table tr#0 td#0 bar] - [ [ 0, 0, 0, 0 ], [ 0, 0, 0, 3 ] ], - '
foo
bar
', - '
foo
bar
' - ); - } ); - - it( 'should not be used if selection contains more than just a table cell', () => { - test( - // table tr td#1 f{oo bar, table tr#2 bar] - [ [ 0, 0, 0, 1 ], [ 0, 0, 1, 3 ] ], - '
foobar
', - '[
foobar
]' - ); - } ); - } ); - - // Tests if the selection got correctly converted. - // Because `setData` might use selection converters itself to set the selection, we can't use it - // to set the selection (because then we would test converters using converters). - // Instead, the `test` function expects to be passed `selectionPaths` which is an array containing two numbers or two arrays, - // that are offsets or paths of selection positions in root element. - function test( selectionPaths, modelInput, expectedView, selectionAttributes = {} ) { - // Parse passed `modelInput` string and set it as current model. - setModelData( model, modelInput ); - - // Manually set selection ranges using passed `selectionPaths`. - const startPath = typeof selectionPaths[ 0 ] == 'number' ? [ selectionPaths[ 0 ] ] : selectionPaths[ 0 ]; - const endPath = typeof selectionPaths[ 1 ] == 'number' ? [ selectionPaths[ 1 ] ] : selectionPaths[ 1 ]; - - const startPos = model.createPositionFromPath( modelRoot, startPath ); - const endPos = model.createPositionFromPath( modelRoot, endPath ); - - const isBackward = selectionPaths[ 2 ] === 'backward'; - model.change( writer => { - writer.setSelection( writer.createRange( startPos, endPos ), { backward: isBackward } ); - - // And add or remove passed attributes. - for ( const key in selectionAttributes ) { - const value = selectionAttributes[ key ]; - - if ( value ) { - writer.setSelectionAttribute( key, value ); - } else { - writer.removeSelectionAttribute( key ); - } - } - } ); - - // Remove view children manually (without firing additional conversion). - viewRoot._removeChildren( 0, viewRoot.childCount ); - - // Convert model to view. - view.change( writer => { - dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); - dispatcher.convertSelection( docSelection, model.markers, writer ); - } ); - - // Stringify view and check if it is same as expected. - expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal( '
' + expectedView + '
' ); - } -} ); diff --git a/tests/conversion/downcasthelpers.js b/tests/conversion/downcasthelpers.js index 8be542608..290b032c8 100644 --- a/tests/conversion/downcasthelpers.js +++ b/tests/conversion/downcasthelpers.js @@ -20,9 +20,18 @@ import ViewText from '../../src/view/text'; import log from '@ckeditor/ckeditor5-utils/src/log'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; -import DowncastHelpers, { createViewElementFromHighlightDescriptor } from '../../src/conversion/downcasthelpers'; - -import { stringify } from '../../src/dev-utils/view'; +import DowncastHelpers, { + clearAttributes, convertCollapsedSelection, convertRangeSelection, + createViewElementFromHighlightDescriptor, + insertText +} from '../../src/conversion/downcasthelpers'; + +import Mapper from '../../src/conversion/mapper'; +import DowncastDispatcher from '../../src/conversion/downcastdispatcher'; +import { stringify as stringifyView } from '../../src/dev-utils/view'; +import View from '../../src/view/view'; +import createViewRoot from '../view/_utils/createroot'; +import { setData as setModelData } from '../../src/dev-utils/model'; describe( 'DowncastHelpers', () => { let conversion, model, modelRoot, viewRoot, downcastHelpers, controller; @@ -1443,7 +1452,7 @@ describe( 'DowncastHelpers', () => { } ); function expectResult( string ) { - expect( stringify( viewRoot, null, { ignoreRoot: true } ) ).to.equal( string ); + expect( stringifyView( viewRoot, null, { ignoreRoot: true } ) ).to.equal( string ); } function viewAttributesToString( item ) { @@ -1837,3 +1846,575 @@ describe( 'downcast-converters', () => { } ); } ); } ); + +describe( 'downcast-selection-converters', () => { + let dispatcher, mapper, model, view, modelDoc, modelRoot, docSelection, viewDoc, viewRoot, viewSelection, downcastHelpers; + + beforeEach( () => { + model = new Model(); + modelDoc = model.document; + modelRoot = modelDoc.createRoot(); + docSelection = modelDoc.selection; + + model.schema.extend( '$text', { allowIn: '$root' } ); + + view = new View(); + viewDoc = view.document; + viewRoot = createViewRoot( viewDoc ); + viewSelection = viewDoc.selection; + + mapper = new Mapper(); + mapper.bindElements( modelRoot, viewRoot ); + + dispatcher = new DowncastDispatcher( { mapper, viewSelection } ); + + dispatcher.on( 'insert:$text', insertText() ); + + downcastHelpers = new DowncastHelpers( dispatcher ); + downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } ); + downcastHelpers.markerToHighlight( { model: 'marker', view: { classes: 'marker' }, converterPriority: 1 } ); + + // Default selection converters. + dispatcher.on( 'selection', clearAttributes(), { priority: 'low' } ); + dispatcher.on( 'selection', convertRangeSelection(), { priority: 'low' } ); + dispatcher.on( 'selection', convertCollapsedSelection(), { priority: 'low' } ); + } ); + + afterEach( () => { + view.destroy(); + } ); + + describe( 'default converters', () => { + describe( 'range selection', () => { + it( 'in same container', () => { + test( + [ 1, 4 ], + 'foobar', + 'f{oob}ar' + ); + } ); + + it( 'in same container with unicode characters', () => { + test( + [ 2, 6 ], + 'நிலைக்கு', + 'நி{லைக்}கு' + ); + } ); + + it( 'in same container, over attribute', () => { + test( + [ 1, 5 ], + 'fo<$text bold="true">obar', + 'f{ooba}r' + ); + } ); + + it( 'in same container, next to attribute', () => { + test( + [ 1, 2 ], + 'fo<$text bold="true">obar', + 'f{o}obar' + ); + } ); + + it( 'in same attribute', () => { + test( + [ 2, 4 ], + 'f<$text bold="true">oobar', + 'fo{ob}ar' + ); + } ); + + it( 'in same attribute, selection same as attribute', () => { + test( + [ 2, 4 ], + 'fo<$text bold="true">obar', + 'fo{ob}ar' + ); + } ); + + it( 'starts in text node, ends in attribute #1', () => { + test( + [ 1, 3 ], + 'fo<$text bold="true">obar', + 'f{oo}bar' + ); + } ); + + it( 'starts in text node, ends in attribute #2', () => { + test( + [ 1, 4 ], + 'fo<$text bold="true">obar', + 'f{oob}ar' + ); + } ); + + it( 'starts in attribute, ends in text node', () => { + test( + [ 3, 5 ], + 'fo<$text bold="true">obar', + 'foo{ba}r' + ); + } ); + + it( 'consumes consumable values properly', () => { + // Add callback that will fire before default ones. + // This should prevent default callback doing anything. + dispatcher.on( 'selection', ( evt, data, conversionApi ) => { + expect( conversionApi.consumable.consume( data.selection, 'selection' ) ).to.be.true; + }, { priority: 'high' } ); + + // Similar test case as the first in this suite. + test( + [ 1, 4 ], + 'foobar', + 'foobar' // No selection in view. + ); + } ); + + it( 'should convert backward selection', () => { + test( + [ 1, 3, 'backward' ], + 'foobar', + 'f{oo}bar' + ); + + expect( viewSelection.focus.offset ).to.equal( 1 ); + } ); + } ); + + describe( 'collapsed selection', () => { + let marker; + + it( 'in container', () => { + test( + [ 1, 1 ], + 'foobar', + 'f{}oobar' + ); + } ); + + it( 'in attribute', () => { + test( + [ 3, 3 ], + 'f<$text bold="true">oobar', + 'foo{}bar' + ); + } ); + + it( 'in attribute and marker', () => { + setModelData( model, 'fo<$text bold="true">obar' ); + + model.change( writer => { + const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) ); + marker = writer.addMarker( 'marker', { range, usingOperation: false } ); + writer.setSelection( modelRoot, 3 ); + } ); + + // Remove view children manually (without firing additional conversion). + viewRoot._removeChildren( 0, viewRoot.childCount ); + + // Convert model to view. + view.change( writer => { + dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); + dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); + dispatcher.convertSelection( docSelection, model.markers, writer ); + } ); + + // Stringify view and check if it is same as expected. + expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal( + '
foo{}bar
' + ); + } ); + + it( 'in attribute and marker - no attribute', () => { + setModelData( model, 'fo<$text bold="true">obar' ); + + model.change( writer => { + const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) ); + marker = writer.addMarker( 'marker', { range, usingOperation: false } ); + writer.setSelection( modelRoot, 3 ); + writer.removeSelectionAttribute( 'bold' ); + } ); + + // Remove view children manually (without firing additional conversion). + viewRoot._removeChildren( 0, viewRoot.childCount ); + + // Convert model to view. + view.change( writer => { + dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); + dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); + dispatcher.convertSelection( docSelection, model.markers, writer ); + } ); + + // Stringify view and check if it is same as expected. + expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) + .to.equal( '
foo[]bar
' ); + } ); + + it( 'in marker - using highlight descriptor creator', () => { + downcastHelpers.markerToHighlight( { + model: 'marker2', + view: data => ( { classes: data.markerName } ) + } ); + + setModelData( model, 'foobar' ); + + model.change( writer => { + const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) ); + marker = writer.addMarker( 'marker2', { range, usingOperation: false } ); + writer.setSelection( modelRoot, 3 ); + } ); + + // Remove view children manually (without firing additional conversion). + viewRoot._removeChildren( 0, viewRoot.childCount ); + + // Convert model to view. + view.change( writer => { + dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); + dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); + dispatcher.convertSelection( docSelection, model.markers, writer ); + } ); + + // Stringify view and check if it is same as expected. + expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) + .to.equal( '
foo{}bar
' ); + } ); + + it( 'should do nothing if creator return null', () => { + downcastHelpers.markerToHighlight( { + model: 'marker3', + view: () => null + } ); + + setModelData( model, 'foobar' ); + + model.change( writer => { + const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) ); + marker = writer.addMarker( 'marker3', { range, usingOperation: false } ); + writer.setSelection( modelRoot, 3 ); + } ); + + // Remove view children manually (without firing additional conversion). + viewRoot._removeChildren( 0, viewRoot.childCount ); + + // Convert model to view. + view.change( writer => { + dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); + dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); + dispatcher.convertSelection( docSelection, model.markers, writer ); + } ); + + // Stringify view and check if it is same as expected. + expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) + .to.equal( '
foo{}bar
' ); + } ); + + // #1072 - if the container has only ui elements, collapsed selection attribute should be rendered after those ui elements. + it( 'selection with attribute before ui element - no non-ui children', () => { + setModelData( model, '' ); + + // Add two ui elements to view. + viewRoot._appendChild( [ + new ViewUIElement( 'span' ), + new ViewUIElement( 'span' ) + ] ); + + model.change( writer => { + writer.setSelection( writer.createRange( writer.createPositionFromPath( modelRoot, [ 0 ] ) ) ); + writer.setSelectionAttribute( 'bold', true ); + } ); + + // Convert model to view. + view.change( writer => { + dispatcher.convertSelection( docSelection, model.markers, writer ); + } ); + + // Stringify view and check if it is same as expected. + expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) + .to.equal( '
[]
' ); + } ); + + // #1072. + it( 'selection with attribute before ui element - has non-ui children #1', () => { + setModelData( model, 'x' ); + + model.change( writer => { + writer.setSelection( writer.createRange( writer.createPositionFromPath( modelRoot, [ 1 ] ) ) ); + writer.setSelectionAttribute( 'bold', true ); + } ); + + // Convert model to view. + view.change( writer => { + dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); + + // Add ui element to view. + const uiElement = new ViewUIElement( 'span' ); + viewRoot._insertChild( 1, uiElement ); + + dispatcher.convertSelection( docSelection, model.markers, writer ); + } ); + + // Stringify view and check if it is same as expected. + expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) + .to.equal( '
x[]
' ); + } ); + + // #1072. + it( 'selection with attribute before ui element - has non-ui children #2', () => { + setModelData( model, '<$text bold="true">xy' ); + + model.change( writer => { + writer.setSelection( writer.createRange( writer.createPositionFromPath( modelRoot, [ 1 ] ) ) ); + writer.setSelectionAttribute( 'bold', true ); + } ); + + // Convert model to view. + view.change( writer => { + dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); + + // Add ui element to view. + const uiElement = new ViewUIElement( 'span' ); + viewRoot._insertChild( 1, uiElement, writer ); + dispatcher.convertSelection( docSelection, model.markers, writer ); + } ); + + // Stringify view and check if it is same as expected. + expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) + .to.equal( '
x{}y
' ); + } ); + + it( 'consumes consumable values properly', () => { + // Add callbacks that will fire before default ones. + // This should prevent default callbacks doing anything. + dispatcher.on( 'selection', ( evt, data, conversionApi ) => { + expect( conversionApi.consumable.consume( data.selection, 'selection' ) ).to.be.true; + }, { priority: 'high' } ); + + dispatcher.on( 'attribute:bold', ( evt, data, conversionApi ) => { + expect( conversionApi.consumable.consume( data.item, 'attribute:bold' ) ).to.be.true; + }, { priority: 'high' } ); + + // Similar test case as above. + test( + [ 3, 3 ], + 'f<$text bold="true">oobar', + 'foobar' // No selection in view and no attribute. + ); + } ); + } ); + } ); + + describe( 'clean-up', () => { + describe( 'convertRangeSelection', () => { + it( 'should remove all ranges before adding new range', () => { + test( + [ 0, 2 ], + 'foobar', + '{fo}obar' + ); + + test( + [ 3, 5 ], + 'foobar', + 'foo{ba}r' + ); + + expect( viewSelection.rangeCount ).to.equal( 1 ); + } ); + } ); + + describe( 'convertCollapsedSelection', () => { + it( 'should remove all ranges before adding new range', () => { + test( + [ 2, 2 ], + 'foobar', + 'fo{}obar' + ); + + test( + [ 3, 3 ], + 'foobar', + 'foo{}bar' + ); + + expect( viewSelection.rangeCount ).to.equal( 1 ); + } ); + } ); + + describe( 'clearAttributes', () => { + it( 'should remove all ranges before adding new range', () => { + test( + [ 3, 3 ], + 'foobar', + 'foo[]bar', + { bold: 'true' } + ); + + view.change( writer => { + const modelRange = model.createRange( model.createPositionAt( modelRoot, 1 ), model.createPositionAt( modelRoot, 1 ) ); + model.change( writer => { + writer.setSelection( modelRange ); + } ); + + dispatcher.convertSelection( modelDoc.selection, model.markers, writer ); + } ); + + expect( viewSelection.rangeCount ).to.equal( 1 ); + + const viewString = stringifyView( viewRoot, viewSelection, { showType: false } ); + expect( viewString ).to.equal( '
f{}oobar
' ); + } ); + + it( 'should do nothing if the attribute element had been already removed', () => { + test( + [ 3, 3 ], + 'foobar', + 'foo[]bar', + { bold: 'true' } + ); + + view.change( writer => { + // Remove manually. + writer.mergeAttributes( viewSelection.getFirstPosition() ); + + const modelRange = model.createRange( model.createPositionAt( modelRoot, 1 ), model.createPositionAt( modelRoot, 1 ) ); + model.change( writer => { + writer.setSelection( modelRange ); + } ); + + dispatcher.convertSelection( modelDoc.selection, model.markers, writer ); + } ); + + expect( viewSelection.rangeCount ).to.equal( 1 ); + + const viewString = stringifyView( viewRoot, viewSelection, { showType: false } ); + expect( viewString ).to.equal( '
f{}oobar
' ); + } ); + + it( 'should clear fake selection', () => { + const modelRange = model.createRange( model.createPositionAt( modelRoot, 1 ), model.createPositionAt( modelRoot, 1 ) ); + + view.change( writer => { + writer.setSelection( modelRange, { fake: true } ); + + dispatcher.convertSelection( docSelection, model.markers, writer ); + } ); + expect( viewSelection.isFake ).to.be.false; + } ); + } ); + } ); + + describe( 'table cell selection converter', () => { + beforeEach( () => { + model.schema.register( 'table', { isLimit: true } ); + model.schema.register( 'tr', { isLimit: true } ); + model.schema.register( 'td', { isLimit: true } ); + + model.schema.extend( 'table', { allowIn: '$root' } ); + model.schema.extend( 'tr', { allowIn: 'table' } ); + model.schema.extend( 'td', { allowIn: 'tr' } ); + model.schema.extend( '$text', { allowIn: 'td' } ); + + const downcastHelpers = new DowncastHelpers( dispatcher ); + + // "Universal" converter to convert table structure. + downcastHelpers.elementToElement( { model: 'table', view: 'table' } ); + downcastHelpers.elementToElement( { model: 'tr', view: 'tr' } ); + downcastHelpers.elementToElement( { model: 'td', view: 'td' } ); + + // Special converter for table cells. + dispatcher.on( 'selection', ( evt, data, conversionApi ) => { + const selection = data.selection; + + if ( !conversionApi.consumable.test( selection, 'selection' ) || selection.isCollapsed ) { + return; + } + + for ( const range of selection.getRanges() ) { + const node = range.start.parent; + + if ( !!node && node.is( 'td' ) ) { + conversionApi.consumable.consume( selection, 'selection' ); + + const viewNode = conversionApi.mapper.toViewElement( node ); + conversionApi.writer.addClass( 'selected', viewNode ); + } + } + }, { priority: 'high' } ); + } ); + + it( 'should not be used to convert selection that is not on table cell', () => { + test( + [ 1, 5 ], + 'f{o<$text bold="true">oba}r', + 'f{ooba}r' + ); + } ); + + it( 'should add a class to the selected table cell', () => { + test( + // table tr#0 td#0 [foo, table tr#0 td#0 bar] + [ [ 0, 0, 0, 0 ], [ 0, 0, 0, 3 ] ], + '
foo
bar
', + '
foo
bar
' + ); + } ); + + it( 'should not be used if selection contains more than just a table cell', () => { + test( + // table tr td#1 f{oo bar, table tr#2 bar] + [ [ 0, 0, 0, 1 ], [ 0, 0, 1, 3 ] ], + '
foobar
', + '[
foobar
]' + ); + } ); + } ); + + // Tests if the selection got correctly converted. + // Because `setData` might use selection converters itself to set the selection, we can't use it + // to set the selection (because then we would test converters using converters). + // Instead, the `test` function expects to be passed `selectionPaths` which is an array containing two numbers or two arrays, + // that are offsets or paths of selection positions in root element. + function test( selectionPaths, modelInput, expectedView, selectionAttributes = {} ) { + // Parse passed `modelInput` string and set it as current model. + setModelData( model, modelInput ); + + // Manually set selection ranges using passed `selectionPaths`. + const startPath = typeof selectionPaths[ 0 ] == 'number' ? [ selectionPaths[ 0 ] ] : selectionPaths[ 0 ]; + const endPath = typeof selectionPaths[ 1 ] == 'number' ? [ selectionPaths[ 1 ] ] : selectionPaths[ 1 ]; + + const startPos = model.createPositionFromPath( modelRoot, startPath ); + const endPos = model.createPositionFromPath( modelRoot, endPath ); + + const isBackward = selectionPaths[ 2 ] === 'backward'; + model.change( writer => { + writer.setSelection( writer.createRange( startPos, endPos ), { backward: isBackward } ); + + // And add or remove passed attributes. + for ( const key in selectionAttributes ) { + const value = selectionAttributes[ key ]; + + if ( value ) { + writer.setSelectionAttribute( key, value ); + } else { + writer.removeSelectionAttribute( key ); + } + } + } ); + + // Remove view children manually (without firing additional conversion). + viewRoot._removeChildren( 0, viewRoot.childCount ); + + // Convert model to view. + view.change( writer => { + dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); + dispatcher.convertSelection( docSelection, model.markers, writer ); + } ); + + // Stringify view and check if it is same as expected. + expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal( '
' + expectedView + '
' ); + } +} ); + From 721cb8cc64d5eafa6adbea993dc1bb88c17f5459 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 2 Jan 2019 15:56:27 +0100 Subject: [PATCH 79/84] Update documentation links to downcast selection converters. --- src/conversion/downcasthelpers.js | 4 ++-- src/conversion/modelconsumable.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/conversion/downcasthelpers.js b/src/conversion/downcasthelpers.js index 734486938..51215ef00 100644 --- a/src/conversion/downcasthelpers.js +++ b/src/conversion/downcasthelpers.js @@ -470,7 +470,7 @@ export function convertRangeSelection() { * converted, broken attributes might be merged again, or the position where the selection is may be wrapped * with different, appropriate attribute elements. * - * See also {@link module:engine/conversion/downcast-selection-converters~clearAttributes} which does a clean-up + * See also {@link module:engine/conversion/downcasthelpers~clearAttributes} which does a clean-up * by merging attributes. * * @returns {Function} Selection converter. @@ -515,7 +515,7 @@ export function convertCollapsedSelection() { * * modelDispatcher.on( 'selection', clearAttributes() ); * - * See {@link module:engine/conversion/downcast-selection-converters~convertCollapsedSelection} + * See {@link module:engine/conversion/downcasthelpers~convertCollapsedSelection} * which does the opposite by breaking attributes in the selection position. * * @returns {Function} Selection converter. diff --git a/src/conversion/modelconsumable.js b/src/conversion/modelconsumable.js index 50c9cf89a..6f0957b91 100644 --- a/src/conversion/modelconsumable.js +++ b/src/conversion/modelconsumable.js @@ -29,7 +29,7 @@ import TextProxy from '../model/textproxy'; * {@link module:engine/conversion/modelconsumable~ModelConsumable#add add method} directly. * However, it is important to understand how consumable values can be * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed}. - * See {@link module:engine/conversion/downcast-selection-converters default downcast converters} for more information. + * See {@link module:engine/conversion/downcasthelpers default downcast converters} for more information. * * Keep in mind, that one conversion event may have multiple callbacks (converters) attached to it. Each of those is * able to convert one or more parts of the model. However, when one of those callbacks actually converts From 4a2b6dd04c223ceb2887d67a3643cb7aa5037f0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 2 Jan 2019 15:57:21 +0100 Subject: [PATCH 80/84] Remove redundant code from DowncastHelpers tests. --- tests/conversion/downcasthelpers.js | 106 ++++++++++------------------ 1 file changed, 36 insertions(+), 70 deletions(-) diff --git a/tests/conversion/downcasthelpers.js b/tests/conversion/downcasthelpers.js index 290b032c8..d12f21c68 100644 --- a/tests/conversion/downcasthelpers.js +++ b/tests/conversion/downcasthelpers.js @@ -1454,42 +1454,9 @@ describe( 'DowncastHelpers', () => { function expectResult( string ) { expect( stringifyView( viewRoot, null, { ignoreRoot: true } ) ).to.equal( string ); } - - function viewAttributesToString( item ) { - let result = ''; - - for ( const key of item.getAttributeKeys() ) { - const value = item.getAttribute( key ); - - if ( value ) { - result += ' ' + key + '="' + value + '"'; - } - } - - return result; - } - - function viewToString( item ) { - let result = ''; - - if ( item instanceof ViewText ) { - result = item.data; - } else { - // ViewElement or ViewDocumentFragment. - for ( const child of item.getChildren() ) { - result += viewToString( child ); - } - - if ( item instanceof ViewElement ) { - result = '<' + item.name + viewAttributesToString( item ) + '>' + result + ''; - } - } - - return result; - } } ); -describe( 'downcast-converters', () => { +describe( 'downcast converters', () => { let dispatcher, modelDoc, modelRoot, viewRoot, controller, modelRootStart, model; beforeEach( () => { @@ -1511,40 +1478,7 @@ describe( 'downcast-converters', () => { modelRootStart = model.createPositionAt( modelRoot, 0 ); } ); - function viewAttributesToString( item ) { - let result = ''; - - for ( const key of item.getAttributeKeys() ) { - const value = item.getAttribute( key ); - - if ( value ) { - result += ' ' + key + '="' + value + '"'; - } - } - - return result; - } - - function viewToString( item ) { - let result = ''; - - if ( item instanceof ViewText ) { - result = item.data; - } else { - // ViewElement or ViewDocumentFragment. - for ( const child of item.getChildren() ) { - result += viewToString( child ); - } - - if ( item instanceof ViewElement ) { - result = '<' + item.name + viewAttributesToString( item ) + '>' + result + ''; - } - } - - return result; - } - - describe( 'insertText', () => { + describe( 'insertText()', () => { it( 'should downcast text', () => { model.change( writer => { writer.insert( new ModelText( 'foobar' ), modelRootStart ); @@ -1575,7 +1509,7 @@ describe( 'downcast-converters', () => { } ); // Remove converter is by default already added in `EditingController` instance. - describe( 'remove', () => { + describe( 'remove()', () => { it( 'should remove items from view accordingly to changes in model #1', () => { const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar' ) ); @@ -1847,7 +1781,7 @@ describe( 'downcast-converters', () => { } ); } ); -describe( 'downcast-selection-converters', () => { +describe( 'downcast selection converters', () => { let dispatcher, mapper, model, view, modelDoc, modelRoot, docSelection, viewDoc, viewRoot, viewSelection, downcastHelpers; beforeEach( () => { @@ -2418,3 +2352,35 @@ describe( 'downcast-selection-converters', () => { } } ); +function viewToString( item ) { + let result = ''; + + if ( item instanceof ViewText ) { + result = item.data; + } else { + // ViewElement or ViewDocumentFragment. + for ( const child of item.getChildren() ) { + result += viewToString( child ); + } + + if ( item instanceof ViewElement ) { + result = '<' + item.name + viewAttributesToString( item ) + '>' + result + ''; + } + } + + return result; +} + +function viewAttributesToString( item ) { + let result = ''; + + for ( const key of item.getAttributeKeys() ) { + const value = item.getAttribute( key ); + + if ( value ) { + result += ' ' + key + '="' + value + '"'; + } + } + + return result; +} From f16eebaa087999232aa86631824840988ad6774a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 2 Jan 2019 16:01:10 +0100 Subject: [PATCH 81/84] Move upcast conversion helpers for selection to upcasthelpers.js. --- src/controller/editingcontroller.js | 2 +- src/conversion/upcast-selection-converters.js | 48 ------------------- src/conversion/upcasthelpers.js | 36 ++++++++++++++ .../conversion/upcast-selection-converters.js | 2 +- 4 files changed, 38 insertions(+), 50 deletions(-) delete mode 100644 src/conversion/upcast-selection-converters.js diff --git a/src/controller/editingcontroller.js b/src/controller/editingcontroller.js index d7a7ed732..cec69c9a1 100644 --- a/src/controller/editingcontroller.js +++ b/src/controller/editingcontroller.js @@ -12,10 +12,10 @@ import View from '../view/view'; import Mapper from '../conversion/mapper'; import DowncastDispatcher from '../conversion/downcastdispatcher'; import { clearAttributes, convertCollapsedSelection, convertRangeSelection, insertText, remove } from '../conversion/downcasthelpers'; -import { convertSelectionChange } from '../conversion/upcast-selection-converters'; import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; +import { convertSelectionChange } from '../conversion/upcasthelpers'; /** * Controller for the editing pipeline. The editing pipeline controls {@link ~EditingController#model model} rendering, diff --git a/src/conversion/upcast-selection-converters.js b/src/conversion/upcast-selection-converters.js deleted file mode 100644 index 5ff080497..000000000 --- a/src/conversion/upcast-selection-converters.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -/** - * Contains {@link module:engine/view/documentselection~DocumentSelection view selection} - * to {@link module:engine/model/selection~Selection model selection} conversion helpers. - * - * @module engine/conversion/upcast-selection-converters - */ - -import ModelSelection from '../model/selection'; - -/** - * Function factory, creates a callback function which converts a {@link module:engine/view/selection~Selection - * view selection} taken from the {@link module:engine/view/document~Document#event:selectionChange} event - * and sets in on the {@link module:engine/model/document~Document#selection model}. - * - * **Note**: because there is no view selection change dispatcher nor any other advanced view selection to model - * conversion mechanism, the callback should be set directly on view document. - * - * view.document.on( 'selectionChange', convertSelectionChange( modelDocument, mapper ) ); - * - * @param {module:engine/model/model~Model} model Data model. - * @param {module:engine/conversion/mapper~Mapper} mapper Conversion mapper. - * @returns {Function} {@link module:engine/view/document~Document#event:selectionChange} callback function. - */ -export function convertSelectionChange( model, mapper ) { - return ( evt, data ) => { - const viewSelection = data.newSelection; - const modelSelection = new ModelSelection(); - - const ranges = []; - - for ( const viewRange of viewSelection.getRanges() ) { - ranges.push( mapper.toModelRange( viewRange ) ); - } - - modelSelection.setTo( ranges, { backward: viewSelection.isBackward } ); - - if ( !modelSelection.isEqual( model.document.selection ) ) { - model.change( writer => { - writer.setSelection( modelSelection ); - } ); - } - }; -} diff --git a/src/conversion/upcasthelpers.js b/src/conversion/upcasthelpers.js index 5c7f38ce1..34657753e 100644 --- a/src/conversion/upcasthelpers.js +++ b/src/conversion/upcasthelpers.js @@ -8,6 +8,7 @@ import ModelRange from '../model/range'; import { ConversionHelpers } from './conversion'; import { cloneDeep } from 'lodash-es'; +import ModelSelection from '../model/selection'; /** * Contains {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for @@ -352,6 +353,41 @@ export function convertText() { }; } +/** + * Function factory, creates a callback function which converts a {@link module:engine/view/selection~Selection + * view selection} taken from the {@link module:engine/view/document~Document#event:selectionChange} event + * and sets in on the {@link module:engine/model/document~Document#selection model}. + * + * **Note**: because there is no view selection change dispatcher nor any other advanced view selection to model + * conversion mechanism, the callback should be set directly on view document. + * + * view.document.on( 'selectionChange', convertSelectionChange( modelDocument, mapper ) ); + * + * @param {module:engine/model/model~Model} model Data model. + * @param {module:engine/conversion/mapper~Mapper} mapper Conversion mapper. + * @returns {Function} {@link module:engine/view/document~Document#event:selectionChange} callback function. + */ +export function convertSelectionChange( model, mapper ) { + return ( evt, data ) => { + const viewSelection = data.newSelection; + const modelSelection = new ModelSelection(); + + const ranges = []; + + for ( const viewRange of viewSelection.getRanges() ) { + ranges.push( mapper.toModelRange( viewRange ) ); + } + + modelSelection.setTo( ranges, { backward: viewSelection.isBackward } ); + + if ( !modelSelection.isEqual( model.document.selection ) ) { + model.change( writer => { + writer.setSelection( modelSelection ); + } ); + } + }; +} + // View element to model element conversion helper. // // See {@link ~UpcastHelpers#elementToElement `.elementToElement()` upcast helper} for examples. diff --git a/tests/conversion/upcast-selection-converters.js b/tests/conversion/upcast-selection-converters.js index 18a95d92f..fbe75b3d6 100644 --- a/tests/conversion/upcast-selection-converters.js +++ b/tests/conversion/upcast-selection-converters.js @@ -11,10 +11,10 @@ import createViewRoot from '../view/_utils/createroot'; import Model from '../../src/model/model'; import Mapper from '../../src/conversion/mapper'; -import { convertSelectionChange } from '../../src/conversion/upcast-selection-converters'; import { setData as modelSetData, getData as modelGetData } from '../../src/dev-utils/model'; import { setData as viewSetData } from '../../src/dev-utils/view'; +import { convertSelectionChange } from '../../src/conversion/upcasthelpers'; describe( 'convertSelectionChange', () => { let model, view, viewDocument, mapper, convertSelection, modelRoot, viewRoot; From 067934d8f3ca33871d0337247cd8bb6cfd402614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 2 Jan 2019 16:03:25 +0100 Subject: [PATCH 82/84] Remove upcast-selection-converters.js. --- .../conversion/upcast-selection-converters.js | 131 ------------------ tests/conversion/upcasthelpers.js | 124 ++++++++++++++++- 2 files changed, 122 insertions(+), 133 deletions(-) delete mode 100644 tests/conversion/upcast-selection-converters.js diff --git a/tests/conversion/upcast-selection-converters.js b/tests/conversion/upcast-selection-converters.js deleted file mode 100644 index fbe75b3d6..000000000 --- a/tests/conversion/upcast-selection-converters.js +++ /dev/null @@ -1,131 +0,0 @@ -/** - * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -import View from '../../src/view/view'; -import ViewSelection from '../../src/view/selection'; -import ViewRange from '../../src/view/range'; -import createViewRoot from '../view/_utils/createroot'; - -import Model from '../../src/model/model'; - -import Mapper from '../../src/conversion/mapper'; - -import { setData as modelSetData, getData as modelGetData } from '../../src/dev-utils/model'; -import { setData as viewSetData } from '../../src/dev-utils/view'; -import { convertSelectionChange } from '../../src/conversion/upcasthelpers'; - -describe( 'convertSelectionChange', () => { - let model, view, viewDocument, mapper, convertSelection, modelRoot, viewRoot; - - beforeEach( () => { - model = new Model(); - modelRoot = model.document.createRoot(); - model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); - - modelSetData( model, 'foobar' ); - - view = new View(); - viewDocument = view.document; - viewRoot = createViewRoot( viewDocument, 'div', 'main' ); - - viewSetData( view, '

foo

bar

' ); - - mapper = new Mapper(); - mapper.bindElements( modelRoot, viewRoot ); - mapper.bindElements( modelRoot.getChild( 0 ), viewRoot.getChild( 0 ) ); - mapper.bindElements( modelRoot.getChild( 1 ), viewRoot.getChild( 1 ) ); - - convertSelection = convertSelectionChange( model, mapper ); - } ); - - afterEach( () => { - view.destroy(); - } ); - - it( 'should convert collapsed selection', () => { - const viewSelection = new ViewSelection(); - viewSelection.setTo( ViewRange._createFromParentsAndOffsets( - viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 1 ) ); - - convertSelection( null, { newSelection: viewSelection } ); - - expect( modelGetData( model ) ).to.equals( 'f[]oobar' ); - expect( modelGetData( model ) ).to.equal( 'f[]oobar' ); - } ); - - it( 'should support unicode', () => { - modelSetData( model, 'நிலைக்கு' ); - viewSetData( view, '

நிலைக்கு

' ); - - // Re-bind elements that were just re-set. - mapper.bindElements( modelRoot.getChild( 0 ), viewRoot.getChild( 0 ) ); - - const viewSelection = new ViewSelection( [ - ViewRange._createFromParentsAndOffsets( viewRoot.getChild( 0 ).getChild( 0 ), 2, viewRoot.getChild( 0 ).getChild( 0 ), 6 ) - ] ); - - convertSelection( null, { newSelection: viewSelection } ); - - expect( modelGetData( model ) ).to.equal( 'நி[லைக்]கு' ); - } ); - - it( 'should convert multi ranges selection', () => { - const viewSelection = new ViewSelection( [ - ViewRange._createFromParentsAndOffsets( - viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 2 ), - ViewRange._createFromParentsAndOffsets( - viewRoot.getChild( 1 ).getChild( 0 ), 1, viewRoot.getChild( 1 ).getChild( 0 ), 2 ) - ] ); - - convertSelection( null, { newSelection: viewSelection } ); - - expect( modelGetData( model ) ).to.equal( - 'f[o]ob[a]r' ); - - const ranges = Array.from( model.document.selection.getRanges() ); - expect( ranges.length ).to.equal( 2 ); - - expect( ranges[ 0 ].start.parent ).to.equal( modelRoot.getChild( 0 ) ); - expect( ranges[ 0 ].start.offset ).to.equal( 1 ); - expect( ranges[ 0 ].end.parent ).to.equal( modelRoot.getChild( 0 ) ); - expect( ranges[ 0 ].end.offset ).to.equal( 2 ); - - expect( ranges[ 1 ].start.parent ).to.equal( modelRoot.getChild( 1 ) ); - expect( ranges[ 1 ].start.offset ).to.equal( 1 ); - expect( ranges[ 1 ].end.parent ).to.equal( modelRoot.getChild( 1 ) ); - expect( ranges[ 1 ].end.offset ).to.equal( 2 ); - } ); - - it( 'should convert reverse selection', () => { - const viewSelection = new ViewSelection( [ - ViewRange._createFromParentsAndOffsets( - viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 2 ), - ViewRange._createFromParentsAndOffsets( - viewRoot.getChild( 1 ).getChild( 0 ), 1, viewRoot.getChild( 1 ).getChild( 0 ), 2 ) - ], { backward: true } ); - - convertSelection( null, { newSelection: viewSelection } ); - - expect( modelGetData( model ) ).to.equal( 'f[o]ob[a]r' ); - expect( model.document.selection.isBackward ).to.true; - } ); - - it( 'should not enqueue changes if selection has not changed', () => { - const viewSelection = new ViewSelection( [ - ViewRange._createFromParentsAndOffsets( - viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 1 ) - ] ); - - convertSelection( null, { newSelection: viewSelection } ); - - const spy = sinon.spy(); - - model.on( 'change', spy ); - - convertSelection( null, { newSelection: viewSelection } ); - - expect( spy.called ).to.be.false; - } ); -} ); diff --git a/tests/conversion/upcasthelpers.js b/tests/conversion/upcasthelpers.js index c7951e41f..12f017605 100644 --- a/tests/conversion/upcasthelpers.js +++ b/tests/conversion/upcasthelpers.js @@ -19,9 +19,15 @@ import ModelText from '../../src/model/text'; import ModelRange from '../../src/model/range'; import ModelPosition from '../../src/model/position'; -import UpcastHelpers, { convertToModelFragment, convertText } from '../../src/conversion/upcasthelpers'; +import UpcastHelpers, { convertToModelFragment, convertText, convertSelectionChange } from '../../src/conversion/upcasthelpers'; -import { stringify } from '../../src/dev-utils/model'; +import { getData as modelGetData, setData as modelSetData, stringify } from '../../src/dev-utils/model'; +import View from '../../src/view/view'; +import createViewRoot from '../view/_utils/createroot'; +import { setData as viewSetData } from '../../src/dev-utils/view'; +import Mapper from '../../src/conversion/mapper'; +import ViewSelection from '../../src/view/selection'; +import ViewRange from '../../src/view/range'; describe( 'UpcastHelpers', () => { let upcastDispatcher, model, schema, conversion, upcastHelpers; @@ -834,4 +840,118 @@ describe( 'upcast-converters', () => { sinon.assert.calledTwice( spy ); } ); } ); + + describe( 'convertSelectionChange', () => { + let model, view, viewDocument, mapper, convertSelection, modelRoot, viewRoot; + + beforeEach( () => { + model = new Model(); + modelRoot = model.document.createRoot(); + model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); + + modelSetData( model, 'foobar' ); + + view = new View(); + viewDocument = view.document; + viewRoot = createViewRoot( viewDocument, 'div', 'main' ); + + viewSetData( view, '

foo

bar

' ); + + mapper = new Mapper(); + mapper.bindElements( modelRoot, viewRoot ); + mapper.bindElements( modelRoot.getChild( 0 ), viewRoot.getChild( 0 ) ); + mapper.bindElements( modelRoot.getChild( 1 ), viewRoot.getChild( 1 ) ); + + convertSelection = convertSelectionChange( model, mapper ); + } ); + + afterEach( () => { + view.destroy(); + } ); + + it( 'should convert collapsed selection', () => { + const viewSelection = new ViewSelection(); + viewSelection.setTo( ViewRange._createFromParentsAndOffsets( + viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 1 ) ); + + convertSelection( null, { newSelection: viewSelection } ); + + expect( modelGetData( model ) ).to.equals( 'f[]oobar' ); + expect( modelGetData( model ) ).to.equal( 'f[]oobar' ); + } ); + + it( 'should support unicode', () => { + modelSetData( model, 'நிலைக்கு' ); + viewSetData( view, '

நிலைக்கு

' ); + + // Re-bind elements that were just re-set. + mapper.bindElements( modelRoot.getChild( 0 ), viewRoot.getChild( 0 ) ); + + const viewSelection = new ViewSelection( [ + ViewRange._createFromParentsAndOffsets( viewRoot.getChild( 0 ).getChild( 0 ), 2, viewRoot.getChild( 0 ).getChild( 0 ), 6 ) + ] ); + + convertSelection( null, { newSelection: viewSelection } ); + + expect( modelGetData( model ) ).to.equal( 'நி[லைக்]கு' ); + } ); + + it( 'should convert multi ranges selection', () => { + const viewSelection = new ViewSelection( [ + ViewRange._createFromParentsAndOffsets( + viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 2 ), + ViewRange._createFromParentsAndOffsets( + viewRoot.getChild( 1 ).getChild( 0 ), 1, viewRoot.getChild( 1 ).getChild( 0 ), 2 ) + ] ); + + convertSelection( null, { newSelection: viewSelection } ); + + expect( modelGetData( model ) ).to.equal( + 'f[o]ob[a]r' ); + + const ranges = Array.from( model.document.selection.getRanges() ); + expect( ranges.length ).to.equal( 2 ); + + expect( ranges[ 0 ].start.parent ).to.equal( modelRoot.getChild( 0 ) ); + expect( ranges[ 0 ].start.offset ).to.equal( 1 ); + expect( ranges[ 0 ].end.parent ).to.equal( modelRoot.getChild( 0 ) ); + expect( ranges[ 0 ].end.offset ).to.equal( 2 ); + + expect( ranges[ 1 ].start.parent ).to.equal( modelRoot.getChild( 1 ) ); + expect( ranges[ 1 ].start.offset ).to.equal( 1 ); + expect( ranges[ 1 ].end.parent ).to.equal( modelRoot.getChild( 1 ) ); + expect( ranges[ 1 ].end.offset ).to.equal( 2 ); + } ); + + it( 'should convert reverse selection', () => { + const viewSelection = new ViewSelection( [ + ViewRange._createFromParentsAndOffsets( + viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 2 ), + ViewRange._createFromParentsAndOffsets( + viewRoot.getChild( 1 ).getChild( 0 ), 1, viewRoot.getChild( 1 ).getChild( 0 ), 2 ) + ], { backward: true } ); + + convertSelection( null, { newSelection: viewSelection } ); + + expect( modelGetData( model ) ).to.equal( 'f[o]ob[a]r' ); + expect( model.document.selection.isBackward ).to.true; + } ); + + it( 'should not enqueue changes if selection has not changed', () => { + const viewSelection = new ViewSelection( [ + ViewRange._createFromParentsAndOffsets( + viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 1 ) + ] ); + + convertSelection( null, { newSelection: viewSelection } ); + + const spy = sinon.spy(); + + model.on( 'change', spy ); + + convertSelection( null, { newSelection: viewSelection } ); + + expect( spy.called ).to.be.false; + } ); + } ); } ); From 9c9a4c969ea808b02ba33d9b4dee8b49c8f7e6e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 2 Jan 2019 16:12:31 +0100 Subject: [PATCH 83/84] Fix code style of imports in downcasthelepers tests. --- tests/conversion/downcasthelpers.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/conversion/downcasthelpers.js b/tests/conversion/downcasthelpers.js index d12f21c68..8b88a76b4 100644 --- a/tests/conversion/downcasthelpers.js +++ b/tests/conversion/downcasthelpers.js @@ -21,7 +21,9 @@ import log from '@ckeditor/ckeditor5-utils/src/log'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import DowncastHelpers, { - clearAttributes, convertCollapsedSelection, convertRangeSelection, + clearAttributes, + convertCollapsedSelection, + convertRangeSelection, createViewElementFromHighlightDescriptor, insertText } from '../../src/conversion/downcasthelpers'; From 37a981f26770e67446ccdd49cc1a8d33de7a1081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 2 Jan 2019 16:12:52 +0100 Subject: [PATCH 84/84] Fix code style of test names in upcasthelepers tests. --- tests/conversion/upcasthelpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conversion/upcasthelpers.js b/tests/conversion/upcasthelpers.js index 12f017605..00f72e660 100644 --- a/tests/conversion/upcasthelpers.js +++ b/tests/conversion/upcasthelpers.js @@ -841,7 +841,7 @@ describe( 'upcast-converters', () => { } ); } ); - describe( 'convertSelectionChange', () => { + describe( 'convertSelectionChange()', () => { let model, view, viewDocument, mapper, convertSelection, modelRoot, viewRoot; beforeEach( () => {