Przeglądaj źródła

Replaced the old schema with the new one.

Piotrek Koszuliński 8 lat temu
rodzic
commit
6d7c153d42

+ 203 - 648
packages/ckeditor5-engine/src/model/schema.js

@@ -3,743 +3,298 @@
  * For licensing, see LICENSE.md.
  */
 
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
+
 /**
  * @module engine/model/schema
  */
 
-import Position from './position';
-import Element from './element';
-import Range from './range';
-import DocumentSelection from './documentselection';
-import clone from '@ckeditor/ckeditor5-utils/src/lib/lodash/clone';
-import isArray from '@ckeditor/ckeditor5-utils/src/lib/lodash/isArray';
-import isString from '@ckeditor/ckeditor5-utils/src/lib/lodash/isString';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-
 /**
- * Schema is a definition of the structure of the document. It allows to define which tree model items (element, text, etc.)
- * can be nested within which ones and which attributes can be applied to them. It's created during the run-time of the application,
- * typically by features. Also, the features can query the schema to learn what structure is allowed and act accordingly.
- *
- * For instance, if a feature wants to define that an attribute bold is allowed on the text it needs to register this rule like this:
- *
- *		editor.model.schema.allow( '$text', 'bold' );
- *
- * Note: items prefixed with `$` are special group of items. By default, `Schema` defines three special items:
- *
- * * `$inline` represents all inline elements,
- * * `$text` is a sub-group of `$inline` and represents text nodes,
- * * `$block` represents block elements,
- * * `$root` represents default editing roots (those that allow only `$block`s inside them).
- *
- * When registering an item it's possible to tell that this item should inherit from some other existing item.
- * E.g. `p` can inherit from `$block`, so whenever given attribute is allowed on the `$block` it will automatically be
- * also allowed on the `p` element. By default, `$text` item already inherits from `$inline`.
+ * @mixes module:utils/emittermixin~ObservableMixin
  */
 export default class Schema {
-	/**
-	 * Creates Schema instance.
-	 */
 	constructor() {
-		/**
-		 * Names of elements which have "object" nature. This means that these
-		 * elements should be treated as whole, never merged, can be selected from outside, etc.
-		 * Just like images, placeholder widgets, etc.
-		 *
-		 * @member {Set.<String>} module:engine/model/schema~Schema#objects
-		 */
-		this.objects = new Set();
-
-		/**
-		 * Names of elements to which editing operations should be limited.
-		 * For example, the <kbd>Enter</kbd> should not split such elements and
-		 * <kbd>Backspace</kbd> should not be able to leave or modify such elements.
-		 *
-		 * @member {Set.<String>} module:engine/model/schema~Schema#limits
-		 */
-		this.limits = new Set();
-
-		/**
-		 * Schema items registered in the schema.
-		 *
-		 * @private
-		 * @member {Map} module:engine/model/schema~Schema#_items
-		 */
-		this._items = new Map();
-
-		/**
-		 * Description of what entities are a base for given entity.
-		 *
-		 * @private
-		 * @member {Map} module:engine/model/schema~Schema#_extensionChains
-		 */
-		this._extensionChains = new Map();
-
-		// Register some default abstract entities.
-		this.registerItem( '$root' );
-		this.registerItem( '$block' );
-		this.registerItem( '$inline' );
-		this.registerItem( '$text', '$inline' );
-
-		this.allow( { name: '$block', inside: '$root' } );
-		this.allow( { name: '$inline', inside: '$block' } );
-
-		this.limits.add( '$root' );
-
-		// TMP!
-		// Create an "all allowed" context in the schema for processing the pasted content.
-		// Read: https://github.com/ckeditor/ckeditor5-engine/issues/638#issuecomment-255086588
-
-		this.registerItem( '$clipboardHolder', '$root' );
-		this.allow( { name: '$inline', inside: '$clipboardHolder' } );
-	}
+		this._sourceRules = {};
 
-	/**
-	 * Allows given query in the schema.
-	 *
-	 *		// Allow text with bold attribute in all P elements.
-	 *		schema.registerItem( 'p', '$block' );
-	 *		schema.allow( { name: '$text', attributes: 'bold', inside: 'p' } );
-	 *
-	 *		// Allow header in Ps that are in DIVs
-	 *		schema.registerItem( 'header', '$block' );
-	 *		schema.registerItem( 'div', '$block' );
-	 *		schema.allow( { name: 'header', inside: 'div p' } ); // inside: [ 'div', 'p' ] would also work.
-	 *
-	 * @param {module:engine/model/schema~SchemaQuery} query Allowed query.
-	 */
-	allow( query ) {
-		this._getItem( query.name ).allow( Schema._normalizeQueryPath( query.inside ), query.attributes );
-	}
+		// TODO events
+		this.decorate( 'checkChild' );
+		this.decorate( 'checkAttribute' );
 
-	/**
-	 * Disallows given query in the schema.
-	 *
-	 * @see #allow
-	 * @param {module:engine/model/schema~SchemaQuery} query Disallowed query.
-	 */
-	disallow( query ) {
-		this._getItem( query.name ).disallow( Schema._normalizeQueryPath( query.inside ), query.attributes );
-	}
+		this.on( 'checkAttribute', ( evt, args ) => {
+			args[ 0 ] = getContext( args[ 0 ] );
+		}, { priority: 'highest' } );
 
-	/**
-	 * 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.
-	 *
-	 *		// "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 {String} name Entity name.
-	 * @param {Array.<String>} attributes Attributes that has to be set on the entity to make it valid.
-	 */
-	requireAttributes( name, attributes ) {
-		this._getItem( name ).requireAttributes( attributes );
+		this.on( 'checkChild', ( evt, args ) => {
+			args[ 0 ] = getContext( args[ 0 ] );
+		}, { priority: 'highest' } );
 	}
 
-	/**
-	 * Checks whether given query is allowed in schema.
-	 *
-	 *		// Check whether bold text is allowed in header element.
-	 *		let query = {
-	 *			name: '$text',
-	 *			attributes: 'bold',
-	 *			inside: 'header'
-	 *		};
-	 *		if ( schema.check( query ) ) { ... }
-	 *
-	 *		// Check whether bold and italic text can be placed at caret position.
-	 *		let caretPos = editor.model.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 {module:engine/model/schema~SchemaQuery} query Query to check.
-	 * @returns {Boolean} `true` if given query is allowed in schema, `false` otherwise.
-	 */
-	check( query ) {
-		if ( !this.hasItem( query.name ) ) {
-			return false;
-		}
-
-		// If attributes property is a string or undefined, wrap it in an array for easier processing.
-		if ( !isArray( query.attributes ) ) {
-			query.attributes = [ query.attributes ];
-		} else if ( query.attributes.length === 0 ) {
-			// To simplify algorithms, when a SchemaItem path is added "without" attribute, it is added with
-			// attribute equal to undefined. This means that algorithms can work the same way for specified attributes
-			// and no-atrtibutes, but we have to fill empty array with "fake" undefined value for algorithms reasons.
-			query.attributes.push( undefined );
+	register( itemName, rules ) {
+		if ( this._sourceRules[ itemName ] ) {
+			// TODO docs
+			throw new CKEditorError( 'schema-cannot-register-item-twice: A single item cannot be registered twice in the schema.' );
 		}
 
-		// Normalize the path to an array of strings.
-		const path = Schema._normalizeQueryPath( query.inside );
+		this._sourceRules[ itemName ] = [
+			Object.assign( {}, rules )
+		];
 
-		// 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 );
-		} );
+		this._clearCache();
+	}
 
-		// First check if the query meets at required attributes for this item.
-		if ( !this._getItem( query.name )._checkRequiredAttributes( query.attributes ) ) {
-			return false;
+	extend( itemName, rules ) {
+		// TODO it should not throw if we want to allow e.g. adding attrs before element is registered
+		// (which may be done by another feature).
+		if ( !this._sourceRules[ itemName ] ) {
+			// TODO docs
+			throw new CKEditorError( 'schema-cannot-extend-missing-item: Cannot extend an item which was not registered yet.' );
 		}
 
-		// If there is matching disallow path, this query is not valid with schema.
-		for ( const attribute of query.attributes ) {
-			for ( const schemaItem of schemaItems ) {
-				if ( schemaItem._hasMatchingPath( 'disallow', path, attribute ) ) {
-					return false;
-				}
-			}
-		}
+		this._sourceRules[ itemName ].push( Object.assign( {}, rules ) );
 
-		// At this point, the query is not disallowed.
-		// 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 ( const attribute of query.attributes ) {
-			// Skip all attributes that are stored in elements.
-			// This isn't perfect solution but we have to deal with it for now.
-			// `attribute` may have `undefined` value.
-			if ( attribute && DocumentSelection._isStoreAttributeKey( attribute ) ) {
-				continue;
-			}
+		this._clearCache();
+	}
 
-			let matched = false;
+	getRules() {
+		if ( !this._compiledRules ) {
+			this._compile();
+		}
 
-			for ( const schemaItem of schemaItems ) {
-				if ( schemaItem._hasMatchingPath( 'allow', path, attribute ) ) {
-					matched = true;
-					break;
-				}
-			}
+		return this._compiledRules;
+	}
 
-			// The attribute has not been matched, so it is not allowed by any schema item.
-			// The query is disallowed.
-			if ( !matched ) {
-				return false;
-			}
+	getRule( item ) {
+		let itemName;
+
+		if ( typeof item == 'string' ) {
+			itemName = item;
+		} else if ( item.is && item.is( 'text' ) ) {
+			itemName = '$text';
+		}
+		// Element or context item.
+		else {
+			itemName = item.name;
 		}
 
-		return true;
+		return this.getRules()[ itemName ];
 	}
 
-	/**
-	 * Checks whether there is an item registered under given name in schema.
-	 *
-	 * @param itemName
-	 * @returns {Boolean}
-	 */
-	hasItem( itemName ) {
-		return this._items.has( itemName );
+	isRegistered( itemName ) {
+		return !!this.getRule( itemName );
 	}
 
-	/**
-	 * Registers given item name in schema.
-	 *
-	 *		// Register P element that should be treated like all block elements.
-	 *		schema.registerItem( 'p', '$block' );
-	 *
-	 * @param {String} itemName Name to register.
-	 * @param [isExtending] If set, new item will extend item with given name.
-	 */
-	registerItem( itemName, isExtending ) {
-		if ( this.hasItem( itemName ) ) {
-			/**
-			 * Item with specified name already exists in schema.
-			 *
-			 * @error model-schema-item-exists
-			 */
-			throw new CKEditorError( 'model-schema-item-exists: Item with specified name already exists in schema.' );
-		}
+	isBlock( itemName ) {
+		const rule = this.getRule( itemName );
 
-		if ( !!isExtending && !this.hasItem( isExtending ) ) {
-			throw new CKEditorError( 'model-schema-no-item: Item with specified name does not exist in schema.' );
-		}
+		return !!( rule && rule.isBlock );
+	}
 
-		// Create new SchemaItem and add it to the items store.
-		this._items.set( itemName, new SchemaItem( this ) );
+	isLimit( itemName ) {
+		const rule = this.getRule( itemName );
 
-		// Create an extension chain.
-		// Extension chain has all item names that should be checked when that item is on path to check.
-		// This simply means, that if item is not extending anything, it should have only itself in it's extension chain.
-		// Since extending is not dynamic, we can simply get extension chain of extended item and expand it with registered name,
-		// if the registered item is extending something.
-		const chain = this.hasItem( isExtending ) ? this._extensionChains.get( isExtending ).concat( itemName ) : [ itemName ];
-		this._extensionChains.set( itemName, chain );
+		return !!( rule && rule.isLimit );
 	}
 
-	/**
-	 * Checks whether item of given name is extending item of another given name.
-	 *
-	 * @param {String} childItemName Name of the child item.
-	 * @param {String} parentItemName Name of the parent item.
-	 * @returns {Boolean} `true` if child item extends parent item, `false` otherwise.
-	 */
-	itemExtends( childItemName, parentItemName ) {
-		if ( !this.hasItem( childItemName ) || !this.hasItem( parentItemName ) ) {
-			throw new CKEditorError( 'model-schema-no-item: Item with specified name does not exist in schema.' );
-		}
+	isObject( itemName ) {
+		const rule = this.getRule( itemName );
 
-		const chain = this._extensionChains.get( childItemName );
-
-		return chain.some( itemName => itemName == parentItemName );
+		return !!( rule && rule.isObject );
 	}
 
-	/**
-	 * Checks whether the attribute is allowed in selection:
-	 *
-	 * * if the selection is not collapsed, then checks if the attribute is allowed on any of nodes in that range,
-	 * * if the selection is collapsed, then checks if on the selection position there's a text with the
-	 * specified attribute allowed.
-	 *
-	 * @param {module:engine/model/selection~Selection} selection Selection which will be checked.
-	 * @param {String} attribute The name of the attribute to check.
-	 * @returns {Boolean}
-	 */
-	checkAttributeInSelection( selection, attribute ) {
-		if ( selection.isCollapsed ) {
-			// Check whether schema allows for a text with the attribute in the selection.
-			return this.check( { name: '$text', inside: selection.getFirstPosition(), attributes: attribute } );
-		} else {
-			const ranges = selection.getRanges();
-
-			// For all ranges, check nodes in them until you find a node that is allowed to have the attribute.
-			for ( const range of ranges ) {
-				for ( const value of range ) {
-					// If returned item does not have name property, it is a TextFragment.
-					const name = value.item.name || '$text';
-
-					// Attribute should be checked together with existing attributes.
-					// See https://github.com/ckeditor/ckeditor5-engine/issues/1110.
-					const attributes = Array.from( value.item.getAttributeKeys() ).concat( attribute );
-
-					if ( this.check( { name, inside: value.previousPosition, attributes } ) ) {
-						// If we found a node that is allowed to have the attribute, return true.
-						return true;
-					}
-				}
-			}
+	checkChild( context, child ) {
+		const rule = this.getRule( child );
+
+		if ( !rule ) {
+			return false;
 		}
 
-		// If we haven't found such node, return false.
-		return false;
+		return this._checkContextMatch( rule, context );
 	}
 
-	/**
-	 * Transforms the given set ranges into a set of ranges where the given attribute is allowed (and can be applied).
-	 *
-	 * @param {Array.<module:engine/model/range~Range>} ranges Ranges to be validated.
-	 * @param {String} attribute The name of the attribute to check.
-	 * @returns {Array.<module:engine/model/range~Range>} Ranges in which the attribute is allowed.
-	 */
-	getValidRanges( ranges, attribute ) {
-		const validRanges = [];
-
-		for ( const range of ranges ) {
-			let last = range.start;
-			let from = range.start;
-			const to = range.end;
-
-			for ( const value of range.getWalker() ) {
-				const name = value.item.name || '$text';
-				const itemPosition = Position.createBefore( value.item );
-
-				if ( !this.check( { name, inside: itemPosition, attributes: attribute } ) ) {
-					if ( !from.isEqual( last ) ) {
-						validRanges.push( new Range( from, last ) );
-					}
-
-					from = value.nextPosition;
-				}
-
-				last = value.nextPosition;
-			}
+	checkAttribute( context, attributeName ) {
+		const rule = this.getRule( context[ context.length - 1 ] );
 
-			if ( from && !from.isEqual( to ) ) {
-				validRanges.push( new Range( from, to ) );
-			}
+		if ( !rule ) {
+			return false;
 		}
 
-		return validRanges;
+		return rule.allowAttributes.includes( attributeName );
 	}
 
-	/**
-	 * Returns the lowest {@link module:engine/model/schema~Schema#limits limit element} containing the entire
-	 * selection or the root otherwise.
-	 *
-	 * @param {module:engine/model/selection~Selection} selection Selection which returns the common ancestor.
-	 * @returns {module:engine/model/element~Element}
-	 */
-	getLimitElement( selection ) {
-		// Find the common ancestor for all selection's ranges.
-		let element = Array.from( selection.getRanges() )
-			.reduce( ( node, range ) => {
-				if ( !node ) {
-					return range.getCommonAncestor();
-				}
-
-				return node.getCommonAncestor( range.getCommonAncestor() );
-			}, null );
-
-		while ( !this.limits.has( element.name ) ) {
-			if ( element.parent ) {
-				element = element.parent;
-			} else {
-				break;
-			}
+	_clearCache() {
+		this._compiledRules = null;
+	}
+
+	_compile() {
+		const compiledRules = {};
+		const sourceRules = this._sourceRules;
+		const itemNames = Object.keys( sourceRules );
+
+		for ( const itemName of itemNames ) {
+			compiledRules[ itemName ] = compileBaseItemRule( sourceRules[ itemName ], itemName );
 		}
 
-		return element;
-	}
+		for ( const itemName of itemNames ) {
+			compileAllowContentOf( compiledRules, itemName );
+		}
 
-	/**
-	 * Removes disallowed by {@link module:engine/model/schema~Schema schema} attributes from given nodes..
-	 *
-	 * @param {Iterable.<module:engine/model/node~Node>} nodes Nodes that will be filtered.
-	 * @param {module:engine/model/schema~SchemaPath} inside Path inside which schema will be checked.
-	 * @param {module:engine/model/writer~Writer} writer
-	 */
-	removeDisallowedAttributes( nodes, inside, writer ) {
-		for ( const node of nodes ) {
-			const name = node.is( 'text' ) ? '$text' : node.name;
-			const attributes = Array.from( node.getAttributeKeys() );
-			const queryPath = Schema._normalizeQueryPath( inside );
-
-			// When node with attributes is not allowed in current position.
-			if ( !this.check( { name, attributes, inside: queryPath } ) ) {
-				// Let's remove attributes one by one.
-				// TODO: this should be improved to check all combination of attributes.
-				for ( const attribute of node.getAttributeKeys() ) {
-					if ( !this.check( { name, attributes: attribute, inside: queryPath } ) ) {
-						writer.removeAttribute( attribute, node );
-					}
-				}
-			}
+		for ( const itemName of itemNames ) {
+			compileAllowWhere( compiledRules, itemName );
+		}
 
-			if ( node.is( 'element' ) ) {
-				this.removeDisallowedAttributes( node.getChildren(), queryPath.concat( node.name ), writer );
-			}
+		for ( const itemName of itemNames ) {
+			compileAllowAttributesOf( compiledRules, itemName );
 		}
-	}
 
-	/**
-	 * Returns {@link module:engine/model/schema~SchemaItem schema item} that was registered in the schema under given name.
-	 * If item has not been found, throws error.
-	 *
-	 * @private
-	 * @param {String} itemName Name to look for in schema.
-	 * @returns {module:engine/model/schema~SchemaItem} Schema item registered under given name.
-	 */
-	_getItem( itemName ) {
-		if ( !this.hasItem( itemName ) ) {
-			throw new CKEditorError( 'model-schema-no-item: Item with specified name does not exist in schema.' );
+		for ( const itemName of itemNames ) {
+			cleanUpAllowIn( compiledRules, itemName );
+			cleanUpAllowAttributes( compiledRules, itemName );
 		}
 
-		return this._items.get( itemName );
+		this._compiledRules = compiledRules;
 	}
 
-	/**
-	 * Normalizes a path to an entity by converting it from {@link module:engine/model/schema~SchemaPath} to an array of strings.
-	 *
-	 * @protected
-	 * @param {module:engine/model/schema~SchemaPath} path Path to normalize.
-	 * @returns {Array.<String>} Normalized path.
-	 */
-	static _normalizeQueryPath( path ) {
-		let normalized = [];
-
-		if ( isArray( path ) ) {
-			for ( const 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;
+	_checkContextMatch( rule, context, contextItemIndex = context.length - 1 ) {
+		const contextItem = context[ contextItemIndex ];
 
-			while ( parent !== null ) {
-				normalized.push( parent.name );
-				parent = parent.parent;
-			}
+		if ( rule.allowIn.includes( contextItem.name ) ) {
+			if ( contextItemIndex == 0 ) {
+				return true;
+			} else {
+				const parentRule = this.getRule( contextItem );
 
-			normalized.reverse();
-		} else if ( isString( path ) ) {
-			normalized = path.split( ' ' );
+				return this._checkContextMatch( parentRule, context, contextItemIndex - 1 );
+			}
+		} else {
+			return false;
 		}
-
-		return normalized;
 	}
 }
 
-/**
- * SchemaItem is a singular registry item in {@link module:engine/model/schema~Schema} that groups and holds allow/disallow rules for
- * one entity. This class is used internally in {@link module:engine/model/schema~Schema} and should not be used outside it.
- *
- * @see module:engine/model/schema~Schema
- * @protected
- */
-export class SchemaItem {
-	/**
-	 * Creates SchemaItem instance.
-	 *
-	 * @param {module:engine/model/schema~Schema} schema Schema instance that owns this item.
-	 */
-	constructor( schema ) {
-		/**
-		 * Schema instance that owns this item.
-		 *
-		 * @private
-		 * @member {module:engine/model/schema~Schema} module:engine/model/schema~SchemaItem#_schema
-		 */
-		this._schema = schema;
-
-		/**
-		 * Paths in which the entity, represented by this item, is allowed.
-		 *
-		 * @private
-		 * @member {Array} module:engine/model/schema~SchemaItem#_allowed
-		 */
-		this._allowed = [];
-
-		/**
-		 * Paths in which the entity, represented by this item, is disallowed.
-		 *
-		 * @private
-		 * @member {Array} module:engine/model/schema~SchemaItem#_disallowed
-		 */
-		this._disallowed = [];
-
-		/**
-		 * Attributes that are required by the entity represented by this item.
-		 *
-		 * @protected
-		 * @member {Array} module:engine/model/schema~SchemaItem#_requiredAttributes
-		 */
-		this._requiredAttributes = [];
-	}
+mix( Schema, ObservableMixin );
 
-	/**
-	 * Allows entity, represented by this item, to be in given path.
-	 *
-	 * @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, attributes ) {
-		this._addPath( '_allowed', path, attributes );
-	}
+function compileBaseItemRule( sourceItemRules, itemName ) {
+	const itemRule = {
+		name: itemName,
 
-	/**
-	 * Disallows entity, represented by this item, to be in given path.
-	 *
-	 * @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 );
-	}
+		allowIn: [],
+		allowContentOf: [],
+		allowWhere: [],
 
-	/**
-	 * 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.
-	 */
-	requireAttributes( attributes ) {
-		this._requiredAttributes.push( attributes );
-	}
+		allowAttributes: [],
+		allowAttributesOf: []
+	};
 
-	/**
-	 * Custom toJSON method to solve child-parent circular dependencies.
-	 *
-	 * @returns {Object} Clone of this object with the parent property replaced with its name.
-	 */
-	toJSON() {
-		const json = clone( this );
+	copyTypes( sourceItemRules, itemRule );
 
-		// Due to circular references we need to remove parent reference.
-		json._schema = '[model.Schema]';
+	copyProperty( sourceItemRules, itemRule, 'allowIn' );
+	copyProperty( sourceItemRules, itemRule, 'allowContentOf' );
+	copyProperty( sourceItemRules, itemRule, 'allowWhere' );
 
-		return json;
-	}
+	copyProperty( sourceItemRules, itemRule, 'allowAttributes' );
+	copyProperty( sourceItemRules, itemRule, 'allowAttributesOf' );
 
-	/**
-	 * Adds path to the SchemaItem instance.
-	 *
-	 * @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>} 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, attributes ) {
-		path = path.slice();
-
-		if ( !isArray( attributes ) ) {
-			attributes = [ attributes ];
-		}
+	return itemRule;
+}
+
+function compileAllowContentOf( compiledRules, itemName ) {
+	for ( const allowContentOfItemName of compiledRules[ itemName ].allowContentOf ) {
+		// The allowContentOf property may point to an unregistered element.
+		if ( compiledRules[ allowContentOfItemName ] ) {
+			const allowedChildren = getAllowedChildren( compiledRules, allowContentOfItemName );
 
-		for ( const attribute of attributes ) {
-			this[ member ].push( { path, attribute } );
+			allowedChildren.forEach( allowedItem => {
+				allowedItem.allowIn.push( itemName );
+			} );
 		}
 	}
 
-	/**
-	 * Returns all paths of given type that were previously registered in the item.
-	 *
-	 * @private
-	 * @param {String} type Paths' type. Possible values are `allow` or `disallow`.
-	 * @param {String} [attribute] If set, only paths registered for given attribute will be returned.
-	 * @returns {Array} Paths registered in the item.
-	 */
-	_getPaths( type, attribute ) {
-		const source = type === 'allow' ? this._allowed : this._disallowed;
-		const paths = [];
-
-		for ( const item of source ) {
-			if ( item.attribute === attribute ) {
-				paths.push( item.path );
-			}
-		}
+	delete compiledRules[ itemName ].allowContentOf;
+}
 
-		return paths;
-	}
+function compileAllowWhere( compiledRules, itemName ) {
+	for ( const allowWhereItemName of compiledRules[ itemName ].allowWhere ) {
+		const inheritFrom = compiledRules[ allowWhereItemName ];
 
-	/**
-	 * Checks whether given set of attributes fulfills required attributes of this item.
-	 *
-	 * @protected
-	 * @see module:engine/model/schema~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 ( const attributeSet of this._requiredAttributes ) {
-			found = true;
-
-			for ( const attribute of attributeSet ) {
-				if ( attributesToCheck.indexOf( attribute ) == -1 ) {
-					found = false;
-					break;
-				}
-			}
+		// The allowWhere property may point to an unregistered element.
+		if ( inheritFrom ) {
+			const allowedIn = inheritFrom.allowIn;
 
-			if ( found ) {
-				break;
-			}
+			compiledRules[ itemName ].allowIn.push( ...allowedIn );
 		}
-
-		return found;
 	}
 
-	/**
-	 * Checks whether this item has any registered path of given type that matches the provided path.
-	 *
-	 * @protected
-	 * @param {String} type Paths' type. Possible values are `allow` or `disallow`.
-	 * @param {Array.<String>} pathToCheck Path to check.
-	 * @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, pathToCheck, attribute ) {
-		const registeredPaths = this._getPaths( type, attribute );
-
-		for ( const registeredPathPath of registeredPaths ) {
-			if ( matchPaths( this._schema, pathToCheck, registeredPathPath ) ) {
-				return true;
-			}
-		}
+	delete compiledRules[ itemName ].allowWhere;
+}
+
+function compileAllowAttributesOf( compiledRules, itemName ) {
+	for ( const allowAttributeOfItem of compiledRules[ itemName ].allowAttributesOf ) {
+		const inheritFrom = compiledRules[ allowAttributeOfItem ];
+
+		if ( inheritFrom ) {
+			const inheritAttributes = inheritFrom.allowAttributes;
 
-		return false;
+			compiledRules[ itemName ].allowAttributes.push( ...inheritAttributes );
+		}
 	}
+
+	delete compiledRules[ itemName ].allowAttributesOf;
 }
 
-/**
- * Object with query used by {@link module:engine/model/schema~Schema} to query schema or add allow/disallow rules to schema.
- *
- * @typedef {Object} module:engine/model/schema~SchemaQuery
- * @property {String} name Entity name.
- * @property {module:engine/model/schema~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.
- */
+// Remove items which weren't registered (because it may break some checks or we'd need to complicate them).
+// Make sure allowIn doesn't contain repeated values.
+function cleanUpAllowIn( compiledRules, itemName ) {
+	const itemRule = compiledRules[ itemName ];
+	const existingItems = itemRule.allowIn.filter( itemToCheck => compiledRules[ itemToCheck ] );
 
-/**
- * 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|module:engine/model/element~Element>|module:engine/model/position~Position}
- * module:engine/model/schema~SchemaPath
- */
+	itemRule.allowIn = Array.from( new Set( existingItems ) );
+}
 
-// Checks whether the given pathToCheck and registeredPath right ends match.
-//
-// pathToCheck: C, D
-// registeredPath: A, B, C, D
-// result: OK
-//
-// pathToCheck: A, B, C
-// registeredPath: A, B, C, D
-// result: NOK
-//
-// Note – when matching paths, element extension chains (inheritance) are taken into consideration.
-//
-// @param {Schema} schema
-// @param {Array.<String>} pathToCheck
-// @param {Array.<String>} registeredPath
-function matchPaths( schema, pathToCheck, registeredPath ) {
-	// Start checking from the right end of both tables.
-	let registeredPathIndex = registeredPath.length - 1;
-	let pathToCheckIndex = pathToCheck.length - 1;
-
-	// And finish once reaching an end of the shorter table.
-	while ( registeredPathIndex >= 0 && pathToCheckIndex >= 0 ) {
-		const checkName = pathToCheck[ pathToCheckIndex ];
-
-		// Fail when checking a path which contains element which aren't even registered to the schema.
-		if ( !schema.hasItem( checkName ) ) {
-			return false;
-		}
+function cleanUpAllowAttributes( compiledRules, itemName ) {
+	const itemRule = compiledRules[ itemName ];
+
+	itemRule.allowAttributes = Array.from( new Set( itemRule.allowAttributes ) );
+}
 
-		const extChain = schema._extensionChains.get( checkName );
+function copyTypes( sourceItemRules, itemRule ) {
+	for ( const sourceItemRule of sourceItemRules ) {
+		const typeNames = Object.keys( sourceItemRule ).filter( name => name.startsWith( 'is' ) );
 
-		if ( extChain.includes( registeredPath[ registeredPathIndex ] ) ) {
-			registeredPathIndex--;
-			pathToCheckIndex--;
-		} else {
-			return false;
+		for ( const name of typeNames ) {
+			itemRule[ name ] = sourceItemRule[ name ];
 		}
 	}
+}
 
-	return true;
+function copyProperty( sourceItemRules, itemRule, propertyName ) {
+	for ( const sourceItemRule of sourceItemRules ) {
+		if ( typeof sourceItemRule[ propertyName ] == 'string' ) {
+			itemRule[ propertyName ].push( sourceItemRule[ propertyName ] );
+		} else if ( Array.isArray( sourceItemRule[ propertyName ] ) ) {
+			itemRule[ propertyName ].push( ...sourceItemRule[ propertyName ] );
+		}
+	}
 }
 
-/**
- * Item with specified name does not exist in schema.
- *
- * @error model-schema-no-item
- */
+function getAllowedChildren( compiledRules, itemName ) {
+	const itemRule = compiledRules[ itemName ];
+
+	return getValues( compiledRules ).filter( rule => rule.allowIn.includes( itemRule.name ) );
+}
+
+function getContext( node ) {
+	return node.getAncestors( { includeSelf: true } ).map( node => {
+		return {
+			name: node.is( 'text' ) ? '$text' : node.name,
+			* getAttributes() {
+				yield* node.getAttributes();
+			}
+		};
+	} );
+}
+
+function getValues( obj ) {
+	return Object.keys( obj ).map( key => obj[ key ] );
+}

+ 0 - 300
packages/ckeditor5-engine/src/model/schema2.js

@@ -1,300 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
-import mix from '@ckeditor/ckeditor5-utils/src/mix';
-
-/**
- * @module engine/model/schema
- */
-
-/**
- * @mixes module:utils/emittermixin~ObservableMixin
- */
-export default class Schema {
-	constructor() {
-		this._sourceRules = {};
-
-		// TODO events
-		this.decorate( 'checkChild' );
-		this.decorate( 'checkAttribute' );
-
-		this.on( 'checkAttribute', ( evt, args ) => {
-			args[ 0 ] = getContext( args[ 0 ] );
-		}, { priority: 'highest' } );
-
-		this.on( 'checkChild', ( evt, args ) => {
-			args[ 0 ] = getContext( args[ 0 ] );
-		}, { priority: 'highest' } );
-	}
-
-	register( itemName, rules ) {
-		if ( this._sourceRules[ itemName ] ) {
-			// TODO docs
-			throw new CKEditorError( 'schema-cannot-register-item-twice: A single item cannot be registered twice in the schema.' );
-		}
-
-		this._sourceRules[ itemName ] = [
-			Object.assign( {}, rules )
-		];
-
-		this._clearCache();
-	}
-
-	extend( itemName, rules ) {
-		// TODO it should not throw if we want to allow e.g. adding attrs before element is registered
-		// (which may be done by another feature).
-		if ( !this._sourceRules[ itemName ] ) {
-			// TODO docs
-			throw new CKEditorError( 'schema-cannot-extend-missing-item: Cannot extend an item which was not registered yet.' );
-		}
-
-		this._sourceRules[ itemName ].push( Object.assign( {}, rules ) );
-
-		this._clearCache();
-	}
-
-	getRules() {
-		if ( !this._compiledRules ) {
-			this._compile();
-		}
-
-		return this._compiledRules;
-	}
-
-	getRule( item ) {
-		let itemName;
-
-		if ( typeof item == 'string' ) {
-			itemName = item;
-		} else if ( item.is && item.is( 'text' ) ) {
-			itemName = '$text';
-		}
-		// Element or context item.
-		else {
-			itemName = item.name;
-		}
-
-		return this.getRules()[ itemName ];
-	}
-
-	isRegistered( itemName ) {
-		return !!this.getRule( itemName );
-	}
-
-	isBlock( itemName ) {
-		const rule = this.getRule( itemName );
-
-		return !!( rule && rule.isBlock );
-	}
-
-	isLimit( itemName ) {
-		const rule = this.getRule( itemName );
-
-		return !!( rule && rule.isLimit );
-	}
-
-	isObject( itemName ) {
-		const rule = this.getRule( itemName );
-
-		return !!( rule && rule.isObject );
-	}
-
-	checkChild( context, child ) {
-		const rule = this.getRule( child );
-
-		if ( !rule ) {
-			return false;
-		}
-
-		return this._checkContextMatch( rule, context );
-	}
-
-	checkAttribute( context, attributeName ) {
-		const rule = this.getRule( context[ context.length - 1 ] );
-
-		if ( !rule ) {
-			return false;
-		}
-
-		return rule.allowAttributes.includes( attributeName );
-	}
-
-	_clearCache() {
-		this._compiledRules = null;
-	}
-
-	_compile() {
-		const compiledRules = {};
-		const sourceRules = this._sourceRules;
-		const itemNames = Object.keys( sourceRules );
-
-		for ( const itemName of itemNames ) {
-			compiledRules[ itemName ] = compileBaseItemRule( sourceRules[ itemName ], itemName );
-		}
-
-		for ( const itemName of itemNames ) {
-			compileAllowContentOf( compiledRules, itemName );
-		}
-
-		for ( const itemName of itemNames ) {
-			compileAllowWhere( compiledRules, itemName );
-		}
-
-		for ( const itemName of itemNames ) {
-			compileAllowAttributesOf( compiledRules, itemName );
-		}
-
-		for ( const itemName of itemNames ) {
-			cleanUpAllowIn( compiledRules, itemName );
-			cleanUpAllowAttributes( compiledRules, itemName );
-		}
-
-		this._compiledRules = compiledRules;
-	}
-
-	_checkContextMatch( rule, context, contextItemIndex = context.length - 1 ) {
-		const contextItem = context[ contextItemIndex ];
-
-		if ( rule.allowIn.includes( contextItem.name ) ) {
-			if ( contextItemIndex == 0 ) {
-				return true;
-			} else {
-				const parentRule = this.getRule( contextItem );
-
-				return this._checkContextMatch( parentRule, context, contextItemIndex - 1 );
-			}
-		} else {
-			return false;
-		}
-	}
-}
-
-mix( Schema, ObservableMixin );
-
-function compileBaseItemRule( sourceItemRules, itemName ) {
-	const itemRule = {
-		name: itemName,
-
-		allowIn: [],
-		allowContentOf: [],
-		allowWhere: [],
-
-		allowAttributes: [],
-		allowAttributesOf: []
-	};
-
-	copyTypes( sourceItemRules, itemRule );
-
-	copyProperty( sourceItemRules, itemRule, 'allowIn' );
-	copyProperty( sourceItemRules, itemRule, 'allowContentOf' );
-	copyProperty( sourceItemRules, itemRule, 'allowWhere' );
-
-	copyProperty( sourceItemRules, itemRule, 'allowAttributes' );
-	copyProperty( sourceItemRules, itemRule, 'allowAttributesOf' );
-
-	return itemRule;
-}
-
-function compileAllowContentOf( compiledRules, itemName ) {
-	for ( const allowContentOfItemName of compiledRules[ itemName ].allowContentOf ) {
-		// The allowContentOf property may point to an unregistered element.
-		if ( compiledRules[ allowContentOfItemName ] ) {
-			const allowedChildren = getAllowedChildren( compiledRules, allowContentOfItemName );
-
-			allowedChildren.forEach( allowedItem => {
-				allowedItem.allowIn.push( itemName );
-			} );
-		}
-	}
-
-	delete compiledRules[ itemName ].allowContentOf;
-}
-
-function compileAllowWhere( compiledRules, itemName ) {
-	for ( const allowWhereItemName of compiledRules[ itemName ].allowWhere ) {
-		const inheritFrom = compiledRules[ allowWhereItemName ];
-
-		// The allowWhere property may point to an unregistered element.
-		if ( inheritFrom ) {
-			const allowedIn = inheritFrom.allowIn;
-
-			compiledRules[ itemName ].allowIn.push( ...allowedIn );
-		}
-	}
-
-	delete compiledRules[ itemName ].allowWhere;
-}
-
-function compileAllowAttributesOf( compiledRules, itemName ) {
-	for ( const allowAttributeOfItem of compiledRules[ itemName ].allowAttributesOf ) {
-		const inheritFrom = compiledRules[ allowAttributeOfItem ];
-
-		if ( inheritFrom ) {
-			const inheritAttributes = inheritFrom.allowAttributes;
-
-			compiledRules[ itemName ].allowAttributes.push( ...inheritAttributes );
-		}
-	}
-
-	delete compiledRules[ itemName ].allowAttributesOf;
-}
-
-// Remove items which weren't registered (because it may break some checks or we'd need to complicate them).
-// Make sure allowIn doesn't contain repeated values.
-function cleanUpAllowIn( compiledRules, itemName ) {
-	const itemRule = compiledRules[ itemName ];
-	const existingItems = itemRule.allowIn.filter( itemToCheck => compiledRules[ itemToCheck ] );
-
-	itemRule.allowIn = Array.from( new Set( existingItems ) );
-}
-
-function cleanUpAllowAttributes( compiledRules, itemName ) {
-	const itemRule = compiledRules[ itemName ];
-
-	itemRule.allowAttributes = Array.from( new Set( itemRule.allowAttributes ) );
-}
-
-function copyTypes( sourceItemRules, itemRule ) {
-	for ( const sourceItemRule of sourceItemRules ) {
-		const typeNames = Object.keys( sourceItemRule ).filter( name => name.startsWith( 'is' ) );
-
-		for ( const name of typeNames ) {
-			itemRule[ name ] = sourceItemRule[ name ];
-		}
-	}
-}
-
-function copyProperty( sourceItemRules, itemRule, propertyName ) {
-	for ( const sourceItemRule of sourceItemRules ) {
-		if ( typeof sourceItemRule[ propertyName ] == 'string' ) {
-			itemRule[ propertyName ].push( sourceItemRule[ propertyName ] );
-		} else if ( Array.isArray( sourceItemRule[ propertyName ] ) ) {
-			itemRule[ propertyName ].push( ...sourceItemRule[ propertyName ] );
-		}
-	}
-}
-
-function getAllowedChildren( compiledRules, itemName ) {
-	const itemRule = compiledRules[ itemName ];
-
-	return getValues( compiledRules ).filter( rule => rule.allowIn.includes( itemRule.name ) );
-}
-
-function getContext( node ) {
-	return node.getAncestors( { includeSelf: true } ).map( node => {
-		return {
-			name: node.is( 'text' ) ? '$text' : node.name,
-			* getAttributes() {
-				yield* node.getAttributes();
-			}
-		};
-	} );
-}
-
-function getValues( obj ) {
-	return Object.keys( obj ).map( key => obj[ key ] );
-}

+ 2 - 2
packages/ckeditor5-engine/tests/model/schema/schema2.js → packages/ckeditor5-engine/tests/model/schema.js

@@ -3,10 +3,10 @@
  * For licensing, see LICENSE.md.
  */
 
-import Schema from '../../../src/model/schema2';
+import Schema from '../../src/model/schema';
 
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-import Element from '../../../src/model/element';
+import Element from '../../src/model/element';
 
 describe( 'Schema', () => {
 	let schema, root1, r1p1, r1p2, r1bQ, r1bQp, root2;

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

@@ -1,921 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import { default as Schema, SchemaItem } from '../../../src/model/schema';
-import Model from '../../../src/model/model';
-import Element from '../../../src/model/element';
-import Text from '../../../src/model/text';
-import DocumentFragment from '../../../src/model/documentfragment';
-import Position from '../../../src/model/position';
-import Range from '../../../src/model/range';
-import Selection from '../../../src/model/selection';
-import AttributeDelta from '../../../src/model/delta/attributedelta';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
-import { setData, getData, stringify } from '../../../src/dev-utils/model';
-
-testUtils.createSinonSandbox();
-
-describe( 'Schema', () => {
-	let schema;
-
-	beforeEach( () => {
-		schema = new Schema();
-	} );
-
-	describe( 'constructor()', () => {
-		it( 'should register base items: inline, block, root', () => {
-			testUtils.sinon.spy( Schema.prototype, 'registerItem' );
-
-			schema = new Schema();
-
-			expect( schema.registerItem.calledWithExactly( '$root', null ) );
-			expect( schema.registerItem.calledWithExactly( '$block', null ) );
-			expect( schema.registerItem.calledWithExactly( '$inline', null ) );
-		} );
-
-		it( 'should allow block in root', () => {
-			expect( schema.check( { name: '$block', inside: [ '$root' ] } ) ).to.be.true;
-		} );
-
-		it( 'should allow inline in block', () => {
-			expect( schema.check( { name: '$inline', inside: [ '$block' ] } ) ).to.be.true;
-		} );
-
-		it( 'should create the objects set', () => {
-			expect( schema.objects ).to.be.instanceOf( Set );
-		} );
-
-		it( 'should create the limits set', () => {
-			expect( schema.limits ).to.be.instanceOf( Set );
-		} );
-
-		it( 'should mark $root as a limit element', () => {
-			expect( schema.limits.has( '$root' ) ).to.be.true;
-		} );
-
-		describe( '$clipboardHolder', () => {
-			it( 'should allow $block', () => {
-				expect( schema.check( { name: '$block', inside: [ '$clipboardHolder' ] } ) ).to.be.true;
-			} );
-
-			it( 'should allow $inline', () => {
-				expect( schema.check( { name: '$inline', inside: [ '$clipboardHolder' ] } ) ).to.be.true;
-			} );
-
-			it( 'should allow $text', () => {
-				expect( schema.check( { name: '$text', inside: [ '$clipboardHolder' ] } ) ).to.be.true;
-			} );
-		} );
-	} );
-
-	describe( 'registerItem()', () => {
-		it( 'should register in schema item under given name', () => {
-			schema.registerItem( 'new' );
-
-			expect( schema.hasItem( 'new' ) ).to.be.true;
-		} );
-
-		it( 'should build correct base chains', () => {
-			schema.registerItem( 'first' );
-			schema.registerItem( 'secondA', 'first' );
-			schema.registerItem( 'secondB', 'first' );
-			schema.registerItem( 'third', 'secondA' );
-
-			expect( schema._extensionChains.get( 'first' ) ).to.deep.equal( [ 'first' ] );
-			expect( schema._extensionChains.get( 'secondA' ) ).to.deep.equal( [ 'first', 'secondA' ] );
-			expect( schema._extensionChains.get( 'secondB' ) ).to.deep.equal( [ 'first', 'secondB' ] );
-			expect( schema._extensionChains.get( 'third' ) ).to.deep.equal( [ 'first', 'secondA', 'third' ] );
-		} );
-
-		it( 'should make registered item inherit allows from base item', () => {
-			schema.registerItem( 'image', '$inline' );
-
-			expect( schema.check( { name: 'image', inside: [ '$block' ] } ) ).to.be.true;
-		} );
-
-		it( 'should throw if item with given name has already been registered in schema', () => {
-			schema.registerItem( 'new' );
-
-			expect( () => {
-				schema.registerItem( 'new' );
-			} ).to.throw( CKEditorError, /model-schema-item-exists/ );
-		} );
-
-		it( 'should throw if base item has not been registered in schema', () => {
-			expect( () => {
-				schema.registerItem( 'new', 'old' );
-			} ).to.throw( CKEditorError, /model-schema-no-item/ );
-		} );
-	} );
-
-	describe( 'hasItem()', () => {
-		it( 'should return true if given item name has been registered in schema', () => {
-			expect( schema.hasItem( '$block' ) ).to.be.true;
-		} );
-
-		it( 'should return false if given item name has not been registered in schema', () => {
-			expect( schema.hasItem( 'new' ) ).to.be.false;
-		} );
-	} );
-
-	describe( '_getItem()', () => {
-		it( 'should return SchemaItem registered under given name', () => {
-			schema.registerItem( 'new' );
-
-			const item = schema._getItem( 'new' );
-
-			expect( item ).to.be.instanceof( SchemaItem );
-		} );
-
-		it( 'should throw if there is no item registered under given name', () => {
-			expect( () => {
-				schema._getItem( 'new' );
-			} ).to.throw( CKEditorError, /model-schema-no-item/ );
-		} );
-	} );
-
-	describe( 'allow()', () => {
-		it( 'should add passed query to allowed in schema', () => {
-			schema.registerItem( 'p', '$block' );
-			schema.registerItem( 'div', '$block' );
-
-			expect( schema.check( { name: 'p', inside: [ 'div' ] } ) ).to.be.false;
-
-			schema.allow( { name: 'p', inside: 'div' } );
-
-			expect( schema.check( { name: 'p', inside: [ 'div' ] } ) ).to.be.true;
-		} );
-	} );
-
-	describe( 'disallow()', () => {
-		it( 'should add passed query to disallowed in schema', () => {
-			schema.registerItem( 'p', '$block' );
-			schema.registerItem( 'div', '$block' );
-
-			schema.allow( { name: '$block', attributes: 'bold', inside: 'div' } );
-
-			expect( schema.check( { name: 'p', attributes: 'bold', inside: [ 'div' ] } ) ).to.be.true;
-
-			schema.disallow( { name: 'p', attributes: 'bold', inside: 'div' } );
-
-			expect( schema.check( { name: 'p', attributes: 'bold', inside: [ 'div' ] } ) ).to.be.false;
-		} );
-	} );
-
-	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;
-			} );
-
-			it( 'should omit path elements that are added to schema', () => {
-				expect( schema.check( { name: '$inline', inside: '$block new $block' } ) ).to.be.true;
-			} );
-
-			it( 'should ignore attributes stored in elements by document selection', () => {
-				expect( schema.check( { name: '$block', attributes: 'selection:foo', inside: '$root' } ) ).to.be.true;
-			} );
-
-			it( 'should disallow attribute stored in an element if that attribute was explicitly disallowed', () => {
-				schema.disallow( { name: '$block', attributes: [ 'selection:foo' ], inside: '$root' } );
-
-				expect( schema.check( { name: '$block', attributes: [ 'selection:foo' ], inside: '$root' } ) ).to.be.false;
-			} );
-		} );
-
-		describe( 'array of elements as inside', () => {
-			beforeEach( () => {
-				schema.registerItem( 'div', '$block' );
-				schema.registerItem( 'header', '$block' );
-				schema.registerItem( 'p', '$block' );
-				schema.registerItem( 'img', '$inline' );
-
-				schema.allow( { name: '$block', inside: 'div' } );
-				schema.allow( { name: '$inline', attributes: 'bold', inside: '$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;
-
-				// 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;
-
-				// 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;
-			} );
-		} );
-
-		describe( 'position as inside', () => {
-			let doc, root;
-
-			beforeEach( () => {
-				const model = new Model();
-				doc = model.document;
-				root = doc.createRoot( 'div' );
-
-				root.insertChildren( 0, [
-					new Element( 'div' ),
-					new Element( 'header' ),
-					new Element( 'p' )
-				] );
-
-				schema.registerItem( 'div', '$block' );
-				schema.registerItem( 'header', '$block' );
-				schema.registerItem( 'p', '$block' );
-
-				schema.allow( { name: '$block', inside: 'div' } );
-				schema.allow( { name: '$inline', attributes: 'bold', inside: '$block' } );
-
-				schema.disallow( { name: '$inline', attributes: 'bold', inside: 'header' } );
-			} );
-
-			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;
-
-				// P is block and block should be allowed in root.
-				expect( schema.check( { name: 'p', inside: new Position( root, [ 0 ] ) } ) ).to.be.true;
-
-				// P is allowed in DIV by the set rule.
-				expect( schema.check( { name: 'p', inside: new Position( root, [ 0, 0 ] ) } ) ).to.be.true;
-
-				// 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;
-
-				// Header is allowed in DIV.
-				expect( schema.check( { name: 'header', inside: new Position( root, [ 0, 0 ] ) } ) ).to.be.true;
-
-				// 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;
-			} );
-
-			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;
-			} );
-		} );
-
-		describe( 'bug #732', () => {
-			// Ticket case.
-			it( 'should return false if given element is allowed in the root but not deeper', () => {
-				schema.registerItem( 'paragraph', '$block' );
-
-				expect( schema.check( { name: 'paragraph', inside: [ '$root', 'paragraph' ] } ) ).to.be.false;
-			} );
-
-			// Two additional, real life cases accompanying the ticket case.
-			it( 'should return true if checking whether text is allowed in $root > paragraph', () => {
-				schema.registerItem( 'paragraph', '$block' );
-
-				expect( schema.check( { name: '$text', inside: [ '$root', 'paragraph' ] } ) ).to.be.true;
-			} );
-
-			it( 'should return true if checking whether text is allowed in paragraph', () => {
-				schema.registerItem( 'paragraph', '$block' );
-
-				expect( schema.check( { name: '$text', inside: [ 'paragraph' ] } ) ).to.be.true;
-			} );
-
-			// Veryfing the matching algorithm.
-			// The right ends of the element to check and "inside" paths must match.
-			describe( 'right ends of paths must match', () => {
-				beforeEach( () => {
-					schema.registerItem( 'a' );
-					schema.registerItem( 'b' );
-					schema.registerItem( 'c' );
-					schema.registerItem( 'd' );
-					schema.registerItem( 'e' );
-
-					schema.allow( { name: 'a', inside: [ 'b', 'c', 'd' ] } );
-					schema.allow( { name: 'e', inside: [ 'a' ] } );
-				} );
-
-				// Simple chains created by a single allow() call.
-
-				it( 'a inside b, c', () => {
-					expect( schema.check( { name: 'a', inside: [ 'b', 'c' ] } ) ).to.be.false;
-				} );
-
-				it( 'a inside b', () => {
-					expect( schema.check( { name: 'a', inside: [ 'b' ] } ) ).to.be.false;
-				} );
-
-				it( 'a inside b, c, d', () => {
-					expect( schema.check( { name: 'a', inside: [ 'b', 'c', 'd' ] } ) ).to.be.true;
-				} );
-
-				it( 'a inside c, d', () => {
-					expect( schema.check( { name: 'a', inside: [ 'c', 'd' ] } ) ).to.be.true;
-				} );
-
-				it( 'a inside d', () => {
-					expect( schema.check( { name: 'a', inside: [ 'd' ] } ) ).to.be.true;
-				} );
-
-				// "Allowed in" chains created by two separate allow() calls (`e inside a` and `a inside b,c,d`).
-
-				it( 'e inside a, d', () => {
-					expect( schema.check( { name: 'e', inside: [ 'd', 'a' ] } ) ).to.be.true;
-				} );
-
-				it( 'e inside b, c, d', () => {
-					expect( schema.check( { name: 'e', inside: [ 'b', 'c', 'd' ] } ) ).to.be.false;
-				} );
-			} );
-		} );
-	} );
-
-	describe( 'itemExtends()', () => {
-		it( 'should return true if given item extends another given item', () => {
-			schema.registerItem( 'div', '$block' );
-			schema.registerItem( 'myDiv', 'div' );
-
-			expect( schema.itemExtends( 'div', '$block' ) ).to.be.true;
-			expect( schema.itemExtends( 'myDiv', 'div' ) ).to.be.true;
-			expect( schema.itemExtends( 'myDiv', '$block' ) ).to.be.true;
-		} );
-
-		it( 'should return false if given item does not extend another given item', () => {
-			schema.registerItem( 'div' );
-			schema.registerItem( 'myDiv', 'div' );
-
-			expect( schema.itemExtends( 'div', '$block' ) ).to.be.false;
-			expect( schema.itemExtends( 'div', 'myDiv' ) ).to.be.false;
-		} );
-
-		it( 'should throw if one or both given items are not registered in schema', () => {
-			expect( () => {
-				schema.itemExtends( 'foo', '$block' );
-			} ).to.throw( CKEditorError, /model-schema-no-item/ );
-
-			expect( () => {
-				schema.itemExtends( '$block', 'foo' );
-			} ).to.throw( CKEditorError, /model-schema-no-item/ );
-		} );
-	} );
-
-	describe( '_normalizeQueryPath()', () => {
-		it( 'should normalize string with spaces to an array of strings', () => {
-			expect( Schema._normalizeQueryPath( '$root div strong' ) ).to.deep.equal( [ '$root', 'div', 'strong' ] );
-		} );
-
-		it( 'should normalize model position to an array of strings', () => {
-			const model = new Model();
-			const doc = model.document;
-			const root = doc.createRoot();
-
-			root.insertChildren( 0, [
-				new Element( 'div', null, [
-					new Element( 'header' )
-				] )
-			] );
-
-			const position = new Position( root, [ 0, 0, 0 ] );
-
-			expect( Schema._normalizeQueryPath( position ) ).to.deep.equal( [ '$root', 'div', 'header' ] );
-		} );
-
-		it( 'should normalize array with strings and model elements to an array of strings and drop unrecognized parts', () => {
-			const input = [
-				'$root',
-				[ 'div' ],
-				new Element( 'div' ),
-				null,
-				new Element( 'p' ),
-				'strong'
-			];
-
-			expect( Schema._normalizeQueryPath( input ) ).to.deep.equal( [ '$root', 'div', 'p', 'strong' ] );
-		} );
-	} );
-
-	describe( 'checkAttributeInSelection()', () => {
-		const attribute = 'bold';
-		let model, doc, schema;
-
-		beforeEach( () => {
-			model = new Model();
-			doc = model.document;
-			doc.createRoot();
-
-			schema = model.schema;
-
-			schema.registerItem( 'p', '$block' );
-			schema.registerItem( 'h1', '$block' );
-			schema.registerItem( 'img', '$inline' );
-			schema.registerItem( 'figure' );
-
-			// Bold text is allowed only in P.
-			schema.allow( { name: '$text', attributes: 'bold', inside: 'p' } );
-			schema.allow( { name: 'p', attributes: 'bold', inside: '$root' } );
-
-			// Disallow bold on image.
-			schema.disallow( { name: 'img', attributes: 'bold', inside: '$root' } );
-
-			// Figure must have name attribute and optional title attribute.
-			schema.requireAttributes( 'figure', [ 'name' ] );
-			schema.allow( { name: 'figure', attributes: [ 'title', 'name' ], inside: '$root' } );
-		} );
-
-		describe( 'when selection is collapsed', () => {
-			it( 'should return true if characters with the attribute can be placed at caret position', () => {
-				setData( model, '<p>f[]oo</p>' );
-				expect( schema.checkAttributeInSelection( doc.selection, attribute ) ).to.be.true;
-			} );
-
-			it( 'should return false if characters with the attribute cannot be placed at caret position', () => {
-				setData( model, '<h1>[]</h1>' );
-				expect( schema.checkAttributeInSelection( doc.selection, attribute ) ).to.be.false;
-
-				setData( model, '[]' );
-				expect( schema.checkAttributeInSelection( doc.selection, attribute ) ).to.be.false;
-			} );
-		} );
-
-		describe( 'when selection is not collapsed', () => {
-			it( 'should return true if there is at least one node in selection that can have the attribute', () => {
-				// Simple selection on a few characters.
-				setData( model, '<p>[foo]</p>' );
-				expect( schema.checkAttributeInSelection( doc.selection, attribute ) ).to.be.true;
-
-				// Selection spans over characters but also include nodes that can't have attribute.
-				setData( model, '<p>fo[o<img />b]ar</p>' );
-				expect( schema.checkAttributeInSelection( doc.selection, attribute ) ).to.be.true;
-
-				// Selection on whole root content. Characters in P can have an attribute so it's valid.
-				setData( model, '[<p>foo<img />bar</p><h1></h1>]' );
-				expect( schema.checkAttributeInSelection( doc.selection, attribute ) ).to.be.true;
-
-				// Selection on empty P. P can have the attribute.
-				setData( model, '[<p></p>]' );
-				expect( schema.checkAttributeInSelection( doc.selection, attribute ) ).to.be.true;
-			} );
-
-			it( 'should return false if there are no nodes in selection that can have the attribute', () => {
-				// Selection on DIV which can't have bold text.
-				setData( model, '[<h1></h1>]' );
-				expect( schema.checkAttributeInSelection( doc.selection, attribute ) ).to.be.false;
-
-				// Selection on two images which can't be bold.
-				setData( model, '<p>foo[<img /><img />]bar</p>' );
-				expect( schema.checkAttributeInSelection( doc.selection, attribute ) ).to.be.false;
-			} );
-
-			it( 'should return true when checking element with required attribute', () => {
-				setData( model, '[<figure name="figure"></figure>]' );
-				expect( schema.checkAttributeInSelection( doc.selection, 'title' ) ).to.be.true;
-			} );
-
-			it( 'should return true when checking element when attribute is already present', () => {
-				setData( model, '[<figure name="figure" title="title"></figure>]' );
-				expect( schema.checkAttributeInSelection( doc.selection, 'title' ) ).to.be.true;
-			} );
-		} );
-	} );
-
-	describe( 'getValidRanges()', () => {
-		const attribute = 'bold';
-		let model, doc, root, schema, ranges;
-
-		beforeEach( () => {
-			model = new Model();
-			doc = model.document;
-			schema = model.schema;
-			root = doc.createRoot();
-
-			schema.registerItem( 'p', '$block' );
-			schema.registerItem( 'h1', '$block' );
-			schema.registerItem( 'img', '$inline' );
-
-			schema.allow( { name: '$text', attributes: 'bold', inside: 'p' } );
-			schema.allow( { name: 'p', attributes: 'bold', inside: '$root' } );
-
-			setData( model, '<p>foo<img />bar</p>' );
-			ranges = [ Range.createOn( root.getChild( 0 ) ) ];
-		} );
-
-		it( 'should return unmodified ranges when attribute is allowed on each item (text is not allowed in img)', () => {
-			schema.allow( { name: 'img', attributes: 'bold', inside: 'p' } );
-
-			expect( schema.getValidRanges( ranges, attribute ) ).to.deep.equal( ranges );
-		} );
-
-		it( 'should return unmodified ranges when attribute is allowed on each item (text is allowed in img)', () => {
-			schema.allow( { name: 'img', attributes: 'bold', inside: 'p' } );
-			schema.allow( { name: '$text', inside: 'img' } );
-
-			expect( schema.getValidRanges( ranges, attribute ) ).to.deep.equal( ranges );
-		} );
-
-		it( 'should return two ranges when attribute is not allowed on one item', () => {
-			schema.allow( { name: 'img', attributes: 'bold', inside: 'p' } );
-			schema.allow( { name: '$text', inside: 'img' } );
-
-			setData( model, '[<p>foo<img>xxx</img>bar</p>]' );
-
-			const validRanges = schema.getValidRanges( doc.selection.getRanges(), attribute );
-			const sel = new Selection();
-			sel.setRanges( validRanges );
-
-			expect( stringify( root, sel ) ).to.equal( '[<p>foo<img>]xxx[</img>bar</p>]' );
-		} );
-
-		it( 'should return three ranges when attribute is not allowed on one element but is allowed on its child', () => {
-			schema.allow( { name: '$text', inside: 'img' } );
-			schema.allow( { name: '$text', attributes: 'bold', inside: 'img' } );
-
-			setData( model, '[<p>foo<img>xxx</img>bar</p>]' );
-
-			const validRanges = schema.getValidRanges( doc.selection.getRanges(), attribute );
-			const sel = new Selection();
-			sel.setRanges( validRanges );
-
-			expect( stringify( root, sel ) ).to.equal( '[<p>foo]<img>[xxx]</img>[bar</p>]' );
-		} );
-
-		it( 'should not leak beyond the given ranges', () => {
-			setData( model, '<p>[foo<img></img>bar]x[bar<img></img>foo]</p>' );
-
-			const validRanges = schema.getValidRanges( doc.selection.getRanges(), attribute );
-			const sel = new Selection();
-			sel.setRanges( validRanges );
-
-			expect( stringify( root, sel ) ).to.equal( '<p>[foo]<img></img>[bar]x[bar]<img></img>[foo]</p>' );
-		} );
-
-		it( 'should correctly handle a range which ends in a disallowed position', () => {
-			schema.allow( { name: '$text', inside: 'img' } );
-
-			setData( model, '<p>[foo<img>bar]</img>bom</p>' );
-
-			const validRanges = schema.getValidRanges( doc.selection.getRanges(), attribute );
-			const sel = new Selection();
-			sel.setRanges( validRanges );
-
-			expect( stringify( root, sel ) ).to.equal( '<p>[foo]<img>bar</img>bom</p>' );
-		} );
-
-		it( 'should split range into two ranges and omit disallowed element', () => {
-			// Disallow bold on img.
-			model.schema.disallow( { name: 'img', attributes: 'bold', inside: 'p' } );
-
-			const result = schema.getValidRanges( ranges, attribute );
-
-			expect( result ).to.length( 2 );
-			expect( result[ 0 ].start.path ).to.members( [ 0 ] );
-			expect( result[ 0 ].end.path ).to.members( [ 0, 3 ] );
-			expect( result[ 1 ].start.path ).to.members( [ 0, 4 ] );
-			expect( result[ 1 ].end.path ).to.members( [ 1 ] );
-		} );
-	} );
-
-	describe( 'getLimitElement()', () => {
-		let model, doc, root;
-
-		beforeEach( () => {
-			model = new Model();
-			doc = model.document;
-			schema = model.schema;
-			root = doc.createRoot();
-
-			schema.registerItem( 'div', '$block' );
-			schema.registerItem( 'article', '$block' );
-			schema.registerItem( 'section', '$block' );
-			schema.registerItem( 'paragraph', '$block' );
-			schema.registerItem( 'widget', '$block' );
-			schema.registerItem( 'image', '$block' );
-			schema.registerItem( 'caption', '$block' );
-			schema.allow( { name: 'image', inside: 'widget' } );
-			schema.allow( { name: 'caption', inside: 'image' } );
-			schema.allow( { name: 'paragraph', inside: 'article' } );
-			schema.allow( { name: 'article', inside: 'section' } );
-			schema.allow( { name: 'section', inside: 'div' } );
-			schema.allow( { name: 'widget', inside: 'div' } );
-		} );
-
-		it( 'always returns $root element if any other limit was not defined', () => {
-			schema.limits.clear();
-
-			setData( model, '<div><section><article><paragraph>foo[]bar</paragraph></article></section></div>' );
-			expect( schema.getLimitElement( doc.selection ) ).to.equal( root );
-		} );
-
-		it( 'returns the limit element which is the closest element to common ancestor for collapsed selection', () => {
-			schema.limits.add( 'article' );
-			schema.limits.add( 'section' );
-
-			setData( model, '<div><section><article><paragraph>foo[]bar</paragraph></article></section></div>' );
-
-			const article = root.getNodeByPath( [ 0, 0, 0 ] );
-
-			expect( schema.getLimitElement( doc.selection ) ).to.equal( article );
-		} );
-
-		it( 'returns the limit element which is the closest element to common ancestor for non-collapsed selection', () => {
-			schema.limits.add( 'article' );
-			schema.limits.add( 'section' );
-
-			setData( model, '<div><section><article>[foo</article><article>bar]</article></section></div>' );
-
-			const section = root.getNodeByPath( [ 0, 0 ] );
-
-			expect( schema.getLimitElement( doc.selection ) ).to.equal( section );
-		} );
-
-		it( 'works fine with multi-range selections', () => {
-			schema.limits.add( 'article' );
-			schema.limits.add( 'widget' );
-			schema.limits.add( 'div' );
-
-			setData(
-				model,
-				'<div>' +
-					'<section>' +
-						'<article>' +
-							'<paragraph>[foo]</paragraph>' +
-						'</article>' +
-					'</section>' +
-					'<widget>' +
-						'<image>' +
-							'<caption>b[a]r</caption>' +
-						'</image>' +
-					'</widget>' +
-				'</div>'
-			);
-
-			const div = root.getNodeByPath( [ 0 ] );
-			expect( schema.getLimitElement( doc.selection ) ).to.equal( div );
-		} );
-
-		it( 'works fine with multi-range selections even if limit elements are not defined', () => {
-			schema.limits.clear();
-
-			setData(
-				model,
-				'<div>' +
-					'<section>' +
-						'<article>' +
-							'<paragraph>[foo]</paragraph>' +
-						'</article>' +
-					'</section>' +
-				'</div>' +
-				'<section>b[]ar</section>'
-			);
-
-			expect( schema.getLimitElement( doc.selection ) ).to.equal( root );
-		} );
-	} );
-
-	describe( 'removeDisallowedAttributes()', () => {
-		let model, doc, root;
-
-		beforeEach( () => {
-			model = new Model();
-			doc = model.document;
-			root = doc.createRoot();
-			schema = model.schema;
-
-			schema.registerItem( 'paragraph', '$block' );
-			schema.registerItem( 'div', '$block' );
-			schema.registerItem( 'image' );
-			schema.objects.add( 'image' );
-			schema.allow( { name: '$block', inside: 'div' } );
-		} );
-
-		describe( 'filtering attributes from nodes', () => {
-			let text, image;
-
-			beforeEach( () => {
-				schema.allow( { name: '$text', attributes: [ 'a' ], inside: '$root' } );
-				schema.allow( { name: 'image', attributes: [ 'b' ], inside: '$root' } );
-
-				text = new Text( 'foo', { a: 1, b: 1 } );
-				image = new Element( 'image', { a: 1, b: 1 } );
-			} );
-
-			it( 'should filter out disallowed attributes from given nodes', () => {
-				const root = doc.getRoot();
-
-				root.appendChildren( [ text, image ] );
-
-				model.change( writer => {
-					schema.removeDisallowedAttributes( [ text, image ], '$root', writer );
-
-					expect( Array.from( text.getAttributeKeys() ) ).to.deep.equal( [ 'a' ] );
-					expect( Array.from( image.getAttributeKeys() ) ).to.deep.equal( [ 'b' ] );
-
-					expect( writer.batch.deltas ).to.length( 2 );
-					expect( writer.batch.deltas[ 0 ] ).to.instanceof( AttributeDelta );
-					expect( writer.batch.deltas[ 1 ] ).to.instanceof( AttributeDelta );
-				} );
-			} );
-		} );
-
-		describe( 'filtering attributes from child nodes', () => {
-			let div;
-
-			beforeEach( () => {
-				schema.allow( { name: '$text', attributes: [ 'a' ], inside: 'div' } );
-				schema.allow( { name: '$text', attributes: [ 'b' ], inside: 'div paragraph' } );
-				schema.allow( { name: 'image', attributes: [ 'a' ], inside: 'div' } );
-				schema.allow( { name: 'image', attributes: [ 'b' ], inside: 'div paragraph' } );
-
-				const foo = new Text( 'foo', { a: 1, b: 1 } );
-				const bar = new Text( 'bar', { a: 1, b: 1 } );
-				const imageInDiv = new Element( 'image', { a: 1, b: 1 } );
-				const imageInParagraph = new Element( 'image', { a: 1, b: 1 } );
-				const paragraph = new Element( 'paragraph', [], [ foo, imageInParagraph ] );
-
-				div = new Element( 'div', [], [ paragraph, bar, imageInDiv ] );
-			} );
-
-			it( 'should filter out disallowed attributes from child nodes', () => {
-				const root = doc.getRoot();
-
-				root.appendChildren( [ div ] );
-
-				model.change( writer => {
-					schema.removeDisallowedAttributes( [ div ], '$root', writer );
-
-					expect( writer.batch.deltas ).to.length( 4 );
-					expect( writer.batch.deltas[ 0 ] ).to.instanceof( AttributeDelta );
-					expect( writer.batch.deltas[ 1 ] ).to.instanceof( AttributeDelta );
-					expect( writer.batch.deltas[ 2 ] ).to.instanceof( AttributeDelta );
-					expect( writer.batch.deltas[ 3 ] ).to.instanceof( AttributeDelta );
-
-					expect( getData( model, { withoutSelection: true } ) )
-						.to.equal(
-							'<div>' +
-								'<paragraph>' +
-									'<$text b="1">foo</$text>' +
-									'<image b="1"></image>' +
-								'</paragraph>' +
-								'<$text a="1">bar</$text>' +
-								'<image a="1"></image>' +
-							'</div>'
-						);
-				} );
-			} );
-		} );
-
-		describe( 'allowed parameters', () => {
-			let frag;
-
-			beforeEach( () => {
-				schema.allow( { name: '$text', attributes: [ 'a' ], inside: '$root' } );
-				schema.allow( { name: '$text', attributes: [ 'b' ], inside: 'paragraph' } );
-
-				frag = new DocumentFragment( [
-					new Text( 'foo', { a: 1 } ),
-					new Element( 'paragraph', [], [ new Text( 'bar', { a: 1, b: 1 } ) ] ),
-					new Text( 'biz', { b: 1 } )
-				] );
-			} );
-
-			it( 'should accept iterable as nodes', () => {
-				model.change( writer => {
-					schema.removeDisallowedAttributes( frag.getChildren(), '$root', writer );
-				} );
-
-				expect( stringify( frag ) )
-					.to.equal( '<$text a="1">foo</$text><paragraph><$text b="1">bar</$text></paragraph>biz' );
-			} );
-
-			it( 'should accept Position as inside', () => {
-				model.change( writer => {
-					schema.removeDisallowedAttributes( frag.getChildren(), Position.createAt( root ), writer );
-				} );
-
-				expect( stringify( frag ) )
-					.to.equal( '<$text a="1">foo</$text><paragraph><$text b="1">bar</$text></paragraph>biz' );
-			} );
-
-			it( 'should accept Node as inside', () => {
-				model.change( writer => {
-					schema.removeDisallowedAttributes( frag.getChildren(), [ root ], writer );
-				} );
-
-				expect( stringify( frag ) )
-					.to.equal( '<$text a="1">foo</$text><paragraph><$text b="1">bar</$text></paragraph>biz' );
-			} );
-		} );
-
-		it( 'should not filter out allowed combination of attributes', () => {
-			schema.allow( { name: 'image', attributes: [ 'a', 'b' ] } );
-			schema.requireAttributes( 'image', [ 'a', 'b' ] );
-
-			const image = new Element( 'image', { a: 1, b: 1 } );
-
-			model.change( writer => {
-				schema.removeDisallowedAttributes( [ image ], '$root', writer );
-			} );
-
-			expect( Array.from( image.getAttributeKeys() ) ).to.deep.equal( [ 'a', 'b' ] );
-		} );
-	} );
-} );

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

@@ -1,151 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import { default as Schema, SchemaItem } from '../../../src/model/schema';
-
-let schema, item;
-
-describe( 'SchemaItem', () => {
-	beforeEach( () => {
-		schema = new Schema();
-
-		schema.registerItem( 'p', '$block' );
-		schema.registerItem( 'header', '$block' );
-		schema.registerItem( 'div', '$block' );
-		schema.registerItem( 'html', '$block' );
-		schema.registerItem( 'span', '$inline' );
-		schema.registerItem( 'image', '$inline' );
-
-		item = new SchemaItem( schema );
-	} );
-
-	describe( 'constructor()', () => {
-		it( 'should create empty schema item', () => {
-			const item = new SchemaItem( schema );
-
-			expect( item._disallowed ).to.deep.equal( [] );
-			expect( item._allowed ).to.deep.equal( [] );
-		} );
-	} );
-
-	describe( 'allow', () => {
-		it( 'should add paths to the item as copies of passed array', () => {
-			const path1 = [ 'div', 'header' ];
-			const path2 = [ 'p' ];
-
-			item.allow( path1 );
-			item.allow( path2 );
-
-			const paths = item._getPaths( 'allow' );
-
-			expect( paths.length ).to.equal( 2 );
-
-			expect( paths[ 0 ] ).not.to.equal( path1 );
-			expect( paths[ 1 ] ).not.to.equal( path2 );
-
-			expect( paths[ 0 ] ).to.deep.equal( [ 'div', 'header' ] );
-			expect( paths[ 1 ] ).to.deep.equal( [ 'p' ] );
-		} );
-
-		it( 'should group paths by attribute', () => {
-			item.allow( [ 'p' ], 'bold' );
-			item.allow( [ 'div' ] );
-			item.allow( [ 'header' ], 'bold' );
-
-			const pathsWithNoAttribute = item._getPaths( 'allow' );
-			const pathsWithBoldAttribute = item._getPaths( 'allow', 'bold' );
-
-			expect( pathsWithNoAttribute.length ).to.equal( 1 );
-			expect( pathsWithNoAttribute[ 0 ] ).to.deep.equal( [ 'div' ] );
-
-			expect( pathsWithBoldAttribute.length ).to.equal( 2 );
-			expect( pathsWithBoldAttribute[ 0 ] ).to.deep.equal( [ 'p' ] );
-			expect( pathsWithBoldAttribute[ 1 ] ).to.deep.equal( [ 'header' ] );
-		} );
-	} );
-
-	describe( 'disallow', () => {
-		it( 'should add paths to the item as copies of passed array', () => {
-			const path1 = [ 'div', 'header' ];
-			const path2 = [ 'p' ];
-
-			item.disallow( path1 );
-			item.disallow( path2 );
-
-			const paths = item._getPaths( 'disallow' );
-
-			expect( paths.length ).to.equal( 2 );
-
-			expect( paths[ 0 ] ).not.to.equal( path1 );
-			expect( paths[ 1 ] ).not.to.equal( path2 );
-
-			expect( paths[ 0 ] ).to.deep.equal( [ 'div', 'header' ] );
-			expect( paths[ 1 ] ).to.deep.equal( [ 'p' ] );
-		} );
-
-		it( 'should group paths by attribute', () => {
-			item.disallow( [ 'p' ], 'bold' );
-			item.disallow( [ 'div' ] );
-			item.disallow( [ 'header' ], 'bold' );
-
-			const pathsWithNoAttribute = item._getPaths( 'disallow' );
-			const pathsWithBoldAttribute = item._getPaths( 'disallow', 'bold' );
-
-			expect( pathsWithNoAttribute.length ).to.equal( 1 );
-			expect( pathsWithNoAttribute[ 0 ] ).to.deep.equal( [ 'div' ] );
-
-			expect( pathsWithBoldAttribute.length ).to.equal( 2 );
-			expect( pathsWithBoldAttribute[ 0 ] ).to.deep.equal( [ 'p' ] );
-			expect( pathsWithBoldAttribute[ 1 ] ).to.deep.equal( [ 'header' ] );
-		} );
-	} );
-
-	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' ] );
-
-			expect( item._hasMatchingPath( 'allow', [ 'div', 'header' ] ) ).to.be.true;
-			expect( item._hasMatchingPath( 'allow', [ 'html', 'div', 'header' ] ) ).to.be.true;
-		} );
-
-		it( 'should return false if there are no allowed paths that match query path', () => {
-			item.allow( [ 'div', 'p' ] );
-
-			expect( item._hasMatchingPath( 'allow', [ 'div' ] ) ).to.be.false;
-			expect( item._hasMatchingPath( 'allow', [ 'p', 'div' ] ) ).to.be.false;
-			expect( item._hasMatchingPath( 'allow', [ 'div', 'p', 'span' ] ) ).to.be.false;
-		} );
-
-		it( 'should return true if there is at least one disallowed path that matches query path', () => {
-			item.allow( [ 'div', 'header' ] );
-			item.disallow( [ 'p', 'header' ] );
-
-			expect( item._hasMatchingPath( 'disallow', [ 'html', 'div', 'p', 'header' ] ) ).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' );
-
-			expect( item._hasMatchingPath( 'allow', [ 'html', 'div', 'p' ] ) ).to.be.true;
-			expect( item._hasMatchingPath( 'allow', [ 'html', 'div' ] ) ).to.be.false;
-			expect( item._hasMatchingPath( 'allow', [ 'html', 'div' ], 'bold' ) ).to.be.true;
-
-			expect( item._hasMatchingPath( 'disallow', [ 'html', 'div', 'header' ] ) ).to.be.false;
-			expect( item._hasMatchingPath( 'disallow', [ 'html', 'div', 'p', 'header' ], 'bold' ) ).to.be.true;
-		} );
-	} );
-
-	describe( 'toJSON', () => {
-		it( 'should create proper JSON string', () => {
-			const parsedItem = JSON.parse( JSON.stringify( item ) );
-
-			expect( parsedItem._schema ).to.equal( '[model.Schema]' );
-		} );
-	} );
-} );