Преглед изворни кода

Merge pull request #302 from ckeditor/t/300

T/300 Schema
Piotr Jasiun пре 9 година
родитељ
комит
81bbdb9a3b

+ 190 - 70
packages/ckeditor5-engine/src/treemodel/schema.js

@@ -5,7 +5,11 @@
 
 'use strict';
 
+import Position from './position.js';
+import Element from './element.js';
 import clone from '../../utils/lib/lodash/clone.js';
+import isArray from '../../utils/lib/lodash/isArray.js';
+import isString from '../../utils/lib/lodash/isString.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
 
 /**
@@ -46,26 +50,45 @@ export class SchemaItem {
 		 * @member {Array} engine.treeModel.SchemaItem#_disallowed
 		 */
 		this._disallowed = [];
+
+		/**
+		 * Attributes that are required by the entity represented by this item.
+		 *
+		 * @protected
+		 * @member {Array} engine.treeModel.SchemaItem#_requiredAttributes
+		 */
+		this._requiredAttributes = [];
 	}
 
 	/**
 	 * Allows entity, represented by this item, to be in given path.
 	 *
-	 * @param {Array.<String>|String} path Path in which entity is allowed. String with item names separated by spaces may be passed.
-	 * @param {String} [attribute] If set, this path will be used only for entities that have an attribute with this key.
+	 * @param {Array.<String>} path Path in which entity is allowed.
+	 * @param {Array.<String>|String} [attributes] If set, this path will be used only for entities that have attribute(s) with this key.
 	 */
-	allow( path, attribute ) {
-		this._addPath( '_allowed', path, attribute );
+	allow( path, attributes ) {
+		this._addPath( '_allowed', path, attributes );
 	}
 
 	/**
 	 * Disallows entity, represented by this item, to be in given path.
 	 *
-	 * @param {Array.<String>|String} path Path in which entity is disallowed. String with item names separated by spaces may be passed.
-	 * @param {String} [attribute] If set, this path will be used only for entities that have an attribute with this key.
+	 * @param {Array.<String>} path Path in which entity is disallowed.
+	 * @param {Array.<String>|String} [attributes] If set, this path will be used only for entities that have an attribute(s) with this key.
+	 */
+	disallow( path, attributes ) {
+		this._addPath( '_disallowed', path, attributes );
+	}
+
+	/**
+	 * Specifies that the entity, to be valid, requires given attributes set. It is possible to register multiple
+	 * different attributes set. If there are more than one attributes set required, the entity will be valid if
+	 * at least one of them is fulfilled.
+	 *
+	 * @param {Array.<String>} attributes Attributes that has to be set on the entity to make it valid.
 	 */
-	disallow( path, attribute ) {
-		this._addPath( '_disallowed', path, attribute );
+	requireAttributes( attributes ) {
+		this._requiredAttributes.push( attributes );
 	}
 
 	/**
@@ -73,17 +96,23 @@ export class SchemaItem {
 	 *
 	 * @private
 	 * @param {String} member Name of the array member into which the path will be added. Possible values are `_allowed` or `_disallowed`.
-	 * @param {Array.<String>|String} path Path to be added. String with item names separated by spaces may be passed.
-	 * @param {String} [attribute] If set, this path will be used only for entities that have an attribute with this key.
+	 * @param {Array.<String>} path Path to add.
+	 * @param {Array.<String>|String} [attributes] If set, this path will be used only for entities that have attribute(s) with this key.
 	 */
-	_addPath( member, path, attribute ) {
+	_addPath( member, path, attributes ) {
 		if ( typeof path === 'string' ) {
 			path = path.split( ' ' );
 		} else {
 			path = path.slice();
 		}
 
-		this[ member ].push( { path, attribute } );
+		if ( !isArray( attributes ) ) {
+			attributes = [ attributes ];
+		}
+
+		for ( let attribute of attributes ) {
+			this[ member ].push( { path, attribute } );
+		}
 	}
 
 	/**
@@ -100,20 +129,49 @@ export class SchemaItem {
 
 		for ( let item of source ) {
 			if ( item.attribute === attribute ) {
-				paths.push( item.path.slice() );
+				paths.push( item.path );
 			}
 		}
 
 		return paths;
 	}
 
+	/**
+	 * Checks whether given set of attributes fulfills required attributes of this item.
+	 *
+	 * @protected
+	 * @see engine.treeModel.SchemaItem#requireAttributes
+	 * @param {Array.<String>} attributesToCheck Attributes to check.
+	 * @returns {Boolean} `true` if given set or attributes fulfills required attributes, `false` otherwise.
+	 */
+	_checkRequiredAttributes( attributesToCheck ) {
+		let found = true;
+
+		for ( let attributeSet of this._requiredAttributes ) {
+			found = true;
+
+			for ( let attribute of attributeSet ) {
+				if ( attributesToCheck.indexOf( attribute ) == -1 ) {
+					found = false;
+					break;
+				}
+			}
+
+			if ( found ) {
+				break;
+			}
+		}
+
+		return found;
+	}
+
 	/**
 	 * Checks whether this item has any registered path of given type that matches provided path.
 	 *
-	 * @private
+	 * @protected
 	 * @param {String} type Paths' type. Possible values are `ALLOW` or `DISALLOW`.
 	 * @param {Array.<String>} checkPath Path to check.
-	 * @param {String} [attribute] If set, only paths registered for given attribute will be returned.
+	 * @param {String} [attribute] If set, only paths registered for given attribute will be checked.
 	 * @returns {Boolean} `true` if item has any registered matching path, `false` otherwise.
 	 */
 	_hasMatchingPath( type, checkPath, attribute ) {
@@ -121,7 +179,8 @@ export class SchemaItem {
 
 		// We check every path registered (possibly with given attribute) in the item.
 		for ( let itemPath of itemPaths ) {
-			// We have one of paths registered in the item.
+			// Pointer to last found item from `itemPath`.
+			let i = 0;
 
 			// Now we have to check every item name from the path to check.
 			for ( let checkName of checkPath ) {
@@ -134,20 +193,19 @@ export class SchemaItem {
 				// So, if item path is: B D E
 				// And checked path is: A B C D E
 				// It will be matching (A won't match, B will match, C won't match, D and E will match)
-				if ( chain.indexOf( itemPath[ 0 ] ) > -1 ) {
-					// Every time we have a match, we remove it from `itemPath` so we can still check against first item.
-					itemPath.shift();
+				if ( chain.indexOf( itemPath[ i ] ) > -1 ) {
+					// Move pointer as we found element under index `i`.
+					i++;
 				}
 			}
 
 			// If `itemPath` has no items it means that we removed all of them, so we matched all of them.
 			// This means that we found a matching path.
-			if ( itemPath.length === 0 ) {
+			if ( i === itemPath.length ) {
 				return true;
 			}
 		}
 
-		// No matching path found.
 		return false;
 	}
 
@@ -221,7 +279,7 @@ export default class Schema {
 	 *
 	 *		// Allow text with bold attribute in all P elements.
 	 *		schema.registerItem( 'p', '$block' );
-	 *		schema.allow( { name: '$text', attribute: 'bold', inside: 'p' } );
+	 *		schema.allow( { name: '$text', attributes: 'bold', inside: 'p' } );
 	 *
 	 *		// Allow header in Ps that are in DIVs
 	 *		schema.registerItem( 'header', '$block' );
@@ -231,7 +289,7 @@ export default class Schema {
 	 * @param {engine.treeModel.SchemaQuery} query Allowed query.
 	 */
 	allow( query ) {
-		this._getItem( query.name ).allow( query.inside, query.attribute );
+		this._getItem( query.name ).allow( Schema._normalizeQueryPath( query.inside ), query.attributes );
 	}
 
 	/**
@@ -241,31 +299,25 @@ export default class Schema {
 	 * @param {engine.treeModel.SchemaQuery} query Disallowed query.
 	 */
 	disallow( query ) {
-		this._getItem( query.name ).disallow( query.inside, query.attribute );
+		this._getItem( query.name ).disallow( Schema._normalizeQueryPath( query.inside ), query.attributes );
 	}
 
 	/**
-	 * Checks whether entity with given name (and optionally, with given attribute) is allowed at given position.
+	 * Makes a requirement in schema that entity represented by given item has to have given set of attributes. Some
+	 * elements in the model might require some attributes to be set. If multiple sets of attributes are required it
+	 * is enough that the entity fulfills only one set.
 	 *
-	 *		// Check whether bold text can be placed at caret position.
-	 *		let caretPos = editor.document.selection.getFirstPosition();
-	 *		if ( schema.checkAtPosition( caretPos, '$text', 'bold' ) ) { ... }
+	 *		// "a" element must either have "href" attribute or "name" attribute
+	 *		schema.requireAttributes( 'a', [ 'href' ] );
+	 *		schema.requireAttributes( 'a', [ 'name' ] );
+	 *		// "img" element must have both "src" and "alt" attributes
+	 *		schema.requireAttributes( 'img', [ 'src', 'alt' ] );
 	 *
-	 * @param {engine.treeModel.Position} position Position to check at.
-	 * @param {String} name Entity name to check.
-	 * @param {String} [attribute] If set, schema will check for entity with given attribute.
-	 * @returns {Boolean} `true` if entity is allowed, `false` otherwise
+	 * @param {String} name Entity name.
+	 * @param {Array.<String>} attributes Attributes that has to be set on the entity to make it valid.
 	 */
-	checkAtPosition( position, name, attribute ) {
-		if ( !this.hasItem( name ) ) {
-			return false;
-		}
-
-		return this.checkQuery( {
-			name: name,
-			inside: Schema._makeItemsPathFromPosition( position ),
-			attribute: attribute
-		} );
+	requireAttributes( name, attributes ) {
+		this._getItem( name ).requireAttributes( attributes );
 	}
 
 	/**
@@ -274,43 +326,90 @@ export default class Schema {
 	 *		// Check whether bold text is allowed in header element.
 	 *		let query = {
 	 *			name: '$text',
-	 *			attribute: 'bold',
+	 *			attributes: 'bold',
 	 *			inside: 'header'
 	 *		};
-	 *		if ( schema.checkQuery( query ) ) { ... }
+	 *		if ( schema.check( query ) ) { ... }
+	 *
+	 *		// Check whether bold and italic text can be placed at caret position.
+	 *		let caretPos = editor.document.selection.getFirstPosition();
+	 *		let query = {
+	 *			name: '$text',
+	 *			attributes: [ 'bold', 'italic' ],
+	 *			inside: caretPos
+	 *		};
+	 *		if ( schema.check( query ) ) { ... }
+	 *
+	 *		// Check whether image with alt, src and title is allowed in given elements path.
+	 *		let quoteElement = new Element( 'quote' );
+	 *		let query = {
+	 *			name: 'img',
+	 *			attributes: [ 'alt', 'src', 'title' ],
+	 *			// It is possible to mix strings with elements.
+	 *			// Query will check whether "img" can be inside "quoteElement" that is inside a block element.
+	 *			inside: [ '$block', quoteElement ]
+	 *		};
+	 *		if ( schema.check( query ) ) { ... }
 	 *
 	 * @param {engine.treeModel.SchemaQuery} query Query to check.
 	 * @returns {Boolean} `true` if given query is allowed in schema, `false` otherwise.
 	 */
-	checkQuery( query ) {
+	check( query ) {
 		if ( !this.hasItem( query.name ) ) {
 			return false;
 		}
 
-		const path = ( typeof query.inside === 'string' ) ? query.inside.split( ' ' ) : query.inside;
+		// If attributes property is a string or undefined, wrap it in an array for easier processing.
+		if ( !isArray( query.attributes ) ) {
+			query.attributes = [ query.attributes ];
+		}
+
+		// Normalize the path to an array of strings.
+		const path = Schema._normalizeQueryPath( query.inside );
 
 		// Get extension chain of given item and retrieve all schema items that are extended by given item.
 		const schemaItems = this._extensionChains.get( query.name ).map( ( name ) => {
 			return this._getItem( name );
 		} );
 
+		// First check if the query meets at required attributes for this item.
+		if ( !this._getItem( query.name )._checkRequiredAttributes( query.attributes ) ) {
+			return false;
+		}
+
 		// If there is matching disallow path, this query is not valid with schema.
-		for ( let schemaItem of schemaItems ) {
-			if ( schemaItem._hasMatchingPath( 'DISALLOW', path, query.attribute ) ) {
-				return false;
+		for ( let attribute of query.attributes ) {
+			for ( let schemaItem of schemaItems ) {
+				if ( schemaItem._hasMatchingPath( 'DISALLOW', path, attribute ) ) {
+					return false;
+				}
 			}
 		}
 
 		// At this point, the query is not disallowed.
-		// If there is any allow path that matches query, this query is valid with schema.
-		for ( let schemaItem of schemaItems ) {
-			if ( schemaItem._hasMatchingPath( 'ALLOW', path, query.attribute ) ) {
-				return true;
+		// If there are correct allow paths that match the query, this query is valid with schema.
+		// Since we are supporting multiple attributes, we have to make sure that if attributes are set,
+		// we have allowed paths for all of them.
+		// Keep in mind that if the query has no attributes, query.attribute was converted to an array
+		// with a single `undefined` value. This fits the algorithm well.
+		for ( let attribute of query.attributes ) {
+			let matched = false;
+
+			for ( let schemaItem of schemaItems ) {
+				if ( schemaItem._hasMatchingPath( 'ALLOW', path, attribute ) ) {
+					matched = true;
+					break;
+				}
+			}
+
+			// The attribute has not been matched, so it is not allowed by any schema item.
+			// The query is disallowed.
+			if ( !matched ) {
+				return false;
 			}
 		}
 
-		// There are no allow paths that matches query. The query is not valid with schema.
-		return false;
+		return true;
 	}
 
 	/**
@@ -385,24 +484,37 @@ export default class Schema {
 	}
 
 	/**
-	 * Gets position and traverses through it's parents to get their names and returns them.
+	 * Normalizes a path to an entity by converting it from {@link engine.treeModel.SchemaPath} to an array of strings.
 	 *
-	 * @private
-	 * @param {engine.treeModel.Position} position Position to start building path from.
-	 * @returns {Array.<String>} Path containing elements names from top-most to the one containing given position.
+	 * @protected
+	 * @param {engine.treeModel.SchemaPath} path Path to normalize.
+	 * @returns {Array.<String>} Normalized path.
 	 */
-	static _makeItemsPathFromPosition( position ) {
-		const path = [];
-		let parent = position.parent;
+	static _normalizeQueryPath( path ) {
+		let normalized = [];
+
+		if ( isArray( path ) ) {
+			for ( let pathItem of path ) {
+				if ( pathItem instanceof Element ) {
+					normalized.push( pathItem.name );
+				} else if ( isString( pathItem ) ) {
+					normalized.push( pathItem );
+				}
+			}
+		} else if ( path instanceof Position ) {
+			let parent = path.parent;
 
-		while ( parent !== null ) {
-			path.push( parent.name );
-			parent = parent.parent;
-		}
+			while ( parent !== null ) {
+				normalized.push( parent.name );
+				parent = parent.parent;
+			}
 
-		path.reverse();
+			normalized.reverse();
+		} else if ( isString( path ) ) {
+			normalized = path.split( ' ' );
+		}
 
-		return path;
+		return normalized;
 	}
 }
 
@@ -411,6 +523,14 @@ export default class Schema {
  *
  * @typedef {Object} engine.treeModel.SchemaQuery
  * @property {String} name Entity name.
- * @property {Array.<String>|String} inside Path inside which the entity is placed.
- * @property {String} [attribute] If set, the query applies only to entities that has attribute with given key.
+ * @property {engine.treeModel.SchemaPath} inside Path inside which the entity is placed.
+ * @property {Array.<String>|String} [attributes] If set, the query applies only to entities that has attribute(s) with given key.
+ */
+
+/**
+ * Path to an entity, begins from the top-most ancestor. Can be passed in multiple formats. Internally, normalized to
+ * an array of strings. If string is passed, entities from the path should be divided by ` ` (space character). If
+ * an array is passed, unrecognized items are skipped. If position is passed, it is assumed that the entity is at given position.
+ *
+ * @typedef {String|Array.<String|engine.treeModel.Element>|engine.treeModel.Position} engine.treeModel.SchemaPath
  */

+ 167 - 65
packages/ckeditor5-engine/tests/treemodel/schema/schema.js

@@ -33,7 +33,7 @@ describe( 'constructor', () => {
 	} );
 
 	it( 'should allow inline in block', () => {
-		expect( schema.checkQuery( { name: '$inline', inside: [ '$block' ] } ) ).to.be.true;
+		expect( schema.check( { name: '$inline', inside: [ '$block' ] } ) ).to.be.true;
 	} );
 } );
 
@@ -59,7 +59,7 @@ describe( 'registerItem', () => {
 	it( 'should make registered item inherit allows from base item', () => {
 		schema.registerItem( 'image', '$inline' );
 
-		expect( schema.checkQuery( { name: 'image', inside: [ '$block' ] } ) ).to.be.true;
+		expect( schema.check( { name: 'image', inside: [ '$block' ] } ) ).to.be.true;
 	} );
 
 	it( 'should throw if item with given name has already been registered in schema', () => {
@@ -108,11 +108,11 @@ describe( 'allow', () => {
 		schema.registerItem( 'p', '$block' );
 		schema.registerItem( 'div', '$block' );
 
-		expect( schema.checkQuery( { name: 'p', inside: [ 'div' ] } ) ).to.be.false;
+		expect( schema.check( { name: 'p', inside: [ 'div' ] } ) ).to.be.false;
 
 		schema.allow( { name: 'p', inside: 'div' } );
 
-		expect( schema.checkQuery( { name: 'p', inside: [ 'div' ] } ) ).to.be.true;
+		expect( schema.check( { name: 'p', inside: [ 'div' ] } ) ).to.be.true;
 	} );
 } );
 
@@ -121,91 +121,193 @@ describe( 'disallow', () => {
 		schema.registerItem( 'p', '$block' );
 		schema.registerItem( 'div', '$block' );
 
-		schema.allow( { name: '$block', attribute: 'bold', inside: 'div' } );
+		schema.allow( { name: '$block', attributes: 'bold', inside: 'div' } );
 
-		expect( schema.checkQuery( { name: 'p', attribute: 'bold', inside: [ 'div' ] } ) ).to.be.true;
+		expect( schema.check( { name: 'p', attributes: 'bold', inside: [ 'div' ] } ) ).to.be.true;
 
-		schema.disallow( { name: 'p', attribute: 'bold', inside: 'div' } );
+		schema.disallow( { name: 'p', attributes: 'bold', inside: 'div' } );
 
-		expect( schema.checkQuery( { name: 'p', attribute: 'bold', inside: [ 'div' ] } ) ).to.be.false;
+		expect( schema.check( { name: 'p', attributes: 'bold', inside: [ 'div' ] } ) ).to.be.false;
 	} );
 } );
 
-describe( 'checkAtPosition', () => {
-	let doc, root;
+describe( 'check', () => {
+	describe( 'string or array of strings as inside', () => {
+		it( 'should return false if given element is not registered in schema', () => {
+			expect( schema.check( { name: 'new', inside: [ 'div', 'header' ] } ) ).to.be.false;
+		} );
+
+		it( 'should handle path given as string', () => {
+			expect( schema.check( { name: '$inline', inside: '$block $block $block' } ) ).to.be.true;
+		} );
+
+		it( 'should handle attributes', () => {
+			schema.registerItem( 'p', '$block' );
+			schema.allow( { name: 'p', inside: '$block' } );
+
+			expect( schema.check( { name: 'p', inside: [ '$block' ] } ) ).to.be.true;
+			expect( schema.check( { name: 'p', inside: [ '$block' ], attributes: 'bold' } ) ).to.be.false;
+		} );
+
+		it( 'should support required attributes', () => {
+			schema.registerItem( 'a', '$inline' );
+			schema.requireAttributes( 'a', [ 'name' ] );
+			schema.requireAttributes( 'a', [ 'href' ] );
+			schema.allow( { name: 'a', inside: '$block', attributes: [ 'name', 'href', 'title', 'target' ] } );
+
+			// Even though a is allowed in $block thanks to inheriting from $inline, we require href or name attribute.
+			expect( schema.check( { name: 'a', inside: '$block' } ) ).to.be.false;
+
+			// Even though a with title is allowed, we have to meet at least on required attributes set.
+			expect( schema.check( { name: 'a', inside: '$block', attributes: [ 'title' ] } ) ).to.be.false;
+
+			expect( schema.check( { name: 'a', inside: '$block', attributes: [ 'name' ] } ) ).to.be.true;
+			expect( schema.check( { name: 'a', inside: '$block', attributes: [ 'href' ] } ) ).to.be.true;
+			expect( schema.check( { name: 'a', inside: '$block', attributes: [ 'name', 'href' ] } ) ).to.be.true;
+			expect( schema.check( { name: 'a', inside: '$block', attributes: [ 'name', 'title', 'target' ] } ) ).to.be.true;
+		} );
+
+		it( 'should not require attributes from parent schema items', () => {
+			schema.registerItem( 'parent' );
+			schema.registerItem( 'child', 'parent' );
+			schema.allow( { name: 'parent', inside: '$block' } );
+			schema.requireAttributes( 'parent', [ 'required' ] );
+
+			// Even though we require "required" attribute on parent, the requirement should not be inherited.
+			expect( schema.check( { name: 'child', inside: '$block' } ) ).to.be.true;
+		} );
+
+		it( 'should support multiple attributes', () => {
+			// Let's take example case, where image item has to have a pair of "alt" and "src" attributes.
+			// Then it could have other attribute which is allowed on inline elements, i.e. "bold".
+			schema.registerItem( 'img', '$inline' );
+			schema.requireAttributes( 'img', [ 'alt', 'src' ] );
+			schema.allow( { name: '$inline', inside: '$block', attributes: 'bold' } );
+			schema.allow( { name: 'img', inside: '$block', attributes: [ 'alt', 'src' ] } );
+
+			// Image without any attributes is not allowed.
+			expect( schema.check( { name: 'img', inside: '$block', attributes: [ 'alt' ] } ) ).to.be.false;
+
+			// Image can't have just alt or src.
+			expect( schema.check( { name: 'img', inside: '$block', attributes: [ 'alt' ] } ) ).to.be.false;
+			expect( schema.check( { name: 'img', inside: '$block', attributes: [ 'src' ] } ) ).to.be.false;
+
+			expect( schema.check( { name: 'img', inside: '$block', attributes: [ 'alt', 'src' ] } ) ).to.be.true;
+
+			// Because of inherting from $inline, image can have bold
+			expect( schema.check( { name: 'img', inside: '$block', attributes: [ 'alt', 'src', 'bold' ] } ) ).to.be.true;
+			// But it can't have only bold without alt or/and src.
+			expect( schema.check( { name: 'img', inside: '$block', attributes: [ 'alt', 'bold' ] } ) ).to.be.false;
+			expect( schema.check( { name: 'img', inside: '$block', attributes: [ 'src', 'bold' ] } ) ).to.be.false;
+			expect( schema.check( { name: 'img', inside: '$block', attributes: [ 'bold' ] } ) ).to.be.false;
+
+			// Even if image has src and alt, it can't have attributes that weren't allowed
+			expect( schema.check( { name: 'img', inside: '$block', attributes: [ 'alt', 'src', 'attr' ] } ) ).to.be.false;
+		} );
+	} );
 
-	beforeEach( () => {
-		doc = new Document();
-		root = doc.createRoot( 'root', 'div' );
+	describe( 'array of elements as inside', () => {
+		beforeEach( () => {
+			schema.registerItem( 'div', '$block' );
+			schema.registerItem( 'header', '$block' );
+			schema.registerItem( 'p', '$block' );
+			schema.registerItem( 'img', '$inline' );
 
-		root.insertChildren( 0, [
-			new Element( 'div' ),
-			new Element( 'header' ),
-			new Element( 'p' )
-		] );
+			schema.allow( { name: '$block', inside: 'div' } );
+			schema.allow( { name: '$inline', attributes: 'bold', inside: '$block' } );
 
-		schema.registerItem( 'div', '$block' );
-		schema.registerItem( 'header', '$block' );
-		schema.registerItem( 'p', '$block' );
+			schema.disallow( { name: '$inline', attributes: 'bold', inside: 'header' } );
+		} );
+
+		it( 'should return true if given element is allowed by schema at given position', () => {
+			// P is block and block is allowed in DIV.
+			expect( schema.check( { name: 'p', inside: [ new Element( 'div' ) ] } ) ).to.be.true;
+
+			// IMG is inline and inline is allowed in block.
+			expect( schema.check( { name: 'img', inside: [ new Element( 'div' ) ] } ) ).to.be.true;
+			expect( schema.check( { name: 'img', inside: [ new Element( 'p' ) ] } ) ).to.be.true;
 
-		schema.allow( { name: '$block', inside: 'div' } );
-		schema.allow( { name: '$inline', attribute: 'bold', inside: '$block' } );
+			// Inline is allowed in any block and is allowed with attribute bold.
+			expect( schema.check( { name: 'img', inside: [ new Element( 'div' ) ], attributes: [ 'bold' ] } ) ).to.be.true;
+			expect( schema.check( { name: 'img', inside: [ new Element( 'p' ) ], attributes: [ 'bold' ] } ) ).to.be.true;
 
-		schema.disallow( { name: '$inline', attribute: 'bold', inside: 'header' } );
+			// Inline is allowed in header which is allowed in DIV.
+			expect( schema.check( { name: 'header', inside: [ new Element( 'div' ) ] } ) ).to.be.true;
+			expect( schema.check( { name: 'img', inside: [ new Element( 'header' ) ] } ) ).to.be.true;
+			expect( schema.check( { name: 'img', inside: [ new Element( 'div' ), new Element( 'header' ) ] } ) ).to.be.true;
+		} );
+
+		it( 'should return false if given element is not allowed by schema at given position', () => {
+			// P with attribute is not allowed.
+			expect( schema.check( { name: 'p', inside: [ new Element( 'div' ) ], attributes: 'bold' } ) ).to.be.false;
+
+			// Bold text is not allowed in header
+			expect( schema.check( { name: '$text', inside: [ new Element( 'header' ) ], attributes: 'bold' } ) ).to.be.false;
+		} );
+
+		it( 'should return false if given element is not registered in schema', () => {
+			expect( schema.check( { name: 'new', inside: [ new Element( 'div' ) ] } ) ).to.be.false;
+		} );
 	} );
 
-	it( 'should return true if given element is allowed by schema at given position', () => {
-		// Block should be allowed in root.
-		expect( schema.checkAtPosition( new Position( root, [ 0 ] ), '$block' ) ).to.be.true;
+	describe( 'position as inside', () => {
+		let doc, root;
 
-		// P is block and block should be allowed in root.
-		expect( schema.checkAtPosition( new Position( root, [ 0 ] ), 'p' ) ).to.be.true;
+		beforeEach( () => {
+			doc = new Document();
+			root = doc.createRoot( 'root', 'div' );
 
-		// P is allowed in DIV by the set rule.
-		expect( schema.checkAtPosition( new Position( root, [ 0, 0 ] ), 'p' ) ).to.be.true;
+			root.insertChildren( 0, [
+				new Element( 'div' ),
+				new Element( 'header' ),
+				new Element( 'p' )
+			] );
 
-		// Inline is allowed in any block and is allowed with attribute bold.
-		// We do not check if it is allowed in header, because it is disallowed by the set rule.
-		expect( schema.checkAtPosition( new Position( root, [ 0, 0 ] ), '$inline' ) ).to.be.true;
-		expect( schema.checkAtPosition( new Position( root, [ 2, 0 ] ), '$inline' ) ).to.be.true;
-		expect( schema.checkAtPosition( new Position( root, [ 0, 0 ] ), '$inline', 'bold' ) ).to.be.true;
-		expect( schema.checkAtPosition( new Position( root, [ 2, 0 ] ), '$inline', 'bold' ) ).to.be.true;
+			schema.registerItem( 'div', '$block' );
+			schema.registerItem( 'header', '$block' );
+			schema.registerItem( 'p', '$block' );
 
-		// Header is allowed in DIV.
-		expect( schema.checkAtPosition( new Position( root, [ 0, 0 ] ), 'header' ) ).to.be.true;
+			schema.allow( { name: '$block', inside: 'div' } );
+			schema.allow( { name: '$inline', attributes: 'bold', inside: '$block' } );
 
-		// Inline is allowed in block and root is DIV, which is block.
-		expect( schema.checkAtPosition( new Position( root, [ 0 ] ), '$inline' ) ).to.be.true;
-	} );
+			schema.disallow( { name: '$inline', attributes: 'bold', inside: 'header' } );
+		} );
 
-	it( 'should return false if given element is not allowed by schema at given position', () => {
-		// P with attribute is not allowed anywhere.
-		expect( schema.checkAtPosition( new Position( root, [ 0 ] ), 'p', 'bold' ) ).to.be.false;
-		expect( schema.checkAtPosition( new Position( root, [ 0, 0 ] ), 'p', 'bold' ) ).to.be.false;
+		it( 'should return true if given element is allowed by schema at given position', () => {
+			// Block should be allowed in root.
+			expect( schema.check( { name: '$block', inside: new Position( root, [ 0 ] ) } ) ).to.be.true;
 
-		// Bold text is not allowed in header
-		expect( schema.checkAtPosition( new Position( root, [ 1, 0 ] ), '$text', 'bold' ) ).to.be.false;
-	} );
+			// P is block and block should be allowed in root.
+			expect( schema.check( { name: 'p', inside: new Position( root, [ 0 ] ) } ) ).to.be.true;
 
-	it( 'should return false if given element is not registered in schema', () => {
-		expect( schema.checkAtPosition( new Position( root, [ 0 ] ), 'new' ) ).to.be.false;
-	} );
-} );
+			// P is allowed in DIV by the set rule.
+			expect( schema.check( { name: 'p', inside: new Position( root, [ 0, 0 ] ) } ) ).to.be.true;
 
-describe( 'checkQuery', () => {
-	it( 'should return false if given element is not registered in schema', () => {
-		expect( schema.checkQuery( { name: 'new', inside: [ 'div', 'header' ] } ) ).to.be.false;
-	} );
+			// Inline is allowed in any block and is allowed with attribute bold.
+			// We do not check if it is allowed in header, because it is disallowed by the set rule.
+			expect( schema.check( { name: '$inline', inside: new Position( root, [ 0, 0 ] ) } ) ).to.be.true;
+			expect( schema.check( { name: '$inline', inside: new Position( root, [ 2, 0 ] ) } ) ).to.be.true;
+			expect( schema.check( { name: '$inline', inside: new Position( root, [ 0, 0 ] ), attributes: 'bold' } ) ).to.be.true;
+			expect( schema.check( { name: '$inline', inside: new Position( root, [ 2, 0 ] ), attributes: 'bold' } ) ).to.be.true;
 
-	it( 'should handle path given as string', () => {
-		expect( schema.checkQuery( { name: '$inline', inside: '$block $block $block' } ) ).to.be.true;
-	} );
+			// Header is allowed in DIV.
+			expect( schema.check( { name: 'header', inside: new Position( root, [ 0, 0 ] ) } ) ).to.be.true;
 
-	it( 'should handle attributes', () => {
-		schema.registerItem( 'p', '$block' );
-		schema.allow( { name: 'p', inside: '$block' } );
+			// Inline is allowed in block and root is DIV, which is block.
+			expect( schema.check( { name: '$inline', inside: new Position( root, [ 0 ] ) } ) ).to.be.true;
+		} );
+
+		it( 'should return false if given element is not allowed by schema at given position', () => {
+			// P with attribute is not allowed anywhere.
+			expect( schema.check( { name: 'p', inside: new Position( root, [ 0 ] ), attributes: 'bold' } ) ).to.be.false;
+			expect( schema.check( { name: 'p', inside: new Position( root, [ 0, 0 ] ), attributes: 'bold' } ) ).to.be.false;
+
+			// Bold text is not allowed in header
+			expect( schema.check( { name: '$text', inside: new Position( root, [ 1, 0 ] ), attributes: 'bold' } ) ).to.be.false;
+		} );
 
-		expect( schema.checkQuery( { name: 'p', inside: '$block' } ) ).to.be.true;
-		expect( schema.checkQuery( { name: 'p', attribute: 'bold', inside: '$block' } ) ).to.be.false;
+		it( 'should return false if given element is not registered in schema', () => {
+			expect( schema.check( { name: 'new', inside: new Position( root, [ 0 ] ) } ) ).to.be.false;
+		} );
 	} );
 } );

+ 15 - 31
packages/ckeditor5-engine/tests/treemodel/schema/schemaitem.js

@@ -53,18 +53,10 @@ describe( 'allow', () => {
 		expect( paths[ 1 ] ).to.deep.equal( [ 'p' ] );
 	} );
 
-	it( 'should accept paths as string with element names separated with space', () => {
-		item.allow( 'div header' );
-
-		let paths = item._getPaths( 'ALLOW' );
-
-		expect( paths[ 0 ] ).to.deep.equal( [ 'div', 'header' ] );
-	} );
-
 	it( 'should group paths by attribute', () => {
-		item.allow( 'p', 'bold' );
-		item.allow( 'div' );
-		item.allow( 'header', 'bold' );
+		item.allow( [ 'p' ], 'bold' );
+		item.allow( [ 'div' ] );
+		item.allow( [ 'header' ], 'bold' );
 
 		let pathsWithNoAttribute = item._getPaths( 'ALLOW' );
 		let pathsWithBoldAttribute = item._getPaths( 'ALLOW', 'bold' );
@@ -97,18 +89,10 @@ describe( 'disallow', () => {
 		expect( paths[ 1 ] ).to.deep.equal( [ 'p' ] );
 	} );
 
-	it( 'should accept paths as string with element names separated with space', () => {
-		item.disallow( 'div header' );
-
-		let paths = item._getPaths( 'DISALLOW' );
-
-		expect( paths[ 0 ] ).to.deep.equal( [ 'div', 'header' ] );
-	} );
-
 	it( 'should group paths by attribute', () => {
-		item.disallow( 'p', 'bold' );
-		item.disallow( 'div' );
-		item.disallow( 'header', 'bold' );
+		item.disallow( [ 'p' ], 'bold' );
+		item.disallow( [ 'div' ] );
+		item.disallow( [ 'header' ], 'bold' );
 
 		let pathsWithNoAttribute = item._getPaths( 'DISALLOW' );
 		let pathsWithBoldAttribute = item._getPaths( 'DISALLOW', 'bold' );
@@ -124,8 +108,8 @@ describe( 'disallow', () => {
 
 describe( '_hasMatchingPath', () => {
 	it( 'should return true if there is at least one allowed path that matches query path', () => {
-		item.allow( 'div header' );
-		item.allow( 'image' );
+		item.allow( [ 'div' , 'header' ] );
+		item.allow( [ 'image' ] );
 
 		expect( item._hasMatchingPath( 'ALLOW', [ 'div', 'header' ] ) ).to.be.true;
 		expect( item._hasMatchingPath( 'ALLOW', [ 'html', 'div', 'header' ] ) ).to.be.true;
@@ -134,7 +118,7 @@ describe( '_hasMatchingPath', () => {
 	} );
 
 	it( 'should return false if there are no allowed paths that match query path', () => {
-		item.allow( 'div p' );
+		item.allow( [ 'div', 'p' ] );
 
 		expect( item._hasMatchingPath( 'ALLOW', [ 'p' ] ) ).to.be.false;
 		expect( item._hasMatchingPath( 'ALLOW', [ 'div' ] ) ).to.be.false;
@@ -142,17 +126,17 @@ describe( '_hasMatchingPath', () => {
 	} );
 
 	it( 'should return true if there is at least one disallowed path that matches query path', () => {
-		item.allow( 'div header' );
-		item.disallow( 'p header' );
+		item.allow( [ 'div', 'header' ] );
+		item.disallow( [ 'p', 'header' ] );
 
 		expect( item._hasMatchingPath( 'DISALLOW', [ 'html', 'div', 'p', 'header', 'span' ] ) ).to.be.true;
 	} );
 
 	it( 'should use only paths that are registered for given attribute', () => {
-		item.allow( 'div p' );
-		item.allow( 'div', 'bold' );
-		item.allow( 'header' );
-		item.disallow( 'header', 'bold' );
+		item.allow( [ 'div', 'p' ] );
+		item.allow( [ 'div' ], 'bold' );
+		item.allow( [ 'header' ] );
+		item.disallow( [ 'header' ], 'bold' );
 
 		expect( item._hasMatchingPath( 'ALLOW', [ 'html', 'div', 'p' ]  ) ).to.be.true;
 		expect( item._hasMatchingPath( 'ALLOW', [ 'html', 'div' ] ) ).to.be.false;