Browse Source

Merge branch 'master' into t/1209

Maciej Bukowski 7 years ago
parent
commit
3c182a24c6

+ 53 - 0
packages/ckeditor5-engine/src/model/schema.js

@@ -12,6 +12,8 @@ import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 
 import Range from './range';
+import Position from './position';
+import Element from './element';
 
 /**
  * The model's schema. It defines allowed and disallowed structures of nodes as well as nodes' attributes.
@@ -411,6 +413,57 @@ export default class Schema {
 	}
 
 	/**
+	 * Checks whether the given element (`elementToMerge`) can be merged with the specified base element (`positionOrBaseElement`).
+	 *
+	 * In other words – whether `elementToMerge`'s children {@link #checkChild are allowed} in the `positionOrBaseElement`.
+	 *
+	 * This check ensures that elements merged with {@link module:engine/model/writer~Writer#merge `Writer#merge()`}
+	 * will be valid.
+	 *
+	 * Instead of elements, you can pass the instance of {@link module:engine/model/position~Position} class as the `positionOrBaseElement`.
+	 * It means that the elements before and after the position will be checked whether they can be merged.
+	 *
+	 * @param {module:engine/model/position~Position|module:engine/model/Element~Element} positionOrBaseElement The position or base
+	 * element to which the `elementToMerge` will be merged.
+	 * @param {module:engine/model/Element~Element} elementToMerge The element to merge. Required if `positionOrBaseElement` is a element.
+	 * @returns {Boolean}
+	 */
+	checkMerge( positionOrBaseElement, elementToMerge = null ) {
+		if ( positionOrBaseElement instanceof Position ) {
+			const nodeBefore = positionOrBaseElement.nodeBefore;
+			const nodeAfter = positionOrBaseElement.nodeAfter;
+
+			if ( !( nodeBefore instanceof Element ) ) {
+				/**
+				 * Node before merge position must be an element.
+				 *
+				 * @error schema-check-merge-no-element-before
+				 */
+				throw new CKEditorError( 'schema-check-merge-no-element-before: Node before merge position must be an element.' );
+			}
+
+			if ( !( nodeAfter instanceof Element ) ) {
+				/**
+				 * Node after merge position must be an element.
+				 *
+				 * @error schema-check-merge-no-element-after
+				 */
+				throw new CKEditorError( 'schema-check-merge-no-element-after: Node after merge position must be an element.' );
+			}
+
+			return this.checkMerge( nodeBefore, nodeAfter );
+		}
+
+		for ( const child of elementToMerge.getChildren() ) {
+			if ( !this.checkChild( positionOrBaseElement, child ) ) {
+				return false;
+			}
+		}
+
+		return true;
+	}
+
+	/**
 	 * Allows registering a callback to the {@link #checkChild} method calls.
 	 *
 	 * Callbacks allow you to implement rules which are not otherwise possible to achieve

+ 163 - 0
packages/ckeditor5-engine/tests/model/schema.js

@@ -641,6 +641,169 @@ describe( 'Schema', () => {
 		} );
 	} );
 
+	describe( 'checkMerge()', () => {
+		beforeEach( () => {
+			schema.register( '$root' );
+			schema.register( '$block', {
+				allowIn: '$root',
+				isBlock: true
+			} );
+			schema.register( '$text', {
+				allowIn: '$block'
+			} );
+			schema.register( 'paragraph', {
+				inheritAllFrom: '$block'
+			} );
+			schema.register( 'listItem', {
+				inheritAllFrom: '$block'
+			} );
+			schema.register( 'blockQuote', {
+				allowWhere: '$block',
+				allowContentOf: '$root'
+			} );
+		} );
+
+		it( 'returns false if a block cannot be merged with other block (disallowed element is the first child)', () => {
+			const paragraph = new Element( 'paragraph', null, [
+				new Text( 'xyz' )
+			] );
+			const blockQuote = new Element( 'blockQuote', null, [ paragraph ] );
+			const listItem = new Element( 'listItem' );
+
+			expect( schema.checkMerge( listItem, blockQuote ) ).to.be.false;
+		} );
+
+		it( 'returns false if a block cannot be merged with other block (disallowed element is not the first child)', () => {
+			const paragraph = new Element( 'paragraph', null, [
+				new Text( 'foo' )
+			] );
+			const blockQuote = new Element( 'blockQuote', null, [
+				new Text( 'bar', { bold: true } ),
+				new Text( 'xyz' ),
+				paragraph
+			] );
+			const listItem = new Element( 'listItem' );
+
+			expect( schema.checkMerge( listItem, blockQuote ) ).to.be.false;
+		} );
+
+		it( 'returns true if a block can be merged with other block', () => {
+			const listItem = new Element( 'listItem' );
+			const listItemToMerge = new Element( 'listItem', null, [
+				new Text( 'xyz' )
+			] );
+
+			expect( schema.checkMerge( listItem, listItemToMerge ) ).to.be.true;
+		} );
+
+		it( 'return true if two elements between the position can be merged', () => {
+			const listItem = new Element( 'listItem', null, [
+				new Text( 'foo' )
+			] );
+			const listItemToMerge = new Element( 'listItem', null, [
+				new Text( 'bar' )
+			] );
+
+			// eslint-disable-next-line no-new
+			new Element( '$root', null, [
+				listItem, listItemToMerge
+			] );
+			const position = Position.createAfter( listItem );
+
+			expect( schema.checkMerge( position ) ).to.be.true;
+		} );
+
+		it( 'throws an error if there is no element before the position', () => {
+			const listItem = new Element( 'listItem', null, [
+				new Text( 'foo' )
+			] );
+
+			// eslint-disable-next-line no-new
+			new Element( '$root', null, [
+				listItem
+			] );
+
+			const position = Position.createBefore( listItem );
+
+			expect( () => {
+				expect( schema.checkMerge( position ) );
+			} ).to.throw( CKEditorError, /^schema-check-merge-no-element-before:/ );
+		} );
+
+		it( 'throws an error if the node before the position is not the element', () => {
+			const listItem = new Element( 'listItem', null, [
+				new Text( 'foo' )
+			] );
+
+			// eslint-disable-next-line no-new
+			new Element( '$root', null, [
+				new Text( 'bar' ),
+				listItem
+			] );
+
+			const position = Position.createBefore( listItem );
+
+			expect( () => {
+				expect( schema.checkMerge( position ) );
+			} ).to.throw( CKEditorError, /^schema-check-merge-no-element-before:/ );
+		} );
+
+		it( 'throws an error if there is no element after the position', () => {
+			const listItem = new Element( 'listItem', null, [
+				new Text( 'foo' )
+			] );
+
+			// eslint-disable-next-line no-new
+			new Element( '$root', null, [
+				listItem
+			] );
+
+			const position = Position.createAfter( listItem );
+
+			expect( () => {
+				expect( schema.checkMerge( position ) );
+			} ).to.throw( CKEditorError, /^schema-check-merge-no-element-after:/ );
+		} );
+
+		it( 'throws an error if the node after the position is not the element', () => {
+			const listItem = new Element( 'listItem', null, [
+				new Text( 'foo' )
+			] );
+
+			// eslint-disable-next-line no-new
+			new Element( '$root', null, [
+				listItem,
+				new Text( 'bar' )
+			] );
+
+			const position = Position.createBefore( listItem );
+
+			expect( () => {
+				expect( schema.checkMerge( position ) );
+			} ).to.throw( CKEditorError, /^schema-check-merge-no-element-before:/ );
+		} );
+
+		// This is an invalid case by definition – the baseElement should not contain disallowed elements
+		// in the first place. However, the check is focused on the elementToMerge's children so let's make sure
+		// that only them counts.
+		it( 'returns true if element to merge contains a valid content but base element contains disallowed elements', () => {
+			const listItem = new Element( 'listItem', null, [
+				new Text( 'foo' ),
+				new Element( 'paragraph', null, [
+					new Text( 'bar' )
+				] )
+			] );
+			const listItemToMerge = new Element( 'listItem', null, [
+				new Text( 'xyz' )
+			] );
+
+			expect( schema.checkMerge( listItem, listItemToMerge ) ).to.be.true;
+		} );
+
+		// The checkMerge() method should also check whether all ancestors of elementToMerge are allowed in their new
+		// context (now, we check only immediate children), but for now we ignore these cases.
+	} );
+
 	describe( 'getLimitElement()', () => {
 		let model, doc, root;