Explorar o código

#396 Serialization/deserialization of Node, Element, NodeList and NodeListText.

Maciej Gołaszewski %!s(int64=9) %!d(string=hai) anos
pai
achega
4d86592fc7

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

@@ -197,4 +197,17 @@ export default class Element extends Node {
 
 		return text;
 	}
+
+	/**
+	 * Creates Element object from deserilized object, ie. from parsed JSON string.
+	 *
+	 *		let deserialized = JSON.parse( JSON.stringify( someElementObject ) );
+	 *		let element = NodeList.fromJSON( deserialized );
+	 *
+	 * @param object
+	 * @returns {engine.treeModel.Element}
+	 */
+	static fromJSON( object ) {
+		return new Element( object.name, object._attrs, NodeList.fromJSON( object._children ) );
+	}
 }

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

@@ -188,6 +188,9 @@ export default class Node {
 		// Due to circular references we need to remove parent reference.
 		json.parent = this.parent ? this.parent.name : null;
 
+		// Serialize attributes as Map object is represented as "{}" when parsing to JSON.
+		json._attrs = [ ...json._attrs ];
+
 		return json;
 	}
 }

+ 32 - 4
packages/ckeditor5-engine/src/treemodel/nodelist.js

@@ -7,6 +7,7 @@
 
 import CharacterProxy from './characterproxy.js';
 import Text from './text.js';
+import Element from './element.js';
 import DocumentFragment from './documentfragment.js';
 import mapsEqual from '../../utils/mapsequal.js';
 import isIterable from '../../utils/isiterable.js';
@@ -50,14 +51,18 @@ class NodeListText extends Text {
 	}
 
 	/**
-	 * Custom toJSON method to solve child-parent circular dependencies.
+	 * Custom toJSON method to solve child-parent circular dependencies when serializing NodeListText to JSON string.
 	 *
 	 * @returns {Object} Clone of this object with the parent property replaced with its name.
 	 */
 	toJSON() {
 		const json = clone( this );
 
-		json.parent = json.parent ? this.parent.name : null;
+		// Due to circular references we need to remove parent reference.
+		json.parent = this.parent ? this.parent.name : null;
+
+		// Serialize attributes as Map object is represented as "{}" when parsing to JSON.
+		json._attrs = [ ...json._attrs ];
 
 		return json;
 	}
@@ -293,7 +298,7 @@ export default class NodeList {
 
 		this._indexMap.splice( index, number );
 
-		for ( let i = index; i < this._indexMap.length ; i++ ) {
+		for ( let i = index; i < this._indexMap.length; i++ ) {
 			this._indexMap[ i ] -= removed.length;
 		}
 
@@ -353,6 +358,29 @@ export default class NodeList {
 		this._mergeNodeAt( index + number );
 	}
 
+	/**
+	 * Creates NodeList object from deserilized object, ie. from parsed JSON string.
+	 *
+	 *		let deserialized = JSON.parse( JSON.stringify( someNodeList ) );
+	 *		let nodeList = NodeList.fromJSON( deserialized );
+	 *
+	 * @param object
+	 * @returns {engine.treeModel.NodeList}
+	 */
+	static fromJSON( object ) {
+		let nodes = [];
+
+		for ( let node of object._nodes ) {
+			if ( node.text ) {
+				nodes.push( new Text( node.text, node._attrs ) );
+			} else {
+				nodes.push( Element.fromJSON( node ) );
+			}
+		}
+
+		return new NodeList( nodes );
+	}
+
 	/**
 	 * Checks whether given index is inside a text and if so, splits that text node. This method should be used
 	 * to split text objects whenever there are some changes made on a part of text object (i.e. removing part of text,
@@ -425,7 +453,7 @@ export default class NodeList {
 			// Remove text node after index.
 			this._nodes.splice( realIndexAfter, 1 );
 
-			for ( let i = index; i < this._indexMap.length ; i++ ) {
+			for ( let i = index; i < this._indexMap.length; i++ ) {
 				this._indexMap[ i ]--;
 			}
 		}

+ 48 - 1
packages/ckeditor5-engine/tests/treemodel/element.js

@@ -11,6 +11,7 @@ import Node from '/ckeditor5/engine/treemodel/node.js';
 import NodeList from '/ckeditor5/engine/treemodel/nodelist.js';
 import Element from '/ckeditor5/engine/treemodel/element.js';
 import DocumentFragment from '/ckeditor5/engine/treemodel/documentfragment.js';
+import treeModelTestUtils from '/tests/engine/treemodel/_utils/utils.js';
 
 describe( 'Element', () => {
 	describe( 'constructor', () => {
@@ -233,10 +234,56 @@ describe( 'Element', () => {
 				new Element( 'p', null, 'abc' ),
 				'def',
 				new Element( 'p', null, 'ghi' ),
-				'jkl',
+				'jkl'
 			] );
 
 			expect( el.getText() ).to.equal( 'abcdefghijkl' );
 		} );
 	} );
+
+	describe( 'fromJSON', () => {
+		it( 'should create element without attributes', () => {
+			const el = new Element( 'el' );
+
+			let serialized = treeModelTestUtils.jsonParseStringify( el );
+
+			let deserialized = Element.fromJSON( serialized );
+
+			expect( deserialized.parent ).to.be.null;
+			expect( deserialized.name ).to.equal( 'el' );
+			expect( deserialized._children.length ).to.equal( 0 );
+			expect( deserialized._attrs.size ).to.equal( 0 );
+		} );
+
+		it( 'should create element with attributes', () => {
+			const el = new Element( 'el', { foo: true } );
+
+			let serialized = treeModelTestUtils.jsonParseStringify( el );
+
+			let deserialized = Element.fromJSON( serialized );
+
+			expect( deserialized.parent ).to.be.null;
+			expect( deserialized.name ).to.equal( 'el' );
+			expect( deserialized._children.length ).to.equal( 0 );
+			expect( deserialized._attrs.size ).to.equal( 1 );
+			expect( deserialized._attrs.get( 'foo' ) ).to.be.true;
+		} );
+
+		it( 'should create element with children', () => {
+			const p = new Element( 'p' );
+			const el = new Element( 'el', null, p );
+
+			let serialized = treeModelTestUtils.jsonParseStringify( el );
+
+			let deserialized = Element.fromJSON( serialized );
+
+			expect( deserialized.parent ).to.be.null;
+			expect( deserialized.name ).to.equal( 'el' );
+			expect( deserialized._children.length ).to.equal( 1 );
+
+			expect( deserialized._children._indexMap ).to.deep.equal( [ 0 ] );
+			expect( deserialized._children._nodes[ 0 ].name ).to.equal( 'p' );
+			expect( deserialized._children._nodes[ 0 ].parent ).to.equal( deserialized );
+		} );
+	} );
 } );

+ 55 - 0
packages/ckeditor5-engine/tests/treemodel/node.js

@@ -9,6 +9,7 @@
 
 import Element from '/ckeditor5/engine/treemodel/element.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
+import treeModelTestUtils from '/tests/engine/treemodel/_utils/utils.js';
 
 describe( 'Node', () => {
 	let root;
@@ -178,4 +179,58 @@ describe( 'Node', () => {
 			} );
 		} );
 	} );
+
+	describe( 'toJSON', () => {
+		it( 'should serialize empty node', () => {
+			let node = new Element( 'one' );
+
+			expect( treeModelTestUtils.jsonParseStringify( node ) ).to.deep.equal( {
+				_attrs: [],
+				_children: {
+					_indexMap: [],
+					_nodes: []
+				},
+				name: 'one',
+				parent: null
+			} );
+		} );
+
+		it( 'should serialize node with attributes', () => {
+			let node = new Element( 'one', { foo: true, bar: false } );
+
+			expect( treeModelTestUtils.jsonParseStringify( node ) ).to.deep.equal( {
+				_attrs: [ [ 'foo', true ], [ 'bar', false ] ],
+				_children: {
+					_indexMap: [],
+					_nodes: []
+				},
+				name: 'one',
+				parent: null
+			} );
+		} );
+
+		it( 'should serialize node with children', () => {
+			expect( treeModelTestUtils.jsonParseStringify( root ) ).to.deep.equal( {
+				_attrs: [],
+				_children: {
+					_indexMap: [ 0, 1, 2 ],
+					_nodes: [
+						{ _attrs: [], _children: { _indexMap: [], _nodes: [] }, name: 'one', parent: null },
+						{
+							_attrs: [], _children: {
+							_indexMap: [ 0, 0, 1, 2 ], _nodes: [
+								{ _attrs: [], text: 'ba', parent: 'two' },
+								{ _attrs: [], _children: { _indexMap: [], _nodes: [] }, name: 'img', parent: 'two' },
+								{ _attrs: [], text: 'r', parent: 'two' }
+							]
+						}, name: 'two', parent: null
+						},
+						{ _attrs: [], _children: { _indexMap: [], _nodes: [] }, name: 'three', parent: null }
+					]
+				},
+				name: null,
+				parent: null
+			} );
+		} );
+	} );
 } );

+ 126 - 7
packages/ckeditor5-engine/tests/treemodel/nodelist.js

@@ -12,6 +12,7 @@ import DocumentFragment from '/ckeditor5/engine/treemodel/documentfragment.js';
 import Element from '/ckeditor5/engine/treemodel/element.js';
 import Text from '/ckeditor5/engine/treemodel/text.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
+import treeModelTestUtils from '/tests/engine/treemodel/_utils/utils.js';
 
 describe( 'NodeList', () => {
 	describe( 'constructor', () => {
@@ -153,7 +154,7 @@ describe( 'NodeList', () => {
 		it( 'should merge inserted text objects if possible', () => {
 			let attr = { foo: 'bar' };
 			let outerList = new NodeList( [ 'foo', new Text( 'bar', [ attr ] ) ] );
-			let innerList = new NodeList( [ 'x' , new Text( 'y', [ attr ] ) ] );
+			let innerList = new NodeList( [ 'x', new Text( 'y', [ attr ] ) ] );
 
 			outerList.insert( 3, innerList );
 
@@ -358,13 +359,131 @@ describe( 'NodeList', () => {
 		} );
 	} );
 
-	// Additional test for code coverage.
-	describe( 'NodeListText', () => {
-		it( 'should create proper JSON string using toJSON method', () => {
-			let nodeList = new NodeList( 'foo' );
-			let parsed = JSON.parse( JSON.stringify( nodeList ) );
+	describe( 'toJSON', () => {
+		it( 'should return serialized object', () => {
+			let p = new Element( 'p' );
+			let nodeList = new NodeList( p );
+
+			expect( treeModelTestUtils.jsonParseStringify( nodeList ) ).to.deep.equal( {
+				_nodes: [ treeModelTestUtils.jsonParseStringify( p ) ],
+				_indexMap: [ 0 ]
+			} );
+		} );
+
+		it( 'should return serialized object with child text', () => {
+			let p = new Element( 'p', null, 'bar' );
+			let nodeList = new NodeList( p );
+
+			let newVar = {
+				_indexMap: [ 0 ],
+				_nodes: [ {
+					_attrs: [],
+					_children: {
+						_indexMap: [ 0, 0, 0 ],
+						_nodes: [ { _attrs: [], text: 'bar', parent: 'p' } ]
+					},
+					name: 'p',
+					parent: null
+				} ]
+			};
+			let jsonParseStringify = treeModelTestUtils.jsonParseStringify( nodeList );
+
+			expect( jsonParseStringify ).to.deep.equal( newVar );
+		} );
+
+		it( 'should return serialized object for text', () => {
+			let text = new Text( 'bar' );
+			let nodeList = new NodeList( text );
+
+			expect( treeModelTestUtils.jsonParseStringify( nodeList ) ).to.deep.equal( {
+				_nodes: [
+					{ _attrs: [], text: 'bar', parent: null }
+				],
+				_indexMap: [ 0, 0, 0 ]
+			} );
+		} );
+
+		it( 'should return serialized object for text with attributes', () => {
+			let text = new Text( 'bar', { bold: true } );
+			let nodeList = new NodeList( text );
+
+			expect( treeModelTestUtils.jsonParseStringify( nodeList ) ).to.deep.equal( {
+				_nodes: [
+					{ _attrs: [ [ 'bold', true ] ], text: 'bar', parent: null }
+				],
+				_indexMap: [ 0, 0, 0 ]
+			} );
+		} );
+
+		it( 'should return serialized object for text with attributes', () => {
+			let text = new Text( 'bar', { bold: true } );
+			let nodeList = new NodeList( text );
+
+			expect( treeModelTestUtils.jsonParseStringify( nodeList ) ).to.deep.equal( {
+				_nodes: [
+					{ _attrs: [ [ 'bold', true ] ], text: 'bar', parent: null }
+				],
+				_indexMap: [ 0, 0, 0 ]
+			} );
+		} );
+	} );
+
+	describe( 'fromJSON', () => {
+		it( 'should create instance from serialized text with attributes', () => {
+			let text = new Text( 'bar', { bold: true } );
+			let nodeList = new NodeList( text );
+
+			let serialized = treeModelTestUtils.jsonParseStringify( nodeList );
+
+			let deserialized = NodeList.fromJSON( serialized );
+
+			expect( deserialized._indexMap ).to.deep.equal( nodeList._indexMap );
+			expect( deserialized._nodes.length ).to.equal( nodeList._nodes.length );
+			expect( deserialized._nodes[ 0 ].text ).to.equal( nodeList._nodes[ 0 ].text );
+			expect( [ ...deserialized._nodes[ 0 ]._attrs ] ).to.deep.equal( [ ...nodeList._nodes[ 0 ]._attrs ] );
+		} );
+
+		it( 'should create instance from serialized element', () => {
+			let p = new Element( 'p' );
+			let nodeList = new NodeList( p );
+
+			let serialized = treeModelTestUtils.jsonParseStringify( nodeList );
+
+			let deserialized = NodeList.fromJSON( serialized );
+
+			expect( deserialized._indexMap ).to.deep.equal( nodeList._indexMap );
+			expect( deserialized._nodes.length ).to.equal( nodeList._nodes.length );
+			expect( deserialized._nodes[ 0 ].name ).to.equal( nodeList._nodes[ 0 ].name );
+			expect( [ ...deserialized._nodes[ 0 ]._attrs ] ).to.deep.equal( [ ...nodeList._nodes[ 0 ]._attrs ] );
+		} );
+
+		it( 'should create instance from serialized element with attributes', () => {
+			let p = new Element( 'p', { bold: true } );
+			let nodeList = new NodeList( p );
+
+			let serialized = treeModelTestUtils.jsonParseStringify( nodeList );
+
+			let deserialized = NodeList.fromJSON( serialized );
+
+			expect( deserialized._indexMap ).to.deep.equal( nodeList._indexMap );
+			expect( deserialized._nodes.length ).to.equal( nodeList._nodes.length );
+			expect( deserialized._nodes[ 0 ].name ).to.equal( nodeList._nodes[ 0 ].name );
+			expect( [ ...deserialized._nodes[ 0 ]._attrs ] ).to.deep.equal( [ ...nodeList._nodes[ 0 ]._attrs ] );
+		} );
+
+		it( 'should create instance from serialized element with parent', () => {
+			let p = new Element( 'p', null, 'bar' );
+			let nodeList = new NodeList( p );
+
+			let serialized = treeModelTestUtils.jsonParseStringify( nodeList );
+			let deserialized = NodeList.fromJSON( serialized );
+
+			expect( deserialized._indexMap ).to.deep.equal( nodeList._indexMap );
+			expect( deserialized._nodes.length ).to.equal( nodeList._nodes.length );
+			expect( deserialized._nodes[ 0 ].name ).to.equal( nodeList._nodes[ 0 ].name );
+			expect( [ ...deserialized._nodes[ 0 ]._attrs ] ).to.deep.equal( [ ...nodeList._nodes[ 0 ]._attrs ] );
 
-			expect( parsed._nodes[ 0 ].parent ).to.equal( null );
+			expect( deserialized._nodes[ 0 ]._children._indexMap ).to.deep.equal( [ 0, 0, 0 ] );
 		} );
 	} );
 } );