Browse Source

Added parsing function to view test utilities.

Szymon Kupś 9 years ago
parent
commit
ff78974e90

+ 104 - 25
packages/ckeditor5-engine/tests/_utils-tests/view.js

@@ -5,7 +5,7 @@
 
 'use strict';
 
-import { getData, setData } from '/tests/engine/_utils/view.js';
+import { stringify, parse } from '/tests/engine/_utils/view.js';
 import DocumentFragment from '/ckeditor5/engine/treeview/documentfragment.js';
 import Element from '/ckeditor5/engine/treeview/element.js';
 import AttributeElement from '/ckeditor5/engine/treeview/attributeelement.js';
@@ -16,12 +16,17 @@ import Range from '/ckeditor5/engine/treeview/range.js';
 
 describe( 'view test utils', () => {
 	describe( 'getData', () => {
+		it( 'should write text', () => {
+			const text = new Text( 'foobar' );
+			expect( stringify( text ) ).to.equal( 'foobar' );
+		} );
+
 		it( 'should write elements and texts', () => {
 			const text = new Text( 'foobar' );
 			const b = new Element( 'b', null, text );
 			const p = new Element( 'p', null, b );
 
-			expect( getData( p ) ).to.equal( '<p><b>foobar</b></p>' );
+			expect( stringify( p ) ).to.equal( '<p><b>foobar</b></p>' );
 		} );
 
 		it( 'should write elements with attributes', () => {
@@ -35,7 +40,7 @@ describe( 'view test utils', () => {
 				class: 'short wide'
 			}, b );
 
-			expect( getData( p ) ).to.equal( '<p class="short wide" baz="qux" bar="taz"><b foo="bar">foobar</b></p>' );
+			expect( stringify( p ) ).to.equal( '<p class="short wide" baz="qux" bar="taz"><b foo="bar">foobar</b></p>' );
 		} );
 
 		it( 'should write selection ranges inside elements', () => {
@@ -47,7 +52,7 @@ describe( 'view test utils', () => {
 			const range = Range.createFromParentsAndOffsets( p, 1, p, 2 );
 			const selection = new Selection();
 			selection.addRange( range );
-			expect( getData( p, selection ) ).to.equal( '<p><b>foobar</b>[<b>bazqux</b>]</p>' );
+			expect( stringify( p, selection ) ).to.equal( '<p><b>foobar</b>[<b>bazqux</b>]</p>' );
 		} );
 
 		it( 'should write collapsed selection ranges inside elements', () => {
@@ -56,7 +61,7 @@ describe( 'view test utils', () => {
 			const range = Range.createFromParentsAndOffsets( p, 0, p, 0 );
 			const selection = new Selection();
 			selection.addRange( range );
-			expect( getData( p, selection ) ).to.equal( '<p>[]foobar</p>' );
+			expect( stringify( p, selection ) ).to.equal( '<p>[]foobar</p>' );
 		} );
 
 		it( 'should write selection ranges inside text', () => {
@@ -68,7 +73,7 @@ describe( 'view test utils', () => {
 			const range = Range.createFromParentsAndOffsets( text1, 1, text1, 5 );
 			const selection = new Selection();
 			selection.addRange( range );
-			expect( getData( p, selection ) ).to.equal( '<p><b>f{ooba}r</b><b>bazqux</b></p>' );
+			expect( stringify( p, selection ) ).to.equal( '<p><b>f{ooba}r</b><b>bazqux</b></p>' );
 		} );
 
 		it( 'should write collapsed selection ranges inside texts', () => {
@@ -77,7 +82,7 @@ describe( 'view test utils', () => {
 			const range = Range.createFromParentsAndOffsets( text, 0, text, 0 );
 			const selection = new Selection();
 			selection.addRange( range );
-			expect( getData( p, selection ) ).to.equal( '<p>{}foobar</p>' );
+			expect( stringify( p, selection ) ).to.equal( '<p>{}foobar</p>' );
 		} );
 
 		it( 'should write ranges that start inside text end ends between elements', () => {
@@ -89,7 +94,7 @@ describe( 'view test utils', () => {
 			const range = Range.createFromParentsAndOffsets( p, 0, text2, 5 );
 			const selection = new Selection();
 			selection.addRange( range );
-			expect( getData( p, selection ) ).to.equal( '<p>[<b>foobar</b><b>bazqu}x</b></p>' );
+			expect( stringify( p, selection ) ).to.equal( '<p>[<b>foobar</b><b>bazqu}x</b></p>' );
 		} );
 
 		it( 'should write elements types as namespaces when needed', () => {
@@ -97,7 +102,7 @@ describe( 'view test utils', () => {
 			const b = new AttributeElement( 'b', null, text );
 			const p = new ContainerElement( 'p', null, b );
 
-			expect( getData( p, null, { showType: true } ) )
+			expect( stringify( p, null, { showType: true } ) )
 				.to.equal( '<container:p><attribute:b>foobar</attribute:b></container:p>' );
 		} );
 
@@ -106,7 +111,7 @@ describe( 'view test utils', () => {
 			const b = new AttributeElement( 'b', null, text );
 			const p = new ContainerElement( 'p', null, b );
 
-			expect( getData( p, null, { showPriority: true } ) )
+			expect( stringify( p, null, { showPriority: true } ) )
 				.to.equal( '<p><b:10>foobar</b:10></p>' );
 		} );
 
@@ -116,7 +121,7 @@ describe( 'view test utils', () => {
 			const b1 = new Element( 'b', null, text1 );
 			const b2 = new Element( 'b', null, text2 );
 			const fragment = new DocumentFragment( [ b1, b2 ] );
-			expect( getData( fragment, null ) ).to.equal( '<b>foobar</b><b>bazqux</b>' );
+			expect( stringify( fragment, null ) ).to.equal( '<b>foobar</b><b>bazqux</b>' );
 		} );
 
 		it( 'should not write ranges outside elements', () => {
@@ -130,15 +135,15 @@ describe( 'view test utils', () => {
 			const range5 = Range.createFromParentsAndOffsets( text, 6, text, 8 );
 			const selection = new Selection();
 			selection.addRange( range1 );
-			expect( getData( p, selection ) ).to.equal( '<p>[<b>foobar</b></p>' );
+			expect( stringify( p, selection ) ).to.equal( '<p>[<b>foobar</b></p>' );
 			selection.setRanges( [ range2 ] );
-			expect( getData( p, selection ) ).to.equal( '<p><b>foobar</b>]</p>' );
+			expect( stringify( p, selection ) ).to.equal( '<p><b>foobar</b>]</p>' );
 			selection.setRanges( [ range3 ] );
-			expect( getData( p, selection ) ).to.equal( '<p><b>{foobar</b></p>' );
+			expect( stringify( p, selection ) ).to.equal( '<p><b>{foobar</b></p>' );
 			selection.setRanges( [ range4 ] );
-			expect( getData( p, selection ) ).to.equal( '<p><b>fo}obar</b></p>' );
+			expect( stringify( p, selection ) ).to.equal( '<p><b>fo}obar</b></p>' );
 			selection.setRanges( [ range5 ] );
-			expect( getData( p, selection ) ).to.equal( '<p><b>foobar{</b></p>' );
+			expect( stringify( p, selection ) ).to.equal( '<p><b>foobar{</b></p>' );
 		} );
 
 		it( 'should write multiple ranges from selection #1', () => {
@@ -152,7 +157,7 @@ describe( 'view test utils', () => {
 			const selection = new Selection();
 			selection.setRanges( [ range2, range1 ] );
 
-			expect( getData( p, selection ) ).to.equal( '<p>[<b>foobar</b>][]<b>bazqux</b></p>' );
+			expect( stringify( p, selection ) ).to.equal( '<p>[<b>foobar</b>][]<b>bazqux</b></p>' );
 		} );
 
 		it( 'should write multiple ranges from selection #2', () => {
@@ -167,13 +172,13 @@ describe( 'view test utils', () => {
 			const selection = new Selection();
 			selection.setRanges( [ range1, range2, range3, range4 ] );
 
-			expect( getData( p, selection ) ).to.equal( '<p>[<b>foobar</b>][]{baz}{q}ux</p>' );
+			expect( stringify( p, selection ) ).to.equal( '<p>[<b>foobar</b>][]{baz}{q}ux</p>' );
 		} );
 	} );
 
-	describe( 'setData', () => {
+	describe( 'parse', () => {
 		it( 'should parse elements and texts', () => {
-			const view = setData( '<b>foobar</b>' );
+			const view = parse( '<b>foobar</b>' );
 			const element = new Element( 'b' );
 
 			expect( view ).to.be.instanceof( Element );
@@ -185,7 +190,7 @@ describe( 'view test utils', () => {
 		} );
 
 		it( 'should parse element attributes', () => {
-			const view = setData( '<b name="foo" title="bar" class="foo bar" style="color:red;"></b>' );
+			const view = parse( '<b name="foo" title="bar" class="foo bar" style="color:red;"></b>' );
 			const element = new Element( 'b', { name: 'foo', title: 'bar', class: 'foo bar', style: 'color:red;' } );
 
 			expect( view ).to.be.instanceof( Element );
@@ -194,9 +199,9 @@ describe( 'view test utils', () => {
 		} );
 
 		it( 'should parse element type', () => {
-			const view1 = setData( '<attribute:b></attribute:b>' );
+			const view1 = parse( '<attribute:b></attribute:b>' );
 			const attribute = new AttributeElement( 'b' );
-			const view2 = setData( '<container:p></container:p>' );
+			const view2 = parse( '<container:p></container:p>' );
 			const container = new ContainerElement( 'p' );
 
 			expect( view1 ).to.be.instanceof( AttributeElement );
@@ -206,10 +211,10 @@ describe( 'view test utils', () => {
 		} );
 
 		it( 'should parse element priority', () => {
-			const parsed1 = setData( '<b:12></b:12>' );
+			const parsed1 = parse( '<b:12></b:12>' );
 			const attribute1 = new AttributeElement( 'b' );
 			attribute1.priority = 12;
-			const parsed2 = setData( '<attribute:b:44></attribute:b:44>' );
+			const parsed2 = parse( '<attribute:b:44></attribute:b:44>' );
 			const attribute2 = new AttributeElement( 'b' );
 			attribute2.priority = 44;
 
@@ -217,5 +222,79 @@ describe( 'view test utils', () => {
 			expect( parsed1.isSimilar( attribute1 ) ).to.be.true;
 			expect( parsed2.isSimilar( attribute2 ) ).to.be.true;
 		} );
+
+		it( 'should paste nested elements and texts', () => {
+			const parsed = parse( '<container:p>foo<b:12>bar<i:25>qux</i:25></b:12></container:p>' );
+			expect( parsed.isSimilar( new ContainerElement( 'p' ) ) ).to.be.true;
+			expect( parsed.getChildCount() ).to.equal( 2 );
+			expect( parsed.getChild( 0 ) ).to.be.instanceof( Text ).and.have.property( 'data' ).that.equal( 'foo' );
+			const b = parsed.getChild( 1 );
+			expect( b ).to.be.instanceof( AttributeElement );
+			expect( b.priority ).to.equal( 12 );
+			expect( b.getChildCount() ).to.equal( 2 );
+			expect( b.getChild( 0 ) ).to.be.instanceof( Text ).and.have.property( 'data' ).that.equal( 'bar' );
+			const i = b.getChild( 1 );
+			expect( i ).to.be.instanceof( AttributeElement );
+			expect( i.priority ).to.equal( 25 );
+			expect( i.getChild( 0 ) ).to.be.instanceof( Text ).and.have.property( 'data' ).that.equal( 'qux' );
+		} );
+
+		it( 'should parse selection range inside text', () => {
+			const { view, selection } = parse( 'f{oo}b{}ar' );
+			expect( view ).to.be.instanceof( Text );
+			expect( view.data ).to.equal( 'foobar' );
+			expect( selection.rangeCount ).to.equal( 2 );
+			const ranges = Array.from( selection.getRanges() );
+
+			expect( ranges[ 0 ].isEqual( Range.createFromParentsAndOffsets( view, 1, view, 3 ) ) ).to.be.true;
+			expect( ranges[ 1 ].isEqual( Range.createFromParentsAndOffsets( view, 4, view, 4 ) ) ).to.be.true;
+		} );
+
+		it( 'should parse selection range between elements', () => {
+			const { view, selection } = parse( '<p>[<b>foobar]</b>[]</p>' );
+			expect( view ).to.be.instanceof( Element );
+			expect( view.getChildCount() ).to.equal( 1 );
+			const b = view.getChild( 0 );
+			expect( b ).to.be.instanceof( Element );
+			expect( b.name ).to.equal( 'b' );
+			expect( b.getChildCount() ).to.equal( 1 );
+			const text = b.getChild( 0 );
+			expect( text ).to.be.instanceof( Text );
+			expect( text.data ).to.equal( 'foobar' );
+			expect( selection.rangeCount ).to.equal( 2 );
+			const ranges = Array.from( selection.getRanges() );
+			expect( ranges[ 0 ].isEqual( Range.createFromParentsAndOffsets( view, 0, b, 1 ) ) ).to.be.true;
+			expect( ranges[ 1 ].isEqual( Range.createFromParentsAndOffsets( view, 1, view, 1 ) ) ).to.be.true;
+		} );
+
+		it( 'should parse ranges #1', () => {
+			const { view, selection } = parse( '<container:p>foo{bar]</container:p>' );
+			expect( view.isSimilar( new ContainerElement( 'p' ) ) ).to.be.true;
+			expect( view.getChildCount() ).to.equal( 1 );
+			const text = view.getChild( 0 );
+			expect( text ).to.be.instanceof( Text );
+			expect( text.data ).to.equal( 'foobar' );
+			expect( selection.rangeCount ).to.equal( 1 );
+			expect( selection.getFirstRange().isEqual( Range.createFromParentsAndOffsets( text, 3, view, 1 ) ) ).to.be.true;
+		} );
+
+		it( 'should parse ranges #2', () => {
+			const { view, selection } = parse( '<attribute:b>[foob}ar<i>{baz</i>]</attribute:b>' );
+			expect( view.isSimilar( new AttributeElement( 'b' ) ) ).to.be.true;
+			expect( view.getChildCount() ).to.equal( 2 );
+			const text1 = view.getChild( 0 );
+			expect( text1 ).to.be.instanceof( Text );
+			expect( text1.data ).to.equal( 'foobar' );
+			const i = view.getChild( 1 );
+			expect( i.isSimilar( new Element( 'i' ) ) ).to.be.true;
+			expect( i.getChildCount() ).to.equal( 1 );
+			const text2 = i.getChild( 0 );
+			expect( text2 ).to.be.instanceof( Text );
+			expect( text2.data ).to.equal( 'baz' );
+			expect( selection.rangeCount ).to.equal( 2 );
+			const ranges = Array.from( selection.getRanges() );
+			expect( ranges[ 0 ].isEqual( Range.createFromParentsAndOffsets( view, 0, text1, 4 ) ) ).to.be.true;
+			expect( ranges[ 1 ].isEqual( Range.createFromParentsAndOffsets( text2, 0, view, 2 ) ) ).to.be.true;
+		} );
 	} );
 } );

+ 178 - 8
packages/ckeditor5-engine/tests/_utils/view.js

@@ -10,6 +10,9 @@
 import ViewDocumentFragment from '/ckeditor5/engine/treeview/documentfragment.js';
 import HtmlDataProcessor from '/ckeditor5/engine/dataprocessor/htmldataprocessor.js';
 import ViewElement from '/ckeditor5/engine/treeview/element.js';
+import Selection from '/ckeditor5/engine/treeview/selection.js';
+import Range from '/ckeditor5/engine/treeview/range.js';
+import Position from '/ckeditor5/engine/treeview/position.js';
 import AttributeElement from '/ckeditor5/engine/treeview/attributeelement.js';
 import ContainerElement from '/ckeditor5/engine/treeview/containerelement.js';
 import ViewText from '/ckeditor5/engine/treeview/text.js';
@@ -73,7 +76,7 @@ const TEXT_RANGE_END_TOKEN = '}';
  *		attribute.priority = 20;
  *		getData( attribute, null, { showPriority: true } ); // <b priority=20></b>
  *
- * @param {engine.treeView.Element|engine.treeView.DocumentFragment} root
+ * @param {engine.treeView.Node} node
  * @param {engine.treeView.Selection} [selection]
  * @param {Object} [options]
  * @param {Boolean} [options.showType=false] When set to `true` type of elements will be printed ( `<container:p>`
@@ -81,20 +84,187 @@ const TEXT_RANGE_END_TOKEN = '}';
  * @param {Boolean} [options.showPriority=false] When set to `true` AttributeElement's priority will be printed.
  * @returns {String}
  */
-export function getData( root, selection, options ) {
-	const viewStringify = new ViewStringify( root, selection, options );
+export function stringify( node, selection, options = {} ) {
+	const viewStringify = new ViewStringify( node, selection, options );
 
 	return viewStringify.stringify();
 }
 
-export function setData( data ) {
+export function parse( data, options = { } ) {
+	options.order = options.order || [];
 	const viewParser = new ViewParser();
+	const rangeParser = new RangeParser();
 
-	return viewParser.parse( data );
+	const view = viewParser.parse( data );
+	const ranges = rangeParser.parse( view, options.order );
+
+	// When ranges are present - return object containing view, and selection.
+	if ( ranges.length ) {
+		const selection = new Selection();
+		selection.setRanges( ranges, !!options.lastRangeBackward );
+
+		return {
+			view: view,
+			selection: selection
+		};
+	}
+
+	return view;
 }
 
-class ViewParser {
+class RangeParser {
+	constructor() {
+		// Todo - set in parse method.
+		this._positions = [];
+	}
+
+	parse( node, order ) {
+		this._getPositions( node );
+		let ranges = this._createRanges();
+
+		// Sort ranges if needed.
+		if ( order.length ) {
+			if ( order.length != ranges.length ) {
+				throw new Error( `There are ${ ranges.length} ranges found, but ranges order contain ${ order.length } elements.` );
+			}
+
+			ranges = this._sortRanges( ranges,  order );
+		}
+
+		return ranges;
+	}
+
+	_sortRanges( ranges, rangesOrder ) {
+		const sortedRanges = [];
+
+		for ( let index in rangesOrder ) {
+			if ( ranges[ index ] === undefined ) {
+				throw new Error( 'Provided ranges order is invalid.' );
+			}
+
+			sortedRanges.push( ranges[ index ] );
+		}
+
+		return sortedRanges;
+	}
+
+	_getPositions( node ) {
+		if ( node instanceof ViewDocumentFragment || node instanceof ViewElement ) {
+			// Copy elements into the array, when items will be removed from node this array will still have all the
+			// items needed for iteration.
+			const children = Array.from( node.getChildren() );
+
+			for ( let child of children ) {
+				this._getPositions( child );
+			}
+		}
+
+		if ( node instanceof ViewText ) {
+			const regexp = new RegExp(
+				`[ ${TEXT_RANGE_START_TOKEN}${TEXT_RANGE_END_TOKEN}\\${ELEMENT_RANGE_END_TOKEN}\\${ELEMENT_RANGE_START_TOKEN} ]`,
+				'g'
+			);
+			let text = node.data;
+			let match;
+			let offset = 0;
+			const brackets = [];
+
+			// Remove brackets from text and store info about offset inside text node.
+			while ( ( match = regexp.exec( text ) ) ) {
+				const index = match.index;
+				const bracket = match[ 0 ];
 
+				brackets.push( {
+					bracket: bracket,
+					textOffset: index - offset
+				} );
+
+				offset++;
+			}
+			text = text.replace( regexp, '' );
+			node.data = text;
+			const index = node.getIndex();
+			const parent = node.parent;
+
+			// Remove empty text nodes.
+			if ( !text ) {
+				node.remove();
+			}
+
+			for ( let item of brackets ) {
+				// Non-empty text node.
+				if ( text ) {
+					if ( item.bracket == TEXT_RANGE_START_TOKEN || item.bracket == TEXT_RANGE_END_TOKEN ) {
+						// Store information about text range delimiter.
+						this._positions.push( {
+							bracket: item.bracket,
+							position: new Position( node, item.textOffset )
+						} );
+					} else {
+						// Check if element range delimiter is not placed inside text node.
+						if ( item.textOffset !== 0 && item.textOffset !== text.length ) {
+							throw new Error( `Range delimiter '${ item.bracket }' is placed inside text node.` );
+						}
+
+						// If bracket is placed at the end of the text node - it should be positioned after it.
+						const offset = ( item.textOffset === 0 ? index : index + 1 );
+
+						// Store information about element range delimiter.
+						this._positions.push( {
+							bracket: item.bracket,
+							position: new Position( parent, offset )
+						} );
+					}
+				} else {
+					if ( item.bracket == TEXT_RANGE_START_TOKEN || item.bracket == TEXT_RANGE_END_TOKEN ) {
+						throw new Error( `Text range delimiter '${ item.bracket }' is placed inside empty text node. ` );
+					}
+
+					// Store information about element range delimiter.
+					this._positions.push( {
+						bracket: item.bracket,
+						position: new Position( parent, index )
+					} );
+				}
+			}
+		}
+	}
+
+	_createRanges() {
+		const ranges = [];
+		let range = null;
+
+		for ( let item of this._positions ) {
+			// When end of range is found without opening.
+			if ( !range && ( item.bracket == ELEMENT_RANGE_END_TOKEN || item.bracket == TEXT_RANGE_END_TOKEN ) ) {
+				throw new Error( `End of range was found '${ item.bracket }' but range was not started before.` );
+			}
+
+			// When second start of range is found when one is already opened - selection does not allow intersecting
+			// ranges.
+			if ( range && ( item.bracket == ELEMENT_RANGE_START_TOKEN || item.bracket == TEXT_RANGE_START_TOKEN ) ) {
+				throw new Error( `Start of range was found '${ item.bracket }' but one range is already started.` );
+			}
+
+			if ( item.bracket == ELEMENT_RANGE_START_TOKEN || item.bracket == TEXT_RANGE_START_TOKEN ) {
+				range = new Range( item.position, item.position );
+			} else {
+				range.end = item.position;
+				ranges.push( range );
+				range = null;
+			}
+		}
+
+		// Check if all ranges have proper ending.
+		if ( range !== null ) {
+			throw new Error( 'Range was started but no end delimiter was found.' );
+		}
+
+		return ranges;
+	}
+}
+
+class ViewParser {
 	parse( data ) {
 		const htmlProcessor = new HtmlDataProcessor();
 		const domRoot = htmlProcessor.toDom( data );
@@ -196,7 +366,7 @@ class ViewParser {
 				};
 			}
 
-			throw new Error( `Cannot parse element's tag name: ${ element.tagName }.` );
+			throw new Error( `Cannot parse element's tag name: ${ element.tagName.toLowerCase() }.` );
 		}
 
 		// Check if name is in format type:name:priority.
@@ -213,7 +383,7 @@ class ViewParser {
 			}
 		}
 
-		throw new Error( `Cannot parse element's tag name: ${ element.tagName }.` );
+		throw new Error( `Cannot parse element's tag name: ${ element.tagName.toLowerCase() }.` );
 	}
 
 	_convertType( type ) {