Browse Source

Merge branch 'master' into t/1212

Maciej Bukowski 8 years ago
parent
commit
ca0f375718
40 changed files with 3483 additions and 2135 deletions
  1. 1 1
      packages/ckeditor5-engine/src/controller/datacontroller.js
  2. 2 14
      packages/ckeditor5-engine/src/conversion/buildviewconverter.js
  3. 1 6
      packages/ckeditor5-engine/src/conversion/view-to-model-converters.js
  4. 2 6
      packages/ckeditor5-engine/src/conversion/viewconversiondispatcher.js
  5. 7 16
      packages/ckeditor5-engine/src/dev-utils/model.js
  6. 3 3
      packages/ckeditor5-engine/src/model/document.js
  7. 1 1
      packages/ckeditor5-engine/src/model/documentselection.js
  8. 20 1
      packages/ckeditor5-engine/src/model/model.js
  9. 602 525
      packages/ckeditor5-engine/src/model/schema.js
  10. 1 3
      packages/ckeditor5-engine/src/model/selection.js
  11. 7 6
      packages/ckeditor5-engine/src/model/utils/deletecontent.js
  12. 23 69
      packages/ckeditor5-engine/src/model/utils/insertcontent.js
  13. 4 4
      packages/ckeditor5-engine/src/model/utils/modifyselection.js
  14. 31 29
      packages/ckeditor5-engine/tests/controller/datacontroller.js
  15. 4 4
      packages/ckeditor5-engine/tests/controller/editingcontroller.js
  16. 1 1
      packages/ckeditor5-engine/tests/conversion/advanced-converters.js
  17. 75 40
      packages/ckeditor5-engine/tests/conversion/buildviewconverter.js
  18. 13 11
      packages/ckeditor5-engine/tests/conversion/definition-based-converters.js
  19. 9 9
      packages/ckeditor5-engine/tests/conversion/model-selection-to-view-converters.js
  20. 1 1
      packages/ckeditor5-engine/tests/conversion/view-selection-to-model-converters.js
  21. 18 8
      packages/ckeditor5-engine/tests/conversion/view-to-model-converters.js
  22. 28 22
      packages/ckeditor5-engine/tests/dev-utils/model.js
  23. 4 3
      packages/ckeditor5-engine/tests/manual/highlight.js
  24. 9 8
      packages/ckeditor5-engine/tests/manual/nestededitable.js
  25. 57 7
      packages/ckeditor5-engine/tests/manual/tickets/1088/1.js
  26. 1 1
      packages/ckeditor5-engine/tests/manual/tickets/475/1.js
  27. 11 9
      packages/ckeditor5-engine/tests/model/document/document.js
  28. 19 16
      packages/ckeditor5-engine/tests/model/documentselection.js
  29. 8 8
      packages/ckeditor5-engine/tests/model/liverange.js
  30. 45 19
      packages/ckeditor5-engine/tests/model/model.js
  31. 1 1
      packages/ckeditor5-engine/tests/model/operation/utils.js
  32. 2196 0
      packages/ckeditor5-engine/tests/model/schema.js
  33. 0 921
      packages/ckeditor5-engine/tests/model/schema/schema.js
  34. 0 151
      packages/ckeditor5-engine/tests/model/schema/schemaitem.js
  35. 19 16
      packages/ckeditor5-engine/tests/model/selection.js
  36. 102 78
      packages/ckeditor5-engine/tests/model/utils/deletecontent.js
  37. 24 24
      packages/ckeditor5-engine/tests/model/utils/getselectedcontent.js
  38. 101 66
      packages/ckeditor5-engine/tests/model/utils/insertcontent.js
  39. 28 24
      packages/ckeditor5-engine/tests/model/utils/modifyselection.js
  40. 4 3
      packages/ckeditor5-engine/tests/tickets/699.js

+ 1 - 1
packages/ckeditor5-engine/src/controller/datacontroller.js

@@ -32,7 +32,7 @@ import ModelRange from '../model/range';
  * * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher model to view} and
  * * {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher view to model} converters.
  *
- * @mixes module:utils/emittermixin~ObservableMixin
+ * @mixes module:utils/observablemixin~ObservableMixin
  */
 export default class DataController {
 	/**

+ 2 - 14
packages/ckeditor5-engine/src/conversion/buildviewconverter.js

@@ -291,10 +291,7 @@ class ViewConverterBuilder {
 						continue;
 					}
 
-					// Check whether generated structure is okay with `Schema`.
-					const keys = Array.from( modelElement.getAttributeKeys() );
-
-					if ( !conversionApi.schema.check( { name: modelElement.name, attributes: keys, inside: data.context } ) ) {
+					if ( !conversionApi.schema.checkChild( data.context, modelElement ) ) {
 						continue;
 					}
 
@@ -518,16 +515,7 @@ function setAttributeOn( toChange, attribute, data, conversionApi ) {
 		return;
 	}
 
-	const keys = Array.from( toChange.getAttributeKeys() );
-	keys.push( attribute.key );
-
-	const schemaQuery = {
-		name: toChange.name || '$text',
-		attributes: keys,
-		inside: data.context
-	};
-
-	if ( conversionApi.schema.check( schemaQuery ) ) {
+	if ( conversionApi.schema.checkAttribute( toChange, attribute.key ) ) {
 		conversionApi.writer.setAttribute( attribute.key, attribute.value, toChange );
 	}
 }

+ 1 - 6
packages/ckeditor5-engine/src/conversion/view-to-model-converters.js

@@ -41,12 +41,7 @@ export function convertToModelFragment() {
  */
 export function convertText() {
 	return ( evt, data, consumable, conversionApi ) => {
-		const schemaQuery = {
-			name: '$text',
-			inside: data.context
-		};
-
-		if ( conversionApi.schema.check( schemaQuery ) ) {
+		if ( conversionApi.schema.checkChild( data.context, '$text' ) ) {
 			if ( consumable.consume( data.input ) ) {
 				data.output = conversionApi.writer.createText( data.input.data );
 			}

+ 2 - 6
packages/ckeditor5-engine/src/conversion/viewconversiondispatcher.js

@@ -50,12 +50,8 @@ import log from '@ckeditor/ckeditor5-utils/src/log';
  *		// Converter for paragraphs (<p>).
  *		viewDispatcher.on( 'element:p', ( evt, data, consumable, conversionApi ) => {
  *			const paragraph = new ModelElement( 'paragraph' );
- *			const schemaQuery = {
- *				name: 'paragraph',
- *				inside: data.context
- *			};
  *
- *			if ( conversionApi.schema.check( schemaQuery ) ) {
+ *			if ( conversionApi.schema.checkChild( data.context, paragraph ) ) {
  *				if ( !consumable.consume( data.input, { name: true } ) ) {
  *					// Before converting this paragraph's children we have to update their context by this paragraph.
  *					data.context.push( paragraph );
@@ -81,7 +77,7 @@ import log from '@ckeditor/ckeditor5-utils/src/log';
  *						inside: data.context
  *					};
  *
- *					if ( conversionApi.schema.check( schemaQuery ) ) {
+ *					if ( conversionApi.schema.checkAttribute( [ ...data.context, '$text' ], 'link' ) ) {
  *						item.setAttribute( 'link', data.input.getAttribute( 'href' ) );
  *					}
  *				}

+ 7 - 16
packages/ckeditor5-engine/src/dev-utils/model.js

@@ -243,7 +243,7 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
  * @param {Object} [options={}] Additional configuration.
  * @param {Array<Object>} [options.selectionAttributes] List of attributes which will be passed to the selection.
  * @param {Boolean} [options.lastRangeBackward=false] If set to true last range will be added as backward.
- * @param {module:engine/model/schema~SchemaPath} [options.context=[ '$root' ]] The conversion context.
+ * @param {module:engine/model/schema~SchemaContextDefinition} [options.context=[ '$root' ]] The conversion context.
  * If not provided default `[ '$root' ]` will be used.
  * @returns {module:engine/model/element~Element|module:engine/model/text~Text|
  * module:engine/model/documentfragment~DocumentFragment|Object} Returns parsed model node or
@@ -329,14 +329,10 @@ function convertToModelFragment() {
 
 function convertToModelElement() {
 	return ( evt, data, consumable, conversionApi ) => {
-		const schemaQuery = {
-			name: data.input.name,
-			attributes: Array.from( data.input.getAttributeKeys() ),
-			inside: data.context
-		};
-
-		if ( !conversionApi.schema.check( schemaQuery ) ) {
-			throw new Error( `Element '${ schemaQuery.name }' not allowed in context ${ JSON.stringify( data.context ) }.` );
+		const elementName = data.input.name;
+
+		if ( !conversionApi.schema.checkChild( data.context, elementName ) ) {
+			throw new Error( `Element '${ elementName }' was not allowed in context ${ JSON.stringify( data.context ) }.` );
 		}
 
 		// View attribute value is a string so we want to typecast it to the original type.
@@ -356,13 +352,8 @@ function convertToModelElement() {
 
 function convertToModelText( withAttributes = false ) {
 	return ( evt, data, consumable, conversionApi ) => {
-		const schemaQuery = {
-			name: '$text',
-			inside: data.context
-		};
-
-		if ( !conversionApi.schema.check( schemaQuery ) ) {
-			throw new Error( `Element '${ schemaQuery.name }' not allowed in context ${ JSON.stringify( data.context ) }.` );
+		if ( !conversionApi.schema.checkChild( data.context, '$text' ) ) {
+			throw new Error( `Text was not allowed in context ${ JSON.stringify( data.context ) }.` );
 		}
 
 		let node;

+ 3 - 3
packages/ckeditor5-engine/src/model/document.js

@@ -248,7 +248,7 @@ export default class Document {
 		const schema = this.model.schema;
 
 		// Return collapsed range if provided position is valid.
-		if ( schema.check( { name: '$text', inside: position } ) ) {
+		if ( schema.checkChild( position, '$text' ) ) {
 			return new Range( position );
 		}
 
@@ -266,11 +266,11 @@ export default class Document {
 			const type = ( data.walker == backwardWalker ? 'elementEnd' : 'elementStart' );
 			const value = data.value;
 
-			if ( value.type == type && schema.objects.has( value.item.name ) ) {
+			if ( value.type == type && schema.isObject( value.item ) ) {
 				return Range.createOn( value.item );
 			}
 
-			if ( schema.check( { name: '$text', inside: value.nextPosition } ) ) {
+			if ( schema.checkChild( value.nextPosition, '$text' ) ) {
 				return new Range( value.nextPosition );
 			}
 		}

+ 1 - 1
packages/ckeditor5-engine/src/model/documentselection.js

@@ -621,7 +621,7 @@ export default class DocumentSelection extends Selection {
 			// ...look for a first character node in that range and take attributes from it.
 			for ( const value of range ) {
 				// If the item is an object, we don't want to get attributes from its children.
-				if ( value.item.is( 'element' ) && schema.objects.has( value.item.name ) ) {
+				if ( value.item.is( 'element' ) && schema.isObject( value.item ) ) {
 					break;
 				}
 

+ 20 - 1
packages/ckeditor5-engine/src/model/model.js

@@ -32,6 +32,8 @@ import getSelectedContent from './utils/getselectedcontent';
  * {@link module:engine/model/model~Model#document}, and all detached nodes, used to data manipulation. All of them are
  * created and modified by the {@link module:engine/model/writer~Writer}, which can be get using
  * {@link module:engine/model/model~Model#change} or {@link module:engine/model/model~Model#enqueueChange} methods.
+ *
+ * @mixes module:utils/observablemixin~ObservableMixin
  */
 export default class Model {
 	constructor() {
@@ -68,6 +70,23 @@ export default class Model {
 
 		[ 'insertContent', 'deleteContent', 'modifySelection', 'getSelectedContent', 'applyOperation' ]
 			.forEach( methodName => this.decorate( methodName ) );
+
+		// Register some default abstract entities.
+		this.schema.register( '$root', {
+			isLimit: true
+		} );
+		this.schema.register( '$block', {
+			allowIn: '$root',
+			isBlock: true
+		} );
+		this.schema.register( '$text', {
+			allowIn: '$block'
+		} );
+		this.schema.register( '$clipboardHolder', {
+			allowContentOf: '$root',
+			isLimit: true
+		} );
+		this.schema.extend( '$text', { allowIn: '$clipboardHolder' } );
 	}
 
 	/**
@@ -296,7 +315,7 @@ export default class Model {
 
 		for ( const item of rangeOrElement.getItems() ) {
 			// Remember, `TreeWalker` returns always `textProxy` nodes.
-			if ( item.is( 'textProxy' ) || this.schema.objects.has( item.name ) ) {
+			if ( item.is( 'textProxy' ) || this.schema.isObject( item ) ) {
 				return true;
 			}
 		}

+ 602 - 525
packages/ckeditor5-engine/src/model/schema.js

@@ -7,303 +7,402 @@
  * @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';
+import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
+
+import Range from './range';
 
 /**
- * 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.
+ * The model's schema. It defines allowed and disallowed structures of nodes as well as nodes' attributes.
+ * The schema is usually defined by features and based on them the editing framework and features
+ * make decisions how to change and process the model.
+ *
+ * The instance of schema is available in {@link module:engine/model/model~Model#schema `editor.model.schema`}.
+ *
+ * # Schema definitions
+ *
+ * Schema defines allowed model structures and allowed attributes separately. They are also checked separately
+ * by using the {@link ~Schema#checkChild} and {@link ~Schema#checkAttribute} methods.
+ *
+ * ## Defining allowed structures
+ *
+ * When a feature introduces a model element it should registered it in the schema. Besides
+ * defining that such an element may exist in the model, the feature also needs to define where
+ * this element may occur:
+ *
+ *		schema.register( 'myElement', {
+ *			allowIn: '$root'
+ *		} );
+ *
+ * This lets the schema know that `<myElement>` may be a child of the `<$root>` element. `$root` is one of generic
+ * node types defined by the editing framework. By default, the editor names the main root element a `<$root>`,
+ * so the above definition allows `<myElement>` in the main editor element.
+ *
+ * In other words, this would be correct:
+ *
+ *		<$root><myElement></myElement></$root>
+ *
+ * While this would not be correct:
+ *
+ *		<$root><foo><myElement></myElement></foo></$root>
+ *
+ * ## Generic node types
+ *
+ * There are three basic generic node types: `$root`, `$block` and `$text`.
+ * They are defined as follows:
+ *
+ *		this.schema.register( '$root', {
+ *			isLimit: true
+ *		} );
+ *		this.schema.register( '$block', {
+ *			allowIn: '$root',
+ *			isBlock: true
+ *		} );
+ *		this.schema.register( '$text', {
+ *			allowIn: '$block'
+ *		} );
+ *
+ * These definitions can then be reused by features to create their own definitions in a more extensible way.
+ * For example, the {@link module:paragraph/paragraph~Paragraph} feature will define its item as:
+ *
+ *		schema.register( 'paragraph', {
+ *			inheritAllFrom: '$block'
+ *		} );
+ *
+ * Which translates to:
+ *
+ *		schema.register( 'paragraph', {
+ *			allowWhere: '$block',
+ *			allowContentOf: '$block',
+ *			allowAttributesOf: '$block',
+ *			inheritTypesFrom: '$block'
+ *		} );
+ *
+ * Which can be read as:
+ *
+ * * The `<paragraph>` element will be allowed in elements in which `<$block>` is allowed (e.g. in `<$root>`).
+ * * The `<paragraph>` element will allow all nodes which are allowed in `<$block>` (e.g. `$text`).
+ * * The `<paragraph>` element will allow all attributes allowed on `<$block>`.
+ * * The `<paragraph>` element will inherit all `is*` properties of `<$block>` (e.g. `isBlock`).
+ *
+ * Thanks to the fact that `<paragraph>`'s definition is inherited from `<$block>` other features can use the `<$block>`
+ * type to indirectly extend `<paragraph>`'s definition. For example, the {@link module:block-quote/blockquote~BlockQuote}
+ * feature does this:
+ *
+ *		schema.register( 'blockQuote', {
+ *			allowWhere: '$block',
+ *			allowContentOf: '$root'
+ *		} );
+ *
+ * Thanks to that, despite the fact that block quote and paragraph features know nothing about themselves, paragraphs
+ * will be allowed in block quotes and block quotes will be allowed in all places where blocks are. So if anyone will
+ * register a `<section>` element (with `allowContentOf: '$root'` rule), that `<section>` elements will allow
+ * block quotes too.
+ *
+ * The side effect of such a definition inheritance is that now `<blockQuote>` is allowed in `<blockQuote>` which needs to be
+ * resolved by a callback which will disallow this specific structure.
+ *
+ * ## Defining advanced rules in `checkChild()`'s callbacks
+ *
+ * The {@link ~Schema#checkChild} method which is the base method used to check whether some element is allowed in a given structure
+ * is {@link module:utils/observablemixin~ObservableMixin#decorate decorated} with the {@link ~Schema#event:checkChild} event.
+ * It means that you can add listeners to implement your specific rules which are not limited by the declarative
+ * {@link module:engine/model/schema~SchemaItemDefinition} API.
+ *
+ * The block quote feature defines such a listener to disallow nested `<blockQuote>` structures:
+ *
+ * 		schema.on( 'checkChild', ( evt, args ) => {
+ * 			// The checkChild()'s params.
+ *			// Note that context is automatically normalized to SchemaContext instance by a highest-priority listener.
+ *			const context = args[ 0 ];
+ *			const child = args[ 1 ];
+ *
+ *			// Pass the child through getDefinition() to normalize it (child can be passed in multiple formats).
+ *			const childRule = schema.getDefinition( child );
+ *
+ *			// If checkChild() is called with a context that ends with blockQuote and blockQuote as a child
+ *			// to check, make the method return false and stop the event so no other listener will override your decision.
+ *			if ( childRule && childRule.name == 'blockQuote' && context.endsWith( 'blockQuote' ) ) {
+ *				evt.stop();
+ *				evt.return = false;
+ *			}
+ *		}, { priority: 'high' } );
+ *
+ * ## Defining attributes
+ *
+ * TODO
+ *
+ * ## Implementing additional constraints
+ *
+ * Schema's capabilities were limited to simple (and atomic) {@link ~Schema#checkChild} and
+ * {@link ~Schema#checkAttribute} on purpose.
+ * One may imagine that schema should support defining more complex rules such as
+ * "element `<x>` must be always followed by `<y>`".
+ * While it is feasible to create an API which would enable feeding the schema with such definitions,
+ * it is unrealistic to then expect that every editing feature will consider them when processing the model.
+ * It is also unrealistic to expect that it will be done automatically by the schema and the editing engine themselves.
  *
- * 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:
+ * For instance, let's get back to the "element `<x>` must be always followed by `<y>`" rule and this initial content:
  *
- *		editor.model.schema.allow( '$text', 'bold' );
+ *		<$root>
+ *			<x>foo</x>
+ *			<y>bar[bom</y>
+ *			<z>bom]bar</z>
+ *		</$root>
  *
- * Note: items prefixed with `$` are special group of items. By default, `Schema` defines three special items:
+ * Now, imagine the user presses the "block quote" button. Usually it would wrap the two selected blocks (`<y>` and `<z>`)
+ * with a `<blockQuote>` element:
  *
- * * `$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).
+ *		<$root>
+ *			<x>foo</x>
+ *			<blockQuote>
+ *				<y>bar[bom</y>
+ *				<z>bom]bar</z>
+ *			</blockQuote>
+ *		</$root>
  *
- * 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`.
+ * But it turns out that this creates an incorrect structure – `<x>` is not followed by `<y>` anymore.
+ *
+ * What should happen instead? There are at least 4 possible solutions: the block quote feature should not be
+ * applicable in such a context, someone should create a new `<y>` right after `<x>`, `<x>` should be moved
+ * inside `<blockQuote>` together with `<y>` or vice versa.
+ *
+ * While this is a relatively simple scenario (unlike most real-time collaboration scenarios),
+ * it turns out that it's already hard to say what should happen and who should react to fix this content.
+ *
+ * Therefore, if your editor needs to implement such rules, it should do that through model's post-fixers
+ * fixing incorrect content or actively prevent such situations (e.g. by disabling certain features).
+ * It means that those constraints will be defined specifically for your scenario by your code which
+ * makes their implementation much easier.
+ *
+ * So the answer for who and how should implement additional constraints is your features or your editor
+ * through CKEditor 5's rich and open API.
+ *
+ * @mixes module:utils/observablemixin~ObservableMixin
  */
 export default class Schema {
 	/**
-	 * Creates Schema instance.
+	 * 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._sourceDefinitions = {};
+
+		this.decorate( 'checkChild' );
+		this.decorate( 'checkAttribute' );
+
+		this.on( 'checkAttribute', ( evt, args ) => {
+			args[ 0 ] = new SchemaContext( args[ 0 ] );
+		}, { priority: 'highest' } );
+
+		this.on( 'checkChild', ( evt, args ) => {
+			args[ 0 ] = new SchemaContext( args[ 0 ] );
+		}, { priority: 'highest' } );
 	}
 
 	/**
-	 * 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.
+	 * Registers schema item. Can only be called once for every item name.
 	 *
-	 * @param {module:engine/model/schema~SchemaQuery} query Allowed query.
+	 * @param {String} itemName
+	 * @param {module:engine/model/schema~SchemaItemDefinition} definition
 	 */
-	allow( query ) {
-		this._getItem( query.name ).allow( Schema._normalizeQueryPath( query.inside ), query.attributes );
+	register( itemName, definition ) {
+		if ( this._sourceDefinitions[ itemName ] ) {
+			// TODO docs
+			throw new CKEditorError( 'schema-cannot-register-item-twice: A single item cannot be registered twice in the schema.', {
+				itemName
+			} );
+		}
+
+		this._sourceDefinitions[ itemName ] = [
+			Object.assign( {}, definition )
+		];
+
+		this._clearCache();
 	}
 
 	/**
-	 * Disallows given query in the schema.
+	 * Extends a {@link #register registered} item's definition.
+	 *
+	 * Extending properties such as `allowIn` will add more items to the existing properties,
+	 * while redefining properties such as `isBlock` will override the previously defined ones.
+	 *
+	 *		schema.register( 'foo', {
+	 *			allowIn: '$root',
+	 *			isBlock: true;
+	 *		} );
+	 *		schema.extend( 'foo', {
+	 *			allowIn: 'blockQuote',
+	 *			isBlock: false
+	 *		} );
 	 *
-	 * @see #allow
-	 * @param {module:engine/model/schema~SchemaQuery} query Disallowed query.
+	 *		schema.getDefinition( 'foo' );
+	 *		//	{
+	 *		//		allowIn: [ '$root', 'blockQuote' ],
+	 *		// 		isBlock: false
+	 *		//	}
+	 *
+	 * @param {String} itemName
+	 * @param {module:engine/model/schema~SchemaItemDefinition} definition
 	 */
-	disallow( query ) {
-		this._getItem( query.name ).disallow( Schema._normalizeQueryPath( query.inside ), query.attributes );
+	extend( itemName, definition ) {
+		if ( !this._sourceDefinitions[ itemName ] ) {
+			// TODO docs
+			throw new CKEditorError( 'schema-cannot-extend-missing-item: Cannot extend an item which was not registered yet.', {
+				itemName
+			} );
+		}
+
+		this._sourceDefinitions[ itemName ].push( Object.assign( {}, definition ) );
+
+		this._clearCache();
 	}
 
 	/**
-	 * 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.
+	 * Returns all registered items.
 	 *
-	 *		// "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.
+	 * @returns {Object.<String,module:engine/model/schema~SchemaCompiledItemDefinition>}
 	 */
-	requireAttributes( name, attributes ) {
-		this._getItem( name ).requireAttributes( attributes );
+	getDefinitions() {
+		if ( !this._compiledDefinitions ) {
+			this._compile();
+		}
+
+		return this._compiledDefinitions;
 	}
 
 	/**
-	 * 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 ) ) { ... }
+	 * Returns a definition of the given item or `undefined` if item is not registered.
 	 *
-	 *		// 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.
+	 * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
+	 * @returns {module:engine/model/schema~SchemaCompiledItemDefinition}
 	 */
-	check( query ) {
-		if ( !this.hasItem( query.name ) ) {
-			return false;
-		}
+	getDefinition( item ) {
+		let itemName;
 
-		// 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 );
+		if ( typeof item == 'string' ) {
+			itemName = item;
+		} else if ( item.is && ( item.is( 'text' ) || item.is( 'textProxy' ) ) ) {
+			itemName = '$text';
+		}
+		// Element or module:engine/model/schema~SchemaContextItem.
+		else {
+			itemName = item.name;
 		}
 
-		// 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 );
-		} );
+		return this.getDefinitions()[ itemName ];
+	}
 
-		// First check if the query meets at required attributes for this item.
-		if ( !this._getItem( query.name )._checkRequiredAttributes( query.attributes ) ) {
-			return false;
-		}
+	/**
+	 * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
+	 */
+	isRegistered( item ) {
+		return !!this.getDefinition( item );
+	}
 
-		// 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;
-				}
-			}
-		}
+	/**
+	 * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
+	 */
+	isBlock( item ) {
+		const def = this.getDefinition( item );
 
-		// 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;
-			}
+		return !!( def && def.isBlock );
+	}
 
-			let matched = false;
+	/**
+	 * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
+	 */
+	isLimit( item ) {
+		const def = this.getDefinition( item );
 
-			for ( const schemaItem of schemaItems ) {
-				if ( schemaItem._hasMatchingPath( 'allow', path, attribute ) ) {
-					matched = true;
-					break;
-				}
-			}
+		return !!( def && def.isLimit );
+	}
 
-			// The attribute has not been matched, so it is not allowed by any schema item.
-			// The query is disallowed.
-			if ( !matched ) {
-				return false;
-			}
-		}
+	/**
+	 * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
+	 */
+	isObject( item ) {
+		const def = this.getDefinition( item );
 
-		return true;
+		return !!( def && def.isObject );
 	}
 
 	/**
-	 * Checks whether there is an item registered under given name in schema.
+	 * Checks whether the given node (`child`) can be a child of the given context.
 	 *
-	 * @param itemName
-	 * @returns {Boolean}
+	 *		schema.checkChild( model.document.getRoot(), paragraph ); // -> false
+	 *
+	 *		schema.register( 'paragraph', {
+	 *			allowIn: '$root'
+	 *		} );
+	 *		schema.checkChild( model.document.getRoot(), paragraph ); // -> true
+	 *
+	 * @fires checkChild
+	 * @param {module:engine/model/schema~SchemaContextDefinition} context Context in which the child will be checked.
+	 * @param {module:engine/model/node~Node|String} child The child to check.
 	 */
-	hasItem( itemName ) {
-		return this._items.has( itemName );
+	checkChild( context, child ) {
+		const def = this.getDefinition( child );
+
+		if ( !def ) {
+			return false;
+		}
+
+		return this._checkContextMatch( def, context );
 	}
 
 	/**
-	 * Registers given item name in schema.
+	 * Checks whether the given attribute can be applied in the given context (on the last
+	 * item of the context).
+	 *
+	 *		schema.checkAttribute( textNode, 'bold' ); // -> false
 	 *
-	 *		// Register P element that should be treated like all block elements.
-	 *		schema.registerItem( 'p', '$block' );
+	 *		schema.extend( '$text', {
+	 *			allowAttributes: 'bold'
+	 *		} );
+	 *		schema.checkAttribute( textNode, 'bold' ); // -> true
 	 *
-	 * @param {String} itemName Name to register.
-	 * @param [isExtending] If set, new item will extend item with given name.
+	 * @fires checkAttribute
+	 * @param {module:engine/model/schema~SchemaContextDefinition} context
+	 * @param {String} attributeName
 	 */
-	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.' );
-		}
+	checkAttribute( context, attributeName ) {
+		const def = this.getDefinition( context.last );
 
-		if ( !!isExtending && !this.hasItem( isExtending ) ) {
-			throw new CKEditorError( 'model-schema-no-item: Item with specified name does not exist in schema.' );
+		if ( !def ) {
+			return false;
 		}
 
-		// Create new SchemaItem and add it to the items store.
-		this._items.set( itemName, new SchemaItem( this ) );
-
-		// 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 def.allowAttributes.includes( attributeName );
 	}
 
 	/**
-	 * Checks whether item of given name is extending item of another given name.
+	 * Returns the lowest {@link module:engine/model/schema~Schema#isLimit limit element} containing the entire
+	 * selection or the root otherwise.
 	 *
-	 * @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.
+	 * @param {module:engine/model/selection~Selection} selection Selection which returns the common ancestor.
+	 * @returns {module:engine/model/element~Element}
 	 */
-	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.' );
-		}
+	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();
+				}
 
-		const chain = this._extensionChains.get( childItemName );
+				return node.getCommonAncestor( range.getCommonAncestor() );
+			}, null );
 
-		return chain.some( itemName => itemName == parentItemName );
+		while ( !this.isLimit( element ) ) {
+			if ( element.parent ) {
+				element = element.parent;
+			} else {
+				break;
+			}
+		}
+
+		return element;
 	}
 
 	/**
@@ -320,21 +419,14 @@ export default class Schema {
 	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 } );
+			return this.checkAttribute( [ ...selection.getFirstPosition().getAncestors(), '$text' ], 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 ( this.checkAttribute( value.item, attribute ) ) {
 						// If we found a node that is allowed to have the attribute, return true.
 						return true;
 					}
@@ -347,7 +439,7 @@ export default class Schema {
 	}
 
 	/**
-	 * Transforms the given set ranges into a set of ranges where the given attribute is allowed (and can be applied).
+	 * Transforms the given set of 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.
@@ -362,10 +454,7 @@ export default class Schema {
 			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 ( !this.checkAttribute( value.item, attribute ) ) {
 					if ( !from.isEqual( last ) ) {
 						validRanges.push( new Range( from, last ) );
 					}
@@ -385,361 +474,349 @@ export default class Schema {
 	}
 
 	/**
-	 * 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;
-			}
-		}
-
-		return element;
-	}
-
-	/**
-	 * Removes disallowed by {@link module:engine/model/schema~Schema schema} attributes from given nodes..
+	 * Removes attributes disallowed by the schema.
 	 *
 	 * @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 ) {
+	removeDisallowedAttributes( nodes, 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 attribute of node.getAttributeKeys() ) {
+				if ( !this.checkAttribute( node, attribute ) ) {
+					writer.removeAttribute( attribute, node );
 				}
 			}
 
 			if ( node.is( 'element' ) ) {
-				this.removeDisallowedAttributes( node.getChildren(), queryPath.concat( node.name ), writer );
+				this.removeDisallowedAttributes( node.getChildren(), writer );
 			}
 		}
 	}
 
 	/**
-	 * 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.' );
+	_clearCache() {
+		this._compiledDefinitions = null;
+	}
+
+	/**
+	 * @private
+	 */
+	_compile() {
+		const compiledDefinitions = {};
+		const sourceRules = this._sourceDefinitions;
+		const itemNames = Object.keys( sourceRules );
+
+		for ( const itemName of itemNames ) {
+			compiledDefinitions[ itemName ] = compileBaseItemRule( sourceRules[ itemName ], itemName );
+		}
+
+		for ( const itemName of itemNames ) {
+			compileAllowContentOf( compiledDefinitions, itemName );
+		}
+
+		for ( const itemName of itemNames ) {
+			compileAllowWhere( compiledDefinitions, itemName );
+		}
+
+		for ( const itemName of itemNames ) {
+			compileAllowAttributesOf( compiledDefinitions, itemName );
+			compileInheritPropertiesFrom( compiledDefinitions, itemName );
+		}
+
+		for ( const itemName of itemNames ) {
+			cleanUpAllowIn( compiledDefinitions, itemName );
+			cleanUpAllowAttributes( compiledDefinitions, itemName );
 		}
 
-		return this._items.get( itemName );
+		this._compiledDefinitions = compiledDefinitions;
 	}
 
 	/**
-	 * 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.
+	 * @private
+	 * @param {module:engine/model/schema~SchemaCompiledItemDefinition} def
+	 * @param {module:engine/model/schema~SchemaContext} context
+	 * @param {Number} contextItemIndex
 	 */
-	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( def, context, contextItemIndex = context.length - 1 ) {
+		const contextItem = context.getItem( contextItemIndex );
 
-			while ( parent !== null ) {
-				normalized.push( parent.name );
-				parent = parent.parent;
-			}
+		if ( def.allowIn.includes( contextItem.name ) ) {
+			if ( contextItemIndex == 0 ) {
+				return true;
+			} else {
+				const parentRule = this.getDefinition( contextItem );
 
-			normalized.reverse();
-		} else if ( isString( path ) ) {
-			normalized = path.split( ' ' );
+				return this._checkContextMatch( parentRule, context, contextItemIndex - 1 );
+			}
+		} else {
+			return false;
 		}
-
-		return normalized;
 	}
 }
 
+mix( Schema, ObservableMixin );
+
+/**
+ * TODO
+ *
+ * @event checkChild
+ */
+
+/**
+ * TODO
+ *
+ * @event checkAttribute
+ */
+
+/**
+ * TODO
+ *
+ * @typedef {Object} module:engine/model/schema~SchemaItemDefinition
+ */
+
 /**
- * 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.
+ * TODO
  *
- * @see module:engine/model/schema~Schema
- * @protected
+ * @typedef {Object} module:engine/model/schema~SchemaCompiledItemDefinition
  */
-export class SchemaItem {
+
+/**
+ * TODO
+ */
+export class SchemaContext {
 	/**
-	 * Creates SchemaItem instance.
+	 * TODO
 	 *
-	 * @param {module:engine/model/schema~Schema} schema Schema instance that owns this item.
+	 * @param {module:engine/model/schema~SchemaContextDefinition} context
 	 */
-	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 = [];
+	constructor( context ) {
+		if ( Array.isArray( context ) ) {
+			this._items = context.map( mapContextItem );
+		}
+		// Item or position (PS. It's ok that Position#getAncestors() doesn't accept params).
+		else {
+			this._items = context.getAncestors( { includeSelf: true } ).map( mapContextItem );
+		}
 	}
 
-	/**
-	 * 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 );
+	get length() {
+		return this._items.length;
 	}
 
-	/**
-	 * 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 );
+	get last() {
+		return this._items[ this._items.length - 1 ];
 	}
 
 	/**
-	 * 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.
+	 * Returns an iterator that iterates over all context items.
 	 *
-	 * @param {Array.<String>} attributes Attributes that has to be set on the entity to make it valid.
+	 * @returns {Iterator.<module:engine/model/schema~SchemaContextItem>}
 	 */
-	requireAttributes( attributes ) {
-		this._requiredAttributes.push( attributes );
+	[ Symbol.iterator ]() {
+		return this._items[ Symbol.iterator ]();
 	}
 
-	/**
-	 * 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 );
+	getItem( index ) {
+		return this._items[ index ];
+	}
 
-		// Due to circular references we need to remove parent reference.
-		json._schema = '[model.Schema]';
+	* getNames() {
+		yield* this._items.map( item => item.name );
+	}
 
-		return json;
+	endsWith( query ) {
+		return Array.from( this.getNames() ).join( ' ' ).endsWith( query );
 	}
+}
 
-	/**
-	 * 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();
+/**
+ * TODO
+ *
+ * @typedef {module:engine/model/node~Node|module:engine/model/position~Position|
+ * Array.<String|module:engine/model/node~Node>} module:engine/model/schema~SchemaContextDefinition
+ */
 
-		if ( !isArray( attributes ) ) {
-			attributes = [ attributes ];
-		}
+/**
+ * TODO
+ *
+ * @typedef {Object} module:engine/model/schema~SchemaContextItem
+ */
+
+function compileBaseItemRule( sourceItemRules, itemName ) {
+	const itemRule = {
+		name: itemName,
+
+		allowIn: [],
+		allowContentOf: [],
+		allowWhere: [],
+
+		allowAttributes: [],
+		allowAttributesOf: [],
 
-		for ( const attribute of attributes ) {
-			this[ member ].push( { path, attribute } );
+		inheritTypesFrom: []
+	};
+
+	copyTypes( sourceItemRules, itemRule );
+
+	copyProperty( sourceItemRules, itemRule, 'allowIn' );
+	copyProperty( sourceItemRules, itemRule, 'allowContentOf' );
+	copyProperty( sourceItemRules, itemRule, 'allowWhere' );
+
+	copyProperty( sourceItemRules, itemRule, 'allowAttributes' );
+	copyProperty( sourceItemRules, itemRule, 'allowAttributesOf' );
+
+	copyProperty( sourceItemRules, itemRule, 'inheritTypesFrom' );
+
+	makeInheritAllWork( sourceItemRules, itemRule );
+
+	return itemRule;
+}
+
+function compileAllowContentOf( compiledDefinitions, itemName ) {
+	for ( const allowContentOfItemName of compiledDefinitions[ itemName ].allowContentOf ) {
+		// The allowContentOf property may point to an unregistered element.
+		if ( compiledDefinitions[ allowContentOfItemName ] ) {
+			const allowedChildren = getAllowedChildren( compiledDefinitions, allowContentOfItemName );
+
+			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 = [];
+	delete compiledDefinitions[ itemName ].allowContentOf;
+}
 
-		for ( const item of source ) {
-			if ( item.attribute === attribute ) {
-				paths.push( item.path );
-			}
-		}
+function compileAllowWhere( compiledDefinitions, itemName ) {
+	for ( const allowWhereItemName of compiledDefinitions[ itemName ].allowWhere ) {
+		const inheritFrom = compiledDefinitions[ allowWhereItemName ];
+
+		// The allowWhere property may point to an unregistered element.
+		if ( inheritFrom ) {
+			const allowedIn = inheritFrom.allowIn;
 
-		return paths;
+			compiledDefinitions[ itemName ].allowIn.push( ...allowedIn );
+		}
 	}
 
-	/**
-	 * 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;
+	delete compiledDefinitions[ itemName ].allowWhere;
+}
 
-		for ( const attributeSet of this._requiredAttributes ) {
-			found = true;
+function compileAllowAttributesOf( compiledDefinitions, itemName ) {
+	for ( const allowAttributeOfItem of compiledDefinitions[ itemName ].allowAttributesOf ) {
+		const inheritFrom = compiledDefinitions[ allowAttributeOfItem ];
 
-			for ( const attribute of attributeSet ) {
-				if ( attributesToCheck.indexOf( attribute ) == -1 ) {
-					found = false;
-					break;
-				}
-			}
+		if ( inheritFrom ) {
+			const inheritAttributes = inheritFrom.allowAttributes;
 
-			if ( found ) {
-				break;
-			}
+			compiledDefinitions[ itemName ].allowAttributes.push( ...inheritAttributes );
 		}
-
-		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 );
+	delete compiledDefinitions[ itemName ].allowAttributesOf;
+}
 
-		for ( const registeredPathPath of registeredPaths ) {
-			if ( matchPaths( this._schema, pathToCheck, registeredPathPath ) ) {
-				return true;
+function compileInheritPropertiesFrom( compiledDefinitions, itemName ) {
+	const item = compiledDefinitions[ itemName ];
+
+	for ( const inheritPropertiesOfItem of item.inheritTypesFrom ) {
+		const inheritFrom = compiledDefinitions[ inheritPropertiesOfItem ];
+
+		if ( inheritFrom ) {
+			const typeNames = Object.keys( inheritFrom ).filter( name => name.startsWith( 'is' ) );
+
+			for ( const name of typeNames ) {
+				if ( !( name in item ) ) {
+					item[ name ] = inheritFrom[ name ];
+				}
 			}
 		}
-
-		return false;
 	}
+
+	delete item.inheritTypesFrom;
 }
 
-/**
- * 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( compiledDefinitions, itemName ) {
+	const itemRule = compiledDefinitions[ itemName ];
+	const existingItems = itemRule.allowIn.filter( itemToCheck => compiledDefinitions[ 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( compiledDefinitions, itemName ) {
+	const itemRule = compiledDefinitions[ 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 ] );
 		}
+	}
+}
 
-		const extChain = schema._extensionChains.get( checkName );
+function makeInheritAllWork( sourceItemRules, itemRule ) {
+	for ( const sourceItemRule of sourceItemRules ) {
+		const inheritFrom = sourceItemRule.inheritAllFrom;
 
-		if ( extChain.includes( registeredPath[ registeredPathIndex ] ) ) {
-			registeredPathIndex--;
-			pathToCheckIndex--;
-		} else {
-			return false;
+		if ( inheritFrom ) {
+			itemRule.allowContentOf.push( inheritFrom );
+			itemRule.allowWhere.push( inheritFrom );
+			itemRule.allowAttributesOf.push( inheritFrom );
+			itemRule.inheritTypesFrom.push( inheritFrom );
 		}
 	}
+}
 
-	return true;
+function getAllowedChildren( compiledDefinitions, itemName ) {
+	const itemRule = compiledDefinitions[ itemName ];
+
+	return getValues( compiledDefinitions ).filter( def => def.allowIn.includes( itemRule.name ) );
 }
 
-/**
- * Item with specified name does not exist in schema.
- *
- * @error model-schema-no-item
- */
+function getValues( obj ) {
+	return Object.keys( obj ).map( key => obj[ key ] );
+}
+
+function mapContextItem( ctxItem ) {
+	if ( typeof ctxItem == 'string' ) {
+		return {
+			name: ctxItem,
+
+			* getAttributeKeys() {},
+
+			getAttribute() {}
+		};
+	} else {
+		return {
+			// '$text' means text nodes and text proxies.
+			name: ctxItem.is( 'element' ) ? ctxItem.name : '$text',
+
+			* getAttributeKeys() {
+				yield* ctxItem.getAttributeKeys();
+			},
+
+			getAttribute( key ) {
+				return ctxItem.getAttribute( key );
+			}
+		};
+	}
+}

+ 1 - 3
packages/ckeditor5-engine/src/model/selection.js

@@ -785,9 +785,7 @@ function isUnvisitedBlockContainer( element, visited ) {
 
 	visited.add( element );
 
-	// TODO https://github.com/ckeditor/ckeditor5-engine/issues/532#issuecomment-278900072.
-	// This should not be a `$block` check.
-	return element.document.model.schema.itemExtends( element.name, '$block' ) && element.parent;
+	return element.document.model.schema.isBlock( element ) && element.parent;
 }
 
 // Finds the lowest element in position's ancestors which is a block.

+ 7 - 6
packages/ckeditor5-engine/src/model/utils/deletecontent.js

@@ -26,7 +26,7 @@ import Range from '../range';
  * * `<heading>x^y</heading>` with the option disabled (`leaveUnmerged == false`)
  * * `<heading>x^</heading><paragraph>y</paragraph>` with enabled (`leaveUnmerged == true`).
  *
- * Note: {@link module:engine/model/schema~Schema#objects object} and {@link module:engine/model/schema~Schema#limits limit}
+ * Note: {@link module:engine/model/schema~Schema#isObject object} and {@link module:engine/model/schema~Schema#isLimit limit}
  * elements will not be merged.
  *
  * @param {Boolean} [options.doNotResetEntireContent=false] Whether to skip replacing the entire content with a
@@ -73,12 +73,13 @@ export default function deleteContent( model, selection, options = {} ) {
 		if ( !options.leaveUnmerged ) {
 			mergeBranches( writer, startPos, endPos );
 
+			// TMP this will be replaced with a postifxer.
 			// We need to check and strip disallowed attributes in all nested nodes because after merge
 			// some attributes could end up in a path where are disallowed.
 			//
 			// e.g. bold is disallowed for <H1>
 			// <h1>Fo{o</h1><p>b}a<b>r</b><p> -> <h1>Fo{}a<b>r</b><h1> -> <h1>Fo{}ar<h1>.
-			schema.removeDisallowedAttributes( startPos.parent.getChildren(), startPos, writer );
+			schema.removeDisallowedAttributes( startPos.parent.getChildren(), writer );
 		}
 
 		selection.setCollapsedAt( startPos );
@@ -157,8 +158,8 @@ function mergeBranches( writer, startPos, endPos ) {
 }
 
 function shouldAutoparagraph( schema, position ) {
-	const isTextAllowed = schema.check( { name: '$text', inside: position } );
-	const isParagraphAllowed = schema.check( { name: 'paragraph', inside: position } );
+	const isTextAllowed = schema.checkChild( position, '$text' );
+	const isParagraphAllowed = schema.checkChild( position, 'paragraph' );
 
 	return !isTextAllowed && isParagraphAllowed;
 }
@@ -173,7 +174,7 @@ function checkCanBeMerged( leftPos, rightPos, schema ) {
 	const rangeToCheck = new Range( leftPos, rightPos );
 
 	for ( const value of rangeToCheck.getWalker() ) {
-		if ( schema.objects.has( value.item.name ) || schema.limits.has( value.item.name ) ) {
+		if ( schema.isObject( value.item ) || schema.isLimit( value.item ) ) {
 			return false;
 		}
 	}
@@ -212,5 +213,5 @@ function shouldEntireContentBeReplacedWithParagraph( schema, selection ) {
 		return false;
 	}
 
-	return schema.check( { name: 'paragraph', inside: limitElement.name } );
+	return schema.checkChild( limitElement, 'paragraph' );
 }

+ 23 - 69
packages/ckeditor5-engine/src/model/utils/insertcontent.js

@@ -116,6 +116,8 @@ class Insertion {
 		 * @member {module:engine/model/schema~Schema} #schema
 		 */
 		this.schema = model.schema;
+
+		this._filterAttributesOf = [];
 	}
 
 	/**
@@ -136,6 +138,10 @@ class Insertion {
 				isLast: ( i === ( nodes.length - 1 ) ) && parentContext.isLast
 			} );
 		}
+
+		// TMP this will become a postfixer.
+		this.schema.removeDisallowedAttributes( this._filterAttributesOf, this.writer );
+		this._filterAttributesOf = [];
 	}
 
 	/**
@@ -164,7 +170,7 @@ class Insertion {
 		// Let's handle object in a special way.
 		// * They should never be merged with other elements.
 		// * If they are not allowed in any of the selection ancestors, they could be either autoparagraphed or totally removed.
-		if ( this._checkIsObject( node ) ) {
+		if ( this.schema.isObject( node ) ) {
 			this._handleObject( node, context );
 
 			return;
@@ -222,13 +228,7 @@ class Insertion {
 		if ( node.is( 'element' ) ) {
 			this.handleNodes( node.getChildren(), context );
 		}
-		// If the node is a text and bare text is allowed in current position it means that the node
-		// contains disallowed attributes and we have to remove them.
-		else if ( this.schema.check( { name: '$text', inside: this.position } ) ) {
-			this.schema.removeDisallowedAttributes( [ node ], this.position, this.writer );
-			this._handleNode( node, context );
-		}
-		// If text is not allowed, try autoparagraphing.
+		// If text is not allowed, try autoparagraphing it.
 		else {
 			this._tryAutoparagraphing( node, context );
 		}
@@ -239,7 +239,7 @@ class Insertion {
 	 */
 	_insert( node ) {
 		/* istanbul ignore if */
-		if ( !this._checkIsAllowed( node, this.position ) ) {
+		if ( !this.schema.checkChild( this.position, node ) ) {
 			// Algorithm's correctness check. We should never end up here but it's good to know that we did.
 			// Note that it would often be a silent issue if we insert node in a place where it's not allowed.
 			log.error(
@@ -258,11 +258,13 @@ class Insertion {
 		livePos.detach();
 
 		// The last inserted object should be selected because we can't put a collapsed selection after it.
-		if ( this._checkIsObject( node ) && !this.schema.check( { name: '$text', inside: this.position } ) ) {
+		if ( this.schema.isObject( node ) && !this.schema.checkChild( this.position, '$text' ) ) {
 			this.nodeToSelect = node;
 		} else {
 			this.nodeToSelect = null;
 		}
+
+		this._filterAttributesOf.push( node );
 	}
 
 	/**
@@ -284,11 +286,6 @@ class Insertion {
 
 			this.writer.merge( mergePosLeft );
 
-			// We need to check and strip disallowed attributes in all nested nodes because after merge
-			// some attributes could end up in a path where are disallowed.
-			const parent = position.nodeBefore;
-			this.schema.removeDisallowedAttributes( parent.getChildren(), Position.createAt( parent ), this.writer );
-
 			this.position = Position.createFromPosition( position );
 			position.detach();
 		}
@@ -312,22 +309,18 @@ class Insertion {
 
 			this.writer.merge( mergePosRight );
 
-			// We need to check and strip disallowed attributes in all nested nodes because after merge
-			// some attributes could end up in a place where are disallowed.
-			this.schema.removeDisallowedAttributes( position.parent.getChildren(), position, this.writer );
-
 			this.position = Position.createFromPosition( position );
 			position.detach();
 		}
 
+		if ( mergeLeft || mergeRight ) {
+			// After merge elements that were marked by _insert() to be filtered might be gone so
+			// we need to mark the new container.
+			this._filterAttributesOf.push( this.position.parent );
+		}
+
 		mergePosLeft.detach();
 		mergePosRight.detach();
-
-		// When there was no merge we need to check and strip disallowed attributes in all nested nodes of
-		// just inserted node because some attributes could end up in a place where are disallowed.
-		if ( !mergeLeft && !mergeRight ) {
-			this.schema.removeDisallowedAttributes( node.getChildren(), Position.createAt( node ), this.writer );
-		}
 	}
 
 	/**
@@ -342,17 +335,9 @@ class Insertion {
 		// Do not autoparagraph if the paragraph won't be allowed there,
 		// cause that would lead to an infinite loop. The paragraph would be rejected in
 		// the next _handleNode() call and we'd be here again.
-		if ( this._getAllowedIn( paragraph, this.position.parent ) ) {
-			// When node is a text and is disallowed by schema it means that contains disallowed attributes
-			// and we need to remove them.
-			if ( node.is( 'text' ) && !this._checkIsAllowed( node, [ paragraph ] ) ) {
-				this.schema.removeDisallowedAttributes( [ node ], [ paragraph ], this.writer );
-			}
-
-			if ( this._checkIsAllowed( node, [ paragraph ] ) ) {
-				paragraph.appendChildren( node );
-				this._handleNode( paragraph, context );
-			}
+		if ( this._getAllowedIn( paragraph, this.position.parent ) && this.schema.checkChild( paragraph, node ) ) {
+			paragraph.appendChildren( node );
+			this._handleNode( paragraph, context );
 		}
 	}
 
@@ -370,7 +355,7 @@ class Insertion {
 
 		while ( allowedIn != this.position.parent ) {
 			// If a parent which we'd need to leave is a limit element, break.
-			if ( this.schema.limits.has( this.position.parent.name ) ) {
+			if ( this.schema.isLimit( this.position.parent ) ) {
 				return false;
 			}
 
@@ -407,7 +392,7 @@ class Insertion {
 	 * @returns {module:engine/model/element~Element|null}
 	 */
 	_getAllowedIn( node, element ) {
-		if ( this._checkIsAllowed( node, [ element ] ) ) {
+		if ( this.schema.checkChild( element, node ) ) {
 			return element;
 		}
 
@@ -417,35 +402,4 @@ class Insertion {
 
 		return null;
 	}
-
-	/**
-	 * Check whether the given node is allowed in the specified schema path.
-	 *
-	 * @param {module:engine/model/node~Node} node
-	 * @param {module:engine/model/schema~SchemaPath} path
-	 */
-	_checkIsAllowed( node, path ) {
-		return this.schema.check( {
-			name: getNodeSchemaName( node ),
-			attributes: Array.from( node.getAttributeKeys() ),
-			inside: path
-		} );
-	}
-
-	/**
-	 * Checks whether according to the schema this is an object type element.
-	 *
-	 * @param {module:engine/model/node~Node} node The node to check.
-	 */
-	_checkIsObject( node ) {
-		return this.schema.objects.has( getNodeSchemaName( node ) );
-	}
-}
-
-// Gets a name under which we should check this node in the schema.
-//
-// @param {module:engine/model/node~Node} node The node.
-// @returns {String} Node name.
-function getNodeSchemaName( node ) {
-	return node.is( 'text' ) ? '$text' : node.name;
 }

+ 4 - 4
packages/ckeditor5-engine/src/model/utils/modifyselection.js

@@ -82,19 +82,19 @@ function tryExtendingTo( data, value ) {
 	// Entering an element.
 	if ( value.type == ( data.isForward ? 'elementStart' : 'elementEnd' ) ) {
 		// If it's an object, we can select it now.
-		if ( data.schema.objects.has( value.item.name ) ) {
+		if ( data.schema.isObject( value.item ) ) {
 			return Position.createAt( value.item, data.isForward ? 'after' : 'before' );
 		}
 
 		// If text allowed on this position, extend to this place.
-		if ( data.schema.check( { name: '$text', inside: value.nextPosition } ) ) {
+		if ( data.schema.checkChild( value.nextPosition, '$text' ) ) {
 			return value.nextPosition;
 		}
 	}
 	// Leaving an element.
 	else {
 		// If leaving a limit element, stop.
-		if ( data.schema.limits.has( value.item.name ) ) {
+		if ( data.schema.isLimit( value.item ) ) {
 			// NOTE: Fast-forward the walker until the end.
 			data.walker.skip( () => true );
 
@@ -102,7 +102,7 @@ function tryExtendingTo( data, value ) {
 		}
 
 		// If text allowed on this position, extend to this place.
-		if ( data.schema.check( { name: '$text', inside: value.nextPosition } ) ) {
+		if ( data.schema.checkChild( value.nextPosition, '$text' ) ) {
 			return value.nextPosition;
 		}
 	}

+ 31 - 29
packages/ckeditor5-engine/tests/controller/datacontroller.js

@@ -45,7 +45,7 @@ describe( 'DataController', () => {
 
 	describe( 'parse()', () => {
 		it( 'should set text', () => {
-			schema.allow( { name: '$text', inside: '$root' } );
+			schema.extend( '$text', { allowIn: '$root' } );
 			const output = data.parse( '<p>foo<b>bar</b></p>' );
 
 			expect( output ).to.instanceof( ModelDocumentFragment );
@@ -53,7 +53,7 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should set paragraph', () => {
-			schema.registerItem( 'paragraph', '$block' );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 
 			buildViewConverter().for( data.viewToModel ).fromElement( 'p' ).toElement( 'paragraph' );
 
@@ -64,7 +64,7 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should set two paragraphs', () => {
-			schema.registerItem( 'paragraph', '$block' );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 
 			buildViewConverter().for( data.viewToModel ).fromElement( 'p' ).toElement( 'paragraph' );
 
@@ -75,8 +75,10 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should set paragraphs with bold', () => {
-			schema.registerItem( 'paragraph', '$block' );
-			schema.allow( { name: '$text', attributes: [ 'bold' ], inside: '$block' } );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+			schema.extend( '$text', {
+				allowAttributes: [ 'bold' ]
+			} );
 
 			buildViewConverter().for( data.viewToModel ).fromElement( 'p' ).toElement( 'paragraph' );
 			buildViewConverter().for( data.viewToModel ).fromElement( 'b' ).toAttribute( 'bold', true );
@@ -102,7 +104,7 @@ describe( 'DataController', () => {
 
 	describe( 'toModel()', () => {
 		beforeEach( () => {
-			schema.registerItem( 'paragraph', '$block' );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 
 			buildViewConverter().for( data.viewToModel ).fromElement( 'p' ).toElement( 'paragraph' );
 		} );
@@ -126,8 +128,8 @@ describe( 'DataController', () => {
 		it( 'should accept parsing context', () => {
 			modelDocument.createRoot( 'inlineRoot', 'inlineRoot' );
 
-			schema.registerItem( 'inlineRoot' );
-			schema.allow( { name: '$text', inside: 'inlineRoot' } );
+			schema.register( 'inlineRoot' );
+			schema.extend( '$text', { allowIn: 'inlineRoot' } );
 
 			const viewFragment = new ViewDocumentFragment( [ parseView( 'foo' ) ] );
 
@@ -141,14 +143,14 @@ describe( 'DataController', () => {
 
 	describe( 'set()', () => {
 		it( 'should set data to root', () => {
-			schema.allow( { name: '$text', inside: '$root' } );
+			schema.extend( '$text', { allowIn: '$root' } );
 			data.set( 'foo' );
 
 			expect( getData( model, { withoutSelection: true } ) ).to.equal( 'foo' );
 		} );
 
 		it( 'should create a batch', () => {
-			schema.allow( { name: '$text', inside: '$root' } );
+			schema.extend( '$text', { allowIn: '$root' } );
 			data.set( 'foo' );
 
 			expect( count( modelDocument.history.getDeltas() ) ).to.equal( 1 );
@@ -157,7 +159,7 @@ describe( 'DataController', () => {
 		it( 'should fire #changesDone', () => {
 			const spy = sinon.spy();
 
-			schema.allow( { name: '$text', inside: '$root' } );
+			schema.extend( '$text', { allowIn: '$root' } );
 			modelDocument.on( 'changesDone', spy );
 
 			data.set( 'foo' );
@@ -166,7 +168,7 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should get root name as a parameter', () => {
-			schema.allow( { name: '$text', inside: '$root' } );
+			schema.extend( '$text', { allowIn: '$root' } );
 			data.set( 'foo', 'main' );
 			data.set( 'Bar', 'title' );
 
@@ -179,7 +181,7 @@ describe( 'DataController', () => {
 		// This case was added when order of params was different and it really didn't work. Let's keep it
 		// if anyone will ever try to change this.
 		it( 'should allow setting empty data', () => {
-			schema.allow( { name: '$text', inside: '$root' } );
+			schema.extend( '$text', { allowIn: '$root' } );
 
 			data.set( 'foo', 'title' );
 
@@ -193,7 +195,7 @@ describe( 'DataController', () => {
 
 	describe( 'get()', () => {
 		it( 'should get paragraph with text', () => {
-			schema.registerItem( 'paragraph', '$block' );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			setData( model, '<paragraph>foo</paragraph>' );
 
 			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
@@ -202,7 +204,7 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should get empty paragraph', () => {
-			schema.registerItem( 'paragraph', '$block' );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			setData( model, '<paragraph></paragraph>' );
 
 			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
@@ -211,7 +213,7 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should get two paragraphs', () => {
-			schema.registerItem( 'paragraph', '$block' );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			setData( model, '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
 
 			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
@@ -220,14 +222,14 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should get text directly in root', () => {
-			schema.allow( { name: '$text', inside: '$root' } );
+			schema.extend( '$text', { allowIn: '$root' } );
 			setData( model, 'foo' );
 
 			expect( data.get() ).to.equal( 'foo' );
 		} );
 
 		it( 'should get paragraphs without bold', () => {
-			schema.registerItem( 'paragraph', '$block' );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			setData( model, '<paragraph>foo<$text bold="true">bar</$text></paragraph>' );
 
 			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
@@ -236,7 +238,7 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should get paragraphs with bold', () => {
-			schema.registerItem( 'paragraph', '$block' );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			setData( model, '<paragraph>foo<$text bold="true">bar</$text></paragraph>' );
 
 			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
@@ -246,8 +248,8 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should get root name as a parameter', () => {
-			schema.registerItem( 'paragraph', '$block' );
-			schema.allow( { name: '$text', inside: '$root' } );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+			schema.extend( '$text', { allowIn: '$root' } );
 
 			setData( model, '<paragraph>foo</paragraph>', { rootName: 'main' } );
 			setData( model, 'Bar', { rootName: 'title' } );
@@ -263,11 +265,11 @@ describe( 'DataController', () => {
 
 	describe( 'stringify()', () => {
 		beforeEach( () => {
-			schema.registerItem( 'paragraph', '$block' );
-			schema.registerItem( 'div' );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+			schema.register( 'div' );
 
-			schema.allow( { name: '$block', inside: 'div' } );
-			schema.allow( { name: 'div', inside: '$root' } );
+			schema.extend( '$block', { allowIn: 'div' } );
+			schema.extend( 'div', { allowIn: '$root' } );
 
 			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
 		} );
@@ -287,11 +289,11 @@ describe( 'DataController', () => {
 
 	describe( 'toView()', () => {
 		beforeEach( () => {
-			schema.registerItem( 'paragraph', '$block' );
-			schema.registerItem( 'div' );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+			schema.register( 'div' );
 
-			schema.allow( { name: '$block', inside: 'div' } );
-			schema.allow( { name: 'div', inside: '$root' } );
+			schema.extend( '$block', { allowIn: 'div' } );
+			schema.extend( 'div', { allowIn: '$root' } );
 
 			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
 		} );

+ 4 - 4
packages/ckeditor5-engine/tests/controller/editingcontroller.js

@@ -140,8 +140,8 @@ describe( 'EditingController', () => {
 			document.body.appendChild( domRoot );
 			viewRoot = editing.createRoot( domRoot );
 
-			model.schema.registerItem( 'paragraph', '$block' );
-			model.schema.registerItem( 'div', '$block' );
+			model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+			model.schema.register( 'div', { inheritAllFrom: '$block' } );
 			buildModelConverter().for( editing.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
 			buildModelConverter().for( editing.modelToView ).fromElement( 'div' ).toElement( 'div' );
 			buildModelConverter().for( editing.modelToView ).fromMarker( 'marker' ).toHighlight( {} );
@@ -396,7 +396,7 @@ describe( 'EditingController', () => {
 		it( 'should remove listenters', () => {
 			const model = new Model();
 			model.document.createRoot();
-			model.schema.registerItem( 'paragraph', '$block' );
+			model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 
 			const editing = new EditingController( model );
 
@@ -420,7 +420,7 @@ describe( 'EditingController', () => {
 		it( 'should destroy view', () => {
 			const model = new Model();
 			model.document.createRoot();
-			model.schema.registerItem( 'paragraph', '$block' );
+			model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 
 			const editing = new EditingController( model );
 

+ 1 - 1
packages/ckeditor5-engine/tests/conversion/advanced-converters.js

@@ -42,7 +42,7 @@ describe( 'advanced-converters', () => {
 
 		viewRoot = editing.createRoot( 'div' );
 
-		viewDispatcher = new ViewConversionDispatcher( model, { schema: { check: () => true } } );
+		viewDispatcher = new ViewConversionDispatcher( model, { schema: { checkChild: () => true } } );
 		viewDispatcher.on( 'text', convertText() );
 		viewDispatcher.on( 'documentFragment', convertToModelFragment() );
 

+ 75 - 40
packages/ckeditor5-engine/tests/conversion/buildviewconverter.js

@@ -6,7 +6,6 @@
 import buildViewConverter from '../../src/conversion/buildviewconverter';
 
 import Model from '../../src/model/model';
-import ModelSchema from '../../src/model/schema';
 import ModelDocumentFragment from '../../src/model/documentfragment';
 import ModelElement from '../../src/model/element';
 import ModelTextProxy from '../../src/model/textproxy';
@@ -60,36 +59,50 @@ function modelToString( item ) {
 	return result;
 }
 
-const textAttributes = [ undefined, 'linkHref', 'linkTitle', 'bold', 'italic', 'style' ];
-const pAttributes = [ undefined, 'class', 'important', 'theme', 'decorated', 'size' ];
+const textAttributes = [ 'linkHref', 'linkTitle', 'bold', 'italic', 'style' ];
+const pAttributes = [ 'class', 'important', 'theme', 'decorated', 'size' ];
 
 describe( 'View converter builder', () => {
-	let dispatcher, schema, additionalData;
-
-	const model = new Model();
+	let dispatcher, model, schema, additionalData;
 
 	beforeEach( () => {
+		model = new Model();
+
 		// `additionalData` parameter for `.convert` calls.
 		additionalData = { context: [ '$root' ] };
 
-		schema = new ModelSchema();
-
-		schema.registerItem( 'paragraph', '$block' );
-		schema.registerItem( 'div', '$block' );
-		schema.registerItem( 'customP', 'paragraph' );
-		schema.registerItem( 'image', '$inline' );
-		schema.registerItem( 'span', '$inline' );
-		schema.registerItem( 'MEGATRON', '$inline' ); // Yes, folks, we are building MEGATRON.
-		schema.registerItem( 'abcd', '$inline' );
-		schema.allow( { name: '$inline', attributes: textAttributes, inside: '$root' } );
-		schema.allow( { name: 'image', attributes: [ 'src' ], inside: '$root' } );
-		schema.allow( { name: 'image', attributes: [ 'src' ], inside: '$block' } );
-		schema.allow( { name: '$text', inside: '$inline' } );
-		schema.allow( { name: '$text', attributes: textAttributes, inside: '$block' } );
-		schema.allow( { name: '$text', attributes: textAttributes, inside: '$root' } );
-		schema.allow( { name: 'paragraph', attributes: pAttributes, inside: '$root' } );
-		schema.allow( { name: 'span', attributes: [ 'transformer' ], inside: '$root' } );
-		schema.allow( { name: 'div', attributes: [ 'class' ], inside: '$root' } );
+		schema = model.schema;
+
+		schema.register( 'paragraph', {
+			inheritAllFrom: '$block',
+			allowAttributes: pAttributes
+		} );
+		schema.register( 'div', {
+			inheritAllFrom: '$block',
+			allowAttributes: 'class'
+		} );
+		schema.register( 'customP', {
+			inheritAllFrom: 'paragraph'
+		} );
+		schema.register( 'image', {
+			inheritAllFrom: '$text',
+			allowAttributes: 'src'
+		} );
+		schema.register( 'span', {
+			inheritAllFrom: '$text',
+			allowAttributes: 'transformer'
+		} );
+		// Yes, folks, we are building MEGATRON.
+		schema.register( 'MEGATRON', {
+			inheritAllFrom: '$text'
+		} );
+		schema.register( 'abcd', {
+			inheritAllFrom: '$text'
+		} );
+		schema.extend( '$text', {
+			allowAttributes: textAttributes,
+			allowIn: [ '$root', 'span', 'abcd', 'MEGATRON' ]
+		} );
 
 		dispatcher = new ViewConversionDispatcher( model, { schema } );
 		dispatcher.on( 'text', convertText() );
@@ -152,7 +165,7 @@ describe( 'View converter builder', () => {
 	} );
 
 	it( 'should convert from view attribute and key to model attribute', () => {
-		schema.allow( { name: 'paragraph', attributes: [ 'type' ], inside: '$root' } );
+		schema.extend( 'paragraph', { allowAttributes: 'type' } );
 
 		dispatcher.on( 'documentFragment', convertToModelFragment() );
 
@@ -481,7 +494,17 @@ describe( 'View converter builder', () => {
 		buildViewConverter().for( dispatcher ).fromElement( 'div' ).toElement( 'div' );
 		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
 
-		schema.disallow( { name: 'div', inside: '$root' } );
+		// Disallow $root>div.
+		schema.on( 'checkChild', ( evt, args ) => {
+			const ctx = args[ 0 ];
+			const child = args[ 1 ];
+			const childRule = schema.getDefinition( child );
+
+			if ( childRule.name == 'div' && ctx.endsWith( '$root' ) ) {
+				evt.stop();
+				evt.return = false;
+			}
+		}, { priority: 'high' } );
 
 		dispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
 
@@ -496,24 +519,36 @@ describe( 'View converter builder', () => {
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph>foo</paragraph>' );
 	} );
 
-	it( 'should filter out structure that is wrong with schema - attributes', () => {
-		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
-		buildViewConverter().for( dispatcher ).fromElement( 'strong' ).toAttribute( 'bold', true );
+	// TMP We can't make this test work for now.
+	// See https://github.com/ckeditor/ckeditor5-engine/issues/1213#issuecomment-354454906
+	//
+	// it( 'should filter out structure that is wrong with schema - attributes', () => {
+	// 	buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
+	// 	buildViewConverter().for( dispatcher ).fromElement( 'strong' ).toAttribute( 'bold', true );
 
-		schema.disallow( { name: '$text', attributes: 'bold', inside: 'paragraph' } );
+	// 	// Disallow bold in paragraph>$text.
+	// 	schema.on( 'checkAttribute', ( evt, args ) => {
+	// 		const context = args[ 0 ];
+	// 		const attributeName = args[ 1 ];
 
-		dispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
+	// 		if ( ctx.endsWith( 'paragraph $text' ) && attributeName == 'bold' ) {
+	// 			evt.stop();
+	// 			evt.return = false;
+	// 		}
+	// 	}, { priority: 'high' } );
 
-		const viewElement = new ViewContainerElement( 'p', null,
-			new ViewAttributeElement( 'strong', null,
-				new ViewText( 'foo' )
-			)
-		);
+	// 	dispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
 
-		const conversionResult = dispatcher.convert( viewElement, additionalData );
+	// 	const viewElement = new ViewContainerElement( 'p', null,
+	// 		new ViewAttributeElement( 'strong', null,
+	// 			new ViewText( 'foo' )
+	// 		)
+	// 	);
 
-		expect( modelToString( conversionResult ) ).to.equal( '<paragraph>foo</paragraph>' );
-	} );
+	// 	const conversionResult = dispatcher.convert( viewElement, additionalData );
+
+	// 	expect( modelToString( conversionResult ) ).to.equal( '<paragraph>foo</paragraph>' );
+	// } );
 
 	it( 'should stop to element conversion if creating function returned null', () => {
 		buildViewConverter()
@@ -535,7 +570,7 @@ describe( 'View converter builder', () => {
 	} );
 
 	it( 'should stop to attribute conversion if creating function returned null', () => {
-		schema.allow( { name: 'paragraph', attributes: [ 'type' ], inside: '$root' } );
+		schema.extend( 'paragraph', { allowAttributes: 'type' } );
 
 		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
 

+ 13 - 11
packages/ckeditor5-engine/tests/conversion/definition-based-converters.js

@@ -21,7 +21,6 @@ import {
 } from '../../src/conversion/definition-based-converters';
 
 import ViewConversionDispatcher from '../../src/conversion/viewconversiondispatcher';
-import ModelSchema from '../../src/model/schema';
 import ModelWalker from '../../src/model/treewalker';
 import ModelTextProxy from '../../src/model/textproxy';
 import Model from '../../src/model/model';
@@ -104,7 +103,7 @@ describe( 'definition-based-converters', () => {
 
 	function setupViewToModelTests() {
 		additionalData = { context: [ '$root' ] };
-		schema = new ModelSchema();
+		schema = model.schema;
 		dispatcher = new ViewConversionDispatcher( model, { schema } );
 	}
 
@@ -214,10 +213,11 @@ describe( 'definition-based-converters', () => {
 			beforeEach( () => {
 				setupViewToModelTests();
 
-				schema.registerItem( 'div', '$block' );
-
-				schema.allow( { name: '$inline', attributes: [ 'foo' ], inside: '$root' } );
-				schema.allow( { name: '$text', inside: '$root' } );
+				schema.register( 'div', { inheritAllFrom: '$block' } );
+				schema.extend( '$text', {
+					allowIn: '$root',
+					allowAttributes: 'foo'
+				} );
 
 				dispatcher.on( 'text', convertText() );
 			} );
@@ -377,12 +377,14 @@ describe( 'definition-based-converters', () => {
 			beforeEach( () => {
 				setupViewToModelTests();
 
-				schema.registerItem( 'div', '$block' );
-				schema.registerItem( 'bar', '$block' );
-				schema.registerItem( 'baz', '$block' );
+				schema.register( 'div', { inheritAllFrom: '$block' } );
+				schema.register( 'bar', { inheritAllFrom: '$block' } );
+				schema.register( 'baz', { inheritAllFrom: '$block' } );
 
-				schema.allow( { name: '$inline', attribute: [ 'foo' ], inside: '$root' } );
-				schema.allow( { name: '$text', inside: '$inline' } );
+				schema.extend( '$text', {
+					allowIn: '$root',
+					allowAttributes: 'foo'
+				} );
 
 				dispatcher.on( 'text', convertText() );
 			} );

+ 9 - 9
packages/ckeditor5-engine/tests/conversion/model-selection-to-view-converters.js

@@ -46,7 +46,7 @@ describe( 'model-selection-to-view-converters', () => {
 		modelRoot = modelDoc.createRoot();
 		modelSelection = modelDoc.selection;
 
-		model.schema.allow( { name: '$text', inside: '$root' } );
+		model.schema.extend( '$text', { allowIn: '$root' } );
 
 		viewDoc = new ViewDocument();
 		viewRoot = viewDoc.createRoot( 'div' );
@@ -637,14 +637,14 @@ describe( 'model-selection-to-view-converters', () => {
 
 	describe( 'table cell selection converter', () => {
 		beforeEach( () => {
-			model.schema.registerItem( 'table' );
-			model.schema.registerItem( 'tr' );
-			model.schema.registerItem( 'td' );
-
-			model.schema.allow( { name: 'table', inside: '$root' } );
-			model.schema.allow( { name: 'tr', inside: 'table' } );
-			model.schema.allow( { name: 'td', inside: 'tr' } );
-			model.schema.allow( { name: '$text', inside: 'td' } );
+			model.schema.register( 'table' );
+			model.schema.register( 'tr' );
+			model.schema.register( 'td' );
+
+			model.schema.extend( 'table', { allowIn: '$root' } );
+			model.schema.extend( 'tr', { allowIn: 'table' } );
+			model.schema.extend( 'td', { allowIn: 'tr' } );
+			model.schema.extend( '$text', { allowIn: 'td' } );
 
 			// "Universal" converter to convert table structure.
 			const tableConverter = insertElement( data => new ViewContainerElement( data.item.name ) );

+ 1 - 1
packages/ckeditor5-engine/tests/conversion/view-selection-to-model-converters.js

@@ -21,7 +21,7 @@ describe( 'convertSelectionChange', () => {
 	beforeEach( () => {
 		model = new Model();
 		modelRoot = model.document.createRoot();
-		model.schema.registerItem( 'paragraph', '$block' );
+		model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 
 		modelSetData( model, '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
 

+ 18 - 8
packages/ckeditor5-engine/tests/conversion/view-to-model-converters.js

@@ -9,7 +9,6 @@ import ViewDocumentFragment from '../../src/view/documentfragment';
 import ViewText from '../../src/view/text';
 
 import Model from '../../src/model/model';
-import ModelSchema from '../../src/model/schema';
 import ModelDocumentFragment from '../../src/model/documentfragment';
 import ModelElement from '../../src/model/element';
 import ModelText from '../../src/model/text';
@@ -17,15 +16,17 @@ import ModelText from '../../src/model/text';
 import { convertToModelFragment, convertText } from '../../src/conversion/view-to-model-converters';
 
 describe( 'view-to-model-converters', () => {
-	let dispatcher, schema, additionalData;
-
-	const model = new Model();
+	let dispatcher, schema, additionalData, model;
 
 	beforeEach( () => {
-		schema = new ModelSchema();
-		schema.registerItem( 'paragraph', '$block' );
-		schema.allow( { name: '$text', inside: '$root' } );
+		model = new Model();
+		schema = model.schema;
+
+		schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+		schema.extend( '$text', { allowIn: '$root' } );
+
 		additionalData = { context: [ '$root' ] };
+
 		dispatcher = new ViewConversionDispatcher( model, { schema } );
 	} );
 
@@ -62,7 +63,16 @@ describe( 'view-to-model-converters', () => {
 		} );
 
 		it( 'should not convert text if it is wrong with schema', () => {
-			schema.disallow( { name: '$text', inside: '$root' } );
+			schema.on( 'checkChild', ( evt, args ) => {
+				const ctx = args[ 0 ];
+				const child = args[ 1 ];
+				const childRule = schema.getDefinition( child );
+
+				if ( childRule.name == '$text' && ctx.endsWith( '$root' ) ) {
+					evt.stop();
+					evt.return = false;
+				}
+			}, { priority: 'high' } );
 
 			const viewText = new ViewText( 'foobar' );
 			dispatcher.on( 'text', convertText() );

+ 28 - 22
packages/ckeditor5-engine/tests/dev-utils/model.js

@@ -23,22 +23,28 @@ describe( 'model test utils', () => {
 		sandbox = sinon.sandbox.create();
 		selection.removeAllRanges();
 
-		model.schema.registerItem( 'a', '$inline' );
-		model.schema.allow( { name: 'a', inside: '$root' } );
-		model.schema.allow( { name: 'a', inside: '$root', attributes: [ 'bar', 'car', 'foo' ] } );
-
-		model.schema.registerItem( 'b', '$inline' );
-		model.schema.allow( { name: 'b', inside: '$root' } );
-		model.schema.allow( { name: 'b', inside: '$root', attributes: [ 'barFoo', 'fooBar', 'x' ] } );
-
-		model.schema.registerItem( 'c', '$inline' );
-		model.schema.allow( { name: 'c', inside: '$root' } );
-
-		model.schema.registerItem( 'paragraph', '$block' );
-		model.schema.allow( { name: '$text', inside: '$root' } );
-		model.schema.allow( { name: '$text', inside: 'a' } );
-		model.schema.allow( { name: '$text', inside: 'b' } );
-		model.schema.allow( { name: 'c', inside: 'b' } );
+		model.schema.register( 'a', {
+			allowWhere: '$text',
+			allowIn: '$root',
+			allowAttributes: [ 'bar', 'car', 'foo' ]
+		} );
+
+		model.schema.register( 'b', {
+			allowWhere: '$text',
+			allowIn: '$root',
+			allowAttributes: [ 'barFoo', 'fooBar', 'x' ]
+		} );
+
+		model.schema.register( 'c', {
+			allowWhere: '$text',
+			allowIn: [ '$root', 'b' ]
+		} );
+
+		model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+
+		model.schema.extend( '$text', {
+			allowIn: [ '$root', 'a', 'b' ]
+		} );
 	} );
 
 	afterEach( () => {
@@ -162,8 +168,8 @@ describe( 'model test utils', () => {
 		it( 'should work in a special root', () => {
 			const model = new Model();
 
-			model.schema.registerItem( 'textOnly' );
-			model.schema.allow( { name: '$text', inside: 'textOnly' } );
+			model.schema.register( 'textOnly' );
+			model.schema.extend( '$text', { allowIn: 'textOnly' } );
 			model.document.createRoot( 'textOnly', 'textOnly' );
 
 			setData( model, 'a[b]c', { rootName: 'textOnly' } );
@@ -504,7 +510,7 @@ describe( 'model test utils', () => {
 		it( 'throws when try to set element not registered in schema', () => {
 			expect( () => {
 				parse( '<xyz></xyz>', model.schema );
-			} ).to.throw( Error, 'Element \'xyz\' not allowed in context ["$root"].' );
+			} ).to.throw( Error, 'Element \'xyz\' was not allowed in context ["$root"].' );
 		} );
 
 		it( 'throws when try to set text directly to $root without registering it', () => {
@@ -512,13 +518,13 @@ describe( 'model test utils', () => {
 
 			expect( () => {
 				parse( 'text', model.schema );
-			} ).to.throw( Error, 'Element \'$text\' not allowed in context ["$root"].' );
+			} ).to.throw( Error, 'Text was not allowed in context ["$root"].' );
 		} );
 
 		it( 'converts data in the specified context', () => {
 			const model = new Model();
-			model.schema.registerItem( 'foo' );
-			model.schema.allow( { name: '$text', inside: 'foo' } );
+			model.schema.register( 'foo' );
+			model.schema.extend( '$text', { allowIn: 'foo' } );
 
 			expect( () => {
 				parse( 'text', model.schema, { context: [ 'foo' ] } );

+ 4 - 3
packages/ckeditor5-engine/tests/manual/highlight.js

@@ -37,9 +37,10 @@ class FancyWidget extends Plugin {
 		const editing = editor.editing;
 
 		// Configure schema.
-		schema.registerItem( 'fancywidget' );
-		schema.allow( { name: 'fancywidget', inside: '$root' } );
-		schema.objects.add( 'fancywidget' );
+		schema.register( 'fancywidget', {
+			isObject: true
+		} );
+		schema.extend( 'fancywidget', { allowIn: '$root' } );
 
 		// Build converter from model to view for editing pipeline.
 		buildModelConverter().for( editing.modelToView )

+ 9 - 8
packages/ckeditor5-engine/tests/manual/nestededitable.js

@@ -28,14 +28,15 @@ class NestedEditable extends Plugin {
 		const data = editor.data;
 		const schema = editor.model.schema;
 
-		schema.registerItem( 'figure' );
-		schema.registerItem( 'figcaption' );
-		schema.allow( { name: 'figure', inside: '$root' } );
-		schema.allow( { name: 'figcaption', inside: 'figure' } );
-		schema.objects.add( 'figure' );
-
-		schema.allow( { name: '$inline', inside: 'figure' } );
-		schema.allow( { name: '$inline', inside: 'figcaption' } );
+		schema.register( 'figure', {
+			isObject: true
+		} );
+		schema.register( 'figcaption' );
+		schema.extend( 'figure', { allowIn: '$root' } );
+		schema.extend( 'figcaption', { allowIn: 'figure' } );
+		schema.extend( '$text', {
+			allowIn: [ 'figure', 'figcaption' ]
+		} );
 
 		buildModelConverter().for( data.modelToView, editing.modelToView )
 			.fromElement( 'figure' )

+ 57 - 7
packages/ckeditor5-engine/tests/manual/tickets/1088/1.js

@@ -11,9 +11,26 @@ import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articleplugi
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 		plugins: [ ArticlePluginSet ],
-		toolbar: [ 'headings', 'undo', 'redo' ],
+		toolbar: {
+			items: [
+				'headings',
+				'bold',
+				'italic',
+				'link',
+				'bulletedList',
+				'numberedList',
+				'blockQuote',
+				'undo',
+				'redo'
+			]
+		},
 		image: {
-			toolbar: [ 'imageTextAlternative' ]
+			toolbar: [
+				'imageStyleFull',
+				'imageStyleSide',
+				'|',
+				'imageTextAlternative'
+			]
 		}
 	} )
 	.then( editor => {
@@ -21,11 +38,44 @@ ClassicEditor
 
 		const schema = editor.model.schema;
 
-		schema.disallow( { name: '$text', attributes: [ 'linkHref', 'italic' ], inside: 'heading1' } );
-		schema.disallow( { name: '$text', attributes: [ 'italic' ], inside: 'heading2' } );
-		schema.disallow( { name: '$text', attributes: [ 'linkHref' ], inside: 'blockQuote listItem' } );
-		schema.disallow( { name: '$text', attributes: [ 'bold' ], inside: 'paragraph' } );
-		schema.disallow( { name: 'heading3', inside: '$root' } );
+		schema.on( 'checkAttribute', ( evt, args ) => {
+			const ctx = args[ 0 ];
+			const attributeName = args[ 1 ];
+
+			if ( ctx.endsWith( 'heading1 $text' ) && [ 'linkHref', 'italic' ].includes( attributeName ) ) {
+				evt.stop();
+				evt.return = false;
+			}
+
+			if ( ctx.endsWith( 'heading2 $text' ) && attributeName == 'italic' ) {
+				evt.stop();
+				evt.return = false;
+			}
+
+			if ( ctx.endsWith( 'heading2 $text' ) && attributeName == 'italic' ) {
+				evt.stop();
+				evt.return = false;
+			}
+
+			if ( ctx.endsWith( 'blockQuote listItem $text' ) && attributeName == 'linkHref' ) {
+				evt.stop();
+				evt.return = false;
+			}
+
+			if ( ctx.endsWith( 'paragraph $text' ) && attributeName == 'bold' ) {
+				evt.stop();
+				evt.return = false;
+			}
+		} );
+
+		schema.on( 'checkChild', ( evt, args ) => {
+			const def = schema.getDefinition( args[ 1 ] );
+
+			if ( args[ 0 ].endsWith( '$root' ) && def.name == 'heading3' ) {
+				evt.stop();
+				evt.return = false;
+			}
+		} );
 	} )
 	.catch( err => {
 		console.error( err.stack );

+ 1 - 1
packages/ckeditor5-engine/tests/manual/tickets/475/1.js

@@ -31,7 +31,7 @@ class Link extends Plugin {
 		const editing = editor.editing;
 
 		// Allow bold attribute on all inline nodes.
-		editor.model.schema.allow( { name: '$inline', attributes: [ 'link' ] } );
+		editor.model.schema.extend( '$text', { allowAttributes: 'link' } );
 
 		// Build converter from model to view for data and editing pipelines.
 		buildModelConverter().for( data.modelToView, editing.modelToView )

+ 11 - 9
packages/ckeditor5-engine/tests/model/document/document.js

@@ -246,18 +246,20 @@ describe( 'Document', () => {
 		let selection;
 
 		beforeEach( () => {
-			model.schema.registerItem( 'paragraph', '$block' );
+			model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 
-			model.schema.registerItem( 'emptyBlock' );
-			model.schema.allow( { name: 'emptyBlock', inside: '$root' } );
+			model.schema.register( 'emptyBlock', { allowIn: '$root' } );
 
-			model.schema.registerItem( 'widget' );
-			model.schema.allow( { name: 'widget', inside: '$root' } );
-			model.schema.objects.add( 'widget' );
+			model.schema.register( 'widget', {
+				allowIn: '$root',
+				isObject: true
+			} );
 
-			model.schema.registerItem( 'blockWidget', '$block' );
-			model.schema.allow( { name: 'blockWidget', inside: '$root' } );
-			model.schema.objects.add( 'blockWidget' );
+			model.schema.register( 'blockWidget', {
+				allowIn: '$root',
+				allowContentOf: '$block',
+				isObject: true
+			} );
 
 			doc.createRoot();
 			selection = doc.selection;

+ 19 - 16
packages/ckeditor5-engine/tests/model/documentselection.js

@@ -46,10 +46,11 @@ describe( 'DocumentSelection', () => {
 			new Element( 'p', [], new Text( 'foobar' ) )
 		] );
 		selection = doc.selection;
-		model.schema.registerItem( 'p', '$block' );
+		model.schema.register( 'p', { inheritAllFrom: '$block' } );
 
-		model.schema.registerItem( 'widget' );
-		model.schema.objects.add( 'widget' );
+		model.schema.register( 'widget', {
+			isObject: true
+		} );
 
 		liveRange = new LiveRange( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) );
 		range = new Range( new Position( root, [ 2 ] ), new Position( root, [ 2, 2 ] ) );
@@ -94,8 +95,8 @@ describe( 'DocumentSelection', () => {
 				new Element( 'img' ),
 				new Element( 'p', [], new Text( 'foobar' ) )
 			] );
-			model.schema.registerItem( 'img' );
-			model.schema.registerItem( 'p', '$block' );
+			model.schema.register( 'img' );
+			model.schema.register( 'p', { inheritAllFrom: '$block' } );
 			selection = doc.selection;
 
 			const ranges = Array.from( selection.getRanges() );
@@ -360,7 +361,7 @@ describe( 'DocumentSelection', () => {
 
 		// See #630.
 		it( 'should refresh attributes – integration test for #630', () => {
-			model.schema.allow( { name: '$text', inside: '$root' } );
+			model.schema.extend( '$text', { allowIn: '$root' } );
 
 			setData( model, 'f<$text italic="true">[o</$text><$text bold="true">ob]a</$text>r' );
 
@@ -941,16 +942,18 @@ describe( 'DocumentSelection', () => {
 		// #986
 		describe( 'are not inherited from the inside of object elements', () => {
 			beforeEach( () => {
-				model.schema.registerItem( 'image' );
-				model.schema.allow( { name: 'image', inside: '$root' } );
-				model.schema.allow( { name: 'image', inside: '$block' } );
-				model.schema.allow( { name: '$inline', inside: 'image' } );
-				model.schema.objects.add( 'image' );
-
-				model.schema.registerItem( 'caption' );
-				model.schema.allow( { name: '$inline', inside: 'caption' } );
-				model.schema.allow( { name: 'caption', inside: 'image' } );
-				model.schema.allow( { name: '$text', attributes: 'bold', inside: 'caption' } );
+				model.schema.register( 'image', {
+					isObject: true
+				} );
+				model.schema.extend( 'image', { allowIn: '$root' } );
+				model.schema.extend( 'image', { allowIn: '$block' } );
+
+				model.schema.register( 'caption' );
+				model.schema.extend( 'caption', { allowIn: 'image' } );
+				model.schema.extend( '$text', {
+					allowIn: [ 'image', 'caption' ],
+					allowAttributes: 'bold'
+				} );
 			} );
 
 			it( 'ignores attributes inside an object if selection contains that object', () => {

+ 8 - 8
packages/ckeditor5-engine/tests/model/liverange.js

@@ -430,11 +430,11 @@ describe( 'LiveRange', () => {
 			let live;
 
 			beforeEach( () => {
-				model.schema.registerItem( 'p', '$block' );
-				model.schema.registerItem( 'w' );
+				model.schema.register( 'p', { inheritAllFrom: '$block' } );
+				model.schema.register( 'w' );
 
-				model.schema.allow( { name: 'p', inside: 'w' } );
-				model.schema.allow( { name: 'w', inside: '$root' } );
+				model.schema.extend( 'p', { allowIn: 'w' } );
+				model.schema.extend( 'w', { allowIn: '$root' } );
 			} );
 
 			afterEach( () => {
@@ -526,11 +526,11 @@ describe( 'LiveRange', () => {
 			let live;
 
 			beforeEach( () => {
-				model.schema.registerItem( 'p', '$block' );
-				model.schema.registerItem( 'w' );
+				model.schema.register( 'p', { inheritAllFrom: '$block' } );
+				model.schema.register( 'w' );
 
-				model.schema.allow( { name: 'p', inside: 'w' } );
-				model.schema.allow( { name: 'w', inside: '$root' } );
+				model.schema.extend( 'p', { allowIn: 'w' } );
+				model.schema.extend( 'w', { allowIn: '$root' } );
 			} );
 
 			afterEach( () => {

+ 45 - 19
packages/ckeditor5-engine/tests/model/model.js

@@ -27,7 +27,32 @@ describe( 'Model', () => {
 		changes = '';
 	} );
 
-	describe( 'change & enqueueChange', () => {
+	describe( 'constructor()', () => {
+		it( 'registers $root to the schema', () => {
+			expect( schema.isRegistered( '$root' ) ).to.be.true;
+			expect( schema.isLimit( '$root' ) ).to.be.true;
+		} );
+
+		it( 'registers $block to the schema', () => {
+			expect( schema.isRegistered( '$block' ) ).to.be.true;
+			expect( schema.isBlock( '$block' ) ).to.be.true;
+			expect( schema.checkChild( [ '$root' ], '$block' ) ).to.be.true;
+		} );
+
+		it( 'registers $text to the schema', () => {
+			expect( schema.isRegistered( '$text' ) ).to.be.true;
+			expect( schema.checkChild( [ '$block' ], '$text' ) ).to.be.true;
+		} );
+
+		it( 'registers $clipboardHolder to the schema', () => {
+			expect( schema.isRegistered( '$clipboardHolder' ) ).to.be.true;
+			expect( schema.isLimit( '$clipboardHolder' ) ).to.be.true;
+			expect( schema.checkChild( [ '$clipboardHolder' ], '$text' ) ).to.be.true;
+			expect( schema.checkChild( [ '$clipboardHolder' ], '$block' ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'change() & enqueueChange()', () => {
 		it( 'should execute changes immediately', () => {
 			model.change( () => {
 				changes += 'A';
@@ -318,7 +343,7 @@ describe( 'Model', () => {
 		} );
 	} );
 
-	describe( 'applyOperation', () => {
+	describe( 'applyOperation()', () => {
 		it( 'should execute provided operation end return the result of operation', () => {
 			const returnValue = { foo: 'bar' };
 
@@ -334,7 +359,7 @@ describe( 'Model', () => {
 		} );
 	} );
 
-	describe( 'transformDeltas', () => {
+	describe( 'transformDeltas()', () => {
 		it( 'should use deltaTransform.transformDeltaSets', () => {
 			sinon.spy( deltaTransform, 'transformDeltaSets' );
 
@@ -374,7 +399,7 @@ describe( 'Model', () => {
 
 	describe( 'insertContent()', () => {
 		it( 'should be decorated', () => {
-			schema.allow( { name: '$text', inside: '$root' } ); // To surpress warnings.
+			schema.extend( '$text', { allowIn: '$root' } ); // To surpress warnings.
 
 			const spy = sinon.spy();
 
@@ -386,7 +411,7 @@ describe( 'Model', () => {
 		} );
 
 		it( 'should insert content (item)', () => {
-			schema.registerItem( 'paragraph', '$block' );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 
 			setData( model, '<paragraph>fo[]ar</paragraph>' );
 
@@ -396,7 +421,7 @@ describe( 'Model', () => {
 		} );
 
 		it( 'should insert content (document fragment)', () => {
-			schema.registerItem( 'paragraph', '$block' );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 
 			setData( model, '<paragraph>fo[]ar</paragraph>' );
 
@@ -406,7 +431,7 @@ describe( 'Model', () => {
 		} );
 
 		it( 'should use parent batch', () => {
-			schema.registerItem( 'paragraph', '$block' );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			setData( model, '<paragraph>[]</paragraph>' );
 
 			model.change( writer => {
@@ -428,7 +453,7 @@ describe( 'Model', () => {
 		} );
 
 		it( 'should delete selected content', () => {
-			schema.registerItem( 'paragraph', '$block' );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 
 			setData( model, '<paragraph>fo[ob]ar</paragraph>' );
 
@@ -438,7 +463,7 @@ describe( 'Model', () => {
 		} );
 
 		it( 'should use parent batch', () => {
-			schema.registerItem( 'paragraph', '$block' );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 
 			setData( model, '<paragraph>fo[ob]ar</paragraph>' );
 
@@ -451,7 +476,7 @@ describe( 'Model', () => {
 
 	describe( 'modifySelection()', () => {
 		it( 'should be decorated', () => {
-			schema.registerItem( 'paragraph', '$block' );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			setData( model, '<paragraph>fo[ob]ar</paragraph>' );
 
 			const spy = sinon.spy();
@@ -464,7 +489,7 @@ describe( 'Model', () => {
 		} );
 
 		it( 'should modify a selection', () => {
-			schema.registerItem( 'paragraph', '$block' );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 
 			setData( model, '<paragraph>fo[ob]ar</paragraph>' );
 
@@ -487,7 +512,7 @@ describe( 'Model', () => {
 		} );
 
 		it( 'should return selected content', () => {
-			schema.registerItem( 'paragraph', '$block' );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 
 			setData( model, '<paragraph>fo[ob]ar</paragraph>' );
 
@@ -497,7 +522,7 @@ describe( 'Model', () => {
 		} );
 
 		it( 'should use parent batch', () => {
-			schema.registerItem( 'paragraph', '$block' );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 
 			setData( model, '<paragraph>fo[ob]ar</paragraph>' );
 
@@ -512,12 +537,13 @@ describe( 'Model', () => {
 		let root;
 
 		beforeEach( () => {
-			schema.registerItem( 'paragraph', '$block' );
-			schema.registerItem( 'div', '$block' );
-			schema.allow( { name: '$block', inside: 'div' } );
-			schema.registerItem( 'image' );
-			schema.allow( { name: 'image', inside: 'div' } );
-			schema.objects.add( 'image' );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+			schema.register( 'div', { inheritAllFrom: '$block' } );
+			schema.extend( '$block', { allowIn: 'div' } );
+			schema.register( 'image', {
+				isObject: true
+			} );
+			schema.extend( 'image', { allowIn: 'div' } );
 
 			setData(
 				model,

+ 1 - 1
packages/ckeditor5-engine/tests/model/operation/utils.js

@@ -21,7 +21,7 @@ describe( 'Operation utils', () => {
 	beforeEach( () => {
 		model = new Model();
 		doc = model.document;
-		model.schema.allow( { name: '$text', inside: '$root' } );
+		model.schema.extend( '$text', { allowIn: '$root' } );
 
 		root = doc.createRoot();
 

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

@@ -0,0 +1,2196 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Schema, { SchemaContext } from '../../src/model/schema';
+
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+import Model from '../../src/model/model';
+
+import Element from '../../src/model/element';
+import Text from '../../src/model/text';
+import TextProxy from '../../src/model/textproxy';
+import Position from '../../src/model/position';
+import Range from '../../src/model/range';
+import Selection from '../../src/model/selection';
+
+import { setData, getData, stringify } from '../../src/dev-utils/model';
+
+import AttributeDelta from '../../src/model/delta/attributedelta';
+
+describe( 'Schema', () => {
+	let schema, root1, r1p1, r1p2, r1bQ, r1bQp, root2;
+
+	beforeEach( () => {
+		schema = new Schema();
+
+		root1 = new Element( '$root', null, [
+			new Element( 'paragraph', null, 'foo' ),
+			new Element( 'paragraph', { align: 'right' }, 'bar' ),
+			new Element( 'blockQuote', null, [
+				new Element( 'paragraph', null, 'foo' )
+			] )
+		] );
+		r1p1 = root1.getChild( 0 );
+		r1p2 = root1.getChild( 1 );
+		r1bQ = root1.getChild( 2 );
+		r1bQp = r1bQ.getChild( 0 );
+
+		root2 = new Element( '$root2' );
+	} );
+
+	describe( 'register()', () => {
+		it( 'allows registering an item', () => {
+			schema.register( 'foo' );
+
+			expect( schema.getDefinition( 'foo' ) ).to.be.an( 'object' );
+		} );
+
+		it( 'copies definitions', () => {
+			const definitions = {};
+
+			schema.register( 'foo', definitions );
+
+			definitions.isBlock = true;
+
+			expect( schema.getDefinitions().foo ).to.not.have.property( 'isBlock' );
+		} );
+
+		it( 'throws when trying to register for a single item twice', () => {
+			schema.register( 'foo' );
+
+			expect( () => {
+				schema.register( 'foo' );
+			} ).to.throw( CKEditorError, /^schema-cannot-register-item-twice:/ );
+		} );
+	} );
+
+	describe( 'extend()', () => {
+		it( 'allows extending item\'s definitions', () => {
+			schema.register( 'foo' );
+
+			schema.extend( 'foo', {
+				isBlock: true
+			} );
+
+			expect( schema.getDefinition( 'foo' ) ).to.have.property( 'isBlock', true );
+		} );
+
+		it( 'copies definitions', () => {
+			schema.register( 'foo', {} );
+
+			const definitions = {};
+			schema.extend( 'foo', definitions );
+
+			definitions.isBlock = true;
+
+			expect( schema.getDefinitions().foo ).to.not.have.property( 'isBlock' );
+		} );
+
+		it( 'throws when trying to extend a not yet registered item', () => {
+			expect( () => {
+				schema.extend( 'foo' );
+			} ).to.throw( CKEditorError, /^schema-cannot-extend-missing-item:/ );
+		} );
+	} );
+
+	describe( 'getDefinitions()', () => {
+		it( 'returns compiled definitions', () => {
+			schema.register( '$root' );
+
+			schema.register( 'foo', {
+				allowIn: '$root'
+			} );
+
+			schema.extend( 'foo', {
+				isBlock: true
+			} );
+
+			const definitions = schema.getDefinitions();
+
+			expect( definitions.foo ).to.deep.equal( {
+				name: 'foo',
+				allowIn: [ '$root' ],
+				allowAttributes: [],
+				isBlock: true
+			} );
+		} );
+
+		it( 'copies all is* types', () => {
+			schema.register( 'foo', {
+				isBlock: true,
+				isFoo: true
+			} );
+
+			schema.extend( 'foo', {
+				isBar: true,
+				isFoo: false // Just to check that the last one wins.
+			} );
+
+			const definitions = schema.getDefinitions();
+
+			expect( definitions.foo ).to.have.property( 'isBlock', true );
+			expect( definitions.foo ).to.have.property( 'isFoo', false );
+			expect( definitions.foo ).to.have.property( 'isBar', true );
+		} );
+
+		it( 'does not recompile definitions if not needed', () => {
+			schema.register( 'foo' );
+
+			expect( schema.getDefinitions() ).to.equal( schema.getDefinitions() );
+		} );
+
+		it( 'ensures no duplicates in allowIn', () => {
+			schema.register( '$root' );
+			schema.register( 'foo', {
+				allowIn: '$root'
+			} );
+			schema.extend( 'foo', {
+				allowIn: '$root'
+			} );
+
+			const definitions = schema.getDefinitions();
+
+			expect( definitions.foo ).to.deep.equal( {
+				name: 'foo',
+				allowIn: [ '$root' ],
+				allowAttributes: []
+			} );
+		} );
+
+		it( 'ensures no unregistered items in allowIn', () => {
+			schema.register( 'foo', {
+				allowIn: '$root'
+			} );
+
+			const definitions = schema.getDefinitions();
+
+			expect( definitions.foo ).to.deep.equal( {
+				name: 'foo',
+				allowIn: [],
+				allowAttributes: []
+			} );
+		} );
+
+		it( 'ensures no duplicates in allowAttributes', () => {
+			schema.register( 'paragraph', {
+				allowAttributes: 'foo'
+			} );
+			schema.extend( 'paragraph', {
+				allowAttributes: 'foo'
+			} );
+
+			const definitions = schema.getDefinitions();
+
+			expect( definitions.paragraph ).to.deep.equal( {
+				name: 'paragraph',
+				allowIn: [],
+				allowAttributes: [ 'foo' ]
+			} );
+		} );
+
+		it( 'ensures no duplicates in allowAttributes duplicated by allowAttributesOf', () => {
+			schema.register( 'paragraph', {
+				allowAttributes: 'foo',
+				allowAttributesOf: '$block'
+			} );
+			schema.register( '$block', {
+				allowAttributes: 'foo'
+			} );
+
+			const definitions = schema.getDefinitions();
+
+			expect( definitions.paragraph ).to.deep.equal( {
+				name: 'paragraph',
+				allowIn: [],
+				allowAttributes: [ 'foo' ]
+			} );
+		} );
+	} );
+
+	describe( 'getDefinition()', () => {
+		it( 'returns a definition based on an item name', () => {
+			schema.register( 'foo', {
+				isMe: true
+			} );
+
+			expect( schema.getDefinition( 'foo' ).isMe ).to.be.true;
+		} );
+
+		it( 'returns a definition based on an element name', () => {
+			schema.register( 'foo', {
+				isMe: true
+			} );
+
+			expect( schema.getDefinition( new Element( 'foo' ) ).isMe ).to.be.true;
+		} );
+
+		it( 'returns a definition based on a text node', () => {
+			schema.register( '$text', {
+				isMe: true
+			} );
+
+			expect( schema.getDefinition( new Text( 'foo' ) ).isMe ).to.be.true;
+		} );
+
+		it( 'returns a definition based on a text proxy', () => {
+			schema.register( '$text', {
+				isMe: true
+			} );
+
+			const text = new Text( 'foo' );
+			const textProxy = new TextProxy( text, 0, 1 );
+
+			expect( schema.getDefinition( textProxy ).isMe ).to.be.true;
+		} );
+
+		it( 'returns a definition based on a schema context item', () => {
+			schema.register( 'foo', {
+				isMe: true
+			} );
+			const ctx = new SchemaContext( [ '$root', 'foo' ] );
+
+			expect( schema.getDefinition( ctx.last ).isMe ).to.be.true;
+		} );
+
+		it( 'returns undefined when trying to get an unregistered item', () => {
+			expect( schema.getDefinition( '404' ) ).to.be.undefined;
+		} );
+	} );
+
+	describe( 'isRegistered()', () => {
+		it( 'returns true if an item was registered', () => {
+			schema.register( 'foo' );
+
+			expect( schema.isRegistered( 'foo' ) ).to.be.true;
+		} );
+
+		it( 'returns false if an item was not registered', () => {
+			expect( schema.isRegistered( 'foo' ) ).to.be.false;
+		} );
+
+		it( 'uses getDefinition()\'s item to definition normalization', () => {
+			const stub = sinon.stub( schema, 'getDefinition' ).returns( {} );
+
+			expect( schema.isRegistered( 'foo' ) ).to.be.true;
+			expect( stub.calledOnce ).to.be.true;
+		} );
+	} );
+
+	describe( 'isBlock()', () => {
+		it( 'returns true if an item was registered as a block', () => {
+			schema.register( 'foo', {
+				isBlock: true
+			} );
+
+			expect( schema.isBlock( 'foo' ) ).to.be.true;
+		} );
+
+		it( 'returns false if an item was not registered as a block', () => {
+			schema.register( 'foo' );
+
+			expect( schema.isBlock( 'foo' ) ).to.be.false;
+		} );
+
+		it( 'returns false if an item was not registered at all', () => {
+			expect( schema.isBlock( 'foo' ) ).to.be.false;
+		} );
+
+		it( 'uses getDefinition()\'s item to definition normalization', () => {
+			const stub = sinon.stub( schema, 'getDefinition' ).returns( { isBlock: true } );
+
+			expect( schema.isBlock( 'foo' ) ).to.be.true;
+			expect( stub.calledOnce ).to.be.true;
+		} );
+	} );
+
+	describe( 'isLimit()', () => {
+		it( 'returns true if an item was registered as a limit element', () => {
+			schema.register( 'foo', {
+				isLimit: true
+			} );
+
+			expect( schema.isLimit( 'foo' ) ).to.be.true;
+		} );
+
+		it( 'returns false if an item was not registered as a limit element', () => {
+			schema.register( 'foo' );
+
+			expect( schema.isLimit( 'foo' ) ).to.be.false;
+		} );
+
+		it( 'returns false if an item was not registered at all', () => {
+			expect( schema.isLimit( 'foo' ) ).to.be.false;
+		} );
+
+		it( 'uses getDefinition()\'s item to definition normalization', () => {
+			const stub = sinon.stub( schema, 'getDefinition' ).returns( { isLimit: true } );
+
+			expect( schema.isLimit( 'foo' ) ).to.be.true;
+			expect( stub.calledOnce ).to.be.true;
+		} );
+	} );
+
+	describe( 'isObject()', () => {
+		it( 'returns true if an item was registered as an object', () => {
+			schema.register( 'foo', {
+				isObject: true
+			} );
+
+			expect( schema.isObject( 'foo' ) ).to.be.true;
+		} );
+
+		it( 'returns false if an item was not registered as an object', () => {
+			schema.register( 'foo' );
+
+			expect( schema.isObject( 'foo' ) ).to.be.false;
+		} );
+
+		it( 'returns false if an item was not registered at all', () => {
+			expect( schema.isObject( 'foo' ) ).to.be.false;
+		} );
+
+		it( 'uses getDefinition()\'s item to definition normalization', () => {
+			const stub = sinon.stub( schema, 'getDefinition' ).returns( { isObject: true } );
+
+			expect( schema.isObject( 'foo' ) ).to.be.true;
+			expect( stub.calledOnce ).to.be.true;
+		} );
+	} );
+
+	describe( 'checkChild()', () => {
+		beforeEach( () => {
+			schema.register( '$root' );
+			schema.register( 'paragraph', {
+				allowIn: '$root'
+			} );
+			schema.register( '$text', {
+				allowIn: 'paragraph'
+			} );
+		} );
+
+		it( 'accepts an element as a context and a node name as a child', () => {
+			expect( schema.checkChild( root1, 'paragraph' ) ).to.be.true;
+			expect( schema.checkChild( root1, '$text' ) ).to.be.false;
+		} );
+
+		it( 'accepts a position as a context', () => {
+			const posInRoot = Position.createAt( root1 );
+			const posInParagraph = Position.createAt( r1p1 );
+
+			expect( schema.checkChild( posInRoot, 'paragraph' ) ).to.be.true;
+			expect( schema.checkChild( posInRoot, '$text' ) ).to.be.false;
+
+			expect( schema.checkChild( posInParagraph, '$text' ) ).to.be.true;
+			expect( schema.checkChild( posInParagraph, 'paragraph' ) ).to.be.false;
+		} );
+
+		// This is a temporary feature which is needed to make the current V->M conversion works.
+		// It should be removed once V->M conversion uses real positions.
+		// Of course, real positions have this advantage that we know element attributes at this point.
+		it( 'accepts an array of element names as a context', () => {
+			const contextInRoot = [ '$root' ];
+			const contextInParagraph = [ '$root', 'paragraph' ];
+
+			expect( schema.checkChild( contextInRoot, 'paragraph' ) ).to.be.true;
+			expect( schema.checkChild( contextInRoot, '$text' ) ).to.be.false;
+
+			expect( schema.checkChild( contextInParagraph, '$text' ) ).to.be.true;
+			expect( schema.checkChild( contextInParagraph, 'paragraph' ) ).to.be.false;
+		} );
+
+		it( 'accepts an array of elements as a context', () => {
+			const contextInRoot = [ root1 ];
+			const contextInParagraph = [ root1, r1p1 ];
+
+			expect( schema.checkChild( contextInRoot, 'paragraph' ) ).to.be.true;
+			expect( schema.checkChild( contextInRoot, '$text' ) ).to.be.false;
+
+			expect( schema.checkChild( contextInParagraph, '$text' ) ).to.be.true;
+			expect( schema.checkChild( contextInParagraph, 'paragraph' ) ).to.be.false;
+		} );
+
+		// Again, this is needed temporarily to handle current V->M conversion
+		it( 'accepts a mixed array of elements and strings as a context', () => {
+			const contextInParagraph = [ '$root', r1p1 ];
+
+			expect( schema.checkChild( contextInParagraph, '$text' ) ).to.be.true;
+			expect( schema.checkChild( contextInParagraph, 'paragraph' ) ).to.be.false;
+		} );
+
+		it( 'accepts a node as a child', () => {
+			expect( schema.checkChild( root1, r1p1 ) ).to.be.true;
+			expect( schema.checkChild( root1, new Text( 'foo' ) ) ).to.be.false;
+		} );
+
+		// TODO checks fires event
+	} );
+
+	describe( 'checkAttribute()', () => {
+		beforeEach( () => {
+			schema.register( 'paragraph', {
+				allowAttributes: 'align'
+			} );
+			schema.register( '$text', {
+				allowAttributes: 'bold'
+			} );
+		} );
+
+		it( 'accepts an element as a context', () => {
+			expect( schema.checkAttribute( r1p1, 'align' ) ).to.be.true;
+			expect( schema.checkAttribute( r1p1, 'bold' ) ).to.be.false;
+		} );
+
+		it( 'accepts a text as a context', () => {
+			expect( schema.checkAttribute( new Text( 'foo' ), 'bold' ) ).to.be.true;
+			expect( schema.checkAttribute( new Text( 'foo' ), 'align' ) ).to.be.false;
+		} );
+
+		it( 'accepts a position as a context', () => {
+			const posInRoot = Position.createAt( root1 );
+			const posInParagraph = Position.createAt( r1p1 );
+
+			expect( schema.checkAttribute( posInRoot, 'align' ) ).to.be.false;
+			expect( schema.checkAttribute( posInParagraph, 'align' ) ).to.be.true;
+		} );
+
+		it( 'accepts an array of node names as a context', () => {
+			const contextInRoot = [ '$root' ];
+			const contextInParagraph = [ '$root', 'paragraph' ];
+			const contextInText = [ '$root', 'paragraph', '$text' ];
+
+			expect( schema.checkAttribute( contextInRoot, 'align' ) ).to.be.false;
+			expect( schema.checkAttribute( contextInParagraph, 'align' ) ).to.be.true;
+			expect( schema.checkAttribute( contextInText, 'bold' ) ).to.be.true;
+		} );
+
+		it( 'accepts an array of nodes as a context', () => {
+			const contextInRoot = [ root1 ];
+			const contextInParagraph = [ root1, r1p1 ];
+			const contextInText = [ root1, r1p1, r1p1.getChild( 0 ) ];
+
+			expect( schema.checkAttribute( contextInRoot, 'align' ) ).to.be.false;
+			expect( schema.checkAttribute( contextInParagraph, 'align' ) ).to.be.true;
+			expect( schema.checkAttribute( contextInText, 'bold' ) ).to.be.true;
+		} );
+
+		// TODO checks fires event
+	} );
+
+	describe( 'getLimitElement()', () => {
+		let model, doc, root;
+
+		beforeEach( () => {
+			model = new Model();
+			doc = model.document;
+			schema = model.schema;
+			root = doc.createRoot();
+
+			schema.register( 'div', {
+				inheritAllFrom: '$block'
+			} );
+			schema.register( 'article', {
+				inheritAllFrom: '$block',
+				allowIn: 'section'
+			} );
+			schema.register( 'section', {
+				inheritAllFrom: '$block',
+				allowIn: 'div'
+			} );
+			schema.register( 'paragraph', {
+				inheritAllFrom: '$block',
+				allowIn: 'article'
+			} );
+			schema.register( 'widget', {
+				inheritAllFrom: '$block',
+				allowIn: 'div'
+			} );
+			schema.register( 'image', {
+				inheritAllFrom: '$block',
+				allowIn: 'widget'
+			} );
+			schema.register( 'caption', {
+				inheritAllFrom: '$block',
+				allowIn: 'image'
+			} );
+		} );
+
+		it( 'always returns $root element if any other limit was not defined', () => {
+			schema.extend( '$root', {
+				isLimit: false
+			} );
+			expect( schema.isLimit( '$root' ) ).to.be.false;
+
+			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.extend( 'article', { isLimit: true } );
+			schema.extend( 'section', { isLimit: true } );
+
+			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.extend( 'article', { isLimit: true } );
+			schema.extend( 'section', { isLimit: true } );
+
+			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.extend( 'article', { isLimit: true } );
+			schema.extend( 'widget', { isLimit: true } );
+			schema.extend( 'div', { isLimit: true } );
+
+			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', () => {
+			setData(
+				model,
+				'<div>' +
+					'<section>' +
+						'<article>' +
+							'<paragraph>[foo]</paragraph>' +
+						'</article>' +
+					'</section>' +
+				'</div>' +
+				'<section>b[]ar</section>'
+			);
+
+			expect( schema.getLimitElement( doc.selection ) ).to.equal( root );
+		} );
+	} );
+
+	describe( 'checkAttributeInSelection()', () => {
+		const attribute = 'bold';
+		let model, doc, schema;
+
+		beforeEach( () => {
+			model = new Model();
+			doc = model.document;
+			doc.createRoot();
+
+			schema = model.schema;
+
+			schema.register( 'p', { inheritAllFrom: '$block' } );
+			schema.register( 'h1', { inheritAllFrom: '$block' } );
+			schema.register( 'img', { allowWhere: '$text' } );
+			schema.register( 'figure', {
+				allowIn: '$root',
+				allowAttributes: [ 'name', 'title' ]
+			} );
+
+			schema.on( 'checkAttribute', ( evt, args ) => {
+				const ctx = args[ 0 ];
+				const attributeName = args[ 1 ];
+
+				// Allow 'bold' on p>$text.
+				if ( ctx.endsWith( 'p $text' ) && attributeName == 'bold' ) {
+					evt.stop();
+					evt.return = true;
+				}
+
+				// Allow 'bold' on $root>p.
+				if ( ctx.endsWith( '$root p' ) && attributeName == 'bold' ) {
+					evt.stop();
+					evt.return = true;
+				}
+			}, { priority: 'high' } );
+		} );
+
+		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.register( 'p', { inheritAllFrom: '$block' } );
+			schema.register( 'h1', { inheritAllFrom: '$block' } );
+			schema.register( 'img', {
+				allowWhere: '$text'
+			} );
+
+			schema.on( 'checkAttribute', ( evt, args ) => {
+				const ctx = args[ 0 ];
+				const attributeName = args[ 1 ];
+
+				// Allow 'bold' on p>$text.
+				if ( ctx.endsWith( 'p $text' ) && attributeName == 'bold' ) {
+					evt.stop();
+					evt.return = true;
+				}
+
+				// Allow 'bold' on $root>p.
+				if ( ctx.endsWith( '$root p' ) && attributeName == 'bold' ) {
+					evt.stop();
+					evt.return = true;
+				}
+			}, { priority: 'high' } );
+
+			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.extend( 'img', { allowAttributes: 'bold' } );
+
+			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.extend( 'img', { allowAttributes: 'bold' } );
+			schema.extend( '$text', { allowIn: 'img' } );
+
+			expect( schema.getValidRanges( ranges, attribute ) ).to.deep.equal( ranges );
+		} );
+
+		it( 'should return two ranges when attribute is not allowed on one item', () => {
+			schema.extend( 'img', { allowAttributes: 'bold' } );
+			schema.extend( '$text', { allowIn: '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.extend( '$text', { allowIn: 'img' } );
+
+			schema.on( 'checkAttribute', ( evt, args ) => {
+				const ctx = args[ 0 ];
+				const attributeName = args[ 1 ];
+
+				// Allow 'bold' on img>$text.
+				if ( ctx.endsWith( 'img $text' ) && attributeName == 'bold' ) {
+					evt.stop();
+					evt.return = true;
+				}
+			}, { priority: 'high' } );
+
+			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.extend( '$text', { allowIn: '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', () => {
+			schema.on( 'checkAttribute', ( evt, args ) => {
+				const ctx = args[ 0 ];
+				const attributeName = args[ 1 ];
+
+				// Disallow 'bold' on p>img.
+				if ( ctx.endsWith( 'p img' ) && attributeName == 'bold' ) {
+					evt.stop();
+					evt.return = false;
+				}
+			}, { priority: 'high' } );
+
+			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( 'removeDisallowedAttributes()', () => {
+		let model, doc, root;
+
+		beforeEach( () => {
+			model = new Model();
+			doc = model.document;
+			root = doc.createRoot();
+			schema = model.schema;
+
+			schema.register( 'paragraph', {
+				inheritAllFrom: '$block'
+			} );
+			schema.register( 'div', {
+				inheritAllFrom: '$block'
+			} );
+			schema.register( 'image', {
+				isObject: true
+			} );
+			schema.extend( '$block', {
+				allowIn: 'div'
+			} );
+		} );
+
+		it( 'should filter out disallowed attributes from given nodes', () => {
+			schema.extend( '$text', { allowAttributes: 'a' } );
+			schema.extend( 'image', { allowAttributes: 'b' } );
+
+			const text = new Text( 'foo', { a: 1, b: 1 } );
+			const image = new Element( 'image', { a: 1, b: 1 } );
+
+			root.appendChildren( [ text, image ] );
+
+			model.change( writer => {
+				schema.removeDisallowedAttributes( root.getChildren(), 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 );
+
+				expect( getData( model, { withoutSelection: true } ) )
+					.to.equal( '<$text a="1">foo</$text><image b="1"></image>' );
+			} );
+		} );
+
+		it( 'should filter out disallowed attributes from all descendants of given nodes', () => {
+			schema.on( 'checkAttribute', ( evt, args ) => {
+				const ctx = args[ 0 ];
+				const attributeName = args[ 1 ];
+
+				// Allow 'a' on div>$text.
+				if ( ctx.endsWith( 'div $text' ) && attributeName == 'a' ) {
+					evt.stop();
+					evt.return = true;
+				}
+
+				// Allow 'b' on div>paragraph>$text.
+				if ( ctx.endsWith( 'div paragraph $text' ) && attributeName == 'b' ) {
+					evt.stop();
+					evt.return = true;
+				}
+
+				// Allow 'a' on div>image.
+				if ( ctx.endsWith( 'div image' ) && attributeName == 'a' ) {
+					evt.stop();
+					evt.return = true;
+				}
+
+				// Allow 'b' on div>paragraph>image.
+				if ( ctx.endsWith( 'div paragraph image' ) && attributeName == 'b' ) {
+					evt.stop();
+					evt.return = true;
+				}
+			}, { priority: 'high' } );
+
+			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 ] );
+			const div = new Element( 'div', [], [ paragraph, bar, imageInDiv ] );
+
+			root.appendChildren( [ div ] );
+
+			model.change( writer => {
+				schema.removeDisallowedAttributes( root.getChildren(), 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( 'definitions compilation', () => {
+		describe( 'allowIn cases', () => {
+			it( 'passes $root>paragraph', () => {
+				schema.register( '$root' );
+				schema.register( 'paragraph', {
+					allowIn: '$root'
+				} );
+
+				expect( schema.checkChild( root1, r1p1 ) ).to.be.true;
+			} );
+
+			it( 'passes $root>paragraph and $root2>paragraph – support for array values', () => {
+				schema.register( '$root' );
+				schema.register( '$root2' );
+				schema.register( 'paragraph', {
+					allowIn: [ '$root', '$root2' ]
+				} );
+
+				expect( schema.checkChild( root1, r1p1 ) ).to.be.true;
+				expect( schema.checkChild( root2, r1p1 ) ).to.be.true;
+			} );
+
+			it( 'passes $root>paragraph[align] – attributes does not matter', () => {
+				schema.register( '$root' );
+				schema.register( 'paragraph', {
+					allowIn: '$root'
+				} );
+
+				expect( schema.checkChild( root1, r1p2 ) ).to.be.true;
+			} );
+
+			it( 'passes $root>div>div – in case of circular refs', () => {
+				schema.register( '$root' );
+				schema.register( 'div', {
+					allowIn: [ '$root', 'div' ]
+				} );
+
+				const div = new Element( 'div' );
+				root1.appendChildren( div );
+
+				const div2 = new Element( 'div' );
+
+				expect( schema.checkChild( div, div2 ) ).to.be.true;
+			} );
+
+			it( 'passes $root>div>div – in case of circular refs, when div1==div2', () => {
+				schema.register( '$root' );
+				schema.register( 'div', {
+					allowIn: [ '$root', 'div' ]
+				} );
+
+				const div = new Element( 'div' );
+				root1.appendChildren( div );
+
+				expect( schema.checkChild( div, div ) ).to.be.true;
+			} );
+
+			it( 'rejects $root>paragraph – unregistered paragraph', () => {
+				schema.register( '$root' );
+
+				expect( schema.checkChild( root1, r1p1 ) ).to.be.false;
+			} );
+
+			it( 'rejects $root>paragraph – registered different item', () => {
+				schema.register( '$root' );
+				schema.register( 'paragraph' );
+				schema.register( 'listItem', {
+					allowIn: '$root'
+				} );
+
+				expect( schema.checkChild( root1, r1p1 ) ).to.be.false;
+			} );
+
+			it( 'rejects $root>paragraph – paragraph allowed in different context', () => {
+				schema.register( '$root' );
+				schema.register( '$fancyRoot' );
+				schema.register( 'paragraph', {
+					allowIn: '$fancyRoot'
+				} );
+
+				expect( schema.checkChild( root1, r1p1 ) ).to.be.false;
+			} );
+
+			it( 'rejects $root>blockQuote>paragraph – since paragraph is only allowed in $root', () => {
+				schema.register( '$root' );
+				schema.register( 'paragraph', {
+					allowIn: '$root'
+				} );
+
+				expect( schema.checkChild( r1bQ, r1bQp ) ).to.be.false;
+			} );
+
+			it( 'rejects $root>blockQuote>paragraph – since paragraph is only allowed in $root v2', () => {
+				schema.register( '$root' );
+				schema.register( 'blockQuote', {
+					allowIn: '$root'
+				} );
+				schema.register( 'paragraph', {
+					allowIn: '$root'
+				} );
+
+				expect( schema.checkChild( r1bQ, r1bQp ) ).to.be.false;
+			} );
+
+			it( 'rejects $root>blockQuote>paragraph>$text - since paragraph is not allowed in blockQuote', () => {
+				schema.register( '$root' );
+				schema.register( 'paragraph', {
+					allowIn: '$root'
+				} );
+				schema.register( '$text', {
+					allowIn: 'paragraph'
+				} );
+
+				expect( schema.checkChild( root1, r1bQp.getChild( 0 ) ) ).to.be.false;
+			} );
+
+			it( 'rejects $root>blockQuote>paragraph>$text - since blockQuote is not allowed in $root', () => {
+				schema.register( '$root' );
+				schema.register( 'blockQuote' );
+				schema.register( 'paragraph', {
+					allowIn: [ 'blockQuote', '$root' ]
+				} );
+				schema.register( '$text', {
+					allowIn: 'paragraph'
+				} );
+
+				expect( schema.checkChild( root1, r1bQp.getChild( 0 ) ) ).to.be.false;
+			} );
+		} );
+
+		describe( 'allowWhere cases', () => {
+			it( 'passes $root>paragraph – paragraph inherits from $block', () => {
+				schema.register( '$root' );
+				schema.register( '$block', {
+					allowIn: '$root'
+				} );
+				schema.register( 'paragraph', {
+					allowWhere: '$block'
+				} );
+
+				expect( schema.checkChild( root1, r1p1 ) ).to.be.true;
+			} );
+
+			it( 'supports the array syntax', () => {
+				schema.register( '$root' );
+				schema.register( '$root2' );
+				schema.register( '$block', {
+					allowIn: '$root'
+				} );
+				schema.register( '$block2', {
+					allowIn: '$root2'
+				} );
+				schema.register( 'paragraph', {
+					allowWhere: [ '$block', '$block2' ]
+				} );
+
+				expect( schema.checkChild( root1, r1p1 ), '$root' ).to.be.true;
+				expect( schema.checkChild( root2, r1p1 ), '$root2' ).to.be.true;
+			} );
+
+			// This checks if some inapropriate caching or preprocessing isn't applied by register().
+			it( 'passes $root>paragraph – paragraph inherits from $block, order of definitions does not matter', () => {
+				schema.register( '$root' );
+				schema.register( 'paragraph', {
+					allowWhere: '$block'
+				} );
+				schema.register( '$block', {
+					allowIn: '$root'
+				} );
+
+				expect( schema.checkChild( root1, r1p1 ) ).to.be.true;
+			} );
+
+			it( 'passes $root>paragraph – paragraph inherits from $specialBlock which inherits from $block', () => {
+				schema.register( '$root' );
+				schema.register( '$block', {
+					allowIn: '$root'
+				} );
+				schema.register( '$specialBlock', {
+					allowWhere: '$block'
+				} );
+				schema.register( 'paragraph', {
+					allowWhere: '$specialBlock'
+				} );
+
+				expect( schema.checkChild( root1, r1p1 ) ).to.be.true;
+			} );
+
+			it( 'rejects $root>paragraph – paragraph inherits from $block but $block is not allowed in $root', () => {
+				schema.register( '$root' );
+				schema.register( '$block' );
+				schema.register( 'paragraph', {
+					allowWhere: '$block'
+				} );
+
+				expect( schema.checkChild( root1, r1p1 ) ).to.be.false;
+			} );
+
+			it( 'rejects $root>paragraph>$text – paragraph inherits from $block but $block is not allowed in $root', () => {
+				schema.register( '$root' );
+				schema.register( '$block' );
+				schema.register( 'paragraph', {
+					allowWhere: '$block'
+				} );
+				schema.register( '$text', {
+					allowIn: 'paragraph'
+				} );
+
+				expect( schema.checkChild( root1, r1p1.getChild( 0 ) ) ).to.be.false;
+			} );
+		} );
+
+		describe( 'allowContentOf cases', () => {
+			it( 'passes $root2>paragraph – $root2 inherits from $root', () => {
+				schema.register( '$root' );
+				schema.register( 'paragraph', {
+					allowIn: '$root'
+				} );
+				schema.register( '$root2', {
+					allowContentOf: '$root'
+				} );
+
+				expect( schema.checkChild( root2, r1p1 ) ).to.be.true;
+			} );
+
+			it( 'supports the array syntax', () => {
+				schema.register( '$root' );
+				schema.register( '$root2' );
+				schema.register( 'paragraph', {
+					allowIn: '$root'
+				} );
+				schema.register( 'heading1', {
+					allowIn: '$root2'
+				} );
+				schema.register( '$root3', {
+					allowContentOf: [ '$root', '$root2' ]
+				} );
+
+				const root3 = new Element( '$root3' );
+				const heading1 = new Element( 'heading1' );
+
+				expect( schema.checkChild( root3, r1p1 ), 'paragraph' ).to.be.true;
+				expect( schema.checkChild( root3, heading1 ), 'heading1' ).to.be.true;
+			} );
+
+			it( 'passes $root2>paragraph – $root2 inherits from $root, order of definitions does not matter', () => {
+				schema.register( '$root' );
+				schema.register( '$root2', {
+					allowContentOf: '$root'
+				} );
+				schema.register( 'paragraph', {
+					allowIn: '$root'
+				} );
+
+				expect( schema.checkChild( root2, r1p1 ) ).to.be.true;
+			} );
+
+			it( 'passes $root>paragraph>$text – paragraph inherits content of $block', () => {
+				schema.register( '$root' );
+				schema.register( '$block' );
+				schema.register( 'paragraph', {
+					allowIn: '$root',
+					allowContentOf: '$block'
+				} );
+				schema.register( '$text', {
+					allowIn: '$block'
+				} );
+
+				expect( schema.checkChild( r1p1, r1p1.getChild( 0 ) ) ).to.be.true;
+			} );
+
+			it( 'passes $root>blockQuote>paragraph – blockQuote inherits content of $root', () => {
+				schema.register( '$root' );
+				schema.register( 'blockQuote', {
+					allowIn: '$root',
+					allowContentOf: '$root'
+				} );
+				schema.register( 'paragraph', {
+					allowIn: '$root'
+				} );
+
+				expect( schema.checkChild( r1bQ, r1bQp ) ).to.be.true;
+			} );
+
+			it( 'rejects $root2>paragraph – $root2 inherits from $root, but paragraph is not allowed there anyway', () => {
+				schema.register( '$root' );
+				schema.register( 'paragraph' );
+				schema.register( '$root2', {
+					allowContentOf: '$root'
+				} );
+
+				expect( schema.checkChild( root2, r1p1 ) ).to.be.false;
+			} );
+		} );
+
+		describe( 'mix of allowContentOf and allowWhere', () => {
+			it( 'passes $root>paragraph>$text – paragraph inherits all from $block', () => {
+				schema.register( '$root' );
+				schema.register( '$block', {
+					allowIn: '$root'
+				} );
+				schema.register( 'paragraph', {
+					allowContentOf: '$block',
+					allowWhere: '$block'
+				} );
+				schema.register( '$text', {
+					allowIn: '$block'
+				} );
+
+				expect( schema.checkChild( r1p1, r1p1.getChild( 0 ) ) ).to.be.true;
+			} );
+
+			it( 'passes $root>paragraph and $root2>paragraph – where $root2 inherits content of $root' +
+				'and paragraph inherits allowWhere from $block', () => {
+				schema.register( '$root' );
+				schema.register( '$root2', {
+					allowContentOf: '$root'
+				} );
+				schema.register( '$block', {
+					allowIn: '$root'
+				} );
+				schema.register( 'paragraph', {
+					allowWhere: '$block'
+				} );
+
+				expect( schema.checkChild( root1, 'paragraph' ), 'root1' ).to.be.true;
+				expect( schema.checkChild( root2, 'paragraph' ), 'root2' ).to.be.true;
+			} );
+
+			it( 'passes d>a where d inherits content of c which inherits content of b', () => {
+				schema.register( 'b' );
+				schema.register( 'a', { allowIn: 'b' } );
+				schema.register( 'c', { allowContentOf: 'b' } );
+				schema.register( 'd', { allowContentOf: 'c' } );
+
+				const d = new Element( 'd' );
+
+				expect( schema.checkChild( d, 'a' ) ).to.be.true;
+			} );
+
+			// This case won't pass becuase we compile the definitions in a pretty naive way.
+			// To make chains like this work we'd need to repeat compilation of allowContentOf definitions
+			// as long as the previous iteration found something to compile.
+			// This way, even though we'd not compile d<-c in the first run, we'd still find b<-c
+			// and since we've found something, we'd now try d<-c which would work.
+			//
+			// We ignore those situations for now as they are very unlikely to happen and would
+			// significantly raised the complexity of definition compilation.
+			//
+			// it( 'passes d>a where d inherits content of c which inherits content of b', () => {
+			// 	schema.register( 'b' );
+			// 	schema.register( 'a', { allowIn: 'b' } );
+			// 	schema.register( 'd', { allowContentOf: 'c' } );
+			// 	schema.register( 'c', { allowContentOf: 'b' } );
+			//
+			// 	const d = new Element( 'd' );
+			//
+			// 	expect( schema.checkChild( d, 'a' ) ).to.be.true;
+			// } );
+		} );
+
+		describe( 'inheritTypesFrom', () => {
+			it( 'inherit properties of another item', () => {
+				schema.register( '$block', {
+					isBlock: true,
+					isLimit: true
+				} );
+				schema.register( 'paragraph', {
+					inheritTypesFrom: '$block'
+				} );
+
+				expect( schema.getDefinition( 'paragraph' ).isBlock ).to.be.true;
+				expect( schema.getDefinition( 'paragraph' ).isLimit ).to.be.true;
+			} );
+
+			it( 'inherit properties of other items – support for arrays', () => {
+				schema.register( '$block', {
+					isBlock: true
+				} );
+				schema.register( '$block2', {
+					isLimit: true
+				} );
+				schema.register( 'paragraph', {
+					inheritTypesFrom: [ '$block', '$block2' ]
+				} );
+
+				expect( schema.getDefinition( 'paragraph' ).isBlock ).to.be.true;
+				expect( schema.getDefinition( 'paragraph' ).isLimit ).to.be.true;
+			} );
+
+			it( 'does not override existing props', () => {
+				schema.register( '$block', {
+					isBlock: true,
+					isLimit: true
+				} );
+				schema.register( 'paragraph', {
+					inheritTypesFrom: '$block',
+					isLimit: false
+				} );
+
+				expect( schema.getDefinition( 'paragraph' ).isBlock ).to.be.true;
+				expect( schema.getDefinition( 'paragraph' ).isLimit ).to.be.false;
+			} );
+		} );
+
+		describe( 'inheritAllFrom', () => {
+			it( 'passes $root>paragraph – paragraph inherits allowIn from $block', () => {
+				schema.register( '$root' );
+				schema.register( '$block', {
+					allowIn: '$root'
+				} );
+				schema.register( 'paragraph', {
+					inheritAllFrom: '$block'
+				} );
+
+				expect( schema.checkChild( root1, r1p1 ) ).to.be.true;
+			} );
+
+			it( 'paragraph inherit properties of $block', () => {
+				schema.register( '$block', {
+					isBlock: true
+				} );
+				schema.register( 'paragraph', {
+					inheritAllFrom: '$block'
+				} );
+
+				expect( schema.isBlock( r1p1 ) ).to.be.true;
+			} );
+
+			it( 'passes $root>paragraph>$text – paragraph inherits allowed content of $block', () => {
+				schema.register( '$root' );
+				schema.register( '$block', {
+					allowIn: '$root'
+				} );
+				schema.register( '$text', {
+					allowIn: '$block'
+				} );
+				schema.register( 'paragraph', {
+					inheritAllFrom: '$block'
+				} );
+
+				expect( schema.checkChild( r1p1, r1p1.getChild( 0 ) ) ).to.be.true;
+			} );
+
+			it( 'passes $root>paragraph>$text – paragraph inherits allowIn from $block through $block\'s allowWhere', () => {
+				schema.register( '$root' );
+				schema.register( '$blockProto', {
+					allowIn: '$root'
+				} );
+				schema.register( '$block', {
+					allowWhere: '$blockProto'
+				} );
+				schema.register( 'paragraph', {
+					inheritAllFrom: '$block'
+				} );
+
+				expect( schema.checkChild( root1, r1p1 ) ).to.be.true;
+			} );
+
+			it( 'passes $root>paragraph>$text – paragraph inherits allowIn from $block through $block\'s allowWhere', () => {
+				schema.register( '$root' );
+				schema.register( '$blockProto' );
+				schema.register( '$block', {
+					allowContentOf: '$blockProto',
+					allowIn: '$root'
+				} );
+				schema.register( '$text', {
+					allowIn: '$blockProto'
+				} );
+				schema.register( 'paragraph', {
+					inheritAllFrom: '$block'
+				} );
+
+				expect( schema.checkChild( r1p1, r1p1.getChild( 0 ) ) ).to.be.true;
+			} );
+		} );
+
+		// We need to handle cases where some independent features registered definitions which might use
+		// optional elements (elements which might not have been registered).
+		describe( 'missing structure definitions', () => {
+			it( 'does not break when trying to check a child which is not registered', () => {
+				schema.register( '$root' );
+
+				expect( schema.checkChild( root1, 'foo404' ) ).to.be.false;
+			} );
+
+			it( 'does not break when trying to check registered child in a context which contains unregistered elements', () => {
+				const foo404 = new Element( 'foo404' );
+
+				root1.appendChildren( foo404 );
+
+				schema.register( '$root' );
+				schema.register( '$text', {
+					allowIn: '$root'
+				} );
+
+				expect( schema.checkChild( foo404, '$text' ) ).to.be.false;
+			} );
+
+			it( 'does not break when used allowedIn pointing to an unregistered element', () => {
+				schema.register( '$root' );
+				schema.register( '$text', {
+					allowIn: 'foo404'
+				} );
+
+				expect( schema.checkChild( root1, '$text' ) ).to.be.false;
+			} );
+
+			it( 'does not break when used allowWhere pointing to an unregistered element', () => {
+				schema.register( '$root' );
+				schema.register( '$text', {
+					allowWhere: 'foo404'
+				} );
+
+				expect( schema.checkChild( root1, '$text' ) ).to.be.false;
+			} );
+
+			it( 'does not break when used allowContentOf pointing to an unregistered element', () => {
+				schema.register( '$root', {
+					allowContentOf: 'foo404'
+				} );
+				schema.register( '$text', {
+					allowIn: '$root'
+				} );
+
+				expect( schema.checkChild( root1, '$text' ) ).to.be.true;
+			} );
+
+			it( 'checks whether allowIn uses a registered element', () => {
+				schema.register( 'paragraph', {
+					allowIn: '$root'
+				} );
+				// $root isn't registered!
+
+				expect( schema.checkChild( root1, 'paragraph' ) ).to.be.false;
+			} );
+
+			it( 'does not break when inheriting all from an unregistered element', () => {
+				schema.register( 'paragraph', {
+					inheritAllFrom: '$block'
+				} );
+
+				expect( schema.checkChild( root1, r1p1 ) ).to.be.false;
+			} );
+		} );
+
+		describe( 'allowAttributes', () => {
+			it( 'passes paragraph[align]', () => {
+				schema.register( 'paragraph', {
+					allowAttributes: 'align'
+				} );
+
+				expect( schema.checkAttribute( r1p1, 'align' ) ).to.be.true;
+			} );
+
+			it( 'passes paragraph[align] and paragraph[dir] – support for array values', () => {
+				schema.register( 'paragraph', {
+					allowAttributes: [ 'align', 'dir' ]
+				} );
+
+				expect( schema.checkAttribute( r1p1, 'align' ), 'align' ).to.be.true;
+				expect( schema.checkAttribute( r1p1, 'dir' ), 'dir' ).to.be.true;
+			} );
+
+			it( 'passes paragraph>$text[bold]', () => {
+				schema.register( 'paragraph' );
+				schema.register( '$text', {
+					allowIn: 'paragraph',
+					allowAttributes: 'bold'
+				} );
+
+				expect( schema.checkAttribute( r1p1.getChild( 0 ), 'bold' ) ).to.be.true;
+			} );
+		} );
+
+		describe( 'allowAttributesOf', () => {
+			it( 'passes paragraph[align] – paragraph inherits from $block', () => {
+				schema.register( '$block', {
+					allowAttributes: 'align'
+				} );
+				schema.register( 'paragraph', {
+					allowAttributesOf: '$block'
+				} );
+
+				expect( schema.checkAttribute( r1p1, 'align' ) ).to.be.true;
+			} );
+
+			it( 'passes paragraph[align] and paragraph[dir] – support for array values', () => {
+				schema.register( '$block', {
+					allowAttributes: 'align'
+				} );
+				schema.register( '$block2', {
+					allowAttributes: 'dir'
+				} );
+				schema.register( 'paragraph', {
+					allowAttributesOf: [ '$block', '$block2' ]
+				} );
+
+				expect( schema.checkAttribute( r1p1, 'align' ), 'align' ).to.be.true;
+				expect( schema.checkAttribute( r1p1, 'dir' ), 'dir' ).to.be.true;
+			} );
+
+			it( 'passes paragraph[align] and paragraph[dir] – support for combined allowAttributes and allowAttributesOf', () => {
+				schema.register( '$block', {
+					allowAttributes: 'align'
+				} );
+				schema.register( 'paragraph', {
+					allowAttributes: 'dir',
+					allowAttributesOf: '$block'
+				} );
+
+				expect( schema.checkAttribute( r1p1, 'align' ), 'align' ).to.be.true;
+				expect( schema.checkAttribute( r1p1, 'dir' ), 'dir' ).to.be.true;
+			} );
+
+			// The support for allowAttributesOf is broken in the similar way as for allowContentOf (see the comment above).
+			// However, those situations are rather theoretical, so we're not going to waste time on them now.
+		} );
+
+		describe( 'inheritAllFrom', () => {
+			it( 'passes paragraph[align] – paragraph inherits attributes of $block', () => {
+				schema.register( '$block', {
+					allowAttributes: 'align'
+				} );
+				schema.register( 'paragraph', {
+					inheritAllFrom: '$block'
+				} );
+
+				expect( schema.checkAttribute( r1p1, 'align' ) ).to.be.true;
+			} );
+
+			it( 'passes paragraph[align] – paragraph inherits attributes of $block through allowAttributesOf', () => {
+				schema.register( '$blockProto', {
+					allowAttributes: 'align'
+				} );
+				schema.register( '$block', {
+					allowAttributesOf: '$blockProto'
+				} );
+				schema.register( 'paragraph', {
+					inheritAllFrom: '$block'
+				} );
+
+				expect( schema.checkAttribute( r1p1, 'align' ) ).to.be.true;
+			} );
+		} );
+
+		describe( 'missing attribute definitions', () => {
+			it( 'does not crash when checking an attribute of a unregistered element', () => {
+				expect( schema.checkAttribute( r1p1, 'align' ) ).to.be.false;
+			} );
+
+			it( 'does not crash when inheriting attributes of a unregistered element', () => {
+				schema.register( 'paragraph', {
+					allowAttributesOf: '$block'
+				} );
+
+				expect( schema.checkAttribute( r1p1, 'whatever' ) ).to.be.false;
+			} );
+
+			it( 'does not crash when inheriting all from a unregistered element', () => {
+				schema.register( 'paragraph', {
+					allowAttributesOf: '$block'
+				} );
+
+				expect( schema.checkAttribute( r1p1, 'whatever' ) ).to.be.false;
+			} );
+		} );
+
+		describe( 'missing types definitions', () => {
+			it( 'does not crash when inheriting types of an unregistered element', () => {
+				schema.register( 'paragraph', {
+					inheritTypesFrom: '$block'
+				} );
+
+				expect( schema.getDefinition( 'paragraph' ) ).to.be.an( 'object' );
+			} );
+		} );
+	} );
+
+	describe( 'real scenarios', () => {
+		let r1bQi, r1i, r1lI, r1h, r1bQlI;
+
+		const definitions = [
+			() => {
+				schema.register( 'paragraph', {
+					inheritAllFrom: '$block'
+				} );
+			},
+			() => {
+				schema.register( 'heading1', {
+					inheritAllFrom: '$block'
+				} );
+			},
+			() => {
+				schema.register( 'listItem', {
+					inheritAllFrom: '$block',
+					allowAttributes: [ 'indent', 'type' ]
+				} );
+			},
+			() => {
+				schema.register( 'blockQuote', {
+					allowWhere: '$block',
+					allowContentOf: '$root'
+				} );
+
+				// Disallow blockQuote in blockQuote.
+				schema.on( 'checkChild', ( evt, args ) => {
+					const ctx = args[ 0 ];
+					const child = args[ 1 ];
+					const childRule = schema.getDefinition( child );
+
+					if ( childRule.name == 'blockQuote' && ctx.endsWith( 'blockQuote' ) ) {
+						evt.stop();
+						evt.return = false;
+					}
+				}, { priority: 'high' } );
+			},
+			() => {
+				schema.register( 'image', {
+					allowWhere: '$block',
+					allowAttributes: [ 'src', 'alt' ],
+					isObject: true,
+					isBlock: true
+				} );
+			},
+			() => {
+				schema.register( 'caption', {
+					allowIn: 'image',
+					allowContentOf: '$block',
+					isLimit: true
+				} );
+			},
+			() => {
+				schema.extend( '$text', {
+					allowAttributes: [ 'bold', 'italic' ]
+				} );
+
+				// Disallow bold in heading1.
+				schema.on( 'checkAttribute', ( evt, args ) => {
+					const ctx = args[ 0 ];
+					const attributeName = args[ 1 ];
+
+					if ( ctx.endsWith( 'heading1 $text' ) && attributeName == 'bold' ) {
+						evt.stop();
+						evt.return = false;
+					}
+				}, { priority: 'high' } );
+			},
+			() => {
+				schema.extend( '$block', {
+					allowAttributes: 'alignment'
+				} );
+			}
+		];
+
+		beforeEach( () => {
+			schema.register( '$root', {
+				isLimit: true
+			} );
+			schema.register( '$block', {
+				allowIn: '$root',
+				isBlock: true
+			} );
+			schema.register( '$text', {
+				allowIn: '$block'
+			} );
+
+			for ( const definition of definitions ) {
+				definition();
+			}
+
+			// or...
+			//
+			// Use the below code to shuffle the definitions.
+			// Don't look here, Szymon!
+			//
+			// const definitionsCopy = definitions.slice();
+			//
+			// while ( definitionsCopy.length ) {
+			// 	const r = Math.floor( Math.random() * definitionsCopy.length );
+			// 	definitionsCopy.splice( r, 1 )[ 0 ]();
+			// }
+
+			root1 = new Element( '$root', null, [
+				new Element( 'paragraph', null, 'foo' ),
+				new Element( 'paragraph', { alignment: 'right' }, 'bar' ),
+				new Element( 'listItem', { type: 'x', indent: 0 }, 'foo' ),
+				new Element( 'heading1', null, 'foo' ),
+				new Element( 'blockQuote', null, [
+					new Element( 'paragraph', null, 'foo' ),
+					new Element( 'listItem', { type: 'x', indent: 0 }, 'foo' ),
+					new Element( 'image', null, [
+						new Element( 'caption', null, 'foo' )
+					] )
+				] ),
+				new Element( 'image', null, [
+					new Element( 'caption', null, 'foo' )
+				] )
+			] );
+			r1p1 = root1.getChild( 0 );
+			r1p2 = root1.getChild( 1 );
+			r1lI = root1.getChild( 2 );
+			r1h = root1.getChild( 3 );
+			r1bQ = root1.getChild( 4 );
+			r1i = root1.getChild( 5 );
+			r1bQp = r1bQ.getChild( 0 );
+			r1bQlI = r1bQ.getChild( 1 );
+			r1bQi = r1bQ.getChild( 2 );
+		} );
+
+		it( 'passes $root>paragraph', () => {
+			expect( schema.checkChild( root1, 'paragraph' ) ).to.be.true;
+		} );
+
+		it( 'passes $root>paragraph>$text', () => {
+			expect( schema.checkChild( r1p1, '$text' ), 'paragraph' ).to.be.true;
+			expect( schema.checkChild( r1p2, '$text' ), 'paragraph[alignment]' ).to.be.true;
+		} );
+
+		it( 'passes $root>listItem', () => {
+			expect( schema.checkChild( root1, 'listItem' ) ).to.be.true;
+		} );
+
+		it( 'passes $root>listItem>$text', () => {
+			expect( schema.checkChild( r1lI, '$text' ) ).to.be.true;
+		} );
+
+		it( 'passes $root>blockQuote>paragraph', () => {
+			expect( schema.checkChild( r1bQ, 'paragraph' ) ).to.be.true;
+		} );
+
+		it( 'passes $root>blockQuote>paragraph>$text', () => {
+			expect( schema.checkChild( r1bQp, '$text' ) ).to.be.true;
+		} );
+
+		it( 'passes $root>blockQuote>listItem', () => {
+			expect( schema.checkChild( r1bQ, 'listItem' ) ).to.be.true;
+		} );
+
+		it( 'passes $root>blockQuote>listItem>$text', () => {
+			expect( schema.checkChild( r1bQlI, '$text' ) ).to.be.true;
+		} );
+
+		it( 'passes $root>blockQuote>image', () => {
+			expect( schema.checkChild( r1bQ, 'image' ) ).to.be.true;
+		} );
+
+		it( 'passes $root>blockQuote>image>caption', () => {
+			expect( schema.checkChild( r1bQi, 'caption' ) ).to.be.true;
+		} );
+
+		it( 'passes $root>blockQuote>image>caption>$text', () => {
+			expect( schema.checkChild( r1bQi.getChild( 0 ), '$text' ) ).to.be.true;
+		} );
+
+		it( 'passes $root>image', () => {
+			expect( schema.checkChild( root1, 'image' ) ).to.be.true;
+		} );
+
+		it( 'passes $root>image>caption', () => {
+			expect( schema.checkChild( r1i, 'caption' ) ).to.be.true;
+		} );
+
+		it( 'passes $root>image>caption>$text', () => {
+			expect( schema.checkChild( r1i.getChild( 0 ), '$text' ) ).to.be.true;
+		} );
+
+		it( 'rejects $root>$root', () => {
+			expect( schema.checkChild( root1, '$root' ) ).to.be.false;
+		} );
+
+		it( 'rejects $root>$text', () => {
+			expect( schema.checkChild( root1, '$text' ) ).to.be.false;
+		} );
+
+		it( 'rejects $root>caption', () => {
+			expect( schema.checkChild( root1, 'caption' ) ).to.be.false;
+		} );
+
+		it( 'rejects $root>paragraph>paragraph', () => {
+			expect( schema.checkChild( r1p1, 'paragraph' ) ).to.be.false;
+		} );
+
+		it( 'rejects $root>paragraph>paragraph>$text', () => {
+			// Edge case because p>p should not exist in the first place.
+			// But it's good to know that it blocks also this.
+			const p = new Element( 'p' );
+			r1p1.appendChildren( p );
+
+			expect( schema.checkChild( p, '$text' ) ).to.be.false;
+		} );
+
+		it( 'rejects $root>paragraph>$block', () => {
+			expect( schema.checkChild( r1p1, '$block' ) ).to.be.false;
+		} );
+
+		it( 'rejects $root>paragraph>blockQuote', () => {
+			expect( schema.checkChild( r1p1, 'blockQuote' ) ).to.be.false;
+		} );
+
+		it( 'rejects $root>paragraph>image', () => {
+			expect( schema.checkChild( r1p1, 'image' ) ).to.be.false;
+		} );
+
+		it( 'rejects $root>paragraph>caption', () => {
+			expect( schema.checkChild( r1p1, 'caption' ) ).to.be.false;
+		} );
+
+		it( 'rejects $root>blockQuote>blockQuote', () => {
+			expect( schema.checkChild( r1bQ, 'blockQuote' ) ).to.be.false;
+		} );
+
+		it( 'rejects $root>blockQuote>caption', () => {
+			expect( schema.checkChild( r1p1, 'image' ) ).to.be.false;
+		} );
+
+		it( 'rejects $root>blockQuote>$text', () => {
+			expect( schema.checkChild( r1bQ, '$text' ) ).to.be.false;
+		} );
+
+		it( 'rejects $root>image>$text', () => {
+			expect( schema.checkChild( r1i, '$text' ) ).to.be.false;
+		} );
+
+		it( 'rejects $root>image>paragraph', () => {
+			expect( schema.checkChild( r1i, 'paragraph' ) ).to.be.false;
+		} );
+
+		it( 'rejects $root>image>caption>paragraph', () => {
+			expect( schema.checkChild( r1i.getChild( 0 ), 'paragraph' ) ).to.be.false;
+		} );
+
+		it( 'rejects $root>image>caption>blockQuote', () => {
+			expect( schema.checkChild( r1i.getChild( 0 ), 'blockQuote' ) ).to.be.false;
+		} );
+
+		it( 'accepts attribute $root>paragraph[alignment]', () => {
+			expect( schema.checkAttribute( r1p1, 'alignment' ) ).to.be.true;
+		} );
+
+		it( 'accepts attribute $root>paragraph>$text[bold]', () => {
+			expect( schema.checkAttribute( r1p1.getChild( 0 ), 'bold' ) ).to.be.true;
+		} );
+
+		it( 'accepts attribute $root>heading1>$text[italic]', () => {
+			expect( schema.checkAttribute( r1h.getChild( 0 ), 'italic' ) ).to.be.true;
+		} );
+
+		it( 'accepts attribute $root>blockQuote>paragraph>$text[bold]', () => {
+			expect( schema.checkAttribute( r1bQp.getChild( 0 ), 'bold' ) ).to.be.true;
+		} );
+
+		it( 'accepts attribute $root>listItem[alignment]', () => {
+			expect( schema.checkAttribute( r1lI, 'alignment' ) ).to.be.true;
+		} );
+
+		it( 'accepts attribute $root>listItem[indent]', () => {
+			expect( schema.checkAttribute( r1lI, 'indent' ) ).to.be.true;
+		} );
+
+		it( 'accepts attribute $root>listItem[type]', () => {
+			expect( schema.checkAttribute( r1lI, 'type' ) ).to.be.true;
+		} );
+
+		it( 'accepts attribute $root>image[src]', () => {
+			expect( schema.checkAttribute( r1i, 'src' ) ).to.be.true;
+		} );
+
+		it( 'accepts attribute $root>image[alt]', () => {
+			expect( schema.checkAttribute( r1i, 'alt' ) ).to.be.true;
+		} );
+
+		it( 'accepts attribute $root>image>caption>$text[bold]', () => {
+			expect( schema.checkAttribute( r1i.getChild( 0 ).getChild( 0 ), 'bold' ) ).to.be.true;
+		} );
+
+		it( 'rejects attribute $root[indent]', () => {
+			expect( schema.checkAttribute( root1, 'indent' ) ).to.be.false;
+		} );
+
+		it( 'rejects attribute $root>paragraph[indent]', () => {
+			expect( schema.checkAttribute( r1p1, 'indent' ) ).to.be.false;
+		} );
+
+		it( 'accepts attribute $root>heading1>$text[bold]', () => {
+			expect( schema.checkAttribute( r1h.getChild( 0 ), 'bold' ) ).to.be.false;
+		} );
+
+		it( 'rejects attribute $root>paragraph>$text[alignment]', () => {
+			expect( schema.checkAttribute( r1p1.getChild( 0 ), 'alignment' ) ).to.be.false;
+		} );
+
+		it( 'rejects attribute $root>blockQuote[indent]', () => {
+			expect( schema.checkAttribute( r1bQ, 'indent' ) ).to.be.false;
+		} );
+
+		it( 'rejects attribute $root>blockQuote[alignment]', () => {
+			expect( schema.checkAttribute( r1bQ, 'alignment' ) ).to.be.false;
+		} );
+
+		it( 'rejects attribute $root>image[indent]', () => {
+			expect( schema.checkAttribute( r1i, 'indent' ) ).to.be.false;
+		} );
+
+		it( 'rejects attribute $root>image[alignment]', () => {
+			expect( schema.checkAttribute( r1i, 'alignment' ) ).to.be.false;
+		} );
+
+		it( '$root is limit', () => {
+			expect( schema.isLimit( '$root' ) ).to.be.true;
+			expect( schema.isBlock( '$root' ) ).to.be.false;
+			expect( schema.isObject( '$root' ) ).to.be.false;
+		} );
+
+		it( 'paragraph is block', () => {
+			expect( schema.isLimit( 'paragraph' ) ).to.be.false;
+			expect( schema.isBlock( 'paragraph' ) ).to.be.true;
+			expect( schema.isObject( 'paragraph' ) ).to.be.false;
+		} );
+
+		it( 'heading1 is block', () => {
+			expect( schema.isLimit( 'heading1' ) ).to.be.false;
+			expect( schema.isBlock( 'heading1' ) ).to.be.true;
+			expect( schema.isObject( 'heading1' ) ).to.be.false;
+		} );
+
+		it( 'listItem is block', () => {
+			expect( schema.isLimit( 'listItem' ) ).to.be.false;
+			expect( schema.isBlock( 'listItem' ) ).to.be.true;
+			expect( schema.isObject( 'listItem' ) ).to.be.false;
+		} );
+
+		it( 'image is block object', () => {
+			expect( schema.isLimit( 'image' ) ).to.be.false;
+			expect( schema.isBlock( 'image' ) ).to.be.true;
+			expect( schema.isObject( 'image' ) ).to.be.true;
+		} );
+
+		it( 'caption is limit', () => {
+			expect( schema.isLimit( 'caption' ) ).to.be.true;
+			expect( schema.isBlock( 'caption' ) ).to.be.false;
+			expect( schema.isObject( 'caption' ) ).to.be.false;
+		} );
+	} );
+} );
+
+describe( 'SchemaContext', () => {
+	let root;
+
+	beforeEach( () => {
+		root = new Element( '$root', null, [
+			new Element( 'blockQuote', { foo: 1 }, [
+				new Element( 'paragraph', { align: 'left' }, [
+					new Text( 'foo', { bold: true, italic: true } )
+				] )
+			] )
+		] );
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'creates context based on an array of strings', () => {
+			const ctx = new SchemaContext( [ 'a', 'b', 'c' ] );
+
+			expect( ctx.length ).to.equal( 3 );
+
+			expect( Array.from( ctx.getNames() ) ).to.deep.equal( [ 'a', 'b', 'c' ] );
+			expect( ctx.getItem( 0 ).name ).to.equal( 'a' );
+
+			expect( Array.from( ctx.getItem( 0 ).getAttributeKeys() ) ).to.be.empty;
+			expect( ctx.getItem( 0 ).getAttribute( 'foo' ) ).to.be.undefined;
+		} );
+
+		it( 'creates context based on an array of elements', () => {
+			const blockQuote = root.getChild( 0 );
+			const text = blockQuote.getChild( 0 ).getChild( 0 );
+
+			const ctx = new SchemaContext( [ blockQuote, text ] );
+
+			expect( ctx.length ).to.equal( 2 );
+
+			expect( Array.from( ctx.getNames() ) ).to.deep.equal( [ 'blockQuote', '$text' ] );
+			expect( ctx.getItem( 0 ).name ).to.equal( 'blockQuote' );
+
+			expect( Array.from( ctx.getItem( 1 ).getAttributeKeys() ).sort() ).to.deep.equal( [ 'bold', 'italic' ] );
+			expect( ctx.getItem( 1 ).getAttribute( 'bold' ) ).to.be.true;
+		} );
+
+		it( 'creates context based on a mixed array of strings and elements', () => {
+			const blockQuote = root.getChild( 0 );
+			const text = blockQuote.getChild( 0 ).getChild( 0 );
+
+			const ctx = new SchemaContext( [ blockQuote, 'paragraph', text ] );
+
+			expect( ctx.length ).to.equal( 3 );
+
+			expect( Array.from( ctx.getNames() ) ).to.deep.equal( [ 'blockQuote', 'paragraph', '$text' ] );
+		} );
+
+		it( 'creates context based on a root element', () => {
+			const ctx = new SchemaContext( root );
+
+			expect( ctx.length ).to.equal( 1 );
+
+			expect( Array.from( ctx.getNames() ) ).to.deep.equal( [ '$root' ] );
+
+			expect( Array.from( ctx.getItem( 0 ).getAttributeKeys() ) ).to.be.empty;
+			expect( ctx.getItem( 0 ).getAttribute( 'foo' ) ).to.be.undefined;
+		} );
+
+		it( 'creates context based on a nested element', () => {
+			const ctx = new SchemaContext( root.getChild( 0 ).getChild( 0 ) );
+
+			expect( ctx.length ).to.equal( 3 );
+
+			expect( Array.from( ctx.getNames() ) ).to.deep.equal( [ '$root', 'blockQuote', 'paragraph' ] );
+
+			expect( Array.from( ctx.getItem( 1 ).getAttributeKeys() ) ).to.deep.equal( [ 'foo' ] );
+			expect( ctx.getItem( 1 ).getAttribute( 'foo' ) ).to.equal( 1 );
+			expect( Array.from( ctx.getItem( 2 ).getAttributeKeys() ) ).to.deep.equal( [ 'align' ] );
+			expect( ctx.getItem( 2 ).getAttribute( 'align' ) ).to.equal( 'left' );
+		} );
+
+		it( 'creates context based on a text node', () => {
+			const ctx = new SchemaContext( root.getChild( 0 ).getChild( 0 ).getChild( 0 ) );
+
+			expect( ctx.length ).to.equal( 4 );
+
+			expect( Array.from( ctx.getNames() ) ).to.deep.equal( [ '$root', 'blockQuote', 'paragraph', '$text' ] );
+
+			expect( Array.from( ctx.getItem( 3 ).getAttributeKeys() ).sort() ).to.deep.equal( [ 'bold', 'italic' ] );
+			expect( ctx.getItem( 3 ).getAttribute( 'bold' ) ).to.be.true;
+		} );
+
+		it( 'creates context based on a text proxy', () => {
+			const text = root.getChild( 0 ).getChild( 0 ).getChild( 0 );
+			const textProxy = new TextProxy( text, 0, 1 );
+			const ctx = new SchemaContext( textProxy );
+
+			expect( ctx.length ).to.equal( 4 );
+
+			expect( Array.from( ctx.getNames() ) ).to.deep.equal( [ '$root', 'blockQuote', 'paragraph', '$text' ] );
+
+			expect( Array.from( ctx.getItem( 3 ).getAttributeKeys() ).sort() ).to.deep.equal( [ 'bold', 'italic' ] );
+			expect( ctx.getItem( 3 ).getAttribute( 'bold' ) ).to.be.true;
+		} );
+
+		it( 'creates context based on a position', () => {
+			const pos = Position.createAt( root.getChild( 0 ).getChild( 0 ) );
+			const ctx = new SchemaContext( pos );
+
+			expect( ctx.length ).to.equal( 3 );
+
+			expect( Array.from( ctx.getNames() ) ).to.deep.equal( [ '$root', 'blockQuote', 'paragraph' ] );
+
+			expect( Array.from( ctx.getItem( 2 ).getAttributeKeys() ).sort() ).to.deep.equal( [ 'align' ] );
+		} );
+	} );
+
+	describe( 'length', () => {
+		it( 'gets the number of items', () => {
+			const ctx = new SchemaContext( [ 'a', 'b', 'c' ] );
+
+			expect( ctx.length ).to.equal( 3 );
+		} );
+	} );
+
+	describe( 'last', () => {
+		it( 'gets the last item', () => {
+			const ctx = new SchemaContext( [ 'a', 'b', 'c' ] );
+
+			expect( ctx.last ).to.be.an( 'object' );
+			expect( ctx.last.name ).to.equal( 'c' );
+		} );
+	} );
+
+	describe( 'Symbol.iterator', () => {
+		it( 'exists', () => {
+			const ctx = new SchemaContext( [ 'a', 'b', 'c' ] );
+
+			expect( ctx[ Symbol.iterator ] ).to.be.a( 'function' );
+			expect( Array.from( ctx ).map( item => item.name ) ).to.deep.equal( [ 'a', 'b', 'c' ] );
+		} );
+	} );
+
+	describe( 'getItem()', () => {
+		it( 'returns item by index', () => {
+			const ctx = new SchemaContext( [ 'a', 'b', 'c' ] );
+
+			expect( ctx.getItem( 1 ) ).to.be.an( 'object' );
+			expect( ctx.getItem( 1 ).name ).to.equal( 'b' );
+		} );
+
+		it( 'returns undefined if index exceeds the range', () => {
+			const ctx = new SchemaContext( [ 'a', 'b', 'c' ] );
+
+			expect( ctx.getItem( 3 ) ).to.be.undefined;
+		} );
+	} );
+
+	describe( 'getNames()', () => {
+		it( 'returns an iterator', () => {
+			const ctx = new SchemaContext( [ 'a', 'b', 'c' ] );
+
+			expect( ctx.getNames().next ).to.be.a( 'function' );
+		} );
+
+		it( 'returns an iterator which returns all item names', () => {
+			const ctx = new SchemaContext( [ 'a', 'b', 'c' ] );
+
+			expect( Array.from( ctx.getNames() ) ).to.deep.equal( [ 'a', 'b', 'c' ] );
+		} );
+	} );
+
+	describe( 'endsWith()', () => {
+		it( 'returns true if the end of the context matches the query - 1 item', () => {
+			const ctx = new SchemaContext( [ 'foo', 'bar', 'bom', 'dom' ] );
+
+			expect( ctx.endsWith( 'dom' ) ).to.be.true;
+		} );
+
+		it( 'returns true if the end of the context matches the query - 2 items', () => {
+			const ctx = new SchemaContext( [ 'foo', 'bar', 'bom', 'dom' ] );
+
+			expect( ctx.endsWith( 'bom dom' ) ).to.be.true;
+		} );
+
+		it( 'returns true if the end of the context matches the query - full match of 3 items', () => {
+			const ctx = new SchemaContext( [ 'foo', 'bar', 'bom' ] );
+
+			expect( ctx.endsWith( 'foo bar bom' ) ).to.be.true;
+		} );
+
+		it( 'returns true if the end of the context matches the query - full match of 1 items', () => {
+			const ctx = new SchemaContext( [ 'foo' ] );
+
+			expect( ctx.endsWith( 'foo' ) ).to.be.true;
+		} );
+
+		it( 'returns true if not only the end of the context matches the query', () => {
+			const ctx = new SchemaContext( [ 'foo', 'foo', 'foo', 'foo' ] );
+
+			expect( ctx.endsWith( 'foo foo' ) ).to.be.true;
+		} );
+
+		it( 'returns false if query matches the middle of the context', () => {
+			const ctx = new SchemaContext( [ 'foo', 'bar', 'bom', 'dom' ] );
+
+			expect( ctx.endsWith( 'bom' ) ).to.be.false;
+		} );
+
+		it( 'returns false if query matches the start of the context', () => {
+			const ctx = new SchemaContext( [ 'foo', 'bar', 'bom', 'dom' ] );
+
+			expect( ctx.endsWith( 'foo' ) ).to.be.false;
+		} );
+
+		it( 'returns false if query does not match', () => {
+			const ctx = new SchemaContext( [ 'foo', 'bar', 'bom', 'dom' ] );
+
+			expect( ctx.endsWith( 'dom bar' ) ).to.be.false;
+		} );
+
+		it( 'returns false if query is longer than context', () => {
+			const ctx = new SchemaContext( [ 'foo' ] );
+
+			expect( ctx.endsWith( 'bar', 'foo' ) ).to.be.false;
+		} );
+	} );
+} );

+ 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]' );
-		} );
-	} );
-} );

+ 19 - 16
packages/ckeditor5-engine/tests/model/selection.js

@@ -891,7 +891,9 @@ describe( 'Selection', () => {
 
 		beforeEach( () => {
 			schema = new Schema();
-			schema.registerItem( 'p', '$block' );
+			schema.register( '$root' );
+			schema.register( 'p', { allowIn: '$root' } );
+			schema.register( '$text', { allowIn: 'p' } );
 		} );
 
 		it( 'should return selected element', () => {
@@ -926,21 +928,21 @@ describe( 'Selection', () => {
 
 	describe( 'getSelectedBlocks()', () => {
 		beforeEach( () => {
-			model.schema.registerItem( 'p', '$block' );
-			model.schema.registerItem( 'h', '$block' );
+			model.schema.register( 'p', { inheritAllFrom: '$block' } );
+			model.schema.register( 'h', { inheritAllFrom: '$block' } );
 
-			model.schema.registerItem( 'blockquote' );
-			model.schema.allow( { name: 'blockquote', inside: '$root' } );
-			model.schema.allow( { name: '$block', inside: 'blockquote' } );
+			model.schema.register( 'blockquote' );
+			model.schema.extend( 'blockquote', { allowIn: '$root' } );
+			model.schema.extend( '$block', { allowIn: 'blockquote' } );
 
-			model.schema.registerItem( 'image' );
-			model.schema.allow( { name: 'image', inside: '$root' } );
-			model.schema.allow( { name: 'image', inside: '$block' } );
-			model.schema.allow( { name: '$text', inside: 'image' } );
+			model.schema.register( 'image', {
+				allowIn: [ '$root', '$block' ]
+			} );
+			model.schema.extend( '$text', { allowIn: 'image' } );
 
 			// Special block which can contain another blocks.
-			model.schema.registerItem( 'nestedBlock', '$block' );
-			model.schema.allow( { name: 'nestedBlock', inside: '$block' } );
+			model.schema.register( 'nestedBlock', { inheritAllFrom: '$block' } );
+			model.schema.extend( 'nestedBlock', { allowIn: '$block' } );
 		} );
 
 		it( 'returns an iterator', () => {
@@ -1310,8 +1312,8 @@ describe( 'Selection', () => {
 
 	describe( 'containsEntireContent()', () => {
 		beforeEach( () => {
-			model.schema.registerItem( 'p', '$block' );
-			model.schema.allow( { name: 'p', inside: '$root' } );
+			model.schema.register( 'p', { inheritAllFrom: '$block' } );
+			model.schema.extend( 'p', { allowIn: '$root' } );
 		} );
 
 		it( 'returns true if the entire content in $root is selected', () => {
@@ -1345,8 +1347,9 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'returns false when the entire content except an empty element is selected', () => {
-			model.schema.registerItem( 'img', '$inline' );
-			model.schema.allow( { name: 'img', inside: 'p' } );
+			model.schema.register( 'img', {
+				allowIn: 'p'
+			} );
 
 			setData( model, '<p><img></img>[Foo]</p>' );
 

+ 102 - 78
packages/ckeditor5-engine/tests/model/utils/deletecontent.js

@@ -19,7 +19,7 @@ describe( 'DataController utils', () => {
 			doc = model.document;
 			doc.createRoot();
 
-			model.schema.allow( { name: '$text', inside: '$root' } );
+			model.schema.extend( '$text', { allowIn: '$root' } );
 			setData( model, 'x[abc]x' );
 
 			model.change( writer => {
@@ -36,10 +36,8 @@ describe( 'DataController utils', () => {
 
 				const schema = model.schema;
 
-				schema.registerItem( 'image', '$inline' );
-
-				schema.allow( { name: '$text', inside: '$root' } );
-				schema.allow( { name: 'image', inside: '$root' } );
+				schema.register( 'image', { allowWhere: '$text' } );
+				schema.extend( '$text', { allowIn: '$root' } );
 			} );
 
 			test(
@@ -101,11 +99,13 @@ describe( 'DataController utils', () => {
 
 				const schema = model.schema;
 
-				schema.registerItem( 'image', '$inline' );
-				schema.registerItem( 'paragraph', '$block' );
+				schema.register( 'image', { allowWhere: '$text' } );
+				schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 
-				schema.allow( { name: '$text', inside: '$root' } );
-				schema.allow( { name: '$text', attributes: [ 'bold', 'italic' ] } );
+				schema.extend( '$text', {
+					allowIn: '$root',
+					allowAttributes: [ 'bold', 'italic' ]
+				} );
 			} );
 
 			it( 'deletes characters (first half has attrs)', () => {
@@ -170,20 +170,16 @@ describe( 'DataController utils', () => {
 
 				const schema = model.schema;
 
-				schema.registerItem( 'paragraph', '$block' );
-				schema.registerItem( 'heading1', '$block' );
-				schema.registerItem( 'image', '$inline' );
-				schema.registerItem( 'pchild' );
-				schema.registerItem( 'pparent' );
-
-				schema.allow( { name: 'pchild', inside: 'paragraph' } );
-				schema.allow( { name: '$text', inside: 'pchild' } );
-
-				schema.allow( { name: 'paragraph', inside: 'pparent' } );
-				schema.allow( { name: 'pparent', inside: '$root' } );
-				schema.allow( { name: '$text', inside: 'pparent' } );
-
-				schema.allow( { name: 'paragraph', attributes: [ 'align' ] } );
+				schema.register( 'paragraph', {
+					inheritAllFrom: '$block',
+					allowIn: 'pparent',
+					allowAttributes: 'align'
+				} );
+				schema.register( 'heading1', { inheritAllFrom: '$block' } );
+				schema.register( 'image', { inheritAllFrom: '$text' } );
+				schema.register( 'pchild', { allowIn: 'paragraph' } );
+				schema.register( 'pparent', { allowIn: '$root' } );
+				schema.extend( '$text', { allowIn: [ 'pchild', 'pparent' ] } );
 			} );
 
 			test(
@@ -440,16 +436,17 @@ describe( 'DataController utils', () => {
 				beforeEach( () => {
 					const schema = model.schema;
 
-					schema.registerItem( 'blockWidget' );
-					schema.registerItem( 'nestedEditable' );
-
-					schema.allow( { name: 'blockWidget', inside: '$root' } );
+					schema.register( 'blockWidget', {
+						isObject: true
+					} );
+					schema.register( 'nestedEditable', {
+						isLimit: true
+					} );
 
-					schema.allow( { name: 'nestedEditable', inside: 'blockWidget' } );
-					schema.allow( { name: '$text', inside: 'nestedEditable' } );
+					schema.extend( 'blockWidget', { allowIn: '$root' } );
 
-					schema.objects.add( 'blockWidget' );
-					schema.limits.add( 'nestedEditable' );
+					schema.extend( 'nestedEditable', { allowIn: 'blockWidget' } );
+					schema.extend( '$text', { allowIn: 'nestedEditable' } );
 				} );
 
 				test(
@@ -469,10 +466,30 @@ describe( 'DataController utils', () => {
 				beforeEach( () => {
 					const schema = model.schema;
 
-					schema.allow( { name: '$text', attributes: [ 'a', 'b' ], inside: 'paragraph' } );
-					schema.allow( { name: '$text', attributes: [ 'b', 'c' ], inside: 'pchild' } );
-					schema.allow( { name: 'pchild', inside: 'pchild' } );
-					schema.disallow( { name: '$text', attributes: [ 'c' ], inside: 'pchild pchild' } );
+					schema.on( 'checkAttribute', ( evt, args ) => {
+						const ctx = args[ 0 ];
+						const attributeName = args[ 1 ];
+
+						// Allow 'a' and 'b' on paragraph>$text.
+						if ( ctx.endsWith( 'paragraph $text' ) && [ 'a', 'b' ].includes( attributeName ) ) {
+							evt.stop();
+							evt.return = true;
+						}
+
+						// Allow 'b' and 'c' in pchild>$text.
+						if ( ctx.endsWith( 'pchild $text' ) && [ 'b', 'c' ].includes( attributeName ) ) {
+							evt.stop();
+							evt.return = true;
+						}
+
+						// Disallow 'c' on pchild>pchild>$text.
+						if ( ctx.endsWith( 'pchild pchild $text' ) && attributeName == 'c' ) {
+							evt.stop();
+							evt.return = false;
+						}
+					}, { priority: 'high' } );
+
+					schema.extend( 'pchild', { allowIn: 'pchild' } );
 				} );
 
 				test(
@@ -525,24 +542,26 @@ describe( 'DataController utils', () => {
 
 				const schema = model.schema;
 
-				schema.limits.add( 'restrictedRoot' );
-
-				schema.registerItem( 'image', '$inline' );
-				schema.registerItem( 'paragraph', '$block' );
-				schema.registerItem( 'heading1', '$block' );
-				schema.registerItem( 'blockWidget' );
-				schema.registerItem( 'restrictedRoot' );
+				schema.register( 'image', { allowWhere: '$text' } );
+				schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+				schema.register( 'heading1', { inheritAllFrom: '$block' } );
+				schema.register( 'blockWidget' );
+				schema.register( 'restrictedRoot', {
+					isLimit: true
+				} );
 
-				schema.allow( { name: '$block', inside: '$root' } );
-				schema.allow( { name: 'blockWidget', inside: '$root' } );
+				schema.extend( '$block', { allowIn: '$root' } );
+				schema.extend( 'blockWidget', { allowIn: '$root' } );
 
-				schema.allow( { name: 'blockWidget', inside: 'restrictedRoot' } );
+				schema.extend( 'blockWidget', { allowIn: 'restrictedRoot' } );
 			} );
 
 			// See also "in simple scenarios => deletes an element".
 
 			it( 'deletes two inline elements', () => {
-				model.schema.limits.add( 'paragraph' );
+				model.schema.extend( 'paragraph', {
+					isLimit: true
+				} );
 
 				setData(
 					model,
@@ -643,16 +662,14 @@ describe( 'DataController utils', () => {
 
 				const schema = model.schema;
 
-				schema.registerItem( 'inlineLimit' );
-				schema.allow( { name: 'inlineLimit', inside: '$root' } );
-				schema.allow( { name: '$text', inside: 'inlineLimit' } );
-				schema.limits.add( 'inlineLimit' );
-
-				schema.allow( { name: '$inline', inside: '$root' } );
-
-				schema.registerItem( 'x' );
-				schema.allow( { name: '$text', inside: 'x' } );
-				schema.allow( { name: 'x', inside: '$root' } );
+				schema.register( 'inlineLimit', {
+					isLimit: true,
+					allowIn: '$root'
+				} );
+				schema.extend( '$text', {
+					allowIn: [ 'inlineLimit', '$root', 'x' ]
+				} );
+				schema.register( 'x', { allowIn: '$root' } );
 			} );
 
 			test(
@@ -700,12 +717,13 @@ describe( 'DataController utils', () => {
 
 				const schema = model.schema;
 
-				schema.registerItem( 'blockLimit' );
-				schema.allow( { name: 'blockLimit', inside: '$root' } );
-				schema.allow( { name: '$block', inside: 'blockLimit' } );
-				schema.limits.add( 'blockLimit' );
+				schema.register( 'blockLimit', {
+					isLimit: true
+				} );
+				schema.extend( 'blockLimit', { allowIn: '$root' } );
+				schema.extend( '$block', { allowIn: 'blockLimit' } );
 
-				schema.registerItem( 'paragraph', '$block' );
+				schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			} );
 
 			test(
@@ -748,27 +766,33 @@ describe( 'DataController utils', () => {
 
 				const schema = model.schema;
 
-				schema.registerItem( 'div', '$block' );
-				schema.limits.add( 'div' );
+				schema.register( 'div', {
+					inheritAllFrom: '$block',
+					isLimit: true
+				} );
 
-				schema.registerItem( 'article', '$block' );
-				schema.limits.add( 'article' );
+				schema.register( 'article', {
+					inheritAllFrom: '$block',
+					isLimit: true
+				} );
 
-				schema.registerItem( 'image', '$inline' );
-				schema.objects.add( 'image' );
+				schema.register( 'image', {
+					allowWhere: '$text',
+					isObject: true
+				} );
 
-				schema.registerItem( 'paragraph', '$block' );
-				schema.registerItem( 'heading1', '$block' );
-				schema.registerItem( 'heading2', '$block' );
+				schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+				schema.register( 'heading1', { inheritAllFrom: '$block' } );
+				schema.register( 'heading2', { inheritAllFrom: '$block' } );
 
-				schema.allow( { name: '$text', inside: '$root' } );
+				schema.extend( '$text', { allowIn: '$root' } );
 
-				schema.allow( { name: 'image', inside: '$root' } );
-				schema.allow( { name: 'image', inside: 'heading1' } );
-				schema.allow( { name: 'heading1', inside: 'div' } );
-				schema.allow( { name: 'paragraph', inside: 'div' } );
-				schema.allow( { name: 'heading1', inside: 'article' } );
-				schema.allow( { name: 'heading2', inside: 'article' } );
+				schema.extend( 'image', { allowIn: '$root' } );
+				schema.extend( 'image', { allowIn: 'heading1' } );
+				schema.extend( 'heading1', { allowIn: 'div' } );
+				schema.extend( 'paragraph', { allowIn: 'div' } );
+				schema.extend( 'heading1', { allowIn: 'article' } );
+				schema.extend( 'heading2', { allowIn: 'article' } );
 			} );
 
 			test(
@@ -813,7 +837,7 @@ describe( 'DataController utils', () => {
 				'<heading1>[]</heading1>'
 			);
 
-			it( 'when root element was not added as Schema.limits works fine as well', () => {
+			it( 'when root element was not added as Schema limits work fine as well', () => {
 				doc.createRoot( 'paragraph', 'paragraphRoot' );
 
 				setData(

+ 24 - 24
packages/ckeditor5-engine/tests/model/utils/getselectedcontent.js

@@ -17,7 +17,7 @@ describe( 'DataController utils', () => {
 			doc = model.document;
 			doc.createRoot();
 
-			model.schema.allow( { name: '$text', inside: '$root' } );
+			model.schema.extend( '$text', { allowIn: '$root' } );
 			setData( model, 'x[abc]x' );
 
 			model.change( writer => {
@@ -34,12 +34,11 @@ describe( 'DataController utils', () => {
 
 				const schema = model.schema;
 
-				schema.registerItem( 'image', '$inline' );
-
-				schema.allow( { name: '$text', inside: '$root' } );
-				schema.allow( { name: 'image', inside: '$root' } );
-				schema.allow( { name: '$inline', attributes: [ 'bold' ] } );
-				schema.allow( { name: '$inline', attributes: [ 'italic' ] } );
+				schema.register( 'image', { allowWhere: '$text', allowIn: '$root' } );
+				schema.extend( '$text', {
+					allowIn: '$root',
+					allowAttributes: [ 'bold', 'italic' ]
+				} );
 			} );
 
 			it( 'returns empty fragment for no selection', () => {
@@ -128,17 +127,18 @@ describe( 'DataController utils', () => {
 
 				const schema = model.schema;
 
-				schema.registerItem( 'paragraph', '$block' );
-				schema.registerItem( 'heading1', '$block' );
-				schema.registerItem( 'blockImage' );
-				schema.registerItem( 'caption' );
-				schema.registerItem( 'image', '$inline' );
-
-				schema.allow( { name: 'blockImage', inside: '$root' } );
-				schema.allow( { name: 'caption', inside: 'blockImage' } );
-				schema.allow( { name: '$inline', inside: 'caption' } );
+				schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+				schema.register( 'heading1', { inheritAllFrom: '$block' } );
+				schema.register( 'blockImage' );
+				schema.register( 'caption' );
+				schema.register( 'image', { allowWhere: '$text' } );
 
-				schema.allow( { name: '$inline', attributes: [ 'bold' ] } );
+				schema.extend( 'blockImage', { allowIn: '$root' } );
+				schema.extend( 'caption', { allowIn: 'blockImage' } );
+				schema.extend( '$text', {
+					allowIn: 'caption',
+					allowAttributes: 'bold'
+				} );
 			} );
 
 			it( 'gets one character', () => {
@@ -268,12 +268,12 @@ describe( 'DataController utils', () => {
 
 				const schema = model.schema;
 
-				schema.registerItem( 'paragraph', '$block' );
-				schema.registerItem( 'heading1', '$block' );
-				schema.registerItem( 'quote' );
+				schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+				schema.register( 'heading1', { inheritAllFrom: '$block' } );
+				schema.register( 'quote' );
 
-				schema.allow( { name: '$block', inside: 'quote' } );
-				schema.allow( { name: 'quote', inside: '$root' } );
+				schema.extend( '$block', { allowIn: 'quote' } );
+				schema.extend( 'quote', { allowIn: '$root' } );
 			} );
 
 			it( 'gets content when ends are equally deeply nested', () => {
@@ -329,8 +329,8 @@ describe( 'DataController utils', () => {
 
 			// See: https://github.com/ckeditor/ckeditor5-engine/pull/1043#issuecomment-318012286
 			it( 'ensures that elements are retrieved by indexes instead of offsets', () => {
-				model.schema.allow( { name: '$text', inside: '$root' } );
-				model.schema.allow( { name: '$text', inside: 'quote' } );
+				model.schema.extend( '$text', { allowIn: '$root' } );
+				model.schema.extend( '$text', { allowIn: 'quote' } );
 
 				setData( model,
 					'foo' +

+ 101 - 66
packages/ckeditor5-engine/tests/model/utils/insertcontent.js

@@ -20,7 +20,7 @@ describe( 'DataController utils', () => {
 			doc = model.document;
 			doc.createRoot();
 
-			model.schema.allow( { name: '$text', inside: '$root' } );
+			model.schema.extend( '$text', { allowIn: '$root' } );
 			setData( model, 'x[]x' );
 
 			model.change( writer => {
@@ -34,7 +34,7 @@ describe( 'DataController utils', () => {
 			doc = model.document;
 			doc.createRoot();
 
-			model.schema.allow( { name: '$text', inside: '$root' } );
+			model.schema.extend( '$text', { allowIn: '$root' } );
 
 			setData( model, 'x[]x' );
 
@@ -48,7 +48,7 @@ describe( 'DataController utils', () => {
 			doc = model.document;
 			doc.createRoot();
 
-			model.schema.allow( { name: '$text', inside: '$root' } );
+			model.schema.extend( '$text', { allowIn: '$root' } );
 
 			setData( model, 'x[]x' );
 
@@ -64,9 +64,11 @@ describe( 'DataController utils', () => {
 
 			const content = new Element( 'image' );
 
-			model.schema.registerItem( 'paragraph', '$block' );
-			model.schema.registerItem( 'image', '$inline' );
-			model.schema.objects.add( 'image' );
+			model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+			model.schema.register( 'image', {
+				allowWhere: '$text',
+				isObject: true
+			} );
 
 			setData( model, '<paragraph>foo[]</paragraph>' );
 
@@ -83,19 +85,20 @@ describe( 'DataController utils', () => {
 
 				const schema = model.schema;
 
-				schema.registerItem( 'image', '$inline' );
-				schema.registerItem( 'disallowedElement' );
+				schema.register( 'image', {
+					allowWhere: '$text',
+					isObject: true
+				} );
+				schema.register( 'disallowedElement' );
 
-				schema.allow( { name: '$text', inside: '$root' } );
-				schema.allow( { name: 'image', inside: '$root' } );
+				schema.extend( '$text', { allowIn: '$root' } );
+				schema.extend( 'image', { allowIn: '$root' } );
 				// Otherwise it won't be passed to the temporary model fragment used inside insert().
-				schema.allow( { name: 'disallowedElement', inside: '$clipboardHolder' } );
-				model.schema.allow( { name: '$text', inside: 'disallowedElement' } );
-
-				schema.allow( { name: '$inline', attributes: [ 'bold' ] } );
-				schema.allow( { name: '$inline', attributes: [ 'italic' ] } );
-
-				schema.objects.add( 'image' );
+				schema.extend( 'disallowedElement', { allowIn: '$clipboardHolder' } );
+				schema.extend( '$text', {
+					allowIn: 'disallowedElement',
+					allowAttributes: [ 'bold', 'italic' ]
+				} );
 			} );
 
 			it( 'inserts one text node', () => {
@@ -213,25 +216,21 @@ describe( 'DataController utils', () => {
 
 				const schema = model.schema;
 
-				schema.registerItem( 'paragraph', '$block' );
-				schema.registerItem( 'heading1', '$block' );
-				schema.registerItem( 'heading2', '$block' );
-				schema.registerItem( 'blockWidget' );
-				schema.registerItem( 'inlineWidget' );
-				schema.registerItem( 'listItem', '$block' );
-
-				schema.allow( { name: 'blockWidget', inside: '$root' } );
-				schema.allow( { name: 'inlineWidget', inside: '$block' } );
-				schema.allow( { name: 'inlineWidget', inside: '$clipboardHolder' } );
-				schema.allow( {
-					name: 'listItem',
-					inside: '$root',
-					attributes: [ 'type', 'indent' ]
+				schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+				schema.register( 'heading1', { inheritAllFrom: '$block' } );
+				schema.register( 'heading2', { inheritAllFrom: '$block' } );
+				schema.register( 'blockWidget', {
+					isObject: true,
+					allowIn: '$root'
+				} );
+				schema.register( 'inlineWidget', {
+					isObject: true,
+					allowIn: [ '$block', '$clipboardHolder' ],
+				} );
+				schema.register( 'listItem', {
+					inheritAllFrom: '$block',
+					allowAttributes: [ 'type', 'indent' ]
 				} );
-				schema.requireAttributes( 'listItem', [ 'type', 'indent' ] );
-
-				schema.objects.add( 'blockWidget' );
-				schema.objects.add( 'inlineWidget' );
 			} );
 
 			it( 'inserts one text node', () => {
@@ -273,7 +272,17 @@ describe( 'DataController utils', () => {
 			} );
 
 			it( 'not insert autoparagraph when paragraph is disallowed at the current position', () => {
-				model.schema.disallow( { name: 'paragraph', inside: '$root' } );
+				// Disallow paragraph in $root.
+				model.schema.on( 'checkChild', ( evt, args ) => {
+					const ctx = args[ 0 ];
+					const child = args[ 1 ];
+					const childRule = model.schema.getDefinition( child );
+
+					if ( childRule.name == 'paragraph' && ctx.endsWith( '$root' ) ) {
+						evt.stop();
+						evt.return = false;
+					}
+				} );
 
 				const content = new DocumentFragment( [
 					new Element( 'heading1', [], [ new Text( 'bar' ) ] ),
@@ -585,33 +594,58 @@ describe( 'DataController utils', () => {
 
 				const schema = model.schema;
 
-				schema.registerItem( 'paragraph', '$block' );
-				schema.registerItem( 'heading1', '$block' );
-				schema.registerItem( 'element', '$block' );
+				schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+				schema.register( 'heading1', { inheritAllFrom: '$block' } );
+				schema.register( 'element', { inheritAllFrom: '$block' } );
+
+				schema.register( 'table' );
+				schema.register( 'td' );
+				schema.register( 'disallowedWidget', {
+					isObject: true
+				} );
+
+				schema.extend( 'table', { allowIn: '$clipboardHolder' } );
+				schema.extend( 'td', { allowIn: '$clipboardHolder' } );
+				schema.extend( 'td', { allowIn: 'table' } );
+				schema.extend( 'element', { allowIn: 'td' } );
+				schema.extend( '$block', { allowIn: 'td' } );
+				schema.extend( '$text', { allowIn: 'td' } );
+				schema.extend( 'table', { allowIn: 'element' } );
 
-				schema.registerItem( 'table' );
-				schema.registerItem( 'td' );
-				schema.registerItem( 'disallowedWidget' );
+				schema.extend( 'disallowedWidget', { allowIn: '$clipboardHolder' } );
+				schema.extend( '$text', { allowIn: 'disallowedWidget' } );
 
-				schema.allow( { name: 'table', inside: '$clipboardHolder' } );
-				schema.allow( { name: 'td', inside: '$clipboardHolder' } );
-				schema.allow( { name: 'td', inside: 'table' } );
-				schema.allow( { name: 'element', inside: 'td' } );
-				schema.allow( { name: '$block', inside: 'td' } );
-				schema.allow( { name: '$text', inside: 'td' } );
-				schema.allow( { name: 'table', inside: 'element' } );
+				schema.extend( 'element', { allowIn: 'paragraph' } );
+				schema.extend( 'element', { allowIn: 'heading1' } );
 
-				schema.allow( { name: 'disallowedWidget', inside: '$clipboardHolder' } );
-				schema.allow( { name: '$text', inside: 'disallowedWidget' } );
-				schema.objects.add( 'disallowedWidget' );
+				schema.on( 'checkAttribute', ( evt, args ) => {
+					const ctx = args[ 0 ];
+					const attributeName = args[ 1 ];
 
-				schema.allow( { name: 'element', inside: 'paragraph' } );
-				schema.allow( { name: 'element', inside: 'heading1' } );
-				schema.allow( { name: '$text', attributes: 'b', inside: 'paragraph' } );
-				schema.allow( { name: '$text', attributes: [ 'b' ], inside: 'paragraph element' } );
-				schema.allow( { name: '$text', attributes: [ 'a', 'b' ], inside: 'heading1 element' } );
-				schema.allow( { name: '$text', attributes: [ 'a', 'b' ], inside: 'td element' } );
-				schema.allow( { name: '$text', attributes: [ 'b' ], inside: 'element table td' } );
+					// Allow 'b' on paragraph>$text.
+					if ( ctx.endsWith( 'paragraph $text' ) && attributeName == 'b' ) {
+						evt.stop();
+						evt.return = true;
+					}
+
+					// Allow 'b' on paragraph>element>$text.
+					if ( ctx.endsWith( 'paragraph element $text' ) && attributeName == 'b' ) {
+						evt.stop();
+						evt.return = true;
+					}
+
+					// Allow 'a' and 'b' on heading1>element>$text.
+					if ( ctx.endsWith( 'heading1 element $text' ) && [ 'a', 'b' ].includes( attributeName ) ) {
+						evt.stop();
+						evt.return = true;
+					}
+
+					// Allow 'b' on element>table>td>$text.
+					if ( ctx.endsWith( 'element table td $text' ) && attributeName == 'b' ) {
+						evt.stop();
+						evt.return = true;
+					}
+				}, { priority: 'high' } );
 			} );
 
 			it( 'filters out disallowed elements and leaves out the text', () => {
@@ -691,15 +725,16 @@ describe( 'DataController utils', () => {
 
 			const schema = model.schema;
 
-			schema.registerItem( 'limit' );
-			schema.allow( { name: 'limit', inside: '$root' } );
-			schema.allow( { name: '$text', inside: 'limit' } );
-			schema.limits.add( 'limit' );
+			schema.register( 'limit', {
+				isLimit: true
+			} );
+			schema.extend( 'limit', { allowIn: '$root' } );
+			schema.extend( '$text', { allowIn: 'limit' } );
 
-			schema.registerItem( 'disallowedElement' );
-			schema.allow( { name: 'disallowedElement', inside: '$clipboardHolder' } );
+			schema.register( 'disallowedElement' );
+			schema.extend( 'disallowedElement', { allowIn: '$clipboardHolder' } );
 
-			schema.registerItem( 'paragraph', '$block' );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 		} );
 
 		it( 'should insert limit element', () => {

+ 28 - 24
packages/ckeditor5-engine/tests/model/utils/modifyselection.js

@@ -15,9 +15,9 @@ describe( 'DataController utils', () => {
 		model = new Model();
 		doc = model.document;
 
-		model.schema.registerItem( 'p', '$block' );
-		model.schema.registerItem( 'x', '$block' );
-		model.schema.allow( { name: 'x', inside: 'p' } );
+		model.schema.register( 'p', { inheritAllFrom: '$block' } );
+		model.schema.register( 'x', { inheritAllFrom: '$block' } );
+		model.schema.extend( 'x', { allowIn: 'p' } );
 
 		doc.createRoot();
 	} );
@@ -287,9 +287,9 @@ describe( 'DataController utils', () => {
 
 			describe( 'beyond element – skipping incorrect positions', () => {
 				beforeEach( () => {
-					model.schema.registerItem( 'quote' );
-					model.schema.allow( { name: 'quote', inside: '$root' } );
-					model.schema.allow( { name: '$block', inside: 'quote' } );
+					model.schema.register( 'quote' );
+					model.schema.extend( 'quote', { allowIn: '$root' } );
+					model.schema.extend( '$block', { allowIn: 'quote' } );
 				} );
 
 				test(
@@ -335,7 +335,7 @@ describe( 'DataController utils', () => {
 
 		describe( 'unit=codePoint', () => {
 			it( 'does nothing on empty content', () => {
-				model.schema.allow( { name: '$text', inside: '$root' } );
+				model.schema.extend( '$text', { allowIn: '$root' } );
 
 				setData( model, '' );
 
@@ -413,14 +413,17 @@ describe( 'DataController utils', () => {
 
 		describe( 'objects handling', () => {
 			beforeEach( () => {
-				model.schema.registerItem( 'obj' );
-				model.schema.allow( { name: 'obj', inside: '$root' } );
-				model.schema.allow( { name: '$text', inside: 'obj' } );
-				model.schema.objects.add( 'obj' );
-
-				model.schema.registerItem( 'inlineObj', '$inline' );
-				model.schema.allow( { name: '$text', inside: 'inlineObj' } );
-				model.schema.objects.add( 'inlineObj' );
+				model.schema.register( 'obj', {
+					isObject: true
+				} );
+				model.schema.extend( 'obj', { allowIn: '$root' } );
+				model.schema.extend( '$text', { allowIn: 'obj' } );
+
+				model.schema.register( 'inlineObj', {
+					allowIn: 'p',
+					isObject: true
+				} );
+				model.schema.extend( '$text', { allowIn: 'inlineObj' } );
 			} );
 
 			test(
@@ -480,16 +483,17 @@ describe( 'DataController utils', () => {
 
 		describe( 'limits handling', () => {
 			beforeEach( () => {
-				model.schema.registerItem( 'inlineLimit' );
-				model.schema.allow( { name: 'inlineLimit', inside: '$block' } );
-				model.schema.allow( { name: '$text', inside: 'inlineLimit' } );
-
-				model.schema.registerItem( 'blockLimit' );
-				model.schema.allow( { name: 'blockLimit', inside: '$root' } );
-				model.schema.allow( { name: 'p', inside: 'blockLimit' } );
+				model.schema.register( 'inlineLimit', {
+					isLimit: true
+				} );
+				model.schema.extend( 'inlineLimit', { allowIn: '$block' } );
+				model.schema.extend( '$text', { allowIn: 'inlineLimit' } );
 
-				model.schema.limits.add( 'inlineLimit' );
-				model.schema.limits.add( 'blockLimit' );
+				model.schema.register( 'blockLimit', {
+					isLimit: true
+				} );
+				model.schema.extend( 'blockLimit', { allowIn: '$root' } );
+				model.schema.extend( 'p', { allowIn: 'blockLimit' } );
 			} );
 
 			test(

+ 4 - 3
packages/ckeditor5-engine/tests/tickets/699.js

@@ -44,9 +44,10 @@ describe( 'Bug ckeditor5-engine#699', () => {
 function WidgetPlugin( editor ) {
 	const schema = editor.model.schema;
 
-	schema.registerItem( 'widget' );
-	schema.allow( { name: 'widget', inside: '$root' } );
-	schema.objects.add( 'widget' );
+	schema.register( 'widget', {
+		isObject: true
+	} );
+	schema.extend( 'widget', { allowIn: '$root' } );
 
 	buildModelConverter().for( editor.data.modelToView, editor.editing.modelToView )
 		.fromElement( 'widget' )