浏览代码

Add layer of abstraction above AttributeList. Make instance of AttributeList a protected property in multiple classes.

Szymon Cofalik 10 年之前
父节点
当前提交
a826580618
共有 25 个文件被更改,包括 652 次插入128 次删除
  1. 1 1
      packages/ckeditor5-engine/src/treemodel/characterproxy.js
  2. 2 2
      packages/ckeditor5-engine/src/treemodel/delta/attributedelta.js
  3. 1 1
      packages/ckeditor5-engine/src/treemodel/delta/splitdelta.js
  4. 1 1
      packages/ckeditor5-engine/src/treemodel/delta/weakinsertdelta.js
  5. 39 0
      packages/ckeditor5-engine/src/treemodel/element.js
  6. 43 3
      packages/ckeditor5-engine/src/treemodel/node.js
  7. 8 8
      packages/ckeditor5-engine/src/treemodel/nodelist.js
  8. 4 17
      packages/ckeditor5-engine/src/treemodel/operation/attributeoperation.js
  9. 82 3
      packages/ckeditor5-engine/src/treemodel/selection.js
  10. 2 1
      packages/ckeditor5-engine/src/treemodel/text.js
  11. 74 4
      packages/ckeditor5-engine/src/treemodel/textfragment.js
  12. 1 1
      packages/ckeditor5-engine/tests/treemodel/characterproxy.js
  13. 8 8
      packages/ckeditor5-engine/tests/treemodel/delta/attributedelta.js
  14. 2 2
      packages/ckeditor5-engine/tests/treemodel/delta/mergedelta.js
  15. 8 8
      packages/ckeditor5-engine/tests/treemodel/delta/splitdelta.js
  16. 4 4
      packages/ckeditor5-engine/tests/treemodel/delta/weakinsertdelta.js
  17. 65 3
      packages/ckeditor5-engine/tests/treemodel/element.js
  18. 65 5
      packages/ckeditor5-engine/tests/treemodel/node.js
  19. 11 10
      packages/ckeditor5-engine/tests/treemodel/nodelist.js
  20. 37 37
      packages/ckeditor5-engine/tests/treemodel/operation/attributeoperation.js
  21. 1 1
      packages/ckeditor5-engine/tests/treemodel/rootelement.js
  22. 111 2
      packages/ckeditor5-engine/tests/treemodel/selection.js
  23. 2 2
      packages/ckeditor5-engine/tests/treemodel/text.js
  24. 78 3
      packages/ckeditor5-engine/tests/treemodel/textfragment.js
  25. 2 1
      packages/ckeditor5-engine/tests/treemodel/treewalker.js

+ 1 - 1
packages/ckeditor5-engine/src/treemodel/characterproxy.js

@@ -35,7 +35,7 @@ export default class CharacterProxy extends Node {
 	 * @constructor
 	 */
 	constructor( nodeListText, index ) {
-		super( nodeListText.attrs );
+		super( nodeListText._attrs );
 
 		/**
 		 * Reference to a text object in a node list containing this character.

+ 2 - 2
packages/ckeditor5-engine/src/treemodel/delta/attributedelta.js

@@ -65,7 +65,7 @@ function attribute( batch, key, value, nodeOrRange ) {
 }
 
 function changeNode( doc, delta, key, value, node ) {
-	const previousValue = node.attrs.getValue( key );
+	const previousValue = node.getAttributeValue( key );
 	let range;
 
 	if ( previousValue != value ) {
@@ -114,7 +114,7 @@ function changeRange( doc, delta, key, value, range ) {
 		// We check values only when the range contains given element, that is when the iterator "enters" the element.
 		// To prevent double-checking or not needed checking, we filter-out iterator values for ELEMENT_END position.
 		if ( next.value.type != 'ELEMENT_END' ) {
-			valueAfter = next.value.item.attrs.getValue( key );
+			valueAfter = next.value.item.getAttributeValue( key );
 
 			// At the first run of the iterator the position in undefined. We also do not have a valueBefore, but
 			// because valueAfter may be null, valueBefore may be equal valueAfter ( undefined == null ).

+ 1 - 1
packages/ckeditor5-engine/src/treemodel/delta/splitdelta.js

@@ -45,7 +45,7 @@ register( 'split', function( position ) {
 		throw new CKEditorError( 'batch-split-root: Root element can not be split.' );
 	}
 
-	const copy = new Element( splitElement.name, splitElement.attrs );
+	const copy = new Element( splitElement.name, splitElement._attrs );
 	const insert = new InsertOperation( Position.createAfter( splitElement ), copy, this.doc.version );
 
 	this.doc.applyOperation( insert );

+ 1 - 1
packages/ckeditor5-engine/src/treemodel/delta/weakinsertdelta.js

@@ -42,7 +42,7 @@ register( 'weakInsert', function( position, nodes ) {
 	nodes = new NodeList( nodes );
 
 	for ( let node of nodes._nodes ) {
-		node.attrs.setTo( this.doc.selection.attrs );
+		node._attrs.setTo( this.doc.selection._attrs );
 	}
 
 	const operation = new InsertOperation( position, nodes, this.doc.version );

+ 39 - 0
packages/ckeditor5-engine/src/treemodel/element.js

@@ -117,4 +117,43 @@ export default class Element extends Node {
 
 		return nodeList;
 	}
+
+	/**
+	 * Sets attribute on the element. If attribute with the same key already is set, it overwrites its values.
+	 *
+	 * @chainable
+	 * @param {treeModel.Attribute} attr Attribute to set or overwrite with.
+	 * @returns {treeModel.Element} This element.
+	 */
+	setAttribute( attr ) {
+		this._attrs.set( attr );
+
+		return this;
+	}
+
+	/**
+	 * Removes all attributes from the element and sets given attributes.
+	 *
+	 * @param {Iterable.<treeModel.Attribute>} attrs Iterable object containing {@link treeModel.Attribute attributes} to be set.
+	 */
+	setAttributesTo( attrs ) {
+		this._attrs.setTo( attrs );
+	}
+
+	/**
+	 * Removes an attribute with given key from the element.
+	 *
+	 * @param {String} key Key of attribute to remove.
+	 * @returns {Boolean} `true` if the attribute was set on the element, `false` otherwise.
+	 */
+	removeAttribute( key ) {
+		return this._attrs.delete( key );
+	}
+
+	/**
+	 * Removes all attributes from the element.
+	 */
+	clearAttributes() {
+		this._attrs.clear();
+	}
 }

+ 43 - 3
packages/ckeditor5-engine/src/treemodel/node.js

@@ -37,12 +37,12 @@ export default class Node {
 		 * List of attributes set on this node.
 		 * **Note:** It is **important** that attributes of nodes already attached to the document must be changed
 		 * only by an {@link treeModel.operation.AttributeOperation}. Do not set attributes of such nodes
-		 * using {@link treeModel.AttributeList} API.
+		 * using {@link treeModel.Node} methods.
 		 *
-		 * @readonly
+		 * @protected
 		 * @property {treeModel.AttributeList} attrs
 		 */
-		this.attrs = new AttributeList( attrs );
+		this._attrs = new AttributeList( attrs );
 	}
 
 	/**
@@ -161,4 +161,44 @@ export default class Node {
 
 		return json;
 	}
+
+	/**
+	 * Checks if the node has an attribute that is {@link treeModel.Attribute#isEqual equal} to given attribute or
+	 * attribute with given key if string was passed.
+	 *
+	 * @param {treeModel.Attribute|String} attrOrKey Attribute or key of attribute to check.
+	 * @returns {Boolean} `true` if given attribute or attribute with given key is set on node, `false` otherwise.
+	 */
+	hasAttribute( attrOrKey ) {
+		return this._attrs.has( attrOrKey );
+	}
+
+	/**
+	 * Gets a node's attribute by its key.
+	 *
+	 * @param {String} key Key of attribute to look for.
+	 * @returns {treeModel.Attribute|null} Attribute with given key or null if the attribute has not been set on node.
+	 */
+	getAttribute( key ) {
+		return this._attrs.get( key );
+	}
+
+	/**
+	 * Gets a node's attribute value by attribute key.
+	 *
+	 * @param {String} key Key of attribute to look for.
+	 * @returns {*} Value of attribute with given key or null if the attribute has not been set on node.
+	 */
+	getAttributeValue( key ) {
+		return this._attrs.getValue( key );
+	}
+
+	/**
+	 * Returns iterator that iterates over this nodes attributes.
+	 *
+	 * @returns {Iterable.<treeModel.Attribute>}
+	 */
+	getAttributes() {
+		return this._attrs[ Symbol.iterator ]();
+	}
 }

+ 8 - 8
packages/ckeditor5-engine/src/treemodel/nodelist.js

@@ -96,9 +96,9 @@ export default class NodeList {
 	 *
 	 *		let nodeList = new NodeList( new Text( 'foo', [ new Attribute( 'bar', 'bom' ) ] ) );
 	 *		nodeList.length; // 3
-	 *		nodeList.get( 0 ).attrs.get( 'bar' ); // 'bom'
-	 *		nodeList.get( 1 ).attrs.get( 'bar' ); // 'bom'
-	 *		nodeList.get( 2 ).attrs.get( 'bar' ); // 'bom'
+	 *		nodeList.get( 0 ).getAttribute( 'bar' ); // 'bom'
+	 *		nodeList.get( 1 ).getAttribute( 'bar' ); // 'bom'
+	 *		nodeList.get( 2 ).getAttribute( 'bar' ); // 'bom'
 	 *
 	 *		let nodeListA = new NodeList( 'foo' );
 	 *		let nodeListB = new NodeList( nodeListA );
@@ -145,9 +145,9 @@ export default class NodeList {
 				let length = 1;
 
 				if ( node instanceof CharacterProxy ) {
-					node = new NodeListText( node.character, node.attrs );
+					node = new NodeListText( node.character, node._attrs );
 				} else if ( node instanceof Text ) {
-					node = new NodeListText( node.text, node.attrs );
+					node = new NodeListText( node.text, node._attrs );
 				} else if ( typeof node == 'string' ) {
 					node = new NodeListText( node, [] );
 				}
@@ -157,7 +157,7 @@ export default class NodeList {
 
 					let prev = this._nodes[ this._nodes.length - 1 ];
 
-					if ( prev instanceof NodeListText && prev.attrs.isEqual( node.attrs ) ) {
+					if ( prev instanceof NodeListText && prev._attrs.isEqual( node._attrs ) ) {
 						// If previously added text has same attributes, merge this text with it.
 						prev.text += node.text;
 						mergedWithPrev = true;
@@ -339,7 +339,7 @@ export default class NodeList {
 		node.text = textBefore;
 
 		// Create a new text node with the "removed" part and splice it after original node.
-		let newText = new NodeListText( textAfter, node.attrs );
+		let newText = new NodeListText( textAfter, node._attrs );
 		newText.parent = node.parent;
 		this._nodes.splice.call( this._nodes, realIndex + 1, 0, newText );
 
@@ -374,7 +374,7 @@ export default class NodeList {
 		let nodeAfter = this._nodes[ realIndexAfter ];
 
 		// Check if both of those nodes are text objects with same attributes.
-		if ( nodeBefore instanceof Text && nodeAfter instanceof Text && nodeBefore.attrs.isEqual( nodeAfter.attrs ) ) {
+		if ( nodeBefore instanceof NodeListText && nodeAfter instanceof NodeListText && nodeBefore._attrs.isEqual( nodeAfter._attrs ) ) {
 			// Append text of text node after index to the before one.
 			nodeBefore.text += nodeAfter.text;
 

+ 4 - 17
packages/ckeditor5-engine/src/treemodel/operation/attributeoperation.js

@@ -7,7 +7,6 @@
 
 import Operation from './operation.js';
 import Range from '../range.js';
-import TextFragment from '../textfragment.js';
 import CKEditorError from '../../ckeditorerror.js';
 
 /**
@@ -99,13 +98,7 @@ export default class AttributeOperation extends Operation {
 		// Remove or change.
 		if ( oldAttr !== null ) {
 			for ( let node of this.range.getAllNodes( true ) ) {
-				if ( node instanceof TextFragment ) {
-					// Because instance of TextFragment is kind-of a proxy, not a real, original item,
-					// we have to assign `node` a real item that is added to the node list.
-					node = node.first._nodeListText;
-				}
-
-				if ( !node.attrs.has( oldAttr ) ) {
+				if ( !node.hasAttribute( oldAttr ) ) {
 					/**
 					 * The attribute which should be removed does not exists for the given node.
 					 *
@@ -120,7 +113,7 @@ export default class AttributeOperation extends Operation {
 				}
 
 				if ( newAttr === null ) {
-					node.attrs.delete( oldAttr.key );
+					node.removeAttribute( oldAttr.key );
 				}
 			}
 		}
@@ -128,13 +121,7 @@ export default class AttributeOperation extends Operation {
 		// Insert or change.
 		if ( newAttr !== null ) {
 			for ( let node of this.range.getAllNodes( true ) ) {
-				if ( node instanceof TextFragment ) {
-					// Because instance of TextFragment is kind-of a proxy, not a real, original item,
-					// we have to assign `node` a real item that is added to the node list.
-					node = node.first._nodeListText;
-				}
-
-				if ( oldAttr === null && node.attrs.has( newAttr.key ) ) {
+				if ( oldAttr === null && node.hasAttribute( newAttr.key ) ) {
 					/**
 					 * The attribute with given key already exists for the given node.
 					 *
@@ -148,7 +135,7 @@ export default class AttributeOperation extends Operation {
 					);
 				}
 
-				node.attrs.set( newAttr );
+				node.setAttribute( newAttr );
 			}
 		}
 

+ 82 - 3
packages/ckeditor5-engine/src/treemodel/selection.js

@@ -27,10 +27,10 @@ export default class Selection {
 		/**
 		 * List of attributes set on current selection.
 		 *
-		 * @readonly
-		 * @property {treeModel.AttributeList} attrs
+		 * @protected
+		 * @property {treeModel.AttributeList}
 		 */
-		this.attrs = new AttributeList();
+		this._attrs = new AttributeList();
 
 		/**
 		 * Stores all ranges that are selected.
@@ -167,6 +167,85 @@ export default class Selection {
 		this._lastRangeBackward = !!isLastBackward;
 		this.fire( 'update' );
 	}
+
+	/**
+	 * Checks if the selection has an attribute that is {@link treeModel.Attribute#isEqual equal} to given attribute or
+	 * attribute with given key if string was passed.
+	 *
+	 * @param {treeModel.Attribute|String} attrOrKey Attribute or key of attribute to check.
+	 * @returns {Boolean} `true` if given attribute or attribute with given key is set on selection, `false` otherwise.
+	 */
+	hasAttribute( attrOrKey ) {
+		return this._attrs.has( attrOrKey );
+	}
+
+	/**
+	 * Gets a selection's attribute by its key.
+	 *
+	 * @param {String} key Key of attribute to look for.
+	 * @returns {treeModel.Attribute|null} Attribute with given key or null if the attribute has not been set on selection.
+	 */
+	getAttribute( key ) {
+		return this._attrs.get( key );
+	}
+
+	/**
+	 * Gets a selection's attribute value by attribute key.
+	 *
+	 * @param {String} key Key of attribute to look for.
+	 * @returns {*} Value of attribute with given key or null if the attribute has not been set on selection.
+	 */
+	getAttributeValue( key ) {
+		return this._attrs.getValue( key );
+	}
+
+	/**
+	 * Returns iterator that iterates over this nodes attributes.
+	 *
+	 * @returns {Iterable.<treeModel.Attribute>}
+	 */
+	getAttributes() {
+		return this._attrs[ Symbol.iterator ]();
+	}
+
+	/**
+	 * Sets attribute on the selection. If attribute with the same key already is set, it overwrites its values.
+	 *
+	 * @chainable
+	 * @param {treeModel.Attribute} attr Attribute to set or overwrite with.
+	 * @returns {treeModel.Selection} This selection.
+	 */
+	setAttribute( attr ) {
+		this._attrs.set( attr );
+
+		return this;
+	}
+
+	/**
+	 * Removes all attributes from the selection and sets given attributes.
+	 *
+	 * @param {Iterable.<treeModel.Attribute>} attrs Iterable object containing {@link treeModel.Attribute attributes} to be set.
+	 */
+	setAttributesTo( attrs ) {
+		this._attrs.setTo( attrs );
+	}
+
+	/**
+	 * Removes an attribute with given key from the selection.
+	 *
+	 * @param {String} key Key of attribute to remove.
+	 * @returns {Boolean} `true` if the attribute was set on the element, `false` otherwise.
+	 */
+	removeAttribute( key ) {
+		return this._attrs.delete( key );
+	}
+
+	/**
+	 * Removes all attributes from the element.
+	 */
+	clearAttributes() {
+		this._attrs.clear();
+	}
 }
 
 /**

+ 2 - 1
packages/ckeditor5-engine/src/treemodel/text.js

@@ -37,8 +37,9 @@ export default class Text {
 		/**
 		 * {@link treeModel.Attribute AttributesList} bound with the text.
 		 *
+		 * @protected
 		 * @property {treeModel.AttributeList}
 		 */
-		this.attrs = new AttributeList( attrs );
+		this._attrs = new AttributeList( attrs );
 	}
 }

+ 74 - 4
packages/ckeditor5-engine/src/treemodel/textfragment.js

@@ -72,14 +72,84 @@ export default class TextFragment {
 	 */
 	getCharAt( index ) {
 		return this.first.parent.getChild( this.first._index + index );
+	/**
+	 * Checks if the text fragment has an attribute that is {@link treeModel.Attribute#isEqual equal} to given attribute or
+	 * attribute with given key if string was passed.
+	 *
+	 * @param {treeModel.Attribute|String} attrOrKey Attribute or key of attribute to check.
+	 * @returns {Boolean} `true` if given attribute or attribute with given key is set on text fragment, `false` otherwise.
+	 */
+	hasAttribute( attrOrKey ) {
+		return this.first.hasAttribute( attrOrKey );
+	}
+
+	/**
+	 * Gets a text fragment attribute by its key.
+	 *
+	 * @param {String} key Key of attribute to look for.
+	 * @returns {treeModel.Attribute|null} Attribute with given key or null if the attribute has not been set on the text fragment.
+	 */
+	getAttribute( key ) {
+		return this.first.getAttribute( key );
+	}
+
+	/**
+	 * Gets a text fragment attribute value by attribute key.
+	 *
+	 * @param {String} key Key of attribute to look for.
+	 * @returns {*} Value of attribute with given key or null if the attribute has not been set on the text fragment.
+	 */
+	getAttributeValue( key ) {
+		return this.first.getAttributeValue( key );
+	}
+
+	/**
+	 * Returns iterator that iterates over this text fragment's attributes.
+	 *
+	 * @returns {Iterable.<treeModel.Attribute>}
+	 */
+	getAttributes() {
+		return this.first.getAttributes();
+	}
+
+	/**
+	 * Sets attribute on the text fragment. If attribute with the same key already is set, it overwrites its values.
+	 *
+	 * To change attributes of nodes (also characters) that are attached to the tree model, you
+	 * should use {@link treeModel.AttributeDelta}. This method is used by tree model internal mechanisms.
+	 *
+	 * @protected
+	 * @param {treeModel.Attribute} attr Attribute to set or overwrite with.
+	 */
+	setAttribute( attr ) {
+		// Do note that this changes attributes on whole NodeListText, not only on character nodes specified by
+		// this TextFragment. Split NodeList at proper index before using this.
+		this.first._nodeListText._attrs.set( attr );
+
+		// Refreshing first and last character proxies because they would have wrong attributes.
+		this.first = this.getCharAt( 0 );
+		this.last = this.getCharAt( this.text.length - 1 );
 	}
 
 	/**
-	 * Creates and returns a range containing all characters from this {@link treeModel.TextFragment}.
+	 * Removes an attribute with given key from the text fragment.
+	 *
+	 * To change attributes of nodes (also characters) that are attached to the tree model, you
+	 * should use {@link treeModel.AttributeDelta}. This method is used by tree model internal mechanisms.
 	 *
-	 * @returns {Range}
+	 * @protected
+	 * @param {String} key Key of attribute to remove.
+	 * @returns {Boolean} `true` if the attribute was set on the element, `false` otherwise.
 	 */
-	getRange() {
-		return new Range( Position.createBefore( this.first ), Position.createAfter( this.last ) );
+	removeAttribute( key ) {
+		// Do note that this changes attributes on whole NodeListText, not only on character nodes specified by
+		// this TextFragment. Split NodeList at proper index before using this.
+		let result = this.first._nodeListText._attrs.delete( key );
+
+		// Refreshing first and last character proxies because they would have wrong attributes.
+		this.first = this.getCharAt( 0 );
+		this.last = this.getCharAt( this.text.length - 1 );
+
+		return result;
 	}
 }

+ 1 - 1
packages/ckeditor5-engine/tests/treemodel/characterproxy.js

@@ -37,7 +37,7 @@ describe( 'CharacterProxy', () => {
 	} );
 
 	it( 'should have attributes list equal to passed to Text instance', () => {
-		expect( char.attrs.isEqual( text.attrs ) ).to.be.true;
+		expect( char._attrs.isEqual( text._attrs ) ).to.be.true;
 	} );
 
 	it( 'should return correct index in parent node', () => {

+ 8 - 8
packages/ckeditor5-engine/tests/treemodel/delta/attributedelta.js

@@ -52,31 +52,31 @@ describe( 'Batch', () => {
 			it( 'should create the attribute on element', () => {
 				batch.setAttr( 'b', 2, node );
 				expect( getOperationsCount() ).to.equal( 1 );
-				expect( node.attrs.getValue( 'b' ) ).to.equal( 2 );
+				expect( node.getAttributeValue( 'b' ) ).to.equal( 2 );
 			} );
 
 			it( 'should change the attribute of element', () => {
 				batch.setAttr( 'a', 2, node );
 				expect( getOperationsCount() ).to.equal( 1 );
-				expect( node.attrs.getValue( 'a' ) ).to.equal( 2 );
+				expect( node.getAttributeValue( 'a' ) ).to.equal( 2 );
 			} );
 
 			it( 'should create the attribute on text node', () => {
 				batch.setAttr( 'b', 2, char );
 				expect( getOperationsCount() ).to.equal( 1 );
-				expect( root.getChild( 1 ).attrs.getValue( 'b' ) ).to.equal( 2 );
+				expect( root.getChild( 1 ).getAttributeValue( 'b' ) ).to.equal( 2 );
 			} );
 
 			it( 'should change the attribute of text node', () => {
 				batch.setAttr( 'a', 2, char );
 				expect( getOperationsCount() ).to.equal( 1 );
-				expect( root.getChild( 1 ).attrs.getValue( 'a' ) ).to.equal( 2 );
+				expect( root.getChild( 1 ).getAttributeValue( 'a' ) ).to.equal( 2 );
 			} );
 
 			it( 'should do nothing if the attribute value is the same', () => {
 				batch.setAttr( 'a', 1, node );
 				expect( getOperationsCount() ).to.equal( 0 );
-				expect( node.attrs.getValue( 'a' ) ).to.equal( 1 );
+				expect( node.getAttributeValue( 'a' ) ).to.equal( 1 );
 			} );
 
 			it( 'should be chainable', () => {
@@ -89,13 +89,13 @@ describe( 'Batch', () => {
 			it( 'should remove the attribute from element', () => {
 				batch.removeAttr( 'a', node );
 				expect( getOperationsCount() ).to.equal( 1 );
-				expect( node.attrs.getValue( 'a' ) ).to.be.null;
+				expect( node.getAttributeValue( 'a' ) ).to.be.null;
 			} );
 
 			it( 'should remove the attribute from character', () => {
 				batch.removeAttr( 'a', char );
 				expect( getOperationsCount() ).to.equal( 1 );
-				expect( root.getChild( 1 ).attrs.getValue( 'a' ) ).to.be.null;
+				expect( root.getChild( 1 ).getAttributeValue( 'a' ) ).to.be.null;
 			} );
 
 			it( 'should do nothing if the attribute is not set', () => {
@@ -147,7 +147,7 @@ describe( 'Batch', () => {
 			// default: 111---111222---1112------
 			const range = Range.createFromElement( root );
 
-			return Array.from( range.getAllNodes() ).map( node => node.attrs.getValue( 'a' ) || '-' ).join( '' );
+			return Array.from( range.getAllNodes() ).map( node => node.getAttributeValue( 'a' ) || '-' ).join( '' );
 		}
 
 		describe( 'setAttr', () => {

+ 2 - 2
packages/ckeditor5-engine/tests/treemodel/delta/mergedelta.js

@@ -33,8 +33,8 @@ describe( 'Batch', () => {
 			expect( root.getChildCount() ).to.equal( 1 );
 			expect( root.getChild( 0 ).name ).to.equal( 'p' );
 			expect( root.getChild( 0 ).getChildCount() ).to.equal( 6 );
-			expect( root.getChild( 0 ).attrs.size ).to.equal( 1 );
-			expect( root.getChild( 0 ).attrs.getValue( 'key1' ) ).to.equal( 'value1' );
+			expect( root.getChild( 0 )._attrs.size ).to.equal( 1 );
+			expect( root.getChild( 0 ).getAttributeValue( 'key1' ) ).to.equal( 'value1' );
 			expect( root.getChild( 0 ).getChild( 0 ).character ).to.equal( 'f' );
 			expect( root.getChild( 0 ).getChild( 1 ).character ).to.equal( 'o' );
 			expect( root.getChild( 0 ).getChild( 2 ).character ).to.equal( 'o' );

+ 8 - 8
packages/ckeditor5-engine/tests/treemodel/delta/splitdelta.js

@@ -33,16 +33,16 @@ describe( 'Batch', () => {
 
 			expect( root.getChild( 0 ).name ).to.equal( 'p' );
 			expect( root.getChild( 0 ).getChildCount() ).to.equal( 3 );
-			expect( root.getChild( 0 ).attrs.size ).to.equal( 1 );
-			expect( root.getChild( 0 ).attrs.getValue( 'key' ) ).to.equal( 'value' );
+			expect( root.getChild( 0 )._attrs.size ).to.equal( 1 );
+			expect( root.getChild( 0 ).getAttributeValue( 'key' ) ).to.equal( 'value' );
 			expect( root.getChild( 0 ).getChild( 0 ).character ).to.equal( 'f' );
 			expect( root.getChild( 0 ).getChild( 1 ).character ).to.equal( 'o' );
 			expect( root.getChild( 0 ).getChild( 2 ).character ).to.equal( 'o' );
 
 			expect( root.getChild( 1 ).name ).to.equal( 'p' );
 			expect( root.getChild( 1 ).getChildCount() ).to.equal( 3 );
-			expect( root.getChild( 1 ).attrs.size ).to.equal( 1 );
-			expect( root.getChild( 1 ).attrs.getValue( 'key' ) ).to.equal( 'value' );
+			expect( root.getChild( 1 )._attrs.size ).to.equal( 1 );
+			expect( root.getChild( 1 ).getAttributeValue( 'key' ) ).to.equal( 'value' );
 			expect( root.getChild( 1 ).getChild( 0 ).character ).to.equal( 'b' );
 			expect( root.getChild( 1 ).getChild( 1 ).character ).to.equal( 'a' );
 			expect( root.getChild( 1 ).getChild( 2 ).character ).to.equal( 'r' );
@@ -55,8 +55,8 @@ describe( 'Batch', () => {
 
 			expect( root.getChild( 0 ).name ).to.equal( 'p' );
 			expect( root.getChild( 0 ).getChildCount() ).to.equal( 6 );
-			expect( root.getChild( 0 ).attrs.size ).to.equal( 1 );
-			expect( root.getChild( 0 ).attrs.getValue( 'key' ) ).to.equal( 'value' );
+			expect( root.getChild( 0 )._attrs.size ).to.equal( 1 );
+			expect( root.getChild( 0 ).getAttributeValue( 'key' ) ).to.equal( 'value' );
 			expect( root.getChild( 0 ).getChild( 0 ).character ).to.equal( 'f' );
 			expect( root.getChild( 0 ).getChild( 1 ).character ).to.equal( 'o' );
 			expect( root.getChild( 0 ).getChild( 2 ).character ).to.equal( 'o' );
@@ -66,8 +66,8 @@ describe( 'Batch', () => {
 
 			expect( root.getChild( 1 ).name ).to.equal( 'p' );
 			expect( root.getChild( 1 ).getChildCount() ).to.equal( 0 );
-			expect( root.getChild( 1 ).attrs.size ).to.equal( 1 );
-			expect( root.getChild( 1 ).attrs.getValue( 'key' ) ).to.equal( 'value' );
+			expect( root.getChild( 1 )._attrs.size ).to.equal( 1 );
+			expect( root.getChild( 1 ).getAttributeValue( 'key' ) ).to.equal( 'value' );
 		} );
 
 		it( 'should throw if we try to split a root', () => {

+ 4 - 4
packages/ckeditor5-engine/tests/treemodel/delta/weakinsertdelta.js

@@ -27,7 +27,7 @@ describe( 'Batch', () => {
 			new Attribute( 'foo', 'bar' )
 		];
 
-		doc.selection.attrs.setTo( attrs );
+		doc.selection.setAttributesTo( attrs );
 
 		chain = batch.weakInsert( new Position( root, [ 2 ] ), 'xyz' );
 	} );
@@ -41,9 +41,9 @@ describe( 'Batch', () => {
 		} );
 
 		it( 'should set inserted nodes attributes to same as current selection attributes', () => {
-			expect( Array.from( root.getChild( 2 ).attrs ) ).to.deep.equal( attrs );
-			expect( Array.from( root.getChild( 3 ).attrs ) ).to.deep.equal( attrs );
-			expect( Array.from( root.getChild( 4 ).attrs ) ).to.deep.equal( attrs );
+			expect( Array.from( root.getChild( 2 )._attrs ) ).to.deep.equal( attrs );
+			expect( Array.from( root.getChild( 3 )._attrs ) ).to.deep.equal( attrs );
+			expect( Array.from( root.getChild( 4 )._attrs ) ).to.deep.equal( attrs );
 		} );
 
 		it( 'should be chainable', () => {

+ 65 - 3
packages/ckeditor5-engine/tests/treemodel/element.js

@@ -21,7 +21,7 @@ describe( 'Element', () => {
 			expect( element ).to.be.an.instanceof( Node );
 			expect( element ).to.have.property( 'name' ).that.equals( 'elem' );
 			expect( element ).to.have.property( 'parent' ).that.equals( parent );
-			expect( element.attrs.size ).to.equal( 0 );
+			expect( element._attrs.size ).to.equal( 0 );
 		} );
 
 		it( 'should create element with attributes', () => {
@@ -33,8 +33,8 @@ describe( 'Element', () => {
 			expect( element ).to.be.an.instanceof( Node );
 			expect( element ).to.have.property( 'name' ).that.equals( 'elem' );
 			expect( element ).to.have.property( 'parent' ).that.equals( parent );
-			expect( element.attrs.size ).to.equal( 1 );
-			expect( element.attrs.getValue( attr.key ) ).to.equal( attr.value );
+			expect( element._attrs.size ).to.equal( 1 );
+			expect( element.getAttributeValue( attr.key ) ).to.equal( attr.value );
 		} );
 
 		it( 'should create element with children', () => {
@@ -106,4 +106,66 @@ describe( 'Element', () => {
 			expect( element.getChildCount() ).to.equal( 3 );
 		} );
 	} );
+
+	describe( 'attributes interface', () => {
+		let node;
+		let attr = new Attribute( 'foo', 'bar' );
+
+		beforeEach( () => {
+			node = new Element();
+		} );
+
+		describe( 'setAttribute', () => {
+			it( 'should set given attribute on the element', () => {
+				node.setAttribute( attr );
+
+				expect( node.getAttributeValue( 'foo' ) ).to.equal( 'bar' );
+			} );
+
+			it( 'should be chainable', () => {
+				let chain = node.setAttribute( attr );
+
+				expect( chain ).to.equal( node );
+			} );
+		} );
+
+		describe( 'setAttributesTo', () => {
+			it( 'should remove all attributes set on element and set the given ones', () => {
+				let attr2 = new Attribute( 'abc', 'xyz' );
+				node.setAttribute( attr2 );
+				node.setAttributesTo( [ attr ] );
+
+				expect( node.getAttributeValue( 'foo' ) ).to.equal( 'bar' );
+				expect( node.getAttributeValue( 'abc' ) ).to.be.null;
+			} );
+		} );
+
+		describe( 'removeAttribute', () => {
+			it( 'should remove attribute set on the element and return true', () => {
+				node.setAttribute( attr );
+				let result = node.removeAttribute( 'foo' );
+
+				expect( node.getAttributeValue( 'foo' ) ).to.be.null;
+				expect( result ).to.be.true;
+			} );
+
+			it( 'should return false if element does not contain given attribute', () => {
+				let result = node.removeAttribute( 'foo' );
+
+				expect( result ).to.be.false;
+			} );
+		} );
+
+		describe( 'clearAttributes', () => {
+			it( 'should remove all attributes from the element', () => {
+				node.setAttribute( attr );
+				node.setAttribute( new Attribute( 'abc', 'xyz' ) );
+
+				node.clearAttributes();
+
+				expect( node.getAttributeValue( 'foo' ) ).to.be.null;
+				expect( node.getAttributeValue( 'abc' ) ).to.be.null;
+			} );
+		} );
+	} );
 } );

+ 65 - 5
packages/ckeditor5-engine/tests/treemodel/node.js

@@ -85,8 +85,8 @@ describe( 'Node', () => {
 		it( 'should create empty attribute list if no parameters were passed', () => {
 			let foo = new Element( 'foo' );
 
-			expect( foo.attrs ).to.be.instanceof( AttributeList );
-			expect( foo.attrs.size ).to.equal( 0 );
+			expect( foo._attrs ).to.be.instanceof( AttributeList );
+			expect( foo._attrs.size ).to.equal( 0 );
 		} );
 
 		it( 'should initialize attribute list with passed attributes', () => {
@@ -96,9 +96,9 @@ describe( 'Node', () => {
 			];
 			let foo = new Element( 'foo', attrs );
 
-			expect( foo.attrs.size ).to.equal( 2 );
-			expect( foo.attrs.getValue( 'foo' ) ).to.equal( true );
-			expect( foo.attrs.getValue( 'bar' ) ).to.equal( false );
+			expect( foo._attrs.size ).to.equal( 2 );
+			expect( foo.getAttributeValue( 'foo' ) ).to.equal( true );
+			expect( foo.getAttributeValue( 'bar' ) ).to.equal( false );
 		} );
 	} );
 
@@ -153,4 +153,64 @@ describe( 'Node', () => {
 			expect( charR.getPath() ).to.deep.equal( [ 1, 3 ] );
 		} );
 	} );
+
+	describe( 'attributes interface', () => {
+		let attr = new Attribute( 'foo', 'bar' );
+		let attr2 = new Attribute( 'foo', 'foo' );
+		let node = new Element( 'p', [ attr ] );
+
+		describe( 'hasAttribute', () => {
+			it( 'should return true if element contains given attribute', () => {
+				expect( node.hasAttribute( attr ) ).to.be.true;
+			} );
+
+			it( 'should return false if element does not contain given attribute', () => {
+				expect( node.hasAttribute( attr2 ) ).to.be.false;
+			} );
+
+			it( 'should return true if element contains attribute with given key', () => {
+				expect( node.hasAttribute( 'foo' ) ).to.be.true;
+			} );
+
+			it( 'should return false if element does not contain attribute with given key', () => {
+				expect( node.hasAttribute( 'bar' ) ).to.be.false;
+			} );
+		} );
+
+		describe( 'getAttribute', () => {
+			it( 'should return attribute with given key if element contains given attribute', () => {
+				expect( node.getAttribute( 'foo' ) ).to.equal( attr );
+			} );
+
+			it( 'should return undefined if element does not contain given attribute', () => {
+				expect( node.getAttribute( 'bar' ) ).to.be.undefined;
+			} );
+		} );
+
+		describe( 'getAttributeValue', () => {
+			it( 'should return attribute value for given key if element contains given attribute', () => {
+				expect( node.getAttributeValue( 'foo' ) ).to.equal( 'bar' );
+			} );
+
+			it( 'should return null if element does not contain given attribute', () => {
+				expect( node.getAttributeValue( 'bar' ) ).to.be.null;
+			} );
+		} );
+
+		describe( 'getAttributes', () => {
+			it( 'should return an iterator that iterates over all attributes set on the element', () => {
+				let it = node.getAttributes();
+				let attrs = [];
+
+				let step = it.next();
+
+				while ( !step.done ) {
+					attrs.push( step.value );
+					step = it.next();
+				}
+
+				expect( attrs ).to.deep.equal( [ attr ] );
+			} );
+		} );
+	} );
 } );

+ 11 - 10
packages/ckeditor5-engine/tests/treemodel/nodelist.js

@@ -45,11 +45,11 @@ describe( 'NodeList', () => {
 
 			expect( nodeList.length ).to.equal( 3 );
 			expect( nodeList.get( 0 ).character ).to.equal( 'f' );
-			expect( nodeList.get( 0 ).attrs.getValue( attr.key ) ).to.equal( attr.value );
+			expect( nodeList.get( 0 ).getAttributeValue( attr.key ) ).to.equal( attr.value );
 			expect( nodeList.get( 1 ).character ).to.equal( 'o' );
-			expect( nodeList.get( 1 ).attrs.getValue( attr.key ) ).to.equal( attr.value );
+			expect( nodeList.get( 1 ).getAttributeValue( attr.key ) ).to.equal( attr.value );
 			expect( nodeList.get( 2 ).character ).to.equal( 'o' );
-			expect( nodeList.get( 2 ).attrs.getValue( attr.key ) ).to.equal( attr.value );
+			expect( nodeList.get( 2 ).getAttributeValue( attr.key ) ).to.equal( attr.value );
 		} );
 
 		it( 'should change array of characters into a set of nodes', () => {
@@ -78,12 +78,12 @@ describe( 'NodeList', () => {
 			expect( nodeList.get( 4 ).character ).to.equal( 'a' );
 			expect( nodeList.get( 5 ).character ).to.equal( 'r' );
 
-			expect( nodeList.get( 0 ).attrs.size ).to.equal( 0 );
-			expect( nodeList.get( 1 ).attrs.size ).to.equal( 0 );
-			expect( nodeList.get( 2 ).attrs.size ).to.equal( 0 );
-			expect( nodeList.get( 3 ).attrs.size ).to.equal( 0 );
-			expect( nodeList.get( 4 ).attrs.size ).to.equal( 0 );
-			expect( nodeList.get( 5 ).attrs.size ).to.equal( 0 );
+			expect( nodeList.get( 0 )._attrs.size ).to.equal( 0 );
+			expect( nodeList.get( 1 )._attrs.size ).to.equal( 0 );
+			expect( nodeList.get( 2 )._attrs.size ).to.equal( 0 );
+			expect( nodeList.get( 3 )._attrs.size ).to.equal( 0 );
+			expect( nodeList.get( 4 )._attrs.size ).to.equal( 0 );
+			expect( nodeList.get( 5 )._attrs.size ).to.equal( 0 );
 		} );
 
 		it( 'should merge strings and text objects if possible', () => {
@@ -244,10 +244,10 @@ describe( 'NodeList', () => {
 		it( 'should merge two text object if they have same attributes', () => {
 			let attr = new Attribute( 'foo', true );
 			let nodeList = new NodeList( [ 'ab', new Text( 'cd', [ attr ] ) ] );
-			nodeList._nodes[ 1 ].attrs.delete( attr.key );
 
 			expect( nodeList._nodes.length ).to.equal( 2 );
 
+			nodeList._nodes[ 1 ]._attrs.delete( attr.key );
 			nodeList._mergeNodeAt( 2 );
 
 			expect( nodeList._nodes.length ).to.equal( 1 );
@@ -256,6 +256,7 @@ describe( 'NodeList', () => {
 
 		it( 'should do nothing if text objects has different attributes', () => {
 			let nodeList = new NodeList( [ new Text( 'ab', [ new Attribute( 'foo', true ) ] ), 'cd' ] );
+
 			nodeList._mergeNodeAt( 2 );
 
 			expect( nodeList._nodes.length ).to.equal( 2 );

+ 37 - 37
packages/ckeditor5-engine/tests/treemodel/operation/attributeoperation.js

@@ -51,9 +51,9 @@ describe( 'AttributeOperation', () => {
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.getChildCount() ).to.equal( 3 );
-		expect( root.getChild( 0 ).attrs.has( newAttr ) ).to.be.true;
-		expect( root.getChild( 1 ).attrs.has( newAttr ) ).to.be.true;
-		expect( root.getChild( 2 ).attrs.size ).to.equal( 0 );
+		expect( root.getChild( 0 ).hasAttribute( newAttr ) ).to.be.true;
+		expect( root.getChild( 1 ).hasAttribute( newAttr ) ).to.be.true;
+		expect( root.getChild( 2 )._attrs.size ).to.equal( 0 );
 	} );
 
 	it( 'should add attribute to the existing attributes', () => {
@@ -74,10 +74,10 @@ describe( 'AttributeOperation', () => {
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.getChildCount() ).to.equal( 1 );
-		expect( root.getChild( 0 ).attrs.size ).to.equal( 3 );
-		expect( root.getChild( 0 ).attrs.has( newAttr ) ).to.be.true;
-		expect( root.getChild( 0 ).attrs.has( fooAttr ) ).to.be.true;
-		expect( root.getChild( 0 ).attrs.has( barAttr ) ).to.be.true;
+		expect( root.getChild( 0 )._attrs.size ).to.equal( 3 );
+		expect( root.getChild( 0 ).hasAttribute( newAttr ) ).to.be.true;
+		expect( root.getChild( 0 ).hasAttribute( fooAttr ) ).to.be.true;
+		expect( root.getChild( 0 ).hasAttribute( barAttr ) ).to.be.true;
 	} );
 
 	it( 'should change attribute to the set of nodes', () => {
@@ -97,12 +97,12 @@ describe( 'AttributeOperation', () => {
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.getChildCount() ).to.equal( 3 );
-		expect( root.getChild( 0 ).attrs.size ).to.equal( 1 );
-		expect( root.getChild( 0 ).attrs.has( newAttr ) ).to.be.true;
-		expect( root.getChild( 1 ).attrs.size ).to.equal( 1 );
-		expect( root.getChild( 1 ).attrs.has( newAttr ) ).to.be.true;
-		expect( root.getChild( 2 ).attrs.size ).to.equal( 1 );
-		expect( root.getChild( 2 ).attrs.has( oldAttr ) ).to.be.true;
+		expect( root.getChild( 0 )._attrs.size ).to.equal( 1 );
+		expect( root.getChild( 0 ).hasAttribute( newAttr ) ).to.be.true;
+		expect( root.getChild( 1 )._attrs.size ).to.equal( 1 );
+		expect( root.getChild( 1 ).hasAttribute( newAttr ) ).to.be.true;
+		expect( root.getChild( 2 )._attrs.size ).to.equal( 1 );
+		expect( root.getChild( 2 ).hasAttribute( oldAttr ) ).to.be.true;
 	} );
 
 	it( 'should change attribute in the middle of existing attributes', () => {
@@ -124,10 +124,10 @@ describe( 'AttributeOperation', () => {
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.getChildCount() ).to.equal( 1 );
-		expect( root.getChild( 0 ).attrs.size ).to.equal( 3 );
-		expect( root.getChild( 0 ).attrs.has( fooAttr ) ).to.be.true;
-		expect( root.getChild( 0 ).attrs.has( x2Attr ) ).to.be.true;
-		expect( root.getChild( 0 ).attrs.has( barAttr ) ).to.be.true;
+		expect( root.getChild( 0 )._attrs.size ).to.equal( 3 );
+		expect( root.getChild( 0 ).hasAttribute( fooAttr ) ).to.be.true;
+		expect( root.getChild( 0 ).hasAttribute( x2Attr ) ).to.be.true;
+		expect( root.getChild( 0 ).hasAttribute( barAttr ) ).to.be.true;
 	} );
 
 	it( 'should remove attribute', () => {
@@ -148,9 +148,9 @@ describe( 'AttributeOperation', () => {
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.getChildCount() ).to.equal( 1 );
-		expect( root.getChild( 0 ).attrs.size ).to.equal( 2 );
-		expect( root.getChild( 0 ).attrs.has( fooAttr ) ).to.be.true;
-		expect( root.getChild( 0 ).attrs.has( barAttr ) ).to.be.true;
+		expect( root.getChild( 0 )._attrs.size ).to.equal( 2 );
+		expect( root.getChild( 0 ).hasAttribute( fooAttr ) ).to.be.true;
+		expect( root.getChild( 0 ).hasAttribute( barAttr ) ).to.be.true;
 	} );
 
 	it( 'should create an AttributeOperation as a reverse', () => {
@@ -186,9 +186,9 @@ describe( 'AttributeOperation', () => {
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.getChildCount() ).to.equal( 3 );
-		expect( root.getChild( 0 ).attrs.size ).to.equal( 0 );
-		expect( root.getChild( 1 ).attrs.size ).to.equal( 0 );
-		expect( root.getChild( 2 ).attrs.size ).to.equal( 0 );
+		expect( root.getChild( 0 )._attrs.size ).to.equal( 0 );
+		expect( root.getChild( 1 )._attrs.size ).to.equal( 0 );
+		expect( root.getChild( 2 )._attrs.size ).to.equal( 0 );
 	} );
 
 	it( 'should not set attribute of element if change range starts in the middle of that element', () => {
@@ -208,7 +208,7 @@ describe( 'AttributeOperation', () => {
 			)
 		);
 
-		expect( root.getChild( 0 ).attrs.has( fooAttr ) ).to.be.false;
+		expect( root.getChild( 0 ).hasAttribute( fooAttr ) ).to.be.false;
 	} );
 
 	it( 'should not remove attribute of element if change range starts in the middle of that element', () => {
@@ -228,7 +228,7 @@ describe( 'AttributeOperation', () => {
 			)
 		);
 
-		expect( root.getChild( 0 ).attrs.has( fooAttr ) ).to.be.true;
+		expect( root.getChild( 0 ).hasAttribute( fooAttr ) ).to.be.true;
 	} );
 
 	it( 'should undo changing attribute by applying reverse operation', () => {
@@ -252,12 +252,12 @@ describe( 'AttributeOperation', () => {
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.getChildCount() ).to.equal( 3 );
-		expect( root.getChild( 0 ).attrs.size ).to.equal( 1 );
-		expect( root.getChild( 0 ).attrs.has( oldAttr ) ).to.be.true;
-		expect( root.getChild( 1 ).attrs.size ).to.equal( 1 );
-		expect( root.getChild( 1 ).attrs.has( oldAttr ) ).to.be.true;
-		expect( root.getChild( 2 ).attrs.size ).to.equal( 1 );
-		expect( root.getChild( 2 ).attrs.has( oldAttr ) ).to.be.true;
+		expect( root.getChild( 0 )._attrs.size ).to.equal( 1 );
+		expect( root.getChild( 0 ).hasAttribute( oldAttr ) ).to.be.true;
+		expect( root.getChild( 1 )._attrs.size ).to.equal( 1 );
+		expect( root.getChild( 1 ).hasAttribute( oldAttr ) ).to.be.true;
+		expect( root.getChild( 2 )._attrs.size ).to.equal( 1 );
+		expect( root.getChild( 2 ).hasAttribute( oldAttr ) ).to.be.true;
 	} );
 
 	it( 'should undo remove attribute by applying reverse operation', () => {
@@ -280,12 +280,12 @@ describe( 'AttributeOperation', () => {
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.getChildCount() ).to.equal( 3 );
-		expect( root.getChild( 0 ).attrs.size ).to.equal( 1 );
-		expect( root.getChild( 0 ).attrs.has( fooAttr ) ).to.be.true;
-		expect( root.getChild( 1 ).attrs.size ).to.equal( 1 );
-		expect( root.getChild( 1 ).attrs.has( fooAttr ) ).to.be.true;
-		expect( root.getChild( 2 ).attrs.size ).to.equal( 1 );
-		expect( root.getChild( 2 ).attrs.has( fooAttr ) ).to.be.true;
+		expect( root.getChild( 0 )._attrs.size ).to.equal( 1 );
+		expect( root.getChild( 0 ).hasAttribute( fooAttr ) ).to.be.true;
+		expect( root.getChild( 1 )._attrs.size ).to.equal( 1 );
+		expect( root.getChild( 1 ).hasAttribute( fooAttr ) ).to.be.true;
+		expect( root.getChild( 2 )._attrs.size ).to.equal( 1 );
+		expect( root.getChild( 2 ).hasAttribute( fooAttr ) ).to.be.true;
 	} );
 
 	it( 'should throw an error when one try to remove and the attribute does not exists', () => {

+ 1 - 1
packages/ckeditor5-engine/tests/treemodel/rootelement.js

@@ -19,7 +19,7 @@ describe( 'Element', () => {
 
 			expect( root ).to.be.an.instanceof( Element );
 			expect( root ).to.have.property( 'document' ).that.equals( doc );
-			expect( root.attrs.size ).to.equal( 0 );
+			expect( root._attrs.size ).to.equal( 0 );
 			expect( root.getChildCount() ).to.equal( 0 );
 		} );
 	} );

+ 111 - 2
packages/ckeditor5-engine/tests/treemodel/selection.js

@@ -48,8 +48,8 @@ describe( 'Selection', () => {
 		expect( ranges.length ).to.equal( 0 );
 		expect( selection.anchor ).to.be.null;
 		expect( selection.focus ).to.be.null;
-		expect( selection.attrs ).to.be.instanceof( AttributeList );
-		expect( selection.attrs.size ).to.equal( 0 );
+		expect( selection._attrs ).to.be.instanceof( AttributeList );
+		expect( selection._attrs.size ).to.equal( 0 );
 	} );
 
 	it( 'should be collapsed if it has no ranges or all ranges are collapsed', () => {
@@ -411,4 +411,113 @@ describe( 'Selection', () => {
 			} );
 		} );
 	} );
+
+	describe( 'attributes interface', () => {
+		let attr = new Attribute( 'foo', 'bar' );
+		let attr2 = new Attribute( 'foo', 'foo' );
+
+		describe( 'setAttribute', () => {
+			it( 'should set given attribute on the selection', () => {
+				selection.setAttribute( attr );
+
+				expect( selection.getAttribute( 'foo' ) ).to.equal( attr );
+			} );
+		} );
+
+		describe( 'hasAttribute', () => {
+			it( 'should return true if element contains given attribute', () => {
+				selection.setAttribute( attr );
+
+				expect( selection.hasAttribute( attr ) ).to.be.true;
+			} );
+
+			it( 'should return false if element does not contain given attribute', () => {
+				expect( selection.hasAttribute( attr2 ) ).to.be.false;
+			} );
+
+			it( 'should return true if element contains attribute with given key', () => {
+				selection.setAttribute( attr );
+
+				expect( selection.hasAttribute( 'foo' ) ).to.be.true;
+			} );
+
+			it( 'should return false if element does not contain attribute with given key', () => {
+				expect( selection.hasAttribute( 'bar' ) ).to.be.false;
+			} );
+		} );
+
+		describe( 'getAttribute', () => {
+			it( 'should return undefined if element does not contain given attribute', () => {
+				expect( selection.getAttribute( 'bar' ) ).to.be.undefined;
+			} );
+		} );
+
+		describe( 'getAttributeValue', () => {
+			it( 'should return attribute value for given key if element contains given attribute', () => {
+				selection.setAttribute( attr );
+
+				expect( selection.getAttributeValue( 'foo' ) ).to.equal( 'bar' );
+			} );
+
+			it( 'should return null if element does not contain given attribute', () => {
+				expect( selection.getAttributeValue( 'bar' ) ).to.be.null;
+			} );
+		} );
+
+		describe( 'getAttributes', () => {
+			it( 'should return an iterator that iterates over all attributes set on the text fragment', () => {
+				selection.setAttribute( attr );
+
+				let it = selection.getAttributes();
+				let attrs = [];
+
+				let step = it.next();
+
+				while ( !step.done ) {
+					attrs.push( step.value );
+					step = it.next();
+				}
+
+				expect( attrs ).to.deep.equal( [ attr ] );
+			} );
+		} );
+
+		describe( 'setAttributesTo', () => {
+			it( 'should remove all attributes set on element and set the given ones', () => {
+				selection.setAttribute( attr2 );
+				selection.setAttributesTo( [ attr ] );
+
+				expect( selection.getAttributeValue( 'foo' ) ).to.equal( 'bar' );
+				expect( selection.getAttributeValue( 'abc' ) ).to.be.null;
+			} );
+		} );
+
+		describe( 'removeAttribute', () => {
+			it( 'should remove attribute set on the text fragment and return true', () => {
+				selection.setAttribute( attr );
+				let result = selection.removeAttribute( 'foo' );
+
+				expect( selection.getAttributeValue( 'foo' ) ).to.be.null;
+				expect( result ).to.be.true;
+			} );
+
+			it( 'should return false if text fragment does not have given attribute', () => {
+				let result = selection.removeAttribute( 'abc' );
+
+				expect( result ).to.be.false;
+			} );
+		} );
+
+		describe( 'clearAttributes', () => {
+			it( 'should remove all attributes from the element', () => {
+				selection.setAttribute( attr );
+				selection.setAttribute( new Attribute( 'abc', 'xyz' ) );
+
+				selection.clearAttributes();
+
+				expect( selection.getAttributeValue( 'foo' ) ).to.be.null;
+				expect( selection.getAttributeValue( 'abc' ) ).to.be.null;
+			} );
+		} );
+	} );
 } );

+ 2 - 2
packages/ckeditor5-engine/tests/treemodel/text.js

@@ -18,8 +18,8 @@ describe( 'Text', () => {
 			let text = new Text( 'bar', attrs );
 
 			expect( text ).to.have.property( 'text' ).that.equals( 'bar' );
-			expect( text ).to.have.property( 'attrs' ).that.is.instanceof( AttributeList );
-			expect( Array.from( text.attrs ) ).to.deep.equal( attrs );
+			expect( text ).to.have.property( '_attrs' ).that.is.instanceof( AttributeList );
+			expect( Array.from( text._attrs ) ).to.deep.equal( attrs );
 		} );
 
 		it( 'should create empty text object', () => {

+ 78 - 3
packages/ckeditor5-engine/tests/treemodel/textfragment.js

@@ -54,9 +54,84 @@ describe( 'TextFragment', () => {
 
 	it( 'getRange should return range containing all characters from TextFragment', () => {
 		let range = textFragment.getRange();
+	describe( 'attributes interface', () => {
+		let attr2 = new Attribute( 'abc', 'xyz' );
 
-		expect( range.root ).to.equal( root );
-		expect( range.start.path ).to.deep.equal( [ 0, 2 ] );
-		expect( range.end.path ).to.deep.equal( [ 0, 5 ] );
+		describe( 'hasAttribute', () => {
+			it( 'should return true if text fragment has given attribute', () => {
+				expect( textFragment.hasAttribute( attr ) ).to.be.true;
+			} );
+
+			it( 'should return false if text fragment does not have attribute', () => {
+				expect( textFragment.hasAttribute( attr2 ) ).to.be.false;
+			} );
+
+			it( 'should return true if text fragment has attribute with given key', () => {
+				expect( textFragment.hasAttribute( 'foo' ) ).to.be.true;
+			} );
+
+			it( 'should return false if text fragment does not have attribute with given key', () => {
+				expect( textFragment.hasAttribute( 'abc' ) ).to.be.false;
+			} );
+		} );
+
+		describe( 'getAttribute', () => {
+			it( 'should return attribute with given key if text fragment has given attribute', () => {
+				expect( textFragment.getAttribute( 'foo' ) ).to.equal( attr );
+			} );
+
+			it( 'should return null if text fragment does not have given attribute', () => {
+				expect( textFragment.getAttribute( 'bar' ) ).to.be.undefined;
+			} );
+		} );
+
+		describe( 'getAttributeValue', () => {
+			it( 'should return attribute value for given key if text fragment has given attribute', () => {
+				expect( textFragment.getAttributeValue( 'foo' ) ).to.equal( 'bar' );
+			} );
+
+			it( 'should return null if text fragment does not have given attribute', () => {
+				expect( textFragment.getAttributeValue( 'bar' ) ).to.be.null;
+			} );
+		} );
+
+		describe( 'getAttributes', () => {
+			it( 'should return an iterator that iterates over all attributes set on the text fragment', () => {
+				let it = textFragment.getAttributes();
+				let attrs = [];
+
+				let step = it.next();
+
+				while ( !step.done ) {
+					attrs.push( step.value );
+					step = it.next();
+				}
+
+				expect( attrs ).to.deep.equal( [ attr ] );
+			} );
+		} );
+
+		describe( 'setAttribute', () => {
+			it( 'should set given attribute on the text fragment', () => {
+				textFragment.setAttribute( new Attribute( 'abc', 'xyz' ) );
+
+				expect( textFragment.getAttributeValue( 'abc' ) ).to.equal( 'xyz' );
+			} );
+		} );
+
+		describe( 'removeAttribute', () => {
+			it( 'should remove attribute set on the text fragment and return true', () => {
+				let result = textFragment.removeAttribute( 'foo' );
+
+				expect( textFragment.getAttributeValue( 'foo' ) ).to.be.null;
+				expect( result ).to.be.true;
+			} );
+
+			it( 'should return false if text fragment does not have given attribute', () => {
+				let result = textFragment.removeAttribute( 'abc' );
+
+				expect( result ).to.be.false;
+			} );
+		} );
 	} );
 } );

+ 2 - 1
packages/ckeditor5-engine/tests/treemodel/treewalker.js

@@ -78,9 +78,10 @@ describe( 'range iterator', () => {
 
 		if ( item.value.type == 'TEXT' || item.value.type == 'CHARACTER' ) {
 			let text = item.value.item.text || item.value.item.character;
+			let attrs = item.value.item._attrs || item.value.item.first._attrs;
 
 			expect( text ).to.equal( expected.text );
-			expect( Array.from( item.value.item.attrs ) ).to.deep.equal( expected.attrs );
+			expect( Array.from( attrs ) ).to.deep.equal( expected.attrs );
 		} else {
 			expect( item.value ).to.deep.equal( expected );
 		}