From 4d51b2e7f2538fb28f7eac1d0b14028067aa04a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Thu, 24 Aug 2017 15:16:35 +0200 Subject: [PATCH 01/32] Internal: `DataController#insertContent` strips disallowed attributes from Text. --- src/controller/insertcontent.js | 11 +++++++++++ tests/controller/insertcontent.js | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/src/controller/insertcontent.js b/src/controller/insertcontent.js index c2443a8b1..0a81d83d5 100644 --- a/src/controller/insertcontent.js +++ b/src/controller/insertcontent.js @@ -226,6 +226,17 @@ class Insertion { if ( node.is( 'element' ) ) { this.handleNodes( node.getChildren(), context ); } + // When disallowed node is a text but text is allowed in current parent it means that our node + // contains disallowed attributes and we have to remove them. + else if ( node.is( 'text' ) && this.schema.check( { name: '$text', inside: [ this.position.parent ] } ) ) { + for ( const attribute of node.getAttributeKeys() ) { + if ( !this.schema.check( { name: '$text', attributes: attribute, inside: [ this.position.parent ] } ) ) { + node.removeAttribute( attribute ); + } + } + + this._handleNode( node, context ); + } // Try autoparagraphing. else { this._tryAutoparagraphing( node, context ); diff --git a/tests/controller/insertcontent.js b/tests/controller/insertcontent.js index 7c44e8b9a..7224f0cf4 100644 --- a/tests/controller/insertcontent.js +++ b/tests/controller/insertcontent.js @@ -591,6 +591,8 @@ describe( 'DataController', () => { schema.allow( { name: 'disallowedWidget', inside: '$clipboardHolder' } ); schema.allow( { name: '$text', inside: 'disallowedWidget' } ); schema.objects.add( 'disallowedWidget' ); + + schema.allow( { name: '$text', attributes: 'b', inside: 'paragraph' } ); } ); it( 'filters out disallowed elements and leaves out the text', () => { @@ -610,6 +612,12 @@ describe( 'DataController', () => { insertHelper( 'xxx' ); expect( getData( doc ) ).to.equal( 'f[]oo' ); } ); + + it( 'filters out disallowed attributes', () => { + setData( doc, 'f[]oo' ); + insertHelper( '
x<$text a="1" b="1">xxy<$text a="1">yy
' ); + expect( getData( doc ) ).to.equal( 'fx<$text b="1">xxyyy[]oo' ); + } ); } ); } ); From c4f59c2bf04014bb682ab7121682613e823f2dd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Thu, 24 Aug 2017 16:03:41 +0200 Subject: [PATCH 02/32] Internal: Autoparagraphing checks and strips disallowed by a schema attributes. --- src/controller/insertcontent.js | 37 ++++++++++++++++++++++--------- tests/controller/insertcontent.js | 6 +++++ 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/controller/insertcontent.js b/src/controller/insertcontent.js index 0a81d83d5..3cf1b2c2d 100644 --- a/src/controller/insertcontent.js +++ b/src/controller/insertcontent.js @@ -229,12 +229,7 @@ class Insertion { // When disallowed node is a text but text is allowed in current parent it means that our node // contains disallowed attributes and we have to remove them. else if ( node.is( 'text' ) && this.schema.check( { name: '$text', inside: [ this.position.parent ] } ) ) { - for ( const attribute of node.getAttributeKeys() ) { - if ( !this.schema.check( { name: '$text', attributes: attribute, inside: [ this.position.parent ] } ) ) { - node.removeAttribute( attribute ); - } - } - + this._stripsDisallowedAttributes( node ); this._handleNode( node, context ); } // Try autoparagraphing. @@ -336,10 +331,17 @@ class Insertion { // Do not autoparagraph if the paragraph won't be allowed there, // cause that would lead to an infinite loop. The paragraph would be rejected in // the next _handleNode() call and we'd be here again. - if ( this._getAllowedIn( paragraph, this.position.parent ) && this._checkIsAllowed( node, [ paragraph ] ) ) { - paragraph.appendChildren( node ); + if ( this._getAllowedIn( paragraph, this.position.parent ) ) { + // When node is a text and is disallowed by schema it means that contains disallowed attributes + // and we need to remove them. + if ( node.is( 'text' ) && !this._checkIsAllowed( node, [ paragraph ] ) ) { + this._stripsDisallowedAttributes( node, [ paragraph ] ); + } - this._handleNode( paragraph, context ); + if ( this._checkIsAllowed( node, [ paragraph ] ) ) { + paragraph.appendChildren( node ); + this._handleNode( paragraph, context ); + } } } @@ -420,7 +422,7 @@ class Insertion { } /** - * Checks wether according to the schema this is an object type element. + * Checks whether according to the schema this is an object type element. * * @param {module:engine/model/node~Node} node The node to check. */ @@ -428,6 +430,21 @@ class Insertion { return this.schema.objects.has( this._getNodeSchemaName( node ) ); } + /** + * Removes disallowed by schema attributes from given node. + * + * @private + * @param {module:engine/model/node~Node} node + * @param {module:engine/model/schema~SchemaPath} path + */ + _stripsDisallowedAttributes( node, path = [ this.position.parent ] ) { + for ( const attribute of node.getAttributeKeys() ) { + if ( !this.schema.check( { name: '$text', attributes: attribute, inside: [ path ] } ) ) { + node.removeAttribute( attribute ); + } + } + } + /** * Gets a name under which we should check this node in the schema. * diff --git a/tests/controller/insertcontent.js b/tests/controller/insertcontent.js index 7224f0cf4..75bb1716c 100644 --- a/tests/controller/insertcontent.js +++ b/tests/controller/insertcontent.js @@ -618,6 +618,12 @@ describe( 'DataController', () => { insertHelper( '
x<$text a="1" b="1">xxy<$text a="1">yy
' ); expect( getData( doc ) ).to.equal( 'fx<$text b="1">xxyyy[]oo' ); } ); + + it( 'filters out disallowed attributes when autoparagraphing', () => { + setData( doc, 'f[]oo' ); + insertHelper( 'xxx<$text a="1" b="1">yyy' ); + expect( getData( doc ) ).to.equal( 'fxxx<$text b="1">yyy[]oo' ); + } ); } ); } ); From 83c0b609b1d39a5bcce1f2ce8c9794bd7cb52120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Thu, 24 Aug 2017 16:13:30 +0200 Subject: [PATCH 03/32] Typo. --- src/controller/insertcontent.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/controller/insertcontent.js b/src/controller/insertcontent.js index 3cf1b2c2d..9304b8f7b 100644 --- a/src/controller/insertcontent.js +++ b/src/controller/insertcontent.js @@ -229,7 +229,7 @@ class Insertion { // When disallowed node is a text but text is allowed in current parent it means that our node // contains disallowed attributes and we have to remove them. else if ( node.is( 'text' ) && this.schema.check( { name: '$text', inside: [ this.position.parent ] } ) ) { - this._stripsDisallowedAttributes( node ); + this._stripDisallowedAttributes( node ); this._handleNode( node, context ); } // Try autoparagraphing. @@ -335,7 +335,7 @@ class Insertion { // When node is a text and is disallowed by schema it means that contains disallowed attributes // and we need to remove them. if ( node.is( 'text' ) && !this._checkIsAllowed( node, [ paragraph ] ) ) { - this._stripsDisallowedAttributes( node, [ paragraph ] ); + this._stripDisallowedAttributes( node, [ paragraph ] ); } if ( this._checkIsAllowed( node, [ paragraph ] ) ) { @@ -437,7 +437,7 @@ class Insertion { * @param {module:engine/model/node~Node} node * @param {module:engine/model/schema~SchemaPath} path */ - _stripsDisallowedAttributes( node, path = [ this.position.parent ] ) { + _stripDisallowedAttributes( node, path = [ this.position.parent ] ) { for ( const attribute of node.getAttributeKeys() ) { if ( !this.schema.check( { name: '$text', attributes: attribute, inside: [ path ] } ) ) { node.removeAttribute( attribute ); From 73c30368dc1f53e9a42b7cb029d6c6bb6bf82b38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Thu, 24 Aug 2017 16:25:25 +0200 Subject: [PATCH 04/32] Removed not necessary array typecasting. --- src/controller/insertcontent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controller/insertcontent.js b/src/controller/insertcontent.js index 9304b8f7b..2bcfb6fab 100644 --- a/src/controller/insertcontent.js +++ b/src/controller/insertcontent.js @@ -439,7 +439,7 @@ class Insertion { */ _stripDisallowedAttributes( node, path = [ this.position.parent ] ) { for ( const attribute of node.getAttributeKeys() ) { - if ( !this.schema.check( { name: '$text', attributes: attribute, inside: [ path ] } ) ) { + if ( !this.schema.check( { name: '$text', attributes: attribute, inside: path } ) ) { node.removeAttribute( attribute ); } } From cc55cbb1b0ba41bfc6e7a31850b03751b1dfb2d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Thu, 24 Aug 2017 16:26:13 +0200 Subject: [PATCH 05/32] Docs. --- src/controller/insertcontent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controller/insertcontent.js b/src/controller/insertcontent.js index 2bcfb6fab..7e4e996f6 100644 --- a/src/controller/insertcontent.js +++ b/src/controller/insertcontent.js @@ -431,7 +431,7 @@ class Insertion { } /** - * Removes disallowed by schema attributes from given node. + * Removes disallowed by schema attributes. * * @private * @param {module:engine/model/node~Node} node From 7de4bb12fd6140ecbaadedc78e4f40b9a41ace61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Thu, 24 Aug 2017 17:09:07 +0200 Subject: [PATCH 06/32] Used simplest format for SchemaPath. --- src/controller/insertcontent.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/controller/insertcontent.js b/src/controller/insertcontent.js index 7e4e996f6..aa2dd1aa6 100644 --- a/src/controller/insertcontent.js +++ b/src/controller/insertcontent.js @@ -228,7 +228,7 @@ class Insertion { } // When disallowed node is a text but text is allowed in current parent it means that our node // contains disallowed attributes and we have to remove them. - else if ( node.is( 'text' ) && this.schema.check( { name: '$text', inside: [ this.position.parent ] } ) ) { + else if ( node.is( 'text' ) && this.schema.check( { name: '$text', inside: this.position } ) ) { this._stripDisallowedAttributes( node ); this._handleNode( node, context ); } @@ -243,7 +243,7 @@ class Insertion { */ _insert( node ) { /* istanbul ignore if */ - if ( !this._checkIsAllowed( node, [ this.position.parent ] ) ) { + if ( !this._checkIsAllowed( node, this.position ) ) { // Algorithm's correctness check. We should never end up here but it's good to know that we did. // Note that it would often be a silent issue if we insert node in a place where it's not allowed. log.error( @@ -262,7 +262,7 @@ class Insertion { livePos.detach(); // The last inserted object should be selected because we can't put a collapsed selection after it. - if ( this._checkIsObject( node ) && !this.schema.check( { name: '$text', inside: [ this.position.parent ] } ) ) { + if ( this._checkIsObject( node ) && !this.schema.check( { name: '$text', inside: this.position } ) ) { this.nodeToSelect = node; } else { this.nodeToSelect = null; @@ -437,7 +437,7 @@ class Insertion { * @param {module:engine/model/node~Node} node * @param {module:engine/model/schema~SchemaPath} path */ - _stripDisallowedAttributes( node, path = [ this.position.parent ] ) { + _stripDisallowedAttributes( node, path = this.position ) { for ( const attribute of node.getAttributeKeys() ) { if ( !this.schema.check( { name: '$text', attributes: attribute, inside: path } ) ) { node.removeAttribute( attribute ); From a0be86af82b41e88a617560039a97d5d3ab67ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Thu, 24 Aug 2017 23:43:32 +0200 Subject: [PATCH 07/32] Added one more test for filtering attributes in inserted content. --- tests/controller/insertcontent.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/controller/insertcontent.js b/tests/controller/insertcontent.js index 75bb1716c..b901c4174 100644 --- a/tests/controller/insertcontent.js +++ b/tests/controller/insertcontent.js @@ -613,7 +613,13 @@ describe( 'DataController', () => { expect( getData( doc ) ).to.equal( 'f[]oo' ); } ); - it( 'filters out disallowed attributes', () => { + it( 'filters out disallowed attributes when inserting text', () => { + setData( doc, 'f[]oo' ); + insertHelper( 'x<$text a="1" b="1">xxy<$text a="1">yy' ); + expect( getData( doc ) ).to.equal( 'fx<$text b="1">xxyyy[]oo' ); + } ); + + it( 'filters out disallowed attributes when inserting text in disallowed elements', () => { setData( doc, 'f[]oo' ); insertHelper( '
x<$text a="1" b="1">xxy<$text a="1">yy
' ); expect( getData( doc ) ).to.equal( 'fx<$text b="1">xxyyy[]oo' ); From 4eecd219f528c65f45c3463d83afc16f780281fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Mon, 28 Aug 2017 12:00:22 +0200 Subject: [PATCH 08/32] Improved filtering to handle disallowed attributes when merge. --- src/controller/insertcontent.js | 35 +++++++++++++++++++++++++------ tests/controller/insertcontent.js | 24 +++++++++++++++++++++ 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/controller/insertcontent.js b/src/controller/insertcontent.js index aa2dd1aa6..9eacf13bb 100644 --- a/src/controller/insertcontent.js +++ b/src/controller/insertcontent.js @@ -10,6 +10,7 @@ import Position from '../model/position'; import LivePosition from '../model/liveposition'; import Element from '../model/element'; +import Text from '../model/text'; import Range from '../model/range'; import log from '@ckeditor/ckeditor5-utils/src/log'; @@ -286,6 +287,14 @@ class Insertion { if ( mergeLeft ) { const position = LivePosition.createFromPosition( this.position ); + const children = Array.from( node.getChildren() ); + + // When Text is a direct child of node which is going to be merged + // we need to strip it from the disallowed attributes according to the new parent. + if ( children.some( child => child instanceof Text ) ) { + this._stripDisallowedAttributes( children, mergePosLeft.nodeBefore ); + } + this.batch.merge( mergePosLeft ); this.position = Position.createFromPosition( position ); @@ -309,6 +318,14 @@ class Insertion { // NOK:

xx[]

+

yy

=>

xxyy[]

(when sticks to next) const position = new LivePosition( this.position.root, this.position.path, 'sticksToPrevious' ); + const children = Array.from( node.getChildren() ); + + // When Text is a direct child of node which is going to be merged + // we need to strip it from the disallowed attributes according to the new parent. + if ( children.some( child => child instanceof Text ) ) { + this._stripDisallowedAttributes( children, mergePosLeft.nodeAfter ); + } + this.batch.merge( mergePosRight ); this.position = Position.createFromPosition( position ); @@ -431,16 +448,22 @@ class Insertion { } /** - * Removes disallowed by schema attributes. + * Removes disallowed by schema attributes from given nodes. * * @private - * @param {module:engine/model/node~Node} node + * @param {module:engine/model/node~Node|Array} nodes * @param {module:engine/model/schema~SchemaPath} path */ - _stripDisallowedAttributes( node, path = this.position ) { - for ( const attribute of node.getAttributeKeys() ) { - if ( !this.schema.check( { name: '$text', attributes: attribute, inside: path } ) ) { - node.removeAttribute( attribute ); + _stripDisallowedAttributes( nodes, path = this.position ) { + if ( !Array.isArray( nodes ) ) { + nodes = [ nodes ]; + } + + for ( const node of nodes ) { + for ( const attribute of node.getAttributeKeys() ) { + if ( !this.schema.check( { name: '$text', attributes: attribute, inside: path } ) ) { + node.removeAttribute( attribute ); + } } } } diff --git a/tests/controller/insertcontent.js b/tests/controller/insertcontent.js index b901c4174..1b09aa0b2 100644 --- a/tests/controller/insertcontent.js +++ b/tests/controller/insertcontent.js @@ -300,6 +300,12 @@ describe( 'DataController', () => { expect( getData( doc ) ).to.equal( 'xyz[]' ); } ); + it( 'inserts one empty paragraph', () => { + setData( doc, 'f[]oo' ); + insertHelper( '' ); + expect( getData( doc ) ).to.equal( 'f[]oo' ); + } ); + it( 'inserts one block into a fully selected content', () => { setData( doc, '[foobar]' ); insertHelper( 'xyz' ); @@ -625,6 +631,24 @@ describe( 'DataController', () => { expect( getData( doc ) ).to.equal( 'fx<$text b="1">xxyyy[]oo' ); } ); + it( 'filters out disallowed attributes when merging #1', () => { + setData( doc, '[]foo' ); + insertHelper( 'x<$text a="1" b="1">xx' ); + expect( getData( doc ) ).to.equal( 'x<$text b="1">xx[]foo' ); + } ); + + it( 'filters out disallowed attributes when merging #2', () => { + setData( doc, 'f[]oo' ); + insertHelper( 'x<$text a="1" b="1">xx' ); + expect( getData( doc ) ).to.equal( 'fx<$text b="1">xx[]oo' ); + } ); + + it( 'filters out disallowed attributes when merging #3', () => { + setData( doc, 'foo[]' ); + insertHelper( 'x<$text a="1" b="1">xx' ); + expect( getData( doc ) ).to.equal( 'foox<$text b="1">xx[]' ); + } ); + it( 'filters out disallowed attributes when autoparagraphing', () => { setData( doc, 'f[]oo' ); insertHelper( 'xxx<$text a="1" b="1">yyy' ); From 4cedf84117c47beee5e384785ff9de2de4fb16df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Mon, 28 Aug 2017 13:06:10 +0200 Subject: [PATCH 09/32] Refactored usage of utils method. --- src/controller/insertcontent.js | 66 ++++++++++++++++----------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/src/controller/insertcontent.js b/src/controller/insertcontent.js index 9eacf13bb..5873a624f 100644 --- a/src/controller/insertcontent.js +++ b/src/controller/insertcontent.js @@ -230,7 +230,7 @@ class Insertion { // When disallowed node is a text but text is allowed in current parent it means that our node // contains disallowed attributes and we have to remove them. else if ( node.is( 'text' ) && this.schema.check( { name: '$text', inside: this.position } ) ) { - this._stripDisallowedAttributes( node ); + removeDisallowedAttributes( node, this.position, this.schema ); this._handleNode( node, context ); } // Try autoparagraphing. @@ -292,7 +292,7 @@ class Insertion { // When Text is a direct child of node which is going to be merged // we need to strip it from the disallowed attributes according to the new parent. if ( children.some( child => child instanceof Text ) ) { - this._stripDisallowedAttributes( children, mergePosLeft.nodeBefore ); + removeDisallowedAttributes( children, mergePosLeft.nodeBefore, this.schema ); } this.batch.merge( mergePosLeft ); @@ -323,7 +323,7 @@ class Insertion { // When Text is a direct child of node which is going to be merged // we need to strip it from the disallowed attributes according to the new parent. if ( children.some( child => child instanceof Text ) ) { - this._stripDisallowedAttributes( children, mergePosLeft.nodeAfter ); + removeDisallowedAttributes( children, mergePosLeft.nodeAfter, this.schema ); } this.batch.merge( mergePosRight ); @@ -352,7 +352,7 @@ class Insertion { // When node is a text and is disallowed by schema it means that contains disallowed attributes // and we need to remove them. if ( node.is( 'text' ) && !this._checkIsAllowed( node, [ paragraph ] ) ) { - this._stripDisallowedAttributes( node, [ paragraph ] ); + removeDisallowedAttributes( node, [ paragraph ], this.schema ); } if ( this._checkIsAllowed( node, [ paragraph ] ) ) { @@ -432,7 +432,7 @@ class Insertion { */ _checkIsAllowed( node, path ) { return this.schema.check( { - name: this._getNodeSchemaName( node ), + name: getNodeSchemaName( node ), attributes: Array.from( node.getAttributeKeys() ), inside: path } ); @@ -444,40 +444,38 @@ class Insertion { * @param {module:engine/model/node~Node} node The node to check. */ _checkIsObject( node ) { - return this.schema.objects.has( this._getNodeSchemaName( node ) ); + return this.schema.objects.has( getNodeSchemaName( node ) ); } +} - /** - * Removes disallowed by schema attributes from given nodes. - * - * @private - * @param {module:engine/model/node~Node|Array} nodes - * @param {module:engine/model/schema~SchemaPath} path - */ - _stripDisallowedAttributes( nodes, path = this.position ) { - if ( !Array.isArray( nodes ) ) { - nodes = [ nodes ]; - } +// Gets a name under which we should check this node in the schema. +// +// @private +// @param {module:engine/model/node~Node} node The node. +function getNodeSchemaName( node ) { + if ( node.is( 'text' ) ) { + return '$text'; + } - for ( const node of nodes ) { - for ( const attribute of node.getAttributeKeys() ) { - if ( !this.schema.check( { name: '$text', attributes: attribute, inside: path } ) ) { - node.removeAttribute( attribute ); - } - } - } + return node.name; +} + +// Removes disallowed by schema attributes from given text nodes. +// +// @private +// @param {module:engine/model/node~Node|Array} nodes +// @param {module:engine/model/schema~SchemaPath} schemaPath +// @param {module:engine/model/schema~Schema} schema +function removeDisallowedAttributes( nodes, schemaPath, schema ) { + if ( !Array.isArray( nodes ) ) { + nodes = [ nodes ]; } - /** - * Gets a name under which we should check this node in the schema. - * - * @param {module:engine/model/node~Node} node The node. - */ - _getNodeSchemaName( node ) { - if ( node.is( 'text' ) ) { - return '$text'; + for ( const node of nodes ) { + for ( const attribute of node.getAttributeKeys() ) { + if ( !schema.check( { name: getNodeSchemaName( node ), attributes: attribute, inside: schemaPath } ) ) { + node.removeAttribute( attribute ); + } } - - return node.name; } } From 1b697574e95d03e223ba075c646fa619d97146d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Mon, 28 Aug 2017 13:10:08 +0200 Subject: [PATCH 10/32] Improved code style. --- src/controller/deletecontent.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/controller/deletecontent.js b/src/controller/deletecontent.js index b260e7392..075aff0b7 100644 --- a/src/controller/deletecontent.js +++ b/src/controller/deletecontent.js @@ -208,9 +208,5 @@ function shouldEntireContentBeReplacedWithParagraph( schema, selection ) { return false; } - if ( !schema.check( { name: 'paragraph', inside: limitElement.name } ) ) { - return false; - } - - return true; + return schema.check( { name: 'paragraph', inside: limitElement.name } ); } From 6dcd92daf8787c70ce9c00bd0fa184ef698dd639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Mon, 28 Aug 2017 13:36:30 +0200 Subject: [PATCH 11/32] Fixed incorrect schema path. --- src/controller/insertcontent.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controller/insertcontent.js b/src/controller/insertcontent.js index 5873a624f..8f1b501f8 100644 --- a/src/controller/insertcontent.js +++ b/src/controller/insertcontent.js @@ -292,7 +292,7 @@ class Insertion { // When Text is a direct child of node which is going to be merged // we need to strip it from the disallowed attributes according to the new parent. if ( children.some( child => child instanceof Text ) ) { - removeDisallowedAttributes( children, mergePosLeft.nodeBefore, this.schema ); + removeDisallowedAttributes( children, [ mergePosLeft.nodeBefore ], this.schema ); } this.batch.merge( mergePosLeft ); @@ -323,7 +323,7 @@ class Insertion { // When Text is a direct child of node which is going to be merged // we need to strip it from the disallowed attributes according to the new parent. if ( children.some( child => child instanceof Text ) ) { - removeDisallowedAttributes( children, mergePosLeft.nodeAfter, this.schema ); + removeDisallowedAttributes( children, [ mergePosLeft.nodeAfter ], this.schema ); } this.batch.merge( mergePosRight ); From bd4b085eb53082df91bb45d3da9f7052d97f82c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Mon, 28 Aug 2017 13:36:58 +0200 Subject: [PATCH 12/32] Removed duplicated test. --- tests/controller/deletecontent.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/controller/deletecontent.js b/tests/controller/deletecontent.js index bb21eab97..a6f9ead9f 100644 --- a/tests/controller/deletecontent.js +++ b/tests/controller/deletecontent.js @@ -188,12 +188,6 @@ describe( 'DataController', () => { { leaveUnmerged: true } ); - test( - 'merges second element into the first one (same name)', - 'xfo[ob]ary', - 'xfo[]ary' - ); - test( 'merges second element into the first one (different name)', 'xfo[ob]ary', From 392ed5bbfabb9d264b7dc78e9fed2505ddbc4784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Mon, 28 Aug 2017 13:51:49 +0200 Subject: [PATCH 13/32] Simplified the code. --- src/controller/insertcontent.js | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/controller/insertcontent.js b/src/controller/insertcontent.js index 8f1b501f8..96d472f2f 100644 --- a/src/controller/insertcontent.js +++ b/src/controller/insertcontent.js @@ -10,7 +10,6 @@ import Position from '../model/position'; import LivePosition from '../model/liveposition'; import Element from '../model/element'; -import Text from '../model/text'; import Range from '../model/range'; import log from '@ckeditor/ckeditor5-utils/src/log'; @@ -287,13 +286,9 @@ class Insertion { if ( mergeLeft ) { const position = LivePosition.createFromPosition( this.position ); - const children = Array.from( node.getChildren() ); - - // When Text is a direct child of node which is going to be merged - // we need to strip it from the disallowed attributes according to the new parent. - if ( children.some( child => child instanceof Text ) ) { - removeDisallowedAttributes( children, [ mergePosLeft.nodeBefore ], this.schema ); - } + // When need to check a direct child of node that is going to be merged + // and strip it from the disallowed attributes according to the new parent. + removeDisallowedAttributes( Array.from( node.getChildren() ), [ mergePosLeft.nodeBefore ], this.schema ); this.batch.merge( mergePosLeft ); @@ -318,13 +313,9 @@ class Insertion { // NOK:

xx[]

+

yy

=>

xxyy[]

(when sticks to next) const position = new LivePosition( this.position.root, this.position.path, 'sticksToPrevious' ); - const children = Array.from( node.getChildren() ); - - // When Text is a direct child of node which is going to be merged - // we need to strip it from the disallowed attributes according to the new parent. - if ( children.some( child => child instanceof Text ) ) { - removeDisallowedAttributes( children, [ mergePosLeft.nodeAfter ], this.schema ); - } + // When need to check a direct child of node that is going to be merged + // and strip it from the disallowed attributes according to the new parent. + removeDisallowedAttributes( Array.from( node.getChildren() ), [ mergePosLeft.nodeAfter ], this.schema ); this.batch.merge( mergePosRight ); From 306853739bef293dd494c4d801258474b84d02ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Mon, 28 Aug 2017 14:47:52 +0200 Subject: [PATCH 14/32] Added attributes filtering when merging after delete. --- src/controller/deletecontent.js | 39 +++++++++++++++++++++++++++++++ tests/controller/deletecontent.js | 24 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/controller/deletecontent.js b/src/controller/deletecontent.js index 075aff0b7..cfec685ff 100644 --- a/src/controller/deletecontent.js +++ b/src/controller/deletecontent.js @@ -132,6 +132,13 @@ function mergeBranches( batch, startPos, endPos ) { // To then become: // xy[]{} batch.merge( startPos ); + + // We need to check and strip disallowed attributes in direct children because after merge + // some attributes could end up in a parent where are disallowed. + // + // e.g. bold is disallowed for

+ //

Fo{o

b}ar

->

Fo{}ar

->

Fo{}ar

. + removeDisallowedAttributes( Array.from( startParent.getChildren() ), [ startParent ], batch.document.schema ); } // Removes empty end ancestors: @@ -210,3 +217,35 @@ function shouldEntireContentBeReplacedWithParagraph( schema, selection ) { return schema.check( { name: 'paragraph', inside: limitElement.name } ); } + +// Gets a name under which we should check this node in the schema. +// +// @private +// @param {module:engine/model/node~Node} node The node. +function getNodeSchemaName( node ) { + if ( node.is( 'text' ) ) { + return '$text'; + } + + return node.name; +} + +// Removes disallowed by schema attributes from given text nodes. +// +// @private +// @param {module:engine/model/node~Node|Array} nodes +// @param {module:engine/model/schema~SchemaPath} schemaPath +// @param {module:engine/model/schema~Schema} schema +function removeDisallowedAttributes( nodes, schemaPath, schema ) { + if ( !Array.isArray( nodes ) ) { + nodes = [ nodes ]; + } + + for ( const node of nodes ) { + for ( const attribute of node.getAttributeKeys() ) { + if ( !schema.check( { name: getNodeSchemaName( node ), attributes: attribute, inside: schemaPath } ) ) { + node.removeAttribute( attribute ); + } + } + } +} diff --git a/tests/controller/deletecontent.js b/tests/controller/deletecontent.js index a6f9ead9f..e8fe7d690 100644 --- a/tests/controller/deletecontent.js +++ b/tests/controller/deletecontent.js @@ -167,6 +167,12 @@ describe( 'DataController', () => { schema.allow( { name: '$text', inside: 'pparent' } ); schema.allow( { name: 'paragraph', attributes: [ 'align' ] } ); + schema.allow( { name: '$text', attributes: 'a', inside: 'paragraph' } ); + schema.allow( { name: '$text', attributes: 'b', inside: 'paragraph' } ); + schema.allow( { name: '$text', attributes: 'c', inside: 'paragraph' } ); + schema.allow( { name: '$text', attributes: 'b', inside: 'heading1' } ); + schema.allow( { name: '$text', attributes: 'c', inside: 'pchild' } ); + schema.allow( { name: '$text', attributes: 'd', inside: 'pchild' } ); } ); test( @@ -263,6 +269,12 @@ describe( 'DataController', () => { expect( spyMerge.called ).to.be.true; } ); + test( + 'filters out disallowed attributes after merge', + 'fo[ob]a<$text a="true" b="true">r', + 'fo[]a<$text b="true">r' + ); + // Note: in all these cases we ignore the direction of merge. // If https://github.com/ckeditor/ckeditor5-engine/issues/470 was fixed we could differently treat // forward and backward delete. @@ -400,6 +412,18 @@ describe( 'DataController', () => { expect( getData( doc ) ) .to.equal( 'fo[]' ); } ); + + test( + 'filters out disallowed attributes after merge when left', + 'xfo[oy]<$text b="true" c="true">z', + 'xfo[]<$text c="true">z' + ); + + test( + 'filters out disallowed attributes after merge when right', + 'fo[ox<$text c="true" d="true">y]z', + 'fo[]<$text c="true">z' + ); } ); describe( 'with object elements', () => { From 59174ec41d91bdb1e99b378f17cc7b769eb8616b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Mon, 28 Aug 2017 15:27:49 +0200 Subject: [PATCH 15/32] Moved common helper functions to the model/utils.js. --- src/controller/deletecontent.js | 33 +---------------- src/controller/insertcontent.js | 33 +---------------- src/model/utils.js | 43 ++++++++++++++++++++++ tests/model/utils.js | 64 +++++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 64 deletions(-) create mode 100644 src/model/utils.js create mode 100644 tests/model/utils.js diff --git a/src/controller/deletecontent.js b/src/controller/deletecontent.js index cfec685ff..98dc0bc20 100644 --- a/src/controller/deletecontent.js +++ b/src/controller/deletecontent.js @@ -11,6 +11,7 @@ import LivePosition from '../model/liveposition'; import Position from '../model/position'; import Range from '../model/range'; import Element from '../model/element'; +import { removeDisallowedAttributes } from '../model/utils'; /** * Deletes content of the selection and merge siblings. The resulting selection is always collapsed. @@ -217,35 +218,3 @@ function shouldEntireContentBeReplacedWithParagraph( schema, selection ) { return schema.check( { name: 'paragraph', inside: limitElement.name } ); } - -// Gets a name under which we should check this node in the schema. -// -// @private -// @param {module:engine/model/node~Node} node The node. -function getNodeSchemaName( node ) { - if ( node.is( 'text' ) ) { - return '$text'; - } - - return node.name; -} - -// Removes disallowed by schema attributes from given text nodes. -// -// @private -// @param {module:engine/model/node~Node|Array} nodes -// @param {module:engine/model/schema~SchemaPath} schemaPath -// @param {module:engine/model/schema~Schema} schema -function removeDisallowedAttributes( nodes, schemaPath, schema ) { - if ( !Array.isArray( nodes ) ) { - nodes = [ nodes ]; - } - - for ( const node of nodes ) { - for ( const attribute of node.getAttributeKeys() ) { - if ( !schema.check( { name: getNodeSchemaName( node ), attributes: attribute, inside: schemaPath } ) ) { - node.removeAttribute( attribute ); - } - } - } -} diff --git a/src/controller/insertcontent.js b/src/controller/insertcontent.js index 96d472f2f..46e369ac3 100644 --- a/src/controller/insertcontent.js +++ b/src/controller/insertcontent.js @@ -12,6 +12,7 @@ import LivePosition from '../model/liveposition'; import Element from '../model/element'; import Range from '../model/range'; import log from '@ckeditor/ckeditor5-utils/src/log'; +import { getNodeSchemaName, removeDisallowedAttributes } from '../model/utils'; /** * Inserts content into the editor (specified selection) as one would expect the paste @@ -438,35 +439,3 @@ class Insertion { return this.schema.objects.has( getNodeSchemaName( node ) ); } } - -// Gets a name under which we should check this node in the schema. -// -// @private -// @param {module:engine/model/node~Node} node The node. -function getNodeSchemaName( node ) { - if ( node.is( 'text' ) ) { - return '$text'; - } - - return node.name; -} - -// Removes disallowed by schema attributes from given text nodes. -// -// @private -// @param {module:engine/model/node~Node|Array} nodes -// @param {module:engine/model/schema~SchemaPath} schemaPath -// @param {module:engine/model/schema~Schema} schema -function removeDisallowedAttributes( nodes, schemaPath, schema ) { - if ( !Array.isArray( nodes ) ) { - nodes = [ nodes ]; - } - - for ( const node of nodes ) { - for ( const attribute of node.getAttributeKeys() ) { - if ( !schema.check( { name: getNodeSchemaName( node ), attributes: attribute, inside: schemaPath } ) ) { - node.removeAttribute( attribute ); - } - } - } -} diff --git a/src/model/utils.js b/src/model/utils.js new file mode 100644 index 000000000..d0a455d9b --- /dev/null +++ b/src/model/utils.js @@ -0,0 +1,43 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module engine/model/utils + */ + +/** + * Gets a name under which we should check this node in the schema. + * + * @param {module:engine/model/node~Node} node The node. + * @returns {String} node name. + */ +export function getNodeSchemaName( node ) { + if ( node.is( 'text' ) ) { + return '$text'; + } + + return node.name; +} + +/** + * Removes disallowed by schema attributes from given nodes. + * + * @param {module:engine/model/node~Node|Array} nodes List of nodes or a single node to filter. + * @param {module:engine/model/schema~SchemaPath} schemaPath + * @param {module:engine/model/schema~Schema} schema + */ +export function removeDisallowedAttributes( nodes, schemaPath, schema ) { + if ( !Array.isArray( nodes ) ) { + nodes = [ nodes ]; + } + + for ( const node of nodes ) { + for ( const attribute of node.getAttributeKeys() ) { + if ( !schema.check( { name: getNodeSchemaName( node ), attributes: attribute, inside: schemaPath } ) ) { + node.removeAttribute( attribute ); + } + } + } +} diff --git a/tests/model/utils.js b/tests/model/utils.js new file mode 100644 index 000000000..05c72c06a --- /dev/null +++ b/tests/model/utils.js @@ -0,0 +1,64 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { getNodeSchemaName, removeDisallowedAttributes } from '../../src/model/utils'; +import Element from '../../src/model/element'; +import Text from '../../src/model/text'; +import Document from '../../src/model/document'; +import { setData, getData } from '../../src/dev-utils/model'; + +describe( 'model utils', () => { + describe( 'getNodeSchemaName()', () => { + it( 'should return schema name for a given Element', () => { + const element = new Element( 'paragraph' ); + + expect( getNodeSchemaName( element ) ).to.equal( 'paragraph' ); + } ); + + it( 'should return schema name for a Text', () => { + const element = new Text(); + + expect( getNodeSchemaName( element ) ).to.equal( '$text' ); + } ); + } ); + + describe( 'removeDisallowedAttributes()', () => { + let doc; + + beforeEach( () => { + doc = new Document(); + doc.createRoot(); + + const schema = doc.schema; + + schema.registerItem( 'paragraph', '$block' ); + + schema.allow( { name: '$text', attributes: 'a', inside: 'paragraph' } ); + schema.allow( { name: '$text', attributes: 'c', inside: 'paragraph' } ); + } ); + + it( 'should remove disallowed by schema attributes from list of nodes', () => { + setData( doc, 'f<$text a="1" b="1">oob<$text b="1" c="1">ar' ); + + const paragraph = doc.getRoot().getChild( 0 ); + + removeDisallowedAttributes( Array.from( paragraph.getChildren() ), [ paragraph ], doc.schema ); + + expect( getData( doc, { withoutSelection: true } ) ) + .to.equal( 'f<$text a="1">oob<$text c="1">ar' ); + } ); + + it( 'should remove disallowed by schema attributes from a single node', () => { + setData( doc, '<$text a="1" b="1">foo' ); + + const paragraph = doc.getRoot().getChild( 0 ); + + removeDisallowedAttributes( paragraph.getChild( 0 ), [ paragraph ], doc.schema ); + + expect( getData( doc, { withoutSelection: true } ) ) + .to.equal( '<$text a="1">foo' ); + } ); + } ); +} ); From ed37d71bbab1b2ef9f5f038775730ad4f659b15b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Mon, 28 Aug 2017 16:04:28 +0200 Subject: [PATCH 16/32] Increased CC to 100%. --- tests/controller/insertcontent.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/controller/insertcontent.js b/tests/controller/insertcontent.js index 1b09aa0b2..727aecc8d 100644 --- a/tests/controller/insertcontent.js +++ b/tests/controller/insertcontent.js @@ -281,6 +281,17 @@ describe( 'DataController', () => { expect( getData( doc ) ).to.equal( 'foxyz[]ar' ); } ); + it( 'not inserts autoparagraph when paragraph is disallowed at the current position', () => { + doc.schema.disallow( { name: 'paragraph', inside: '$root' } ); + doc.schema.disallow( { name: 'heading2', inside: '$root' } ); + + const content = new Element( 'heading2', [], [ new Text( 'bar' ) ] ); + + setData( doc, '[foo]' ); + insertContent( dataController, content, doc.selection ); + expect( getData( doc ) ).to.equal( '[]' ); + } ); + describe( 'block to block handling', () => { it( 'inserts one paragraph', () => { setData( doc, 'f[]oo' ); From e22930f48efca1d4641e8f7286cf8325b533eb72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Mon, 28 Aug 2017 16:31:02 +0200 Subject: [PATCH 17/32] Refactored model util test. --- tests/model/utils.js | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/tests/model/utils.js b/tests/model/utils.js index 05c72c06a..056cfda2c 100644 --- a/tests/model/utils.js +++ b/tests/model/utils.js @@ -7,7 +7,7 @@ import { getNodeSchemaName, removeDisallowedAttributes } from '../../src/model/u import Element from '../../src/model/element'; import Text from '../../src/model/text'; import Document from '../../src/model/document'; -import { setData, getData } from '../../src/dev-utils/model'; +import { stringify } from '../../src/dev-utils/model'; describe( 'model utils', () => { describe( 'getNodeSchemaName()', () => { @@ -34,31 +34,35 @@ describe( 'model utils', () => { const schema = doc.schema; schema.registerItem( 'paragraph', '$block' ); + schema.registerItem( 'el', '$inline' ); schema.allow( { name: '$text', attributes: 'a', inside: 'paragraph' } ); schema.allow( { name: '$text', attributes: 'c', inside: 'paragraph' } ); + schema.allow( { name: 'el', attributes: 'b' } ); } ); it( 'should remove disallowed by schema attributes from list of nodes', () => { - setData( doc, 'f<$text a="1" b="1">oob<$text b="1" c="1">ar' ); + const paragraph = new Element( 'paragraph' ); + const el = new Element( 'el', { a: 1, b: 1, c: 1 } ); + const foo = new Text( 'foo', { a: 1, b: 1 } ); + const bar = new Text( 'bar' ); + const biz = new Text( 'biz', { b: 1, c: 1 } ); - const paragraph = doc.getRoot().getChild( 0 ); + paragraph.appendChildren( [ el, foo, bar, biz ] ); removeDisallowedAttributes( Array.from( paragraph.getChildren() ), [ paragraph ], doc.schema ); - expect( getData( doc, { withoutSelection: true } ) ) - .to.equal( 'f<$text a="1">oob<$text c="1">ar' ); + expect( stringify( paragraph ) ) + .to.equal( '<$text a="1">foobar<$text c="1">biz' ); } ); it( 'should remove disallowed by schema attributes from a single node', () => { - setData( doc, '<$text a="1" b="1">foo' ); + const paragraph = new Element( 'paragraph' ); + const foo = new Text( 'foo', { a: 1, b: 1 } ); - const paragraph = doc.getRoot().getChild( 0 ); + removeDisallowedAttributes( foo, [ paragraph ], doc.schema ); - removeDisallowedAttributes( paragraph.getChild( 0 ), [ paragraph ], doc.schema ); - - expect( getData( doc, { withoutSelection: true } ) ) - .to.equal( '<$text a="1">foo' ); + expect( stringify( foo ) ).to.equal( '<$text a="1">foo' ); } ); } ); } ); From b54c43bb1e5ffa1bfea8955cc5a0827e42322420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Mon, 28 Aug 2017 20:27:47 +0200 Subject: [PATCH 18/32] Removed model utils and refactored removing disallowed attributes. --- src/controller/deletecontent.js | 28 ++++++++++++- src/controller/insertcontent.js | 42 +++++++++++++++---- src/model/utils.js | 43 ------------------- tests/controller/deletecontent.js | 21 +++++----- tests/controller/insertcontent.js | 1 + tests/model/utils.js | 68 ------------------------------- 6 files changed, 71 insertions(+), 132 deletions(-) delete mode 100644 src/model/utils.js delete mode 100644 tests/model/utils.js diff --git a/src/controller/deletecontent.js b/src/controller/deletecontent.js index 98dc0bc20..29f669608 100644 --- a/src/controller/deletecontent.js +++ b/src/controller/deletecontent.js @@ -11,7 +11,6 @@ import LivePosition from '../model/liveposition'; import Position from '../model/position'; import Range from '../model/range'; import Element from '../model/element'; -import { removeDisallowedAttributes } from '../model/utils'; /** * Deletes content of the selection and merge siblings. The resulting selection is always collapsed. @@ -139,7 +138,7 @@ function mergeBranches( batch, startPos, endPos ) { // // e.g. bold is disallowed for

//

Fo{o

b}ar

->

Fo{}ar

->

Fo{}ar

. - removeDisallowedAttributes( Array.from( startParent.getChildren() ), [ startParent ], batch.document.schema ); + removeDisallowedAttributes( batch, Array.from( startParent.getChildren() ), [ startParent ] ); } // Removes empty end ancestors: @@ -218,3 +217,28 @@ function shouldEntireContentBeReplacedWithParagraph( schema, selection ) { return schema.check( { name: 'paragraph', inside: limitElement.name } ); } + +// Gets a name under which we should check this node in the schema. +// +// @param {module:engine/model/node~Node} node The node. +// @returns {String} node name. +function getNodeSchemaName( node ) { + return node.is( 'text' ) ? '$text' : node.name; +} + +// Adds deltas with `removeAttributes` operation for disallowed by schema attributes on given nodes. +// +// @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added. +// @param {module:engine/model/node~Node|Array} nodes List of nodes or a single node to filter. +// @param {module:engine/model/schema~SchemaPath} schemaPath +function removeDisallowedAttributes( batch, nodes, schemaPath ) { + const schema = batch.document.schema; + + for ( const node of nodes ) { + for ( const attribute of node.getAttributeKeys() ) { + if ( !schema.check( { name: getNodeSchemaName( node ), attributes: attribute, inside: schemaPath } ) ) { + batch.removeAttribute( node, attribute ); + } + } + } +} diff --git a/src/controller/insertcontent.js b/src/controller/insertcontent.js index 46e369ac3..d70f570f4 100644 --- a/src/controller/insertcontent.js +++ b/src/controller/insertcontent.js @@ -12,7 +12,6 @@ import LivePosition from '../model/liveposition'; import Element from '../model/element'; import Range from '../model/range'; import log from '@ckeditor/ckeditor5-utils/src/log'; -import { getNodeSchemaName, removeDisallowedAttributes } from '../model/utils'; /** * Inserts content into the editor (specified selection) as one would expect the paste @@ -227,10 +226,10 @@ class Insertion { if ( node.is( 'element' ) ) { this.handleNodes( node.getChildren(), context ); } - // When disallowed node is a text but text is allowed in current parent it means that our node + // When disallowed node is a text but text is allowed in current position it means that our node // contains disallowed attributes and we have to remove them. else if ( node.is( 'text' ) && this.schema.check( { name: '$text', inside: this.position } ) ) { - removeDisallowedAttributes( node, this.position, this.schema ); + removeDisallowedAttributes( this.schema, node, this.position ); this._handleNode( node, context ); } // Try autoparagraphing. @@ -287,9 +286,9 @@ class Insertion { if ( mergeLeft ) { const position = LivePosition.createFromPosition( this.position ); - // When need to check a direct child of node that is going to be merged + // When need to check a children of node that is going to be merged // and strip it from the disallowed attributes according to the new parent. - removeDisallowedAttributes( Array.from( node.getChildren() ), [ mergePosLeft.nodeBefore ], this.schema ); + removeDisallowedAttributes( this.schema, Array.from( node.getChildren() ), [ mergePosLeft.nodeBefore ] ); this.batch.merge( mergePosLeft ); @@ -314,9 +313,9 @@ class Insertion { // NOK:

xx[]

+

yy

=>

xxyy[]

(when sticks to next) const position = new LivePosition( this.position.root, this.position.path, 'sticksToPrevious' ); - // When need to check a direct child of node that is going to be merged + // When need to check a children of node that is going to be merged // and strip it from the disallowed attributes according to the new parent. - removeDisallowedAttributes( Array.from( node.getChildren() ), [ mergePosLeft.nodeAfter ], this.schema ); + removeDisallowedAttributes( this.schema, Array.from( node.getChildren() ), [ mergePosRight.nodeAfter ] ); this.batch.merge( mergePosRight ); @@ -344,7 +343,7 @@ class Insertion { // When node is a text and is disallowed by schema it means that contains disallowed attributes // and we need to remove them. if ( node.is( 'text' ) && !this._checkIsAllowed( node, [ paragraph ] ) ) { - removeDisallowedAttributes( node, [ paragraph ], this.schema ); + removeDisallowedAttributes( this.schema, node, [ paragraph ] ); } if ( this._checkIsAllowed( node, [ paragraph ] ) ) { @@ -439,3 +438,30 @@ class Insertion { return this.schema.objects.has( getNodeSchemaName( node ) ); } } + +// Gets a name under which we should check this node in the schema. +// +// @param {module:engine/model/node~Node} node The node. +// @returns {String} node name. +function getNodeSchemaName( node ) { + return node.is( 'text' ) ? '$text' : node.name; +} + +// Removes disallowed by schema attributes from given nodes. +// +// @param {module:engine/model/schema~Schema} schema +// @param {module:engine/model/node~Node|Array} nodes List of nodes or a single node to filter. +// @param {module:engine/model/schema~SchemaPath} schemaPath +export function removeDisallowedAttributes( schema, nodes, schemaPath ) { + if ( !Array.isArray( nodes ) ) { + nodes = [ nodes ]; + } + + for ( const node of nodes ) { + for ( const attribute of node.getAttributeKeys() ) { + if ( !schema.check( { name: getNodeSchemaName( node ), attributes: attribute, inside: schemaPath } ) ) { + node.removeAttribute( attribute ); + } + } + } +} diff --git a/src/model/utils.js b/src/model/utils.js deleted file mode 100644 index d0a455d9b..000000000 --- a/src/model/utils.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -/** - * @module engine/model/utils - */ - -/** - * Gets a name under which we should check this node in the schema. - * - * @param {module:engine/model/node~Node} node The node. - * @returns {String} node name. - */ -export function getNodeSchemaName( node ) { - if ( node.is( 'text' ) ) { - return '$text'; - } - - return node.name; -} - -/** - * Removes disallowed by schema attributes from given nodes. - * - * @param {module:engine/model/node~Node|Array} nodes List of nodes or a single node to filter. - * @param {module:engine/model/schema~SchemaPath} schemaPath - * @param {module:engine/model/schema~Schema} schema - */ -export function removeDisallowedAttributes( nodes, schemaPath, schema ) { - if ( !Array.isArray( nodes ) ) { - nodes = [ nodes ]; - } - - for ( const node of nodes ) { - for ( const attribute of node.getAttributeKeys() ) { - if ( !schema.check( { name: getNodeSchemaName( node ), attributes: attribute, inside: schemaPath } ) ) { - node.removeAttribute( attribute ); - } - } - } -} diff --git a/tests/controller/deletecontent.js b/tests/controller/deletecontent.js index e8fe7d690..8074b27ca 100644 --- a/tests/controller/deletecontent.js +++ b/tests/controller/deletecontent.js @@ -155,9 +155,9 @@ describe( 'DataController', () => { schema.registerItem( 'paragraph', '$block' ); schema.registerItem( 'heading1', '$block' ); + schema.registerItem( 'image', '$inline' ); schema.registerItem( 'pchild' ); schema.registerItem( 'pparent' ); - schema.registerItem( 'image', '$inline' ); schema.allow( { name: 'pchild', inside: 'paragraph' } ); schema.allow( { name: '$text', inside: 'pchild' } ); @@ -167,12 +167,11 @@ describe( 'DataController', () => { schema.allow( { name: '$text', inside: 'pparent' } ); schema.allow( { name: 'paragraph', attributes: [ 'align' ] } ); - schema.allow( { name: '$text', attributes: 'a', inside: 'paragraph' } ); - schema.allow( { name: '$text', attributes: 'b', inside: 'paragraph' } ); - schema.allow( { name: '$text', attributes: 'c', inside: 'paragraph' } ); - schema.allow( { name: '$text', attributes: 'b', inside: 'heading1' } ); - schema.allow( { name: '$text', attributes: 'c', inside: 'pchild' } ); - schema.allow( { name: '$text', attributes: 'd', inside: 'pchild' } ); + schema.allow( { name: 'image', attributes: [ 'a', 'b' ], inside: 'paragraph' } ); + schema.allow( { name: 'image', attributes: [ 'b' ], inside: 'heading1' } ); + schema.allow( { name: '$text', attributes: [ 'a', 'b', 'c' ], inside: 'paragraph' } ); + schema.allow( { name: '$text', attributes: [ 'b' ], inside: 'heading1' } ); + schema.allow( { name: '$text', attributes: [ 'c', 'd' ], inside: 'pchild' } ); } ); test( @@ -271,8 +270,8 @@ describe( 'DataController', () => { test( 'filters out disallowed attributes after merge', - 'fo[ob]a<$text a="true" b="true">r', - 'fo[]a<$text b="true">r' + 'fo[ob]a<$text a="1" b="1">r', + 'fo[]a<$text b="1">r' ); // Note: in all these cases we ignore the direction of merge. @@ -414,13 +413,13 @@ describe( 'DataController', () => { } ); test( - 'filters out disallowed attributes after merge when left', + 'filters out disallowed attributes after left merge', 'xfo[oy]<$text b="true" c="true">z', 'xfo[]<$text c="true">z' ); test( - 'filters out disallowed attributes after merge when right', + 'filters out disallowed attributes after right merge', 'fo[ox<$text c="true" d="true">y]z', 'fo[]<$text c="true">z' ); diff --git a/tests/controller/insertcontent.js b/tests/controller/insertcontent.js index 727aecc8d..b3499740c 100644 --- a/tests/controller/insertcontent.js +++ b/tests/controller/insertcontent.js @@ -593,6 +593,7 @@ describe( 'DataController', () => { const schema = doc.schema; schema.registerItem( 'paragraph', '$block' ); + schema.registerItem( 'heading1', '$block' ); // Let's use table as an example of content which needs to be filtered out. schema.registerItem( 'table' ); diff --git a/tests/model/utils.js b/tests/model/utils.js deleted file mode 100644 index 056cfda2c..000000000 --- a/tests/model/utils.js +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -import { getNodeSchemaName, removeDisallowedAttributes } from '../../src/model/utils'; -import Element from '../../src/model/element'; -import Text from '../../src/model/text'; -import Document from '../../src/model/document'; -import { stringify } from '../../src/dev-utils/model'; - -describe( 'model utils', () => { - describe( 'getNodeSchemaName()', () => { - it( 'should return schema name for a given Element', () => { - const element = new Element( 'paragraph' ); - - expect( getNodeSchemaName( element ) ).to.equal( 'paragraph' ); - } ); - - it( 'should return schema name for a Text', () => { - const element = new Text(); - - expect( getNodeSchemaName( element ) ).to.equal( '$text' ); - } ); - } ); - - describe( 'removeDisallowedAttributes()', () => { - let doc; - - beforeEach( () => { - doc = new Document(); - doc.createRoot(); - - const schema = doc.schema; - - schema.registerItem( 'paragraph', '$block' ); - schema.registerItem( 'el', '$inline' ); - - schema.allow( { name: '$text', attributes: 'a', inside: 'paragraph' } ); - schema.allow( { name: '$text', attributes: 'c', inside: 'paragraph' } ); - schema.allow( { name: 'el', attributes: 'b' } ); - } ); - - it( 'should remove disallowed by schema attributes from list of nodes', () => { - const paragraph = new Element( 'paragraph' ); - const el = new Element( 'el', { a: 1, b: 1, c: 1 } ); - const foo = new Text( 'foo', { a: 1, b: 1 } ); - const bar = new Text( 'bar' ); - const biz = new Text( 'biz', { b: 1, c: 1 } ); - - paragraph.appendChildren( [ el, foo, bar, biz ] ); - - removeDisallowedAttributes( Array.from( paragraph.getChildren() ), [ paragraph ], doc.schema ); - - expect( stringify( paragraph ) ) - .to.equal( '<$text a="1">foobar<$text c="1">biz' ); - } ); - - it( 'should remove disallowed by schema attributes from a single node', () => { - const paragraph = new Element( 'paragraph' ); - const foo = new Text( 'foo', { a: 1, b: 1 } ); - - removeDisallowedAttributes( foo, [ paragraph ], doc.schema ); - - expect( stringify( foo ) ).to.equal( '<$text a="1">foo' ); - } ); - } ); -} ); From f6de62fc3ca3e43c7390b09ea72aee4f0504ebeb Mon Sep 17 00:00:00 2001 From: Szymon Cofalik Date: Tue, 29 Aug 2017 15:16:24 +0200 Subject: [PATCH 19/32] Docs: minor docs corrections. --- src/controller/insertcontent.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/controller/insertcontent.js b/src/controller/insertcontent.js index d70f570f4..1ade7012c 100644 --- a/src/controller/insertcontent.js +++ b/src/controller/insertcontent.js @@ -222,17 +222,17 @@ class Insertion { * @param {Object} context */ _handleDisallowedNode( node, context ) { - // Try inserting its children (strip the parent). + // If the node is an element, try inserting its children (strip the parent). if ( node.is( 'element' ) ) { this.handleNodes( node.getChildren(), context ); } - // When disallowed node is a text but text is allowed in current position it means that our node + // If the node is a text and bare text is allowed in current position it means that the node // contains disallowed attributes and we have to remove them. - else if ( node.is( 'text' ) && this.schema.check( { name: '$text', inside: this.position } ) ) { + else if ( this.schema.check( { name: '$text', inside: this.position } ) ) { removeDisallowedAttributes( this.schema, node, this.position ); this._handleNode( node, context ); } - // Try autoparagraphing. + // If text is not allowed, try autoparagraphing. else { this._tryAutoparagraphing( node, context ); } @@ -286,7 +286,7 @@ class Insertion { if ( mergeLeft ) { const position = LivePosition.createFromPosition( this.position ); - // When need to check a children of node that is going to be merged + // We need to check a children of node that is going to be merged // and strip it from the disallowed attributes according to the new parent. removeDisallowedAttributes( this.schema, Array.from( node.getChildren() ), [ mergePosLeft.nodeBefore ] ); @@ -313,7 +313,7 @@ class Insertion { // NOK:

xx[]

+

yy

=>

xxyy[]

(when sticks to next) const position = new LivePosition( this.position.root, this.position.path, 'sticksToPrevious' ); - // When need to check a children of node that is going to be merged + // We need to check a children of node that is going to be merged // and strip it from the disallowed attributes according to the new parent. removeDisallowedAttributes( this.schema, Array.from( node.getChildren() ), [ mergePosRight.nodeAfter ] ); From cb305e7e922a831205390bc841a4f2f888557147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Tue, 29 Aug 2017 15:32:11 +0200 Subject: [PATCH 20/32] Remove unnecessary export. --- src/controller/insertcontent.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/controller/insertcontent.js b/src/controller/insertcontent.js index 1ade7012c..bb8376e54 100644 --- a/src/controller/insertcontent.js +++ b/src/controller/insertcontent.js @@ -229,7 +229,7 @@ class Insertion { // If the node is a text and bare text is allowed in current position it means that the node // contains disallowed attributes and we have to remove them. else if ( this.schema.check( { name: '$text', inside: this.position } ) ) { - removeDisallowedAttributes( this.schema, node, this.position ); + removeDisallowedAttributes( this.schema, [ node ], this.position ); this._handleNode( node, context ); } // If text is not allowed, try autoparagraphing. @@ -343,7 +343,7 @@ class Insertion { // When node is a text and is disallowed by schema it means that contains disallowed attributes // and we need to remove them. if ( node.is( 'text' ) && !this._checkIsAllowed( node, [ paragraph ] ) ) { - removeDisallowedAttributes( this.schema, node, [ paragraph ] ); + removeDisallowedAttributes( this.schema, [ node ], [ paragraph ] ); } if ( this._checkIsAllowed( node, [ paragraph ] ) ) { @@ -442,7 +442,7 @@ class Insertion { // Gets a name under which we should check this node in the schema. // // @param {module:engine/model/node~Node} node The node. -// @returns {String} node name. +// @returns {String} Node name. function getNodeSchemaName( node ) { return node.is( 'text' ) ? '$text' : node.name; } @@ -450,13 +450,9 @@ function getNodeSchemaName( node ) { // Removes disallowed by schema attributes from given nodes. // // @param {module:engine/model/schema~Schema} schema -// @param {module:engine/model/node~Node|Array} nodes List of nodes or a single node to filter. +// @param {Array} nodes List of nodes to filter. // @param {module:engine/model/schema~SchemaPath} schemaPath -export function removeDisallowedAttributes( schema, nodes, schemaPath ) { - if ( !Array.isArray( nodes ) ) { - nodes = [ nodes ]; - } - +function removeDisallowedAttributes( schema, nodes, schemaPath ) { for ( const node of nodes ) { for ( const attribute of node.getAttributeKeys() ) { if ( !schema.check( { name: getNodeSchemaName( node ), attributes: attribute, inside: schemaPath } ) ) { From 0dc237fa53a20807b0fdd39bffa9dfb86b3579a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Tue, 29 Aug 2017 16:14:16 +0200 Subject: [PATCH 21/32] Used position instead of parent as a schema path. --- src/controller/deletecontent.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/controller/deletecontent.js b/src/controller/deletecontent.js index 29f669608..0c9952e2e 100644 --- a/src/controller/deletecontent.js +++ b/src/controller/deletecontent.js @@ -111,8 +111,8 @@ function mergeBranches( batch, startPos, endPos ) { // x[]{}y // will become: // xy[]{} - startPos = Position.createAfter( startParent ); - endPos = Position.createBefore( endParent ); + const nextStartPos = Position.createAfter( startParent ); + let nextEndPos = Position.createBefore( endParent ); if ( endParent.isEmpty ) { batch.remove( endParent ); @@ -125,20 +125,20 @@ function mergeBranches( batch, startPos, endPos ) { // Move the end parent only if needed. // E.g. not in this case:

ab

[]{}

cd

- if ( !endPos.isEqual( startPos ) ) { - batch.move( endParent, startPos ); + if ( !nextEndPos.isEqual( nextStartPos ) ) { + batch.move( endParent, nextStartPos ); } // To then become: // xy[]{} - batch.merge( startPos ); + batch.merge( nextStartPos ); // We need to check and strip disallowed attributes in direct children because after merge // some attributes could end up in a parent where are disallowed. // // e.g. bold is disallowed for

//

Fo{o

b}ar

->

Fo{}ar

->

Fo{}ar

. - removeDisallowedAttributes( batch, Array.from( startParent.getChildren() ), [ startParent ] ); + removeDisallowedAttributes( batch, Array.from( startParent.getChildren() ), startPos ); } // Removes empty end ancestors: @@ -146,16 +146,16 @@ function mergeBranches( batch, startPos, endPos ) { // becomes: // fo[]{} // So we can remove and . - while ( endPos.parent.isEmpty ) { - const parentToRemove = endPos.parent; + while ( nextEndPos.parent.isEmpty ) { + const parentToRemove = nextEndPos.parent; - endPos = Position.createBefore( parentToRemove ); + nextEndPos = Position.createBefore( parentToRemove ); batch.remove( parentToRemove ); } // Continue merging next level. - mergeBranches( batch, startPos, endPos ); + mergeBranches( batch, nextStartPos, nextEndPos ); } function shouldAutoparagraph( doc, position ) { From f8bbac9d12abcae8dc984f19e81561bc9d5b7af3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Tue, 29 Aug 2017 17:51:31 +0200 Subject: [PATCH 22/32] Added filtering of nested nodes in deleteContent. --- src/controller/deletecontent.js | 38 +++++++++-------- tests/controller/deletecontent.js | 68 ++++++++++++++++++++----------- 2 files changed, 66 insertions(+), 40 deletions(-) diff --git a/src/controller/deletecontent.js b/src/controller/deletecontent.js index 0c9952e2e..1c6dcf015 100644 --- a/src/controller/deletecontent.js +++ b/src/controller/deletecontent.js @@ -68,6 +68,13 @@ export default function deleteContent( selection, batch, options = {} ) { // want to override that behavior anyway. if ( !options.leaveUnmerged ) { mergeBranches( batch, startPos, endPos ); + + // We need to check and strip disallowed attributes in all nested nodes because after merge + // some attributes could end up in a path where are disallowed. + // + // e.g. bold is disallowed for

+ //

Fo{o

b}ar

->

Fo{}ar

->

Fo{}ar

. + removeDisallowedAttributes( batch, Array.from( startPos.parent.getChildren() ), startPos ); } selection.setCollapsedAt( startPos ); @@ -111,8 +118,8 @@ function mergeBranches( batch, startPos, endPos ) { // x[]{}y // will become: // xy[]{} - const nextStartPos = Position.createAfter( startParent ); - let nextEndPos = Position.createBefore( endParent ); + startPos = Position.createAfter( startParent ); + endPos = Position.createBefore( endParent ); if ( endParent.isEmpty ) { batch.remove( endParent ); @@ -125,20 +132,13 @@ function mergeBranches( batch, startPos, endPos ) { // Move the end parent only if needed. // E.g. not in this case:

ab

[]{}

cd

- if ( !nextEndPos.isEqual( nextStartPos ) ) { - batch.move( endParent, nextStartPos ); + if ( !endPos.isEqual( startPos ) ) { + batch.move( endParent, startPos ); } // To then become: // xy[]{} - batch.merge( nextStartPos ); - - // We need to check and strip disallowed attributes in direct children because after merge - // some attributes could end up in a parent where are disallowed. - // - // e.g. bold is disallowed for

- //

Fo{o

b}ar

->

Fo{}ar

->

Fo{}ar

. - removeDisallowedAttributes( batch, Array.from( startParent.getChildren() ), startPos ); + batch.merge( startPos ); } // Removes empty end ancestors: @@ -146,16 +146,16 @@ function mergeBranches( batch, startPos, endPos ) { // becomes: // fo[]{} // So we can remove and . - while ( nextEndPos.parent.isEmpty ) { - const parentToRemove = nextEndPos.parent; + while ( endPos.parent.isEmpty ) { + const parentToRemove = endPos.parent; - nextEndPos = Position.createBefore( parentToRemove ); + endPos = Position.createBefore( parentToRemove ); batch.remove( parentToRemove ); } // Continue merging next level. - mergeBranches( batch, nextStartPos, nextEndPos ); + mergeBranches( batch, startPos, endPos ); } function shouldAutoparagraph( doc, position ) { @@ -226,7 +226,7 @@ function getNodeSchemaName( node ) { return node.is( 'text' ) ? '$text' : node.name; } -// Adds deltas with `removeAttributes` operation for disallowed by schema attributes on given nodes. +// Adds deltas with `removeAttributes` operation for disallowed by schema attributes on given nodes and its children. // // @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added. // @param {module:engine/model/node~Node|Array} nodes List of nodes or a single node to filter. @@ -240,5 +240,9 @@ function removeDisallowedAttributes( batch, nodes, schemaPath ) { batch.removeAttribute( node, attribute ); } } + + if ( node.is( 'element' ) ) { + removeDisallowedAttributes( batch, Array.from( node.getChildren() ), Position.createAt( node ) ); + } } } diff --git a/tests/controller/deletecontent.js b/tests/controller/deletecontent.js index 8074b27ca..78a537a4f 100644 --- a/tests/controller/deletecontent.js +++ b/tests/controller/deletecontent.js @@ -167,11 +167,6 @@ describe( 'DataController', () => { schema.allow( { name: '$text', inside: 'pparent' } ); schema.allow( { name: 'paragraph', attributes: [ 'align' ] } ); - schema.allow( { name: 'image', attributes: [ 'a', 'b' ], inside: 'paragraph' } ); - schema.allow( { name: 'image', attributes: [ 'b' ], inside: 'heading1' } ); - schema.allow( { name: '$text', attributes: [ 'a', 'b', 'c' ], inside: 'paragraph' } ); - schema.allow( { name: '$text', attributes: [ 'b' ], inside: 'heading1' } ); - schema.allow( { name: '$text', attributes: [ 'c', 'd' ], inside: 'pchild' } ); } ); test( @@ -268,12 +263,6 @@ describe( 'DataController', () => { expect( spyMerge.called ).to.be.true; } ); - test( - 'filters out disallowed attributes after merge', - 'fo[ob]a<$text a="1" b="1">r', - 'fo[]a<$text b="1">r' - ); - // Note: in all these cases we ignore the direction of merge. // If https://github.com/ckeditor/ckeditor5-engine/issues/470 was fixed we could differently treat // forward and backward delete. @@ -411,18 +400,6 @@ describe( 'DataController', () => { expect( getData( doc ) ) .to.equal( 'fo[]' ); } ); - - test( - 'filters out disallowed attributes after left merge', - 'xfo[oy]<$text b="true" c="true">z', - 'xfo[]<$text c="true">z' - ); - - test( - 'filters out disallowed attributes after right merge', - 'fo[ox<$text c="true" d="true">y]z', - 'fo[]<$text c="true">z' - ); } ); describe( 'with object elements', () => { @@ -453,6 +430,51 @@ describe( 'DataController', () => { 'ba[]oo' ); } ); + + describe( 'filtering out', () => { + beforeEach( () => { + const schema = doc.schema; + + schema.allow( { name: '$text', attributes: [ 'a', 'b' ], inside: 'paragraph' } ); + schema.allow( { name: '$text', attributes: [ 'b', 'c' ], inside: 'pchild' } ); + schema.allow( { name: 'pchild', inside: 'pchild' } ); + schema.disallow( { name: '$text', attributes: [ 'c' ], inside: 'pchild pchild' } ); + } ); + + test( + 'filters out disallowed attributes after left merge', + 'xfo[oy]<$text a="1" b="1">z', + 'xfo[]<$text b="1">z' + ); + + test( + 'filters out disallowed attributes from nested nodes after left merge', + '' + + 'x' + + 'fo[o' + + '' + + '' + + 'b]a<$text a="1" b="1">r' + + 'b<$text b="1" c="1">iz' + + 'y' + + '', + + '' + + 'x' + + '' + + 'fo[]a<$text b="1">r' + + 'b<$text b="1">iz' + + 'y' + + '' + + '' + ); + + test( + 'filters out disallowed attributes after right merge', + 'fo[ox<$text b="1" c="1">y]z', + 'fo[]<$text b="1">z' + ); + } ); } ); describe( 'in element selections scenarios', () => { From 82ee93ae6b198545303dfbd874352427ab735c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Wed, 30 Aug 2017 10:55:26 +0200 Subject: [PATCH 23/32] Improved filtering of nested elements after merge. --- src/controller/insertcontent.js | 37 ++++++++++++++++++++++++------- tests/controller/insertcontent.js | 11 +++++++++ 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/controller/insertcontent.js b/src/controller/insertcontent.js index bb8376e54..b8329ce4f 100644 --- a/src/controller/insertcontent.js +++ b/src/controller/insertcontent.js @@ -286,12 +286,12 @@ class Insertion { if ( mergeLeft ) { const position = LivePosition.createFromPosition( this.position ); - // We need to check a children of node that is going to be merged - // and strip it from the disallowed attributes according to the new parent. - removeDisallowedAttributes( this.schema, Array.from( node.getChildren() ), [ mergePosLeft.nodeBefore ] ); - this.batch.merge( mergePosLeft ); + // We need to check and strip disallowed attributes in all nested nodes because after merge + // some attributes could end up in a path where are disallowed. + removeDisallowedAttributesOnBatch( this.batch, Array.from( this.position.parent.getChildren() ), position ); + this.position = Position.createFromPosition( position ); position.detach(); } @@ -313,12 +313,12 @@ class Insertion { // NOK:

xx[]

+

yy

=>

xxyy[]

(when sticks to next) const position = new LivePosition( this.position.root, this.position.path, 'sticksToPrevious' ); - // We need to check a children of node that is going to be merged - // and strip it from the disallowed attributes according to the new parent. - removeDisallowedAttributes( this.schema, Array.from( node.getChildren() ), [ mergePosRight.nodeAfter ] ); - this.batch.merge( mergePosRight ); + // We need to check and strip disallowed attributes in all nested nodes because after merge + // some attributes could end up in a place where are disallowed. + removeDisallowedAttributesOnBatch( this.batch, Array.from( this.position.parent.getChildren() ), position ); + this.position = Position.createFromPosition( position ); position.detach(); } @@ -461,3 +461,24 @@ function removeDisallowedAttributes( schema, nodes, schemaPath ) { } } } + +// Adds deltas with `removeAttributes` operation for disallowed by schema attributes on given nodes and its children. +// +// @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added. +// @param {module:engine/model/node~Node|Array} nodes List of nodes or a single node to filter. +// @param {module:engine/model/schema~SchemaPath} schemaPath +function removeDisallowedAttributesOnBatch( batch, nodes, schemaPath ) { + const schema = batch.document.schema; + + for ( const node of nodes ) { + for ( const attribute of node.getAttributeKeys() ) { + if ( !schema.check( { name: getNodeSchemaName( node ), attributes: attribute, inside: schemaPath } ) ) { + batch.removeAttribute( node, attribute ); + } + } + + if ( node.is( 'element' ) ) { + removeDisallowedAttributesOnBatch( batch, Array.from( node.getChildren() ), Position.createAt( node ) ); + } + } +} diff --git a/tests/controller/insertcontent.js b/tests/controller/insertcontent.js index b3499740c..95a1a7e95 100644 --- a/tests/controller/insertcontent.js +++ b/tests/controller/insertcontent.js @@ -594,6 +594,7 @@ describe( 'DataController', () => { schema.registerItem( 'paragraph', '$block' ); schema.registerItem( 'heading1', '$block' ); + schema.registerItem( 'child', '$block' ); // Let's use table as an example of content which needs to be filtered out. schema.registerItem( 'table' ); @@ -610,7 +611,11 @@ describe( 'DataController', () => { schema.allow( { name: '$text', inside: 'disallowedWidget' } ); schema.objects.add( 'disallowedWidget' ); + schema.allow( { name: 'child', inside: 'paragraph' } ); + schema.allow( { name: 'child', inside: 'heading1' } ); schema.allow( { name: '$text', attributes: 'b', inside: 'paragraph' } ); + schema.allow( { name: '$text', attributes: [ 'b' ], inside: 'paragraph child' } ); + schema.allow( { name: '$text', attributes: [ 'a', 'b' ], inside: 'heading1 child' } ); } ); it( 'filters out disallowed elements and leaves out the text', () => { @@ -661,6 +666,12 @@ describe( 'DataController', () => { expect( getData( doc ) ).to.equal( 'foox<$text b="1">xx[]' ); } ); + it( 'filters out disallowed attributes from nested child when merging', () => { + setData( doc, 'f[]oo' ); + insertHelper( 'xb<$text a="1" b="1">arx' ); + expect( getData( doc ) ).to.equal( 'fxb<$text b="1">arx[]oo' ); + } ); + it( 'filters out disallowed attributes when autoparagraphing', () => { setData( doc, 'f[]oo' ); insertHelper( 'xxx<$text a="1" b="1">yyy' ); From 9cb061fb7e257a860546af08b761be199eb1f327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Wed, 30 Aug 2017 14:24:23 +0200 Subject: [PATCH 24/32] Improved checking for attributes combinations. --- src/controller/deletecontent.js | 23 ++++++++----- src/controller/insertcontent.js | 55 +++++++++++++++---------------- tests/controller/insertcontent.js | 10 +++--- 3 files changed, 47 insertions(+), 41 deletions(-) diff --git a/src/controller/deletecontent.js b/src/controller/deletecontent.js index 1c6dcf015..e9162a351 100644 --- a/src/controller/deletecontent.js +++ b/src/controller/deletecontent.js @@ -74,7 +74,7 @@ export default function deleteContent( selection, batch, options = {} ) { // // e.g. bold is disallowed for

//

Fo{o

b}ar

->

Fo{}ar

->

Fo{}ar

. - removeDisallowedAttributes( batch, Array.from( startPos.parent.getChildren() ), startPos ); + removeDisallowedAttributes( Array.from( startPos.parent.getChildren() ), startPos, batch ); } selection.setCollapsedAt( startPos ); @@ -228,21 +228,28 @@ function getNodeSchemaName( node ) { // Adds deltas with `removeAttributes` operation for disallowed by schema attributes on given nodes and its children. // -// @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added. -// @param {module:engine/model/node~Node|Array} nodes List of nodes or a single node to filter. +// @param {module:engine/model/node~Node|Array} nodes // @param {module:engine/model/schema~SchemaPath} schemaPath -function removeDisallowedAttributes( batch, nodes, schemaPath ) { +// @param {module:engine/model/batch~Batch} batch +function removeDisallowedAttributes( nodes, schemaPath, batch ) { const schema = batch.document.schema; for ( const node of nodes ) { - for ( const attribute of node.getAttributeKeys() ) { - if ( !schema.check( { name: getNodeSchemaName( node ), attributes: attribute, inside: schemaPath } ) ) { - batch.removeAttribute( node, attribute ); + const name = getNodeSchemaName( node ); + + // When node with attributes is not allowed in current position. + if ( !schema.check( { name, inside: schemaPath, attributes: Array.from( node.getAttributeKeys() ) } ) ) { + // Let's remove attributes one by one. + // This should be improved to check all combination of attributes. + for ( const attribute of node.getAttributeKeys() ) { + if ( !schema.check( { name, attributes: attribute, inside: schemaPath } ) ) { + batch.removeAttribute( node, attribute ); + } } } if ( node.is( 'element' ) ) { - removeDisallowedAttributes( batch, Array.from( node.getChildren() ), Position.createAt( node ) ); + removeDisallowedAttributes( Array.from( node.getChildren() ), Position.createAt( node ), batch ); } } } diff --git a/src/controller/insertcontent.js b/src/controller/insertcontent.js index b8329ce4f..9424e92bf 100644 --- a/src/controller/insertcontent.js +++ b/src/controller/insertcontent.js @@ -229,7 +229,7 @@ class Insertion { // If the node is a text and bare text is allowed in current position it means that the node // contains disallowed attributes and we have to remove them. else if ( this.schema.check( { name: '$text', inside: this.position } ) ) { - removeDisallowedAttributes( this.schema, [ node ], this.position ); + removeDisallowedAttributes( [ node ], this.position, this.schema ); this._handleNode( node, context ); } // If text is not allowed, try autoparagraphing. @@ -290,7 +290,7 @@ class Insertion { // We need to check and strip disallowed attributes in all nested nodes because after merge // some attributes could end up in a path where are disallowed. - removeDisallowedAttributesOnBatch( this.batch, Array.from( this.position.parent.getChildren() ), position ); + removeDisallowedAttributes( Array.from( position.parent.getChildren() ), position, this.schema, this.batch ); this.position = Position.createFromPosition( position ); position.detach(); @@ -317,7 +317,7 @@ class Insertion { // We need to check and strip disallowed attributes in all nested nodes because after merge // some attributes could end up in a place where are disallowed. - removeDisallowedAttributesOnBatch( this.batch, Array.from( this.position.parent.getChildren() ), position ); + removeDisallowedAttributes( Array.from( position.parent.getChildren() ), position, this.schema, this.batch ); this.position = Position.createFromPosition( position ); position.detach(); @@ -343,7 +343,7 @@ class Insertion { // When node is a text and is disallowed by schema it means that contains disallowed attributes // and we need to remove them. if ( node.is( 'text' ) && !this._checkIsAllowed( node, [ paragraph ] ) ) { - removeDisallowedAttributes( this.schema, [ node ], [ paragraph ] ); + removeDisallowedAttributes( [ node ], [ paragraph ], this.schema ); } if ( this._checkIsAllowed( node, [ paragraph ] ) ) { @@ -447,38 +447,35 @@ function getNodeSchemaName( node ) { return node.is( 'text' ) ? '$text' : node.name; } -// Removes disallowed by schema attributes from given nodes. +// Removes disallowed by schema attributes from given nodes. When batch parameter is provided then +// attributes will be removed by adding deltas with `removeAttributes` operation to this batch +// otherwise attributes will be removed directly from provided nodes. // -// @param {module:engine/model/schema~Schema} schema -// @param {Array} nodes List of nodes to filter. -// @param {module:engine/model/schema~SchemaPath} schemaPath -function removeDisallowedAttributes( schema, nodes, schemaPath ) { - for ( const node of nodes ) { - for ( const attribute of node.getAttributeKeys() ) { - if ( !schema.check( { name: getNodeSchemaName( node ), attributes: attribute, inside: schemaPath } ) ) { - node.removeAttribute( attribute ); - } - } - } -} - -// Adds deltas with `removeAttributes` operation for disallowed by schema attributes on given nodes and its children. -// -// @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added. -// @param {module:engine/model/node~Node|Array} nodes List of nodes or a single node to filter. +// @param {Array} nodes // @param {module:engine/model/schema~SchemaPath} schemaPath -function removeDisallowedAttributesOnBatch( batch, nodes, schemaPath ) { - const schema = batch.document.schema; - +// @param {module:engine/model/schema~Schema} schema +// @param {module:engine/model/batch~Batch} [batch] +function removeDisallowedAttributes( nodes, schemaPath, schema, batch ) { for ( const node of nodes ) { - for ( const attribute of node.getAttributeKeys() ) { - if ( !schema.check( { name: getNodeSchemaName( node ), attributes: attribute, inside: schemaPath } ) ) { - batch.removeAttribute( node, attribute ); + const name = getNodeSchemaName( node ); + + // When node with attributes is not allowed in current position. + if ( !schema.check( { name, inside: schemaPath, attributes: Array.from( node.getAttributeKeys() ) } ) ) { + // Let's remove attributes one by one. + // This should be improved to check all combination of attributes. + for ( const attribute of node.getAttributeKeys() ) { + if ( !schema.check( { name, inside: schemaPath, attributes: attribute } ) ) { + if ( batch ) { + batch.removeAttribute( node, attribute ); + } else { + node.removeAttribute( attribute ); + } + } } } if ( node.is( 'element' ) ) { - removeDisallowedAttributesOnBatch( batch, Array.from( node.getChildren() ), Position.createAt( node ) ); + removeDisallowedAttributes( Array.from( node.getChildren() ), Position.createAt( node ), schema, batch ); } } } diff --git a/tests/controller/insertcontent.js b/tests/controller/insertcontent.js index 95a1a7e95..439ced166 100644 --- a/tests/controller/insertcontent.js +++ b/tests/controller/insertcontent.js @@ -604,6 +604,7 @@ describe( 'DataController', () => { schema.allow( { name: 'table', inside: '$clipboardHolder' } ); schema.allow( { name: 'td', inside: '$clipboardHolder' } ); schema.allow( { name: 'td', inside: 'table' } ); + schema.allow( { name: 'child', inside: 'td' } ); schema.allow( { name: '$block', inside: 'td' } ); schema.allow( { name: '$text', inside: 'td' } ); @@ -616,6 +617,7 @@ describe( 'DataController', () => { schema.allow( { name: '$text', attributes: 'b', inside: 'paragraph' } ); schema.allow( { name: '$text', attributes: [ 'b' ], inside: 'paragraph child' } ); schema.allow( { name: '$text', attributes: [ 'a', 'b' ], inside: 'heading1 child' } ); + schema.allow( { name: '$text', attributes: [ 'a', 'b' ], inside: 'td child' } ); } ); it( 'filters out disallowed elements and leaves out the text', () => { @@ -648,25 +650,25 @@ describe( 'DataController', () => { expect( getData( doc ) ).to.equal( 'fx<$text b="1">xxyyy[]oo' ); } ); - it( 'filters out disallowed attributes when merging #1', () => { + it( 'filters out disallowed attributes after merge #1', () => { setData( doc, '[]foo' ); insertHelper( 'x<$text a="1" b="1">xx' ); expect( getData( doc ) ).to.equal( 'x<$text b="1">xx[]foo' ); } ); - it( 'filters out disallowed attributes when merging #2', () => { + it( 'filters out disallowed attributes after merge #2', () => { setData( doc, 'f[]oo' ); insertHelper( 'x<$text a="1" b="1">xx' ); expect( getData( doc ) ).to.equal( 'fx<$text b="1">xx[]oo' ); } ); - it( 'filters out disallowed attributes when merging #3', () => { + it( 'filters out disallowed attributes after merge #3', () => { setData( doc, 'foo[]' ); insertHelper( 'x<$text a="1" b="1">xx' ); expect( getData( doc ) ).to.equal( 'foox<$text b="1">xx[]' ); } ); - it( 'filters out disallowed attributes from nested child when merging', () => { + it( 'filters out disallowed attributes from nested elements after merge', () => { setData( doc, 'f[]oo' ); insertHelper( 'xb<$text a="1" b="1">arx' ); expect( getData( doc ) ).to.equal( 'fxb<$text b="1">arx[]oo' ); From 5baf3e3293a80df0a226a1a7c794dd4646bd3a1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Wed, 30 Aug 2017 17:43:45 +0200 Subject: [PATCH 25/32] Limit filtering only to a merged node. --- src/controller/insertcontent.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controller/insertcontent.js b/src/controller/insertcontent.js index 9424e92bf..142870f57 100644 --- a/src/controller/insertcontent.js +++ b/src/controller/insertcontent.js @@ -290,7 +290,8 @@ class Insertion { // We need to check and strip disallowed attributes in all nested nodes because after merge // some attributes could end up in a path where are disallowed. - removeDisallowedAttributes( Array.from( position.parent.getChildren() ), position, this.schema, this.batch ); + const parent = position.nodeBefore; + removeDisallowedAttributes( Array.from( parent.getChildren() ), Position.createAt( parent ), this.schema, this.batch ); this.position = Position.createFromPosition( position ); position.detach(); From ea3bc70712d1c0f6098f547bf06d69d4723d91ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Wed, 30 Aug 2017 21:37:59 +0200 Subject: [PATCH 26/32] Added filtering of nested elements after insert. --- src/controller/insertcontent.js | 6 ++++++ tests/controller/insertcontent.js | 35 ++++++++++++++++++------------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/controller/insertcontent.js b/src/controller/insertcontent.js index 142870f57..3a1a149d4 100644 --- a/src/controller/insertcontent.js +++ b/src/controller/insertcontent.js @@ -326,6 +326,12 @@ class Insertion { mergePosLeft.detach(); mergePosRight.detach(); + + // When there was no merge we need to check and strip disallowed attributes in all nested nodes of + // just inserted node because some attributes could end up in a place where are disallowed. + if ( !mergeLeft && !mergeRight ) { + removeDisallowedAttributes( Array.from( node.getChildren() ), Position.createAt( node ), this.schema, this.batch ); + } } /** diff --git a/tests/controller/insertcontent.js b/tests/controller/insertcontent.js index 439ced166..89535472e 100644 --- a/tests/controller/insertcontent.js +++ b/tests/controller/insertcontent.js @@ -594,9 +594,8 @@ describe( 'DataController', () => { schema.registerItem( 'paragraph', '$block' ); schema.registerItem( 'heading1', '$block' ); - schema.registerItem( 'child', '$block' ); + schema.registerItem( 'element', '$block' ); - // Let's use table as an example of content which needs to be filtered out. schema.registerItem( 'table' ); schema.registerItem( 'td' ); schema.registerItem( 'disallowedWidget' ); @@ -604,20 +603,22 @@ describe( 'DataController', () => { schema.allow( { name: 'table', inside: '$clipboardHolder' } ); schema.allow( { name: 'td', inside: '$clipboardHolder' } ); schema.allow( { name: 'td', inside: 'table' } ); - schema.allow( { name: 'child', inside: 'td' } ); + schema.allow( { name: 'element', inside: 'td' } ); schema.allow( { name: '$block', inside: 'td' } ); schema.allow( { name: '$text', inside: 'td' } ); + schema.allow( { name: 'table', inside: 'element' } ); schema.allow( { name: 'disallowedWidget', inside: '$clipboardHolder' } ); schema.allow( { name: '$text', inside: 'disallowedWidget' } ); schema.objects.add( 'disallowedWidget' ); - schema.allow( { name: 'child', inside: 'paragraph' } ); - schema.allow( { name: 'child', inside: 'heading1' } ); + schema.allow( { name: 'element', inside: 'paragraph' } ); + schema.allow( { name: 'element', inside: 'heading1' } ); schema.allow( { name: '$text', attributes: 'b', inside: 'paragraph' } ); - schema.allow( { name: '$text', attributes: [ 'b' ], inside: 'paragraph child' } ); - schema.allow( { name: '$text', attributes: [ 'a', 'b' ], inside: 'heading1 child' } ); - schema.allow( { name: '$text', attributes: [ 'a', 'b' ], inside: 'td child' } ); + schema.allow( { name: '$text', attributes: [ 'b' ], inside: 'paragraph element' } ); + schema.allow( { name: '$text', attributes: [ 'a', 'b' ], inside: 'heading1 element' } ); + schema.allow( { name: '$text', attributes: [ 'a', 'b' ], inside: 'td element' } ); + schema.allow( { name: '$text', attributes: [ 'b' ], inside: 'element table td' } ); } ); it( 'filters out disallowed elements and leaves out the text', () => { @@ -644,34 +645,40 @@ describe( 'DataController', () => { expect( getData( doc ) ).to.equal( 'fx<$text b="1">xxyyy[]oo' ); } ); + it( 'filters out disallowed attributes when inserting nested elements', () => { + setData( doc, '[]' ); + insertHelper( '
f<$text a="1" b="1" c="1">oo
' ); + expect( getData( doc ) ).to.equal( '
f<$text b="1">oo
[]
' ); + } ); + it( 'filters out disallowed attributes when inserting text in disallowed elements', () => { setData( doc, 'f[]oo' ); insertHelper( '
x<$text a="1" b="1">xxy<$text a="1">yy
' ); expect( getData( doc ) ).to.equal( 'fx<$text b="1">xxyyy[]oo' ); } ); - it( 'filters out disallowed attributes after merge #1', () => { + it( 'filters out disallowed attributes when merging #1', () => { setData( doc, '[]foo' ); insertHelper( 'x<$text a="1" b="1">xx' ); expect( getData( doc ) ).to.equal( 'x<$text b="1">xx[]foo' ); } ); - it( 'filters out disallowed attributes after merge #2', () => { + it( 'filters out disallowed attributes when merging #2', () => { setData( doc, 'f[]oo' ); insertHelper( 'x<$text a="1" b="1">xx' ); expect( getData( doc ) ).to.equal( 'fx<$text b="1">xx[]oo' ); } ); - it( 'filters out disallowed attributes after merge #3', () => { + it( 'filters out disallowed attributes when merging #3', () => { setData( doc, 'foo[]' ); insertHelper( 'x<$text a="1" b="1">xx' ); expect( getData( doc ) ).to.equal( 'foox<$text b="1">xx[]' ); } ); - it( 'filters out disallowed attributes from nested elements after merge', () => { + it( 'filters out disallowed attributes from nested nodes after merge', () => { setData( doc, 'f[]oo' ); - insertHelper( 'xb<$text a="1" b="1">arx' ); - expect( getData( doc ) ).to.equal( 'fxb<$text b="1">arx[]oo' ); + insertHelper( 'xb<$text a="1" b="1">arx' ); + expect( getData( doc ) ).to.equal( 'fxb<$text b="1">arx[]oo' ); } ); it( 'filters out disallowed attributes when autoparagraphing', () => { From 479a4217af90d9a12941fb263f877b1a58a9f143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Wed, 30 Aug 2017 21:38:22 +0200 Subject: [PATCH 27/32] Added manual test for filtering attributes. --- tests/manual/tickets/1088/1.html | 23 +++++++++++++++++++++++ tests/manual/tickets/1088/1.js | 29 +++++++++++++++++++++++++++++ tests/manual/tickets/1088/1.md | 20 ++++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 tests/manual/tickets/1088/1.html create mode 100644 tests/manual/tickets/1088/1.js create mode 100644 tests/manual/tickets/1088/1.md diff --git a/tests/manual/tickets/1088/1.html b/tests/manual/tickets/1088/1.html new file mode 100644 index 000000000..e2e064e77 --- /dev/null +++ b/tests/manual/tickets/1088/1.html @@ -0,0 +1,23 @@ +
diff --git a/tests/manual/tickets/1088/1.js b/tests/manual/tickets/1088/1.js new file mode 100644 index 000000000..0195e6a43 --- /dev/null +++ b/tests/manual/tickets/1088/1.js @@ -0,0 +1,29 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* globals console, window, document */ + +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import ArticlePreset from '@ckeditor/ckeditor5-presets/src/article'; + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ ArticlePreset ], + toolbar: [ 'headings', 'undo', 'redo' ] + } ) + .then( editor => { + window.editor = editor; + + const schema = editor.document.schema; + + schema.disallow( { name: '$text', attributes: [ 'linkHref' ], inside: 'heading1' } ); + schema.disallow( { name: '$text', attributes: [ 'italic' ], inside: 'heading2' } ); + schema.disallow( { name: '$text', attributes: [ 'italic', 'linkHref' ], inside: 'heading3' } ); + schema.disallow( { name: '$text', attributes: [ 'bold' ], inside: 'paragraph' } ); + schema.disallow( { name: '$text', attributes: [ 'bold', 'linkHref' ], inside: 'blockQuote listItem' } ); + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/tests/manual/tickets/1088/1.md b/tests/manual/tickets/1088/1.md new file mode 100644 index 000000000..747c17cfe --- /dev/null +++ b/tests/manual/tickets/1088/1.md @@ -0,0 +1,20 @@ +## Stripping disallowed attributes by `(insert|delete)Content` [#1088](https://github.com/ckeditor/ckeditor5-engine/issues/1088) + +### Simple scenario. + +1. Copy a paragraph with italic and link. +2. Paste it to the Heading 1. Inserted text should have only an italic style. +3. Paste it to the Heading 2. Inserted text should have only a link. +4. Paste it to the Heading 3. Inserted text should be fully stripped. + +### Nested nodes. + +1. Copy a list item with bold and link. +2. Paste it into the empty block (just under the Heading 2). Inserted list item should have a bold style a link. +2. Paste it into the empty block in BlockQuote. Inserted list item should be fully stripped. + +### Auto paragraphing. + +1. Copy a text with bold. +2. Select all content in the editor. +3. Paste copied text. Inserted content should be a paragraph and should be stripped from bold. From cd6cd0bdde1555ce137fe25128d505431d3dd9c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Wed, 30 Aug 2017 21:55:09 +0200 Subject: [PATCH 28/32] Changed test name. --- tests/controller/insertcontent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/controller/insertcontent.js b/tests/controller/insertcontent.js index 89535472e..06ad07961 100644 --- a/tests/controller/insertcontent.js +++ b/tests/controller/insertcontent.js @@ -675,7 +675,7 @@ describe( 'DataController', () => { expect( getData( doc ) ).to.equal( 'foox<$text b="1">xx[]' ); } ); - it( 'filters out disallowed attributes from nested nodes after merge', () => { + it( 'filters out disallowed attributes from nested nodes when merging', () => { setData( doc, 'f[]oo' ); insertHelper( 'xb<$text a="1" b="1">arx' ); expect( getData( doc ) ).to.equal( 'fxb<$text b="1">arx[]oo' ); From 5bb12e2f74cb0a3f50e8a24617416b4df69a235f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 1 Sep 2017 09:58:18 +0200 Subject: [PATCH 29/32] Remove unnecessary transformation to array. --- src/controller/deletecontent.js | 4 ++-- src/controller/insertcontent.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/controller/deletecontent.js b/src/controller/deletecontent.js index e9162a351..559645941 100644 --- a/src/controller/deletecontent.js +++ b/src/controller/deletecontent.js @@ -74,7 +74,7 @@ export default function deleteContent( selection, batch, options = {} ) { // // e.g. bold is disallowed for

//

Fo{o

b}ar

->

Fo{}ar

->

Fo{}ar

. - removeDisallowedAttributes( Array.from( startPos.parent.getChildren() ), startPos, batch ); + removeDisallowedAttributes( startPos.parent.getChildren(), startPos, batch ); } selection.setCollapsedAt( startPos ); @@ -249,7 +249,7 @@ function removeDisallowedAttributes( nodes, schemaPath, batch ) { } if ( node.is( 'element' ) ) { - removeDisallowedAttributes( Array.from( node.getChildren() ), Position.createAt( node ), batch ); + removeDisallowedAttributes( node.getChildren(), Position.createAt( node ), batch ); } } } diff --git a/src/controller/insertcontent.js b/src/controller/insertcontent.js index 3a1a149d4..6d6db7913 100644 --- a/src/controller/insertcontent.js +++ b/src/controller/insertcontent.js @@ -291,7 +291,7 @@ class Insertion { // We need to check and strip disallowed attributes in all nested nodes because after merge // some attributes could end up in a path where are disallowed. const parent = position.nodeBefore; - removeDisallowedAttributes( Array.from( parent.getChildren() ), Position.createAt( parent ), this.schema, this.batch ); + removeDisallowedAttributes( parent.getChildren(), Position.createAt( parent ), this.schema, this.batch ); this.position = Position.createFromPosition( position ); position.detach(); @@ -318,7 +318,7 @@ class Insertion { // We need to check and strip disallowed attributes in all nested nodes because after merge // some attributes could end up in a place where are disallowed. - removeDisallowedAttributes( Array.from( position.parent.getChildren() ), position, this.schema, this.batch ); + removeDisallowedAttributes( position.parent.getChildren(), position, this.schema, this.batch ); this.position = Position.createFromPosition( position ); position.detach(); @@ -330,7 +330,7 @@ class Insertion { // When there was no merge we need to check and strip disallowed attributes in all nested nodes of // just inserted node because some attributes could end up in a place where are disallowed. if ( !mergeLeft && !mergeRight ) { - removeDisallowedAttributes( Array.from( node.getChildren() ), Position.createAt( node ), this.schema, this.batch ); + removeDisallowedAttributes( node.getChildren(), Position.createAt( node ), this.schema, this.batch ); } } @@ -482,7 +482,7 @@ function removeDisallowedAttributes( nodes, schemaPath, schema, batch ) { } if ( node.is( 'element' ) ) { - removeDisallowedAttributes( Array.from( node.getChildren() ), Position.createAt( node ), schema, batch ); + removeDisallowedAttributes( node.getChildren(), Position.createAt( node ), schema, batch ); } } } From c572b110797a4216e1c2377d39576e212a6c3487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 1 Sep 2017 10:00:42 +0200 Subject: [PATCH 30/32] Changed docs. --- src/controller/deletecontent.js | 14 +++++++------- src/controller/insertcontent.js | 18 +++++++++--------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/controller/deletecontent.js b/src/controller/deletecontent.js index 559645941..7a9a91e32 100644 --- a/src/controller/deletecontent.js +++ b/src/controller/deletecontent.js @@ -226,23 +226,23 @@ function getNodeSchemaName( node ) { return node.is( 'text' ) ? '$text' : node.name; } -// Adds deltas with `removeAttributes` operation for disallowed by schema attributes on given nodes and its children. +// Creates AttributeDeltas that removes attributes that are disallowed by schema on given node and its children. // -// @param {module:engine/model/node~Node|Array} nodes -// @param {module:engine/model/schema~SchemaPath} schemaPath -// @param {module:engine/model/batch~Batch} batch -function removeDisallowedAttributes( nodes, schemaPath, batch ) { +// @param {Array} nodes Nodes that will be filtered. +// @param {module:engine/model/schema~SchemaPath} inside Path inside which schema will be checked. +// @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added. +function removeDisallowedAttributes( nodes, inside, batch ) { const schema = batch.document.schema; for ( const node of nodes ) { const name = getNodeSchemaName( node ); // When node with attributes is not allowed in current position. - if ( !schema.check( { name, inside: schemaPath, attributes: Array.from( node.getAttributeKeys() ) } ) ) { + if ( !schema.check( { name, inside, attributes: Array.from( node.getAttributeKeys() ) } ) ) { // Let's remove attributes one by one. // This should be improved to check all combination of attributes. for ( const attribute of node.getAttributeKeys() ) { - if ( !schema.check( { name, attributes: attribute, inside: schemaPath } ) ) { + if ( !schema.check( { name, inside, attributes: attribute } ) ) { batch.removeAttribute( node, attribute ); } } diff --git a/src/controller/insertcontent.js b/src/controller/insertcontent.js index 6d6db7913..c8e24150e 100644 --- a/src/controller/insertcontent.js +++ b/src/controller/insertcontent.js @@ -455,23 +455,23 @@ function getNodeSchemaName( node ) { } // Removes disallowed by schema attributes from given nodes. When batch parameter is provided then -// attributes will be removed by adding deltas with `removeAttributes` operation to this batch -// otherwise attributes will be removed directly from provided nodes. +// attributes will be removed by creating AttributeDeltas otherwise attributes will be removed +// directly from provided nodes. // -// @param {Array} nodes -// @param {module:engine/model/schema~SchemaPath} schemaPath -// @param {module:engine/model/schema~Schema} schema -// @param {module:engine/model/batch~Batch} [batch] -function removeDisallowedAttributes( nodes, schemaPath, schema, batch ) { +// @param {Array} nodes Nodes that will be filtered. +// @param {module:engine/model/schema~SchemaPath} inside Path inside which schema will be checked. +// @param {module:engine/model/schema~Schema} schema Schema instance uses for element validation. +// @param {module:engine/model/batch~Batch} [batch] Batch to which the deltas will be added. +function removeDisallowedAttributes( nodes, inside, schema, batch ) { for ( const node of nodes ) { const name = getNodeSchemaName( node ); // When node with attributes is not allowed in current position. - if ( !schema.check( { name, inside: schemaPath, attributes: Array.from( node.getAttributeKeys() ) } ) ) { + if ( !schema.check( { name, inside, attributes: Array.from( node.getAttributeKeys() ) } ) ) { // Let's remove attributes one by one. // This should be improved to check all combination of attributes. for ( const attribute of node.getAttributeKeys() ) { - if ( !schema.check( { name, inside: schemaPath, attributes: attribute } ) ) { + if ( !schema.check( { name, inside, attributes: attribute } ) ) { if ( batch ) { batch.removeAttribute( node, attribute ); } else { From 109db2e216d380e17c7ac68495b019e186281c5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 1 Sep 2017 12:30:08 +0200 Subject: [PATCH 31/32] Imroved manual test for filtering nodes. --- tests/manual/tickets/1088/1.html | 9 +++++++-- tests/manual/tickets/1088/1.js | 15 +++++++++------ tests/manual/tickets/1088/1.md | 20 +++++++++++++++----- tests/manual/tickets/1088/sample.jpg | Bin 0 -> 114298 bytes 4 files changed, 31 insertions(+), 13 deletions(-) create mode 100644 tests/manual/tickets/1088/sample.jpg diff --git a/tests/manual/tickets/1088/1.html b/tests/manual/tickets/1088/1.html index e2e064e77..647ee42c9 100644 --- a/tests/manual/tickets/1088/1.html +++ b/tests/manual/tickets/1088/1.html @@ -1,6 +1,6 @@
-

Heading 1 (disallowed: link)

+

Heading 1 (disallowed: italic, link)

This is a paragraph

Heading 2 (disallowed: italic)

@@ -8,7 +8,6 @@

Heading 2 (disallowed: italic)

This is a paragraph in a blockQuote

-

Heading 3 (disallowed: italic, link)

@@ -18,6 +17,12 @@

Heading 3 (disallowed: italic, link)

  • List item with link and bold
  • +

    Heading 3 with bold

    +
    Just a text with bold
    + +
    + + Sample image
    diff --git a/tests/manual/tickets/1088/1.js b/tests/manual/tickets/1088/1.js index 0195e6a43..3f333da74 100644 --- a/tests/manual/tickets/1088/1.js +++ b/tests/manual/tickets/1088/1.js @@ -6,23 +6,26 @@ /* globals console, window, document */ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; -import ArticlePreset from '@ckeditor/ckeditor5-presets/src/article'; +import ArticlePresets from '@ckeditor/ckeditor5-presets/src/article'; ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ ArticlePreset ], - toolbar: [ 'headings', 'undo', 'redo' ] + plugins: [ ArticlePresets ], + toolbar: [ 'headings', 'undo', 'redo' ], + image: { + toolbar: [ 'imageTextAlternative' ] + } } ) .then( editor => { window.editor = editor; const schema = editor.document.schema; - schema.disallow( { name: '$text', attributes: [ 'linkHref' ], inside: 'heading1' } ); + schema.disallow( { name: '$text', attributes: [ 'linkHref', 'italic' ], inside: 'heading1' } ); schema.disallow( { name: '$text', attributes: [ 'italic' ], inside: 'heading2' } ); - schema.disallow( { name: '$text', attributes: [ 'italic', 'linkHref' ], inside: 'heading3' } ); + schema.disallow( { name: '$text', attributes: [ 'linkHref' ], inside: 'blockQuote listItem' } ); schema.disallow( { name: '$text', attributes: [ 'bold' ], inside: 'paragraph' } ); - schema.disallow( { name: '$text', attributes: [ 'bold', 'linkHref' ], inside: 'blockQuote listItem' } ); + schema.disallow( { name: 'heading3', inside: '$root' } ); } ) .catch( err => { console.error( err.stack ); diff --git a/tests/manual/tickets/1088/1.md b/tests/manual/tickets/1088/1.md index 747c17cfe..341b1f9c5 100644 --- a/tests/manual/tickets/1088/1.md +++ b/tests/manual/tickets/1088/1.md @@ -3,18 +3,28 @@ ### Simple scenario. 1. Copy a paragraph with italic and link. -2. Paste it to the Heading 1. Inserted text should have only an italic style. -3. Paste it to the Heading 2. Inserted text should have only a link. -4. Paste it to the Heading 3. Inserted text should be fully stripped. +2. Paste it to the Heading 1. Inserted text should be not stripped. +3. Paste it to the Heading 2. Inserted text should be a link only. + +### Simple scenario (element). + +1. Copy image. +2. Paste it to the editor. Image should be inserted with an alternative text "Sample image". ### Nested nodes. 1. Copy a list item with bold and link. -2. Paste it into the empty block (just under the Heading 2). Inserted list item should have a bold style a link. -2. Paste it into the empty block in BlockQuote. Inserted list item should be fully stripped. +2. Paste it into the empty block (directly to the root) . Inserted list item should be a bold link. +2. Paste it into the empty block in BlockQuote. Inserted list item should be a bold only. ### Auto paragraphing. 1. Copy a text with bold. 2. Select all content in the editor. 3. Paste copied text. Inserted content should be a paragraph and should be stripped from bold. + +### Auto paragraphing (disallowed block). + +1. Copy Heading 3 with bold +2. Select all content in the editor. +3. Paste copied text. Inserted content should be a paragraph and should be stripped from bold. diff --git a/tests/manual/tickets/1088/sample.jpg b/tests/manual/tickets/1088/sample.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b77d07e7bfff7fed1563fe2cd190c46ade02905f GIT binary patch literal 114298 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 literal 0 HcmV?d00001 From e592d2d59f904ead6d2ca0dcf73518835740f1e6 Mon Sep 17 00:00:00 2001 From: Szymon Cofalik Date: Fri, 1 Sep 2017 15:12:19 +0200 Subject: [PATCH 32/32] Tests: Improved manual test. --- tests/manual/tickets/1088/1.html | 2 +- tests/manual/tickets/1088/1.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/manual/tickets/1088/1.html b/tests/manual/tickets/1088/1.html index 647ee42c9..efcb091f2 100644 --- a/tests/manual/tickets/1088/1.html +++ b/tests/manual/tickets/1088/1.html @@ -17,7 +17,7 @@

    Heading 2 (disallowed: italic)

  • List item with link and bold
  • -

    Heading 3 with bold

    +

    Heading 3 with bold and italic

    Just a text with bold
    diff --git a/tests/manual/tickets/1088/1.md b/tests/manual/tickets/1088/1.md index 341b1f9c5..0acf53214 100644 --- a/tests/manual/tickets/1088/1.md +++ b/tests/manual/tickets/1088/1.md @@ -3,8 +3,9 @@ ### Simple scenario. 1. Copy a paragraph with italic and link. -2. Paste it to the Heading 1. Inserted text should be not stripped. +2. Paste it to the Heading 1. Inserted text should be stripped 3. Paste it to the Heading 2. Inserted text should be a link only. +4. Paste it to paragraph. Inserted text should not be stripped. ### Simple scenario (element). @@ -25,6 +26,6 @@ ### Auto paragraphing (disallowed block). -1. Copy Heading 3 with bold +1. Copy Heading 3 with bold and italic. 2. Select all content in the editor. 3. Paste copied text. Inserted content should be a paragraph and should be stripped from bold.