8
0
Quellcode durchsuchen

Refactoring: Switched order of classes – public should go first.

Piotrek Koszuliński vor 9 Jahren
Ursprung
Commit
19fdf58d76

+ 423 - 424
packages/ckeditor5-engine/src/conversion/viewconsumable.js

@@ -14,578 +14,577 @@ import ViewText from '../view/text.js';
 import ViewDocumentFragment from '../view/documentfragment.js';
 
 /**
- * This is a private helper-class for {@link module:engine/conversion/viewconsumable~ViewConsumable}.
- * It represents and manipulates consumable parts of a single {@link module:engine/view/element~Element}.
+ * Class used for handling consumption of view {@link module:engine/view/element~Element elements},
+ * {@link module:engine/view/text~Text text nodes} and {@link module:engine/view/documentfragment~DocumentFragment document fragments}.
+ * Element's name and its parts (attributes, classes and styles) can be consumed separately. Consuming an element's name
+ * does not consume its attributes, classes and styles.
+ * To add items for consumption use {@link module:engine/conversion/viewconsumable~ViewConsumable#add add method}.
+ * To test items use {@link module:engine/conversion/viewconsumable~ViewConsumable#test test method}.
+ * To consume items use {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consume method}.
+ * To revert already consumed items use {@link module:engine/conversion/viewconsumable~ViewConsumable#revert revert method}.
  *
- * @private
+ *		viewConsumable.add( element, { name: true } ); // Adds element's name as ready to be consumed.
+ *		viewConsumable.add( textNode ); // Adds text node for consumption.
+ *		viewConsumable.add( docFragment ); // Adds document fragment for consumption.
+ *		viewConsumable.test( element, { name: true }  ); // Tests if element's name can be consumed.
+ *		viewConsumable.test( textNode ); // Tests if text node can be consumed.
+ *		viewConsumable.test( docFragment ); // Tests if document fragment can be consumed.
+ *		viewConsumable.consume( element, { name: true }  ); // Consume element's name.
+ *		viewConsumable.consume( textNode ); // Consume text node.
+ *		viewConsumable.consume( docFragment ); // Consume document fragment.
+ *		viewConsumable.revert( element, { name: true }  ); // Revert already consumed element's name.
+ *		viewConsumable.revert( textNode ); // Revert already consumed text node.
+ *		viewConsumable.revert( docFragment ); // Revert already consumed document fragment.
  */
-class ViewElementConsumables {
-
+export default class ViewConsumable {
 	/**
-	 * Creates ViewElementConsumables instance.
+	 * Creates new ViewConsumable.
 	 */
-	constructor()  {
-		/**
-		 * Flag indicating if name of the element can be consumed.
-		 *
-		 * @private
-		 * @member {Boolean}
-		 */
-		this._canConsumeName = null;
-
+	constructor() {
 		/**
-		 * Contains maps of element's consumables: attributes, classes and styles.
+		 * Map of consumable elements. If {@link module:engine/view/element~Element element} is used as a key,
+		 * {@link module:engine/conversion/viewconsumable~ViewElementConsumables ViewElementConsumables} instance is stored as value.
+		 * For {@link module:engine/view/text~Text text nodes} and {@link module:engine/view/documentfragment~DocumentFragment document fragments}
+		 * boolean value is stored as value.
 		 *
-		 * @private
-		 * @member {Object}
-		 */
-		this._consumables = {
-			attribute: new Map(),
-			style: new Map(),
-			class: new Map()
-		};
+		 * @protected
+		 * @member {Map.<module:engine/conversion/viewconsumable~ViewElementConsumables|Boolean>}
+		*/
+		this._consumables = new Map();
 	}
 
 	/**
-	 * Adds consumable parts of the {@link module:engine/view/element~Element view element}.
-	 * Element's name itself can be marked to be consumed (when element's name is consumed its attributes, classes and
-	 * styles still could be consumed):
-	 *
-	 *		consumables.add( { name: true } );
-	 *
-	 * Attributes classes and styles:
+	 * Adds {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
+	 * {@link module:engine/view/documentfragment~DocumentFragment document fragment} as ready to be consumed.
 	 *
-	 *		consumables.add( { attribute: 'title', class: 'foo', style: 'color' } );
-	 *		consumables.add( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
+	 *		viewConsumable.add( p, { name: true } ); // Adds element's name to consume.
+	 *		viewConsumable.add( p, { attribute: 'name' } ); // Adds element's attribute.
+	 *		viewConsumable.add( p, { class: 'foobar' } ); // Adds element's class.
+	 *		viewConsumable.add( p, { style: 'color' } ); // Adds element's style
+	 *		viewConsumable.add( p, { attribute: 'name', style: 'color' } ); // Adds attribute and style.
+	 *		viewConsumable.add( p, { class: [ 'baz', 'bar' ] } ); // Multiple consumables can be provided.
+	 *		viewConsumable.add( textNode ); // Adds text node to consume.
+	 *		viewConsumable.add( docFragment ); // Adds document fragment to consume.
 	 *
 	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
-	 * attribute is provided - it should be handled separately by providing `style` and `class` in consumables object.
+	 * attribute is provided - it should be handled separately by providing actual style/class.
 	 *
-	 * @param {Object} consumables Object describing which parts of the element can be consumed.
-	 * @param {Boolean} consumables.name If set to `true` element's name will be added as consumable.
-	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to add as consumable.
-	 * @param {String|Array.<String>} consumables.class Class name or array of class names to add as consumable.
-	 * @param {String|Array.<String>} consumables.style Style name or array of style names to add as consumable.
+	 *		viewConsumable.add( p, { attribute: 'style' } ); // This call will throw an exception.
+	 *		viewConsumable.add( p, { style: 'color' } ); // This is properly handled style.
+	 *
+	 * @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
+	 * @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
+	 * @param {Boolean} consumables.name If set to true element's name will be included.
+	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names.
+	 * @param {String|Array.<String>} consumables.class Class name or array of class names.
+	 * @param {String|Array.<String>} consumables.style Style name or array of style names.
 	 */
-	add( consumables ) {
-		if ( consumables.name ) {
-			this._canConsumeName = true;
-		}
+	add( element, consumables ) {
+		let elementConsumables;
 
-		for ( let type in this._consumables ) {
-			if ( type in consumables ) {
-				this._add( type, consumables[ type ] );
-			}
-		}
-	}
+		// For text nodes and document fragments just mark them as consumable.
+		if ( element instanceof ViewText || element instanceof ViewDocumentFragment ) {
+			this._consumables.set( element, true );
 
-	/**
-	 * Tests if parts of the {@link module:engine/view/element~Element view element} can be consumed.
-	 *
-	 * Element's name can be tested:
-	 *
-	 *		consumables.test( { name: true } );
-	 *
-	 * Attributes classes and styles:
-	 *
-	 *		consumables.test( { attribute: 'title', class: 'foo', style: 'color' } );
-	 *		consumables.test( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
-	 *
-	 * @param {Object} consumables Object describing which parts of the element should be tested.
-	 * @param {Boolean} consumables.name If set to `true` element's name will be tested.
-	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to test.
-	 * @param {String|Array.<String>} consumables.class Class name or array of class names to test.
-	 * @param {String|Array.<String>} consumables.style Style name or array of style names to test.
-	 * @returns {Boolean|null} `true` when all tested items can be consumed, `null` when even one of the items
-	 * was never marked for consumption and `false` when even one of the items was already consumed.
-	 */
-	test( consumables ) {
-		// Check if name can be consumed.
-		if ( consumables.name && !this._canConsumeName ) {
-			return this._canConsumeName;
+			return;
 		}
 
-		for ( let type in this._consumables ) {
-			if ( type in consumables ) {
-				const value = this._test( type, consumables[ type ] );
-
-				if ( value !== true ) {
-					return value;
-				}
-			}
+		// For elements create new ViewElementConsumables or update already existing one.
+		if ( !this._consumables.has( element ) ) {
+			elementConsumables = new ViewElementConsumables();
+			this._consumables.set( element, elementConsumables );
+		} else {
+			elementConsumables = this._consumables.get( element );
 		}
 
-		// Return true only if all can be consumed.
-		return true;
+		elementConsumables.add( consumables );
 	}
 
 	/**
-	 * Consumes parts of {@link module:engine/view/element~Element view element}. This function does not check if consumable item
-	 * is already consumed - it consumes all consumable items provided.
-	 * Element's name can be consumed:
+	 * Tests if {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
+	 * {@link module:engine/view/documentfragment~DocumentFragment document fragment} can be consumed.
+	 * It returns `true` when all items included in method's call can be consumed. Returns `false` when
+	 * first already consumed item is found and `null` when first non-consumable item is found.
 	 *
-	 *		consumables.consume( { name: true } );
+	 *		viewConsumable.test( p, { name: true } ); // Tests element's name.
+	 *		viewConsumable.test( p, { attribute: 'name' } ); // Tests attribute.
+	 *		viewConsumable.test( p, { class: 'foobar' } ); // Tests class.
+	 *		viewConsumable.test( p, { style: 'color' } ); // Tests style.
+	 *		viewConsumable.test( p, { attribute: 'name', style: 'color' } ); // Tests attribute and style.
+	 *		viewConsumable.test( p, { class: [ 'baz', 'bar' ] } ); // Multiple consumables can be tested.
+	 *		viewConsumable.test( textNode ); // Tests text node.
+	 *		viewConsumable.test( docFragment ); // Tests document fragment.
 	 *
-	 * Attributes classes and styles:
+	 * Testing classes and styles as attribute will test if all added classes/styles can be consumed.
 	 *
-	 *		consumables.consume( { attribute: 'title', class: 'foo', style: 'color' } );
-	 *		consumables.consume( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
+	 *		viewConsumable.test( p, { attribute: 'class' } ); // Tests if all added classes can be consumed.
+	 *		viewConsumable.test( p, { attribute: 'style' } ); // Tests if all added styles can be consumed.
 	 *
-	 * @param {Object} consumables Object describing which parts of the element should be consumed.
-	 * @param {Boolean} consumables.name If set to `true` element's name will be consumed.
-	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to consume.
-	 * @param {String|Array.<String>} consumables.class Class name or array of class names to consume.
-	 * @param {String|Array.<String>} consumables.style Style name or array of style names to consume.
+	 * @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
+	 * @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
+	 * @param {Boolean} consumables.name If set to true element's name will be included.
+	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names.
+	 * @param {String|Array.<String>} consumables.class Class name or array of class names.
+	 * @param {String|Array.<String>} consumables.style Style name or array of style names.
+	 * @returns {Boolean|null} Returns `true` when all items included in method's call can be consumed. Returns `false`
+	 * when first already consumed item is found and `null` when first non-consumable item is found.
 	 */
-	consume( consumables ) {
-		if ( consumables.name ) {
-			this._canConsumeName = false;
+	test( element, consumables ) {
+		const elementConsumables = this._consumables.get( element );
+
+		if ( elementConsumables === undefined ) {
+			return null;
 		}
 
-		for ( let type in this._consumables ) {
-			if ( type in consumables ) {
-				this._consume( type, consumables[ type ] );
-			}
+		// For text nodes and document fragments return stored boolean value.
+		if ( element instanceof ViewText || element instanceof ViewDocumentFragment ) {
+			return elementConsumables;
 		}
+
+		// For elements test consumables object.
+		return elementConsumables.test( consumables );
 	}
 
 	/**
-	 * Revert already consumed parts of {@link module:engine/view/element~Element view Element}, so they can be consumed once again.
-	 * Element's name can be reverted:
+	 * Consumes {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
+	 * {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
+	 * It returns `true` when all items included in method's call can be consumed, otherwise returns `false`.
 	 *
-	 *		consumables.revert( { name: true } );
+	 *		viewConsumable.consume( p, { name: true } ); // Consumes element's name.
+	 *		viewConsumable.consume( p, { attribute: 'name' } ); // Consumes element's attribute.
+	 *		viewConsumable.consume( p, { class: 'foobar' } ); // Consumes element's class.
+	 *		viewConsumable.consume( p, { style: 'color' } ); // Consumes element's style.
+	 *		viewConsumable.consume( p, { attribute: 'name', style: 'color' } ); // Consumes attribute and style.
+	 *		viewConsumable.consume( p, { class: [ 'baz', 'bar' ] } ); // Multiple consumables can be consumed.
+	 *		viewConsumable.consume( textNode ); // Consumes text node.
+	 *		viewConsumable.consume( docFragment ); // Consumes document fragment.
 	 *
-	 * Attributes classes and styles:
+	 * Consuming classes and styles as attribute will test if all added classes/styles can be consumed.
 	 *
-	 *		consumables.revert( { attribute: 'title', class: 'foo', style: 'color' } );
-	 *		consumables.revert( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
+	 *		viewConsumable.consume( p, { attribute: 'class' } ); // Consume only if all added classes can be consumed.
+	 *		viewConsumable.consume( p, { attribute: 'style' } ); // Consume only if all added styles can be consumed.
 	 *
-	 * @param {Object} consumables Object describing which parts of the element should be reverted.
-	 * @param {Boolean} consumables.name If set to `true` element's name will be reverted.
-	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to revert.
-	 * @param {String|Array.<String>} consumables.class Class name or array of class names to revert.
-	 * @param {String|Array.<String>} consumables.style Style name or array of style names to revert.
+	 * @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
+	 * @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
+	 * @param {Boolean} consumables.name If set to true element's name will be included.
+	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names.
+	 * @param {String|Array.<String>} consumables.class Class name or array of class names.
+	 * @param {String|Array.<String>} consumables.style Style name or array of style names.
+	 * @returns {Boolean} Returns `true` when all items included in method's call can be consumed,
+	 * otherwise returns `false`.
 	 */
-	revert( consumables ) {
-		if ( consumables.name ) {
-			this._canConsumeName = true;
-		}
-
-		for ( let type in this._consumables ) {
-			if ( type in consumables ) {
-				this._revert( type, consumables[ type ] );
+	consume( element, consumables ) {
+		if ( this.test( element, consumables ) ) {
+			if ( element instanceof ViewText || element instanceof ViewDocumentFragment ) {
+				// For text nodes and document fragments set value to false.
+				this._consumables.set( element, false );
+			} else {
+				// For elements - consume consumables object.
+				this._consumables.get( element ).consume( consumables );
 			}
+
+			return true;
 		}
+
+		return false;
 	}
 
 	/**
-	 * Helper method that adds consumables of a given type: attribute, class or style.
+	 * Reverts {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
+	 * {@link module:engine/view/documentfragment~DocumentFragment document fragment} so they can be consumed once again.
+	 * Method does not revert items that were never previously added for consumption, even if they are included in
+	 * method's call.
 	 *
-	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
-	 * type is provided - it should be handled separately by providing actual style/class type.
+	 *		viewConsumable.revert( p, { name: true } ); // Reverts element's name.
+	 *		viewConsumable.revert( p, { attribute: 'name' } ); // Reverts element's attribute.
+	 *		viewConsumable.revert( p, { class: 'foobar' } ); // Reverts element's class.
+	 *		viewConsumable.revert( p, { style: 'color' } ); // Reverts element's style.
+	 *		viewConsumable.revert( p, { attribute: 'name', style: 'color' } ); // Reverts attribute and style.
+	 *		viewConsumable.revert( p, { class: [ 'baz', 'bar' ] } ); // Multiple names can be reverted.
+	 *		viewConsumable.revert( textNode ); // Reverts text node.
+	 *		viewConsumable.revert( docFragment ); // Reverts document fragment.
 	 *
-	 * @private
-	 * @param {String} type Type of the consumable item: `attribute`, `class` or `style`.
-	 * @param {String|Array.<String>} item Consumable item or array of items.
-	 */
-	_add( type, item ) {
-		const items = isArray( item ) ? item : [ item ];
-		const consumables = this._consumables[ type ];
-
-		for ( let name of items ) {
-			if ( type === 'attribute' && ( name === 'class' || name === 'style' ) ) {
-				/**
-				 * Class and style attributes should be handled separately.
-				 *
-				 * @error viewconsumable-invalid-attribute
-				 */
-				throw new CKEditorError( 'viewconsumable-invalid-attribute: Classes and styles should be handled separately.' );
-			}
-
-			consumables.set( name, true );
-		}
-	}
-
-	/**
-	 * Helper method that tests consumables of a given type: attribute, class or style.
+	 * Reverting classes and styles as attribute will revert all classes/styles that were previously added for
+	 * consumption.
 	 *
-	 * @private
-	 * @param {String} type Type of the consumable item: `attribute`, `class` or `style`.
-	 * @param {String|Array.<String>} item Consumable item or array of items.
-	 * @returns {Boolean|null} Returns `true` if all items can be consumed, `null` when one of the items cannot be
-	 * consumed and `false` when one of the items is already consumed.
+	 *		viewConsumable.revert( p, { attribute: 'class' } ); // Reverts all classes added for consumption.
+	 *		viewConsumable.revert( p, { attribute: 'style' } ); // Reverts all styles added for consumption.
+	 *
+	 * @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
+	 * @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
+	 * @param {Boolean} consumables.name If set to true element's name will be included.
+	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names.
+	 * @param {String|Array.<String>} consumables.class Class name or array of class names.
+	 * @param {String|Array.<String>} consumables.style Style name or array of style names.
 	 */
-	_test( type, item ) {
-		const items = isArray( item ) ? item : [ item ];
-		const consumables = this._consumables[ type ];
-
-		for ( let name of items ) {
-			if ( type === 'attribute' && ( name === 'class' || name === 'style' ) )  {
-				// Check all classes/styles if class/style attribute is tested.
-				const value = this._test( name, [ ...this._consumables[ name ].keys() ] );
+	revert( element, consumables ) {
+		const elementConsumables = this._consumables.get( element );
 
-				if ( value !== true ) {
-					return value;
-				}
+		if ( elementConsumables !== undefined ) {
+			if ( element instanceof ViewText || element instanceof ViewDocumentFragment ) {
+				// For text nodes and document fragments - set consumable to true.
+				this._consumables.set( element, true );
 			} else {
-				const value = consumables.get( name );
-				// Return null if attribute is not found.
-				if ( value === undefined ) {
-					return null;
-				}
-
-				if ( !value ) {
-					return false;
-				}
+				// For elements - revert items from consumables object.
+				elementConsumables.revert( consumables );
 			}
 		}
-
-		return true;
 	}
 
 	/**
-	 * Helper method that consumes items of a given type: attribute, class or style.
+	 * Creates consumable object from {@link module:engine/view/element~Element view element}. Consumable object will include
+	 * element's name and all its attributes, classes and styles.
 	 *
-	 * @private
-	 * @param {String} type Type of the consumable item: `attribute`, `class` or `style`.
-	 * @param {String|Array.<String>} item Consumable item or array of items.
+	 * @static
+	 * @param {module:engine/view/element~Element} element
+	 * @returns {Object} consumables
 	 */
-	_consume( type, item ) {
-		const items = isArray( item ) ? item : [ item ];
-		const consumables = this._consumables[ type ];
+	static consumablesFromElement( element ) {
+		const consumables = {
+			name: true,
+			attribute: [],
+			class: [],
+			style: []
+		};
 
-		for ( let name of items ) {
-			if ( type === 'attribute' && ( name === 'class' || name === 'style' ) ) {
-				// If class or style is provided for consumption - consume them all.
-				this._consume( name, [ ...this._consumables[ name ].keys() ] );
-			} else {
-				consumables.set( name, false );
+		const attributes = element.getAttributeKeys();
+
+		for ( let attribute of attributes ) {
+			// Skip classes and styles - will be added separately.
+			if ( attribute == 'style' || attribute == 'class' ) {
+				continue;
 			}
+
+			consumables.attribute.push( attribute );
+		}
+
+		const classes = element.getClassNames();
+
+		for ( let className of classes ) {
+			consumables.class.push( className );
+		}
+
+		const styles = element.getStyleNames();
+
+		for ( let style of styles ) {
+			consumables.style.push( style );
 		}
+
+		return consumables;
 	}
 
 	/**
-	 * Helper method that reverts items of a given type: attribute, class or style.
+	 * Creates {@link module:engine/conversion/viewconsumable~ViewConsumable ViewConsumable} instance from
+	 * {@link module:engine/view/element~Element element} or {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
+	 * Instance will contain all elements, child nodes, attributes, styles and classes added for consumption.
 	 *
-	 * @private
-	 * @param {String} type Type of the consumable item: `attribute`, `class` or , `style`.
-	 * @param {String|Array.<String>} item Consumable item or array of items.
+	 * @static
+	 * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} from View element or document fragment
+	 * from which `ViewConsumable` will be created.
+	 * @param {module:engine/conversion/viewconsumable~ViewConsumable} [instance] If provided, given `ViewConsumable` instance will be used
+	 * to add all consumables. It will be returned instead of a new instance.
 	 */
-	_revert( type, item ) {
-		const items = isArray( item ) ? item : [ item ];
-		const consumables = this._consumables[ type ];
+	static createFrom( from, instance ) {
+		if ( !instance ) {
+			instance = new ViewConsumable();
+		}
 
-		for ( let name of items ) {
-			if ( type === 'attribute' && ( name === 'class' || name === 'style' ) ) {
-				// If class or style is provided for reverting - revert them all.
-				this._revert( name, [ ...this._consumables[ name ].keys() ] );
-			} else {
-				const value = consumables.get( name );
+		if ( from instanceof ViewText ) {
+			instance.add( from );
 
-				if ( value === false ) {
-					consumables.set( name, true );
-				}
-			}
+			return instance;
+		}
+
+		// Add `from` itself, if it is an element.
+		if ( from instanceof ViewElement ) {
+			instance.add( from, ViewConsumable.consumablesFromElement( from ) );
+		}
+
+		if ( from instanceof ViewDocumentFragment ) {
+			instance.add( from );
+		}
+
+		for ( let child of from.getChildren() ) {
+			instance = ViewConsumable.createFrom( child, instance );
 		}
+
+		return instance;
 	}
 }
 
 /**
- * Class used for handling consumption of view {@link module:engine/view/element~Element elements},
- * {@link module:engine/view/text~Text text nodes} and {@link module:engine/view/documentfragment~DocumentFragment document fragments}.
- * Element's name and its parts (attributes, classes and styles) can be consumed separately. Consuming an element's name
- * does not consume its attributes, classes and styles.
- * To add items for consumption use {@link module:engine/conversion/viewconsumable~ViewConsumable#add add method}.
- * To test items use {@link module:engine/conversion/viewconsumable~ViewConsumable#test test method}.
- * To consume items use {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consume method}.
- * To revert already consumed items use {@link module:engine/conversion/viewconsumable~ViewConsumable#revert revert method}.
+ * This is a private helper-class for {@link module:engine/conversion/viewconsumable~ViewConsumable}.
+ * It represents and manipulates consumable parts of a single {@link module:engine/view/element~Element}.
  *
- *		viewConsumable.add( element, { name: true } ); // Adds element's name as ready to be consumed.
- *		viewConsumable.add( textNode ); // Adds text node for consumption.
- *		viewConsumable.add( docFragment ); // Adds document fragment for consumption.
- *		viewConsumable.test( element, { name: true }  ); // Tests if element's name can be consumed.
- *		viewConsumable.test( textNode ); // Tests if text node can be consumed.
- *		viewConsumable.test( docFragment ); // Tests if document fragment can be consumed.
- *		viewConsumable.consume( element, { name: true }  ); // Consume element's name.
- *		viewConsumable.consume( textNode ); // Consume text node.
- *		viewConsumable.consume( docFragment ); // Consume document fragment.
- *		viewConsumable.revert( element, { name: true }  ); // Revert already consumed element's name.
- *		viewConsumable.revert( textNode ); // Revert already consumed text node.
- *		viewConsumable.revert( docFragment ); // Revert already consumed document fragment.
+ * @private
  */
-export default class ViewConsumable {
+class ViewElementConsumables {
 
 	/**
-	 * Creates new ViewConsumable.
+	 * Creates ViewElementConsumables instance.
 	 */
-	constructor() {
+	constructor()  {
 		/**
-		 * Map of consumable elements. If {@link module:engine/view/element~Element element} is used as a key,
-		 * {@link module:engine/conversion/viewconsumable~ViewElementConsumables ViewElementConsumables} instance is stored as value.
-		 * For {@link module:engine/view/text~Text text nodes} and {@link module:engine/view/documentfragment~DocumentFragment document fragments}
-		 * boolean value is stored as value.
+		 * Flag indicating if name of the element can be consumed.
 		 *
-		 * @protected
-		 * @member {Map.<module:engine/conversion/viewconsumable~ViewElementConsumables|Boolean>}
-		*/
-		this._consumables = new Map();
+		 * @private
+		 * @member {Boolean}
+		 */
+		this._canConsumeName = null;
+
+		/**
+		 * Contains maps of element's consumables: attributes, classes and styles.
+		 *
+		 * @private
+		 * @member {Object}
+		 */
+		this._consumables = {
+			attribute: new Map(),
+			style: new Map(),
+			class: new Map()
+		};
 	}
 
 	/**
-	 * Adds {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
-	 * {@link module:engine/view/documentfragment~DocumentFragment document fragment} as ready to be consumed.
+	 * Adds consumable parts of the {@link module:engine/view/element~Element view element}.
+	 * Element's name itself can be marked to be consumed (when element's name is consumed its attributes, classes and
+	 * styles still could be consumed):
 	 *
-	 *		viewConsumable.add( p, { name: true } ); // Adds element's name to consume.
-	 *		viewConsumable.add( p, { attribute: 'name' } ); // Adds element's attribute.
-	 *		viewConsumable.add( p, { class: 'foobar' } ); // Adds element's class.
-	 *		viewConsumable.add( p, { style: 'color' } ); // Adds element's style
-	 *		viewConsumable.add( p, { attribute: 'name', style: 'color' } ); // Adds attribute and style.
-	 *		viewConsumable.add( p, { class: [ 'baz', 'bar' ] } ); // Multiple consumables can be provided.
-	 *		viewConsumable.add( textNode ); // Adds text node to consume.
-	 *		viewConsumable.add( docFragment ); // Adds document fragment to consume.
+	 *		consumables.add( { name: true } );
 	 *
-	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
-	 * attribute is provided - it should be handled separately by providing actual style/class.
+	 * Attributes classes and styles:
 	 *
-	 *		viewConsumable.add( p, { attribute: 'style' } ); // This call will throw an exception.
-	 *		viewConsumable.add( p, { style: 'color' } ); // This is properly handled style.
+	 *		consumables.add( { attribute: 'title', class: 'foo', style: 'color' } );
+	 *		consumables.add( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
 	 *
-	 * @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
-	 * @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
-	 * @param {Boolean} consumables.name If set to true element's name will be included.
-	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names.
-	 * @param {String|Array.<String>} consumables.class Class name or array of class names.
-	 * @param {String|Array.<String>} consumables.style Style name or array of style names.
+	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
+	 * attribute is provided - it should be handled separately by providing `style` and `class` in consumables object.
+	 *
+	 * @param {Object} consumables Object describing which parts of the element can be consumed.
+	 * @param {Boolean} consumables.name If set to `true` element's name will be added as consumable.
+	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to add as consumable.
+	 * @param {String|Array.<String>} consumables.class Class name or array of class names to add as consumable.
+	 * @param {String|Array.<String>} consumables.style Style name or array of style names to add as consumable.
 	 */
-	add( element, consumables ) {
-		let elementConsumables;
-
-		// For text nodes and document fragments just mark them as consumable.
-		if ( element instanceof ViewText || element instanceof ViewDocumentFragment ) {
-			this._consumables.set( element, true );
-
-			return;
+	add( consumables ) {
+		if ( consumables.name ) {
+			this._canConsumeName = true;
 		}
 
-		// For elements create new ViewElementConsumables or update already existing one.
-		if ( !this._consumables.has( element ) ) {
-			elementConsumables = new ViewElementConsumables();
-			this._consumables.set( element, elementConsumables );
-		} else {
-			elementConsumables = this._consumables.get( element );
+		for ( let type in this._consumables ) {
+			if ( type in consumables ) {
+				this._add( type, consumables[ type ] );
+			}
 		}
-
-		elementConsumables.add( consumables );
 	}
 
 	/**
-	 * Tests if {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
-	 * {@link module:engine/view/documentfragment~DocumentFragment document fragment} can be consumed.
-	 * It returns `true` when all items included in method's call can be consumed. Returns `false` when
-	 * first already consumed item is found and `null` when first non-consumable item is found.
+	 * Tests if parts of the {@link module:engine/view/node~Node view node} can be consumed.
 	 *
-	 *		viewConsumable.test( p, { name: true } ); // Tests element's name.
-	 *		viewConsumable.test( p, { attribute: 'name' } ); // Tests attribute.
-	 *		viewConsumable.test( p, { class: 'foobar' } ); // Tests class.
-	 *		viewConsumable.test( p, { style: 'color' } ); // Tests style.
-	 *		viewConsumable.test( p, { attribute: 'name', style: 'color' } ); // Tests attribute and style.
-	 *		viewConsumable.test( p, { class: [ 'baz', 'bar' ] } ); // Multiple consumables can be tested.
-	 *		viewConsumable.test( textNode ); // Tests text node.
-	 *		viewConsumable.test( docFragment ); // Tests document fragment.
+	 * Element's name can be tested:
 	 *
-	 * Testing classes and styles as attribute will test if all added classes/styles can be consumed.
+	 *		consumables.test( { name: true } );
 	 *
-	 *		viewConsumable.test( p, { attribute: 'class' } ); // Tests if all added classes can be consumed.
-	 *		viewConsumable.test( p, { attribute: 'style' } ); // Tests if all added styles can be consumed.
+	 * Attributes classes and styles:
 	 *
-	 * @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
-	 * @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
-	 * @param {Boolean} consumables.name If set to true element's name will be included.
-	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names.
-	 * @param {String|Array.<String>} consumables.class Class name or array of class names.
-	 * @param {String|Array.<String>} consumables.style Style name or array of style names.
-	 * @returns {Boolean|null} Returns `true` when all items included in method's call can be consumed. Returns `false`
-	 * when first already consumed item is found and `null` when first non-consumable item is found.
+	 *		consumables.test( { attribute: 'title', class: 'foo', style: 'color' } );
+	 *		consumables.test( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
+	 *
+	 * @param {Object} consumables Object describing which parts of the element should be tested.
+	 * @param {Boolean} consumables.name If set to `true` element's name will be tested.
+	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to test.
+	 * @param {String|Array.<String>} consumables.class Class name or array of class names to test.
+	 * @param {String|Array.<String>} consumables.style Style name or array of style names to test.
+	 * @returns {Boolean|null} `true` when all tested items can be consumed, `null` when even one of the items
+	 * was never marked for consumption and `false` when even one of the items was already consumed.
 	 */
-	test( element, consumables ) {
-		const elementConsumables = this._consumables.get( element );
-
-		if ( elementConsumables === undefined ) {
-			return null;
+	test( consumables ) {
+		// Check if name can be consumed.
+		if ( consumables.name && !this._canConsumeName ) {
+			return this._canConsumeName;
 		}
 
-		// For text nodes and document fragments return stored boolean value.
-		if ( element instanceof ViewText || element instanceof ViewDocumentFragment ) {
-			return elementConsumables;
+		for ( let type in this._consumables ) {
+			if ( type in consumables ) {
+				const value = this._test( type, consumables[ type ] );
+
+				if ( value !== true ) {
+					return value;
+				}
+			}
 		}
 
-		// For elements test consumables object.
-		return elementConsumables.test( consumables );
+		// Return true only if all can be consumed.
+		return true;
 	}
 
 	/**
-	 * Consumes {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
-	 * {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
-	 * It returns `true` when all items included in method's call can be consumed, otherwise returns `false`.
+	 * Consumes parts of {@link module:engine/view/element~Element view element}. This function does not check if consumable item
+	 * is already consumed - it consumes all consumable items provided.
+	 * Element's name can be consumed:
 	 *
-	 *		viewConsumable.consume( p, { name: true } ); // Consumes element's name.
-	 *		viewConsumable.consume( p, { attribute: 'name' } ); // Consumes element's attribute.
-	 *		viewConsumable.consume( p, { class: 'foobar' } ); // Consumes element's class.
-	 *		viewConsumable.consume( p, { style: 'color' } ); // Consumes element's style.
-	 *		viewConsumable.consume( p, { attribute: 'name', style: 'color' } ); // Consumes attribute and style.
-	 *		viewConsumable.consume( p, { class: [ 'baz', 'bar' ] } ); // Multiple consumables can be consumed.
-	 *		viewConsumable.consume( textNode ); // Consumes text node.
-	 *		viewConsumable.consume( docFragment ); // Consumes document fragment.
+	 *		consumables.consume( { name: true } );
 	 *
-	 * Consuming classes and styles as attribute will test if all added classes/styles can be consumed.
+	 * Attributes classes and styles:
 	 *
-	 *		viewConsumable.consume( p, { attribute: 'class' } ); // Consume only if all added classes can be consumed.
-	 *		viewConsumable.consume( p, { attribute: 'style' } ); // Consume only if all added styles can be consumed.
+	 *		consumables.consume( { attribute: 'title', class: 'foo', style: 'color' } );
+	 *		consumables.consume( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
 	 *
-	 * @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
-	 * @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
-	 * @param {Boolean} consumables.name If set to true element's name will be included.
-	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names.
-	 * @param {String|Array.<String>} consumables.class Class name or array of class names.
-	 * @param {String|Array.<String>} consumables.style Style name or array of style names.
-	 * @returns {Boolean} Returns `true` when all items included in method's call can be consumed,
-	 * otherwise returns `false`.
+	 * @param {Object} consumables Object describing which parts of the element should be consumed.
+	 * @param {Boolean} consumables.name If set to `true` element's name will be consumed.
+	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to consume.
+	 * @param {String|Array.<String>} consumables.class Class name or array of class names to consume.
+	 * @param {String|Array.<String>} consumables.style Style name or array of style names to consume.
 	 */
-	consume( element, consumables ) {
-		if ( this.test( element, consumables ) ) {
-			if ( element instanceof ViewText || element instanceof ViewDocumentFragment ) {
-				// For text nodes and document fragments set value to false.
-				this._consumables.set( element, false );
-			} else {
-				// For elements - consume consumables object.
-				this._consumables.get( element ).consume( consumables );
-			}
-
-			return true;
+	consume( consumables ) {
+		if ( consumables.name ) {
+			this._canConsumeName = false;
 		}
 
-		return false;
+		for ( let type in this._consumables ) {
+			if ( type in consumables ) {
+				this._consume( type, consumables[ type ] );
+			}
+		}
 	}
 
 	/**
-	 * Reverts {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
-	 * {@link module:engine/view/documentfragment~DocumentFragment document fragment} so they can be consumed once again.
-	 * Method does not revert items that were never previously added for consumption, even if they are included in
-	 * method's call.
+	 * Revert already consumed parts of {@link module:engine/view/element~Element view Element}, so they can be consumed once again.
+	 * Element's name can be reverted:
 	 *
-	 *		viewConsumable.revert( p, { name: true } ); // Reverts element's name.
-	 *		viewConsumable.revert( p, { attribute: 'name' } ); // Reverts element's attribute.
-	 *		viewConsumable.revert( p, { class: 'foobar' } ); // Reverts element's class.
-	 *		viewConsumable.revert( p, { style: 'color' } ); // Reverts element's style.
-	 *		viewConsumable.revert( p, { attribute: 'name', style: 'color' } ); // Reverts attribute and style.
-	 *		viewConsumable.revert( p, { class: [ 'baz', 'bar' ] } ); // Multiple names can be reverted.
-	 *		viewConsumable.revert( textNode ); // Reverts text node.
-	 *		viewConsumable.revert( docFragment ); // Reverts document fragment.
+	 *		consumables.revert( { name: true } );
 	 *
-	 * Reverting classes and styles as attribute will revert all classes/styles that were previously added for
-	 * consumption.
+	 * Attributes classes and styles:
 	 *
-	 *		viewConsumable.revert( p, { attribute: 'class' } ); // Reverts all classes added for consumption.
-	 *		viewConsumable.revert( p, { attribute: 'style' } ); // Reverts all styles added for consumption.
+	 *		consumables.revert( { attribute: 'title', class: 'foo', style: 'color' } );
+	 *		consumables.revert( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
 	 *
-	 * @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
-	 * @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
-	 * @param {Boolean} consumables.name If set to true element's name will be included.
-	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names.
-	 * @param {String|Array.<String>} consumables.class Class name or array of class names.
-	 * @param {String|Array.<String>} consumables.style Style name or array of style names.
+	 * @param {Object} consumables Object describing which parts of the element should be reverted.
+	 * @param {Boolean} consumables.name If set to `true` element's name will be reverted.
+	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to revert.
+	 * @param {String|Array.<String>} consumables.class Class name or array of class names to revert.
+	 * @param {String|Array.<String>} consumables.style Style name or array of style names to revert.
 	 */
-	revert( element, consumables ) {
-		const elementConsumables = this._consumables.get( element );
+	revert( consumables ) {
+		if ( consumables.name ) {
+			this._canConsumeName = true;
+		}
 
-		if ( elementConsumables !== undefined ) {
-			if ( element instanceof ViewText || element instanceof ViewDocumentFragment ) {
-				// For text nodes and document fragments - set consumable to true.
-				this._consumables.set( element, true );
-			} else {
-				// For elements - revert items from consumables object.
-				elementConsumables.revert( consumables );
+		for ( let type in this._consumables ) {
+			if ( type in consumables ) {
+				this._revert( type, consumables[ type ] );
 			}
 		}
 	}
 
 	/**
-	 * Creates consumable object from {@link module:engine/view/element~Element view element}. Consumable object will include
-	 * element's name and all its attributes, classes and styles.
+	 * Helper method that adds consumables of a given type: attribute, class or style.
 	 *
-	 * @static
-	 * @param {module:engine/view/element~Element} element
-	 * @returns {Object} consumables
+	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
+	 * type is provided - it should be handled separately by providing actual style/class type.
+	 *
+	 * @private
+	 * @param {String} type Type of the consumable item: `attribute`, `class` or `style`.
+	 * @param {String|Array.<String>} item Consumable item or array of items.
 	 */
-	static consumablesFromElement( element ) {
-		const consumables = {
-			name: true,
-			attribute: [],
-			class: [],
-			style: []
-		};
-
-		const attributes = element.getAttributeKeys();
+	_add( type, item ) {
+		const items = isArray( item ) ? item : [ item ];
+		const consumables = this._consumables[ type ];
 
-		for ( let attribute of attributes ) {
-			// Skip classes and styles - will be added separately.
-			if ( attribute == 'style' || attribute == 'class' ) {
-				continue;
+		for ( let name of items ) {
+			if ( type === 'attribute' && ( name === 'class' || name === 'style' ) ) {
+				/**
+				 * Class and style attributes should be handled separately.
+				 *
+				 * @error viewconsumable-invalid-attribute
+				 */
+				throw new CKEditorError( 'viewconsumable-invalid-attribute: Classes and styles should be handled separately.' );
 			}
 
-			consumables.attribute.push( attribute );
+			consumables.set( name, true );
 		}
+	}
 
-		const classes = element.getClassNames();
+	/**
+	 * Helper method that tests consumables of a given type: attribute, class or style.
+	 *
+	 * @private
+	 * @param {String} type Type of the consumable item: `attribute`, `class` or `style`.
+	 * @param {String|Array.<String>} item Consumable item or array of items.
+	 * @returns {Boolean|null} Returns `true` if all items can be consumed, `null` when one of the items cannot be
+	 * consumed and `false` when one of the items is already consumed.
+	 */
+	_test( type, item ) {
+		const items = isArray( item ) ? item : [ item ];
+		const consumables = this._consumables[ type ];
 
-		for ( let className of classes ) {
-			consumables.class.push( className );
-		}
+		for ( let name of items ) {
+			if ( type === 'attribute' && ( name === 'class' || name === 'style' ) )  {
+				// Check all classes/styles if class/style attribute is tested.
+				const value = this._test( name, [ ...this._consumables[ name ].keys() ] );
 
-		const styles = element.getStyleNames();
+				if ( value !== true ) {
+					return value;
+				}
+			} else {
+				const value = consumables.get( name );
+				// Return null if attribute is not found.
+				if ( value === undefined ) {
+					return null;
+				}
 
-		for ( let style of styles ) {
-			consumables.style.push( style );
+				if ( !value ) {
+					return false;
+				}
+			}
 		}
 
-		return consumables;
+		return true;
 	}
 
 	/**
-	 * Creates {@link module:engine/conversion/viewconsumable~ViewConsumable ViewConsumable} instance from
-	 * {@link module:engine/view/element~Element element} or {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
-	 * Instance will contain all elements, child nodes, attributes, styles and classes added for consumption.
+	 * Helper method that consumes items of a given type: attribute, class or style.
 	 *
-	 * @static
-	 * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} from View element or document fragment
-	 * from which `ViewConsumable` will be created.
-	 * @param {module:engine/conversion/viewconsumable~ViewConsumable} [instance] If provided, given `ViewConsumable` instance will be used
-	 * to add all consumables. It will be returned instead of a new instance.
+	 * @private
+	 * @param {String} type Type of the consumable item: `attribute`, `class` or `style`.
+	 * @param {String|Array.<String>} item Consumable item or array of items.
 	 */
-	static createFrom( from, instance ) {
-		if ( !instance ) {
-			instance = new ViewConsumable();
-		}
-
-		if ( from instanceof ViewText ) {
-			instance.add( from );
+	_consume( type, item ) {
+		const items = isArray( item ) ? item : [ item ];
+		const consumables = this._consumables[ type ];
 
-			return instance;
+		for ( let name of items ) {
+			if ( type === 'attribute' && ( name === 'class' || name === 'style' ) ) {
+				// If class or style is provided for consumption - consume them all.
+				this._consume( name, [ ...this._consumables[ name ].keys() ] );
+			} else {
+				consumables.set( name, false );
+			}
 		}
+	}
 
-		// Add `from` itself, if it is an element.
-		if ( from instanceof ViewElement ) {
-			instance.add( from, ViewConsumable.consumablesFromElement( from ) );
-		}
+	/**
+	 * Helper method that reverts items of a given type: attribute, class or style.
+	 *
+	 * @private
+	 * @param {String} type Type of the consumable item: `attribute`, `class` or , `style`.
+	 * @param {String|Array.<String>} item Consumable item or array of items.
+	 */
+	_revert( type, item ) {
+		const items = isArray( item ) ? item : [ item ];
+		const consumables = this._consumables[ type ];
 
-		if ( from instanceof ViewDocumentFragment ) {
-			instance.add( from );
-		}
+		for ( let name of items ) {
+			if ( type === 'attribute' && ( name === 'class' || name === 'style' ) ) {
+				// If class or style is provided for reverting - revert them all.
+				this._revert( name, [ ...this._consumables[ name ].keys() ] );
+			} else {
+				const value = consumables.get( name );
 
-		for ( let child of from.getChildren() ) {
-			instance = ViewConsumable.createFrom( child, instance );
+				if ( value === false ) {
+					consumables.set( name, true );
+				}
+			}
 		}
-
-		return instance;
 	}
 }

+ 212 - 212
packages/ckeditor5-engine/src/model/schema.js

@@ -14,218 +14,6 @@ import isArray from '../../utils/lib/lodash/isArray.js';
 import isString from '../../utils/lib/lodash/isString.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
 
-/**
- * SchemaItem is a singular registry item in {@link module:engine/model/schema~Schema} that groups and holds allow/disallow rules for
- * one entity. This class is used internally in {@link module:engine/model/schema~Schema} and should not be used outside it.
- *
- * @see module:engine/model/schema~Schema
- * @protected
- */
-export class SchemaItem {
-	/**
-	 * Creates SchemaItem instance.
-	 *
-	 * @param {module:engine/model/schema~Schema} schema Schema instance that owns this item.
-	 */
-	constructor( schema ) {
-		/**
-		 * Schema instance that owns this item.
-		 *
-		 * @private
-		 * @member {module:engine/model/schema~Schema} module:engine/model/schema~SchemaItem#_schema
-		 */
-		this._schema = schema;
-
-		/**
-		 * Paths in which the entity, represented by this item, is allowed.
-		 *
-		 * @private
-		 * @member {Array} module:engine/model/schema~SchemaItem#_allowed
-		 */
-		this._allowed = [];
-
-		/**
-		 * Paths in which the entity, represented by this item, is disallowed.
-		 *
-		 * @private
-		 * @member {Array} module:engine/model/schema~SchemaItem#_disallowed
-		 */
-		this._disallowed = [];
-
-		/**
-		 * Attributes that are required by the entity represented by this item.
-		 *
-		 * @protected
-		 * @member {Array} module:engine/model/schema~SchemaItem#_requiredAttributes
-		 */
-		this._requiredAttributes = [];
-	}
-
-	/**
-	 * 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 );
-	}
-
-	/**
-	 * 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 );
-	}
-
-	/**
-	 * Specifies that the entity, to be valid, requires given attributes set. It is possible to register multiple
-	 * different attributes set. If there are more than one attributes set required, the entity will be valid if
-	 * at least one of them is fulfilled.
-	 *
-	 * @param {Array.<String>} attributes Attributes that has to be set on the entity to make it valid.
-	 */
-	requireAttributes( attributes ) {
-		this._requiredAttributes.push( attributes );
-	}
-
-	/**
-	 * Adds path to the SchemaItem instance.
-	 *
-	 * @private
-	 * @param {String} member Name of the array member into which the path will be added. Possible values are `_allowed` or `_disallowed`.
-	 * @param {Array.<String>} path Path to add.
-	 * @param {Array.<String>|String} [attributes] If set, this path will be used only for entities that have attribute(s) with this key.
-	 */
-	_addPath( member, path, attributes ) {
-		path = path.slice();
-
-		if ( !isArray( attributes ) ) {
-			attributes = [ attributes ];
-		}
-
-		for ( let attribute of attributes ) {
-			this[ member ].push( { path, attribute } );
-		}
-	}
-
-	/**
-	 * Returns all paths of given type that were previously registered in the item.
-	 *
-	 * @private
-	 * @param {String} type Paths' type. Possible values are `allow` or `disallow`.
-	 * @param {String} [attribute] If set, only paths registered for given attribute will be returned.
-	 * @returns {Array} Paths registered in the item.
-	 */
-	_getPaths( type, attribute ) {
-		const source = type === 'allow' ? this._allowed : this._disallowed;
-		const paths = [];
-
-		for ( let item of source ) {
-			if ( item.attribute === attribute ) {
-				paths.push( item.path );
-			}
-		}
-
-		return paths;
-	}
-
-	/**
-	 * Checks whether given set of attributes fulfills required attributes of this item.
-	 *
-	 * @protected
-	 * @see module:engine/model/schema~SchemaItem#requireAttributes
-	 * @param {Array.<String>} attributesToCheck Attributes to check.
-	 * @returns {Boolean} `true` if given set or attributes fulfills required attributes, `false` otherwise.
-	 */
-	_checkRequiredAttributes( attributesToCheck ) {
-		let found = true;
-
-		for ( let attributeSet of this._requiredAttributes ) {
-			found = true;
-
-			for ( let attribute of attributeSet ) {
-				if ( attributesToCheck.indexOf( attribute ) == -1 ) {
-					found = false;
-					break;
-				}
-			}
-
-			if ( found ) {
-				break;
-			}
-		}
-
-		return found;
-	}
-
-	/**
-	 * Checks whether this item has any registered path of given type that matches provided path.
-	 *
-	 * @protected
-	 * @param {String} type Paths' type. Possible values are `allow` or `disallow`.
-	 * @param {Array.<String>} checkPath Path to check.
-	 * @param {String} [attribute] If set, only paths registered for given attribute will be checked.
-	 * @returns {Boolean} `true` if item has any registered matching path, `false` otherwise.
-	 */
-	_hasMatchingPath( type, checkPath, attribute ) {
-		const itemPaths = this._getPaths( type, attribute );
-
-		// We check every path registered (possibly with given attribute) in the item.
-		for ( let itemPath of itemPaths ) {
-			// Pointer to last found item from `itemPath`.
-			let i = 0;
-
-			// Now we have to check every item name from the path to check.
-			for ( let checkName of checkPath ) {
-				// Don't check items that are not registered in schema.
-				if ( !this._schema.hasItem( checkName ) ) {
-					continue;
-				}
-
-				// Every item name is expanded to all names of items that item is extending.
-				// So, if on item path, there is an item that is extended by item from checked path, it will
-				// also be treated as matching.
-				const chain = this._schema._extensionChains.get( checkName );
-
-				// Since our paths have to match in given order, we always check against first item from item path.
-				// So, if item path is: B D E
-				// And checked path is: A B C D E
-				// It will be matching (A won't match, B will match, C won't match, D and E will match)
-				if ( chain.indexOf( itemPath[ i ] ) > -1 ) {
-					// Move pointer as we found element under index `i`.
-					i++;
-				}
-			}
-
-			// If `itemPath` has no items it means that we removed all of them, so we matched all of them.
-			// This means that we found a matching path.
-			if ( i === itemPath.length ) {
-				return true;
-			}
-		}
-
-		return false;
-	}
-
-	/**
-	 * 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 );
-
-		// Due to circular references we need to remove parent reference.
-		json._schema = '[model.Schema]';
-
-		return json;
-	}
-}
-
 /**
  * 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,
@@ -564,6 +352,218 @@ export default class Schema {
 	}
 }
 
+/**
+ * SchemaItem is a singular registry item in {@link module:engine/model/schema~Schema} that groups and holds allow/disallow rules for
+ * one entity. This class is used internally in {@link module:engine/model/schema~Schema} and should not be used outside it.
+ *
+ * @see module:engine/model/schema~Schema
+ * @protected
+ */
+export class SchemaItem {
+	/**
+	 * Creates SchemaItem instance.
+	 *
+	 * @param {module:engine/model/schema~Schema} schema Schema instance that owns this item.
+	 */
+	constructor( schema ) {
+		/**
+		 * Schema instance that owns this item.
+		 *
+		 * @private
+		 * @member {module:engine/model/schema~Schema} module:engine/model/schema~SchemaItem#_schema
+		 */
+		this._schema = schema;
+
+		/**
+		 * Paths in which the entity, represented by this item, is allowed.
+		 *
+		 * @private
+		 * @member {Array} module:engine/model/schema~SchemaItem#_allowed
+		 */
+		this._allowed = [];
+
+		/**
+		 * Paths in which the entity, represented by this item, is disallowed.
+		 *
+		 * @private
+		 * @member {Array} module:engine/model/schema~SchemaItem#_disallowed
+		 */
+		this._disallowed = [];
+
+		/**
+		 * Attributes that are required by the entity represented by this item.
+		 *
+		 * @protected
+		 * @member {Array} module:engine/model/schema~SchemaItem#_requiredAttributes
+		 */
+		this._requiredAttributes = [];
+	}
+
+	/**
+	 * 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 );
+	}
+
+	/**
+	 * 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 );
+	}
+
+	/**
+	 * Specifies that the entity, to be valid, requires given attributes set. It is possible to register multiple
+	 * different attributes set. If there are more than one attributes set required, the entity will be valid if
+	 * at least one of them is fulfilled.
+	 *
+	 * @param {Array.<String>} attributes Attributes that has to be set on the entity to make it valid.
+	 */
+	requireAttributes( attributes ) {
+		this._requiredAttributes.push( attributes );
+	}
+
+	/**
+	 * Adds path to the SchemaItem instance.
+	 *
+	 * @private
+	 * @param {String} member Name of the array member into which the path will be added. Possible values are `_allowed` or `_disallowed`.
+	 * @param {Array.<String>} path Path to add.
+	 * @param {Array.<String>|String} [attributes] If set, this path will be used only for entities that have attribute(s) with this key.
+	 */
+	_addPath( member, path, attributes ) {
+		path = path.slice();
+
+		if ( !isArray( attributes ) ) {
+			attributes = [ attributes ];
+		}
+
+		for ( let attribute of attributes ) {
+			this[ member ].push( { path, attribute } );
+		}
+	}
+
+	/**
+	 * Returns all paths of given type that were previously registered in the item.
+	 *
+	 * @private
+	 * @param {String} type Paths' type. Possible values are `allow` or `disallow`.
+	 * @param {String} [attribute] If set, only paths registered for given attribute will be returned.
+	 * @returns {Array} Paths registered in the item.
+	 */
+	_getPaths( type, attribute ) {
+		const source = type === 'allow' ? this._allowed : this._disallowed;
+		const paths = [];
+
+		for ( let item of source ) {
+			if ( item.attribute === attribute ) {
+				paths.push( item.path );
+			}
+		}
+
+		return paths;
+	}
+
+	/**
+	 * Checks whether given set of attributes fulfills required attributes of this item.
+	 *
+	 * @protected
+	 * @see module:engine/model/schema~SchemaItem#requireAttributes
+	 * @param {Array.<String>} attributesToCheck Attributes to check.
+	 * @returns {Boolean} `true` if given set or attributes fulfills required attributes, `false` otherwise.
+	 */
+	_checkRequiredAttributes( attributesToCheck ) {
+		let found = true;
+
+		for ( let attributeSet of this._requiredAttributes ) {
+			found = true;
+
+			for ( let attribute of attributeSet ) {
+				if ( attributesToCheck.indexOf( attribute ) == -1 ) {
+					found = false;
+					break;
+				}
+			}
+
+			if ( found ) {
+				break;
+			}
+		}
+
+		return found;
+	}
+
+	/**
+	 * Checks whether this item has any registered path of given type that matches provided path.
+	 *
+	 * @protected
+	 * @param {String} type Paths' type. Possible values are `allow` or `disallow`.
+	 * @param {Array.<String>} checkPath Path to check.
+	 * @param {String} [attribute] If set, only paths registered for given attribute will be checked.
+	 * @returns {Boolean} `true` if item has any registered matching path, `false` otherwise.
+	 */
+	_hasMatchingPath( type, checkPath, attribute ) {
+		const itemPaths = this._getPaths( type, attribute );
+
+		// We check every path registered (possibly with given attribute) in the item.
+		for ( let itemPath of itemPaths ) {
+			// Pointer to last found item from `itemPath`.
+			let i = 0;
+
+			// Now we have to check every item name from the path to check.
+			for ( let checkName of checkPath ) {
+				// Don't check items that are not registered in schema.
+				if ( !this._schema.hasItem( checkName ) ) {
+					continue;
+				}
+
+				// Every item name is expanded to all names of items that item is extending.
+				// So, if on item path, there is an item that is extended by item from checked path, it will
+				// also be treated as matching.
+				const chain = this._schema._extensionChains.get( checkName );
+
+				// Since our paths have to match in given order, we always check against first item from item path.
+				// So, if item path is: B D E
+				// And checked path is: A B C D E
+				// It will be matching (A won't match, B will match, C won't match, D and E will match)
+				if ( chain.indexOf( itemPath[ i ] ) > -1 ) {
+					// Move pointer as we found element under index `i`.
+					i++;
+				}
+			}
+
+			// If `itemPath` has no items it means that we removed all of them, so we matched all of them.
+			// This means that we found a matching path.
+			if ( i === itemPath.length ) {
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	 * 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 );
+
+		// Due to circular references we need to remove parent reference.
+		json._schema = '[model.Schema]';
+
+		return json;
+	}
+}
+
 /**
  * Object with query used by {@link module:engine/model/schema~Schema} to query schema or add allow/disallow rules to schema.
  *