浏览代码

Improved documentation and error messages in view test utilities.

Szymon Kupś 9 年之前
父节点
当前提交
8d6db89356
共有 2 个文件被更改,包括 229 次插入56 次删除
  1. 19 0
      packages/ckeditor5-engine/tests/_utils-tests/view.js
  2. 210 56
      packages/ckeditor5-engine/tests/_utils/view.js

+ 19 - 0
packages/ckeditor5-engine/tests/_utils-tests/view.js

@@ -182,6 +182,12 @@ describe( 'view test utils', () => {
 	} );
 
 	describe( 'parse', () => {
+		it( 'should parse text', () => {
+			const text = parse( 'foobar' );
+			expect( text ).to.be.instanceOf( Text );
+			expect( text.data ).to.equal( 'foobar' );
+		} );
+
 		it( 'should parse elements and texts', () => {
 			const view = parse( '<b>foobar</b>' );
 			const element = new Element( 'b' );
@@ -317,6 +323,19 @@ describe( 'view test utils', () => {
 			expect( ranges[ 0 ].isEqual( Range.createFromParentsAndOffsets( view, 3, view, 4 ) ) ).to.be.true;
 			expect( ranges[ 1 ].isEqual( Range.createFromParentsAndOffsets( view, 7, view, 8 ) ) ).to.be.true;
 			expect( ranges[ 2 ].isEqual( Range.createFromParentsAndOffsets( view, 0, view, 1 ) ) ).to.be.true;
+			expect( selection.anchor.isEqual( ranges[ 2 ].start ) ).to.be.true;
+			expect( selection.focus.isEqual( ranges[ 2 ].end ) ).to.be.true;
+		} );
+
+		it( 'should set last range backward if needed', () => {
+			const { view, selection } = parse( '{f}oo{b}arb{a}z', { order: [ 3, 1, 2 ], lastRangeBackward: true } );
+			expect( selection.rangeCount ).to.equal( 3 );
+			const ranges = Array.from( selection.getRanges() );
+			expect( ranges[ 0 ].isEqual( Range.createFromParentsAndOffsets( view, 3, view, 4 ) ) ).to.be.true;
+			expect( ranges[ 1 ].isEqual( Range.createFromParentsAndOffsets( view, 7, view, 8 ) ) ).to.be.true;
+			expect( ranges[ 2 ].isEqual( Range.createFromParentsAndOffsets( view, 0, view, 1 ) ) ).to.be.true;
+			expect( selection.anchor.isEqual( ranges[ 2 ].end ) ).to.be.true;
+			expect( selection.focus.isEqual( ranges[ 2 ].start ) ).to.be.true;
 		} );
 
 		it( 'should throw when ranges order does not include all ranges', () => {

+ 210 - 56
packages/ckeditor5-engine/tests/_utils/view.js

@@ -26,19 +26,29 @@ const TEXT_RANGE_START_TOKEN = '{';
 const TEXT_RANGE_END_TOKEN = '}';
 
 /**
- * Converts view elements to its string representation, an HTML-like string.
- * Root element can be provided as {@link engine.treeView.Element Element} or
- * {@link engine.treeView.DocumentFragment DocumentFragment}.
+ * Converts view elements to its HTML-like string representation.
+ * Root element can be provided as {@link engine.treeView.Text Text}:
+ *
+ *		const text = new Text( 'foobar' );
+ *		stringify( text ); // 'foobar'
+ *
+ * or as {@link engine.treeView.Element Element}:
+ *
+ *		const element = new Element( 'p', null, new Text( 'foobar' ) );
+ *		stringify( element ); // '<p>foobar</p>'
+ *
+ * or as {@link engine.treeView.DocumentFragment DocumentFragment}:
  *
  *		const text = new Text( 'foobar' );
  *		const b = new Element( 'b', { name: 'test' }, text );
- *		const p = new Element( 'p', { style: 'color:red;' }, b );
+ *		const p = new Element( 'p', { style: 'color:red;' } );
+ *		const fragment = new DocumentFragment( [ p, b ] );
  *
- *		getData( p ); // <p style="color:red;"><b name="test">foobar</b></p>
+ *		stringify( fragment ); // '<p style="color:red;"></p><b name="test">foobar</b>'
  *
- * Additionally {@link engine.treeView.Selection Selection}
- * instance can be provided, then ranges from that selection will be converted too. If range position is placed inside
- * element node `[` and `]` will be used there.
+ * Additionally {@link engine.treeView.Selection Selection} instance can be provided, then ranges from that selection
+ * will be included in output data.
+ * If range position is placed inside element node, it will be represented with `[` and `]`:
  *
  *		const text = new Text( 'foobar' );
  *		const b = new Element( 'b', null, text );
@@ -46,9 +56,9 @@ const TEXT_RANGE_END_TOKEN = '}';
  *		const selection = new Selection();
  *		selection.addRange( Range.createFromParentsAndOffsets( p, 0, p, 1 ) );
  *
- *		getData( p, selection ); // <p>[<b>foobar</b>]</p>
+ *		stringify( p, selection ); // <p>[<b>foobar</b>]</p>
  *
- * If range is placed inside text node `{` and `}` will be used there.
+ * If range is placed inside text node, it will be represented with `{` and `}`:
  *
  *		const text = new Text( 'foobar' );
  *		const b = new Element( 'b', null, text );
@@ -56,39 +66,89 @@ const TEXT_RANGE_END_TOKEN = '}';
  *		const selection = new Selection();
  *		selection.addRange( Range.createFromParentsAndOffsets( text, 1, text, 5 ) );
  *
- *		getData( p, selection ); // <p><b>f{ooba}r</b></p>
+ *		stringify( p, selection ); // '<p><b>f{ooba}r</b></p>'
  *
  * Additional options object can be provided.
- * If `options.showType` property is set to `true` element types will be
+ * If `options.showType` is set to `true` element's types will be
  * presented for {@link engine.treeView.AttributeElement AttributeElements} and {@link engine.treeView.ContainerElement
- * ContainerElements}.
+ * ContainerElements}:
  *
  *		const attribute = new AttributeElement( 'b' );
  *		const container = new ContainerElement( 'p' );
- *		getData( attribute, null, { showType: true } ); // <attribute:b></attribute:b>
- *		getData( container, null, { showType: true } ); // <container:p></container:p>
+ *		getData( attribute, null, { showType: true } ); // '<attribute:b></attribute:b>'
+ *		getData( container, null, { showType: true } ); // '<container:p></container:p>'
  *
- * if `options.showPriority` property is set to `true`, priority will be displayed for all
+ * If `options.showPriority` is set to `true`, priority will be displayed for all
  * {@link engine.treeView.AttributeElement AttributeElements}.
  *
  *		const attribute = new AttributeElement( 'b' );
  *		attribute.priority = 20;
- *		getData( attribute, null, { showPriority: true } ); // <b priority=20></b>
- *
- * @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>`
- * instead of `<p>` and `<attribute:b>` instead of `<b>`.
- * @param {Boolean} [options.showPriority=false] When set to `true` AttributeElement's priority will be printed.
- * @returns {String}
+ *		getData( attribute, null, { showPriority: true } ); // <b:20></b:20>
+ *
+ * @param {engine.treeView.Text|engine.treeView.Element|engine.treeView.DocumentFragment} node Node to stringify.
+ * @param {engine.treeView.Selection} [selection = null ] Selection instance which ranges will be included in returned
+ * string data.
+ * @param {Object} [options] Object with additional options.
+ * @param {Boolean} [options.showType=false] When set to `true` type of elements will be printed (`<container:p>`
+ * instead of `<p>` and `<attribute:b>` instead of `<b>`).
+ * @param {Boolean} [options.showPriority=false] When set to `true` AttributeElement's priority will be printed
+ * (`<span:12>`, `<b:10>`).
+ * @returns {String} HTML-like string representing the view.
  */
-export function stringify( node, selection, options = {} ) {
+export function stringify( node, selection = null, options = {} ) {
 	const viewStringify = new ViewStringify( node, selection, options );
 
 	return viewStringify.stringify();
 }
 
+/**
+ * Parses HTML-like string and returns view tree nodes.
+ * Simple string will be converted to {@link engine.treeView.Text Text} node:
+ *
+ *		parse( 'foobar' ); // Returns instance of Text.
+ *
+ * {@link engine.treeView.Element Elements} will be parsed with attributes an children:
+ *
+ *		parse( '<b name="baz">foobar</b>' ); // Returns instance of Element with `baz` attribute and text child node.
+ *
+ * Multiple nodes provided on root level will be converted to {@link engine.treeView.DocumentFragment DocumentFragment}:
+ *
+ *		parse( '<b>foo</b><i>bar</i>' ); // Returns DocumentFragment with two child elements.
+ *
+ * Method can parse multiple {@link engine.treeView.Range ranges} provided in string data and return
+ * {@link engine.treeView.Selection Selection} instance containing these ranges. Ranges placed inside
+ * {@link engine.treeView.Text Text} nodes should be marked using `{` and `}` brackets:
+ *
+ * 		const { text, selection } = parse( 'f{ooba}r' );
+ *
+ * Ranges placed outside text nodes should be marked using `[` and `]` brackets:
+ *
+ *		const { root, selection } = parse( '<p>[<b>foobar</b>]</p>' );
+ *
+ * Sometimes there is a need for defining order of ranges inside created selection. This can be achieved by providing
+ * ranges order array as additional parameter:
+ *
+ *		const { root, selection } = parse( '{fo}ob{ar}{ba}z', { order: [ 2, 3, 1 ] } );
+ *
+ * In above example first range (`{fo}`) will be added to selection as second one, second range (`{ar}`) will be added
+ * as third and third range (`{ba}`) will be added as first one.
+ *
+ * If selection's last range should be added as backward one (so the {@link engine.treeView.Selection#anchor selection
+ * anchor} is represented by `end` position and {@link engine.treeView.Selection#focus selection focus} is
+ * represented by `start` position) use `lastRangeBackward` flag:
+ *
+ *		const { root, selection } = parse( `{foo}bar{baz}`, { lastRangeBackward: true } );
+ *
+ * @param {String} data HTML-like string to be parsed.
+ * @param {Object} options
+ * @param {Array.<Number>} [options.order] Array with order of parsed ranges added to returned
+ * {@link engine.treeView.Selection Selection} instance. Each element should represent desired position of each range in
+ * selection instance. For example: `[2, 3, 1]` means that first range will be placed as second, second as third and third as first.
+ * @param {Boolean} [options.lastRangeBackward=false] If set to true last range will be added as backward to the returned
+ * {@link engine.treeView.Selection Selection} instance.
+ * @returns {engine.treeView.Text|engine.treeView.Element|engine.treeView.DocumentFragment|Object} Returns parsed view node
+ * or object with two fields `view` and `selection` when selection ranges were included in data to parse.
+ */
 export function parse( data, options = { } ) {
 	options.order = options.order || [];
 	const viewParser = new ViewParser();
@@ -111,21 +171,36 @@ export function parse( data, options = { } ) {
 	return view;
 }
 
+/**
+ * Private helper class used for converting ranges text representation inside {@link engine.treeView.Text Text nodes}.
+ *
+ * @private
+ */
 class RangeParser {
-	constructor() {
-		// Todo - set in parse method.
+	/**
+	 * Parses the view, and returns ranges represented inside {@link engine.treeView.Text Text nodes}.
+	 * Method will remove all occurrences of `{`, `}`, `[` and `]` from found text nodes. If text node is empty after
+	 * the process - it will be removed too.
+	 *
+	 * @param {engine.treeView.Node} node Starting node.
+	 * @param {Array.<Number>} order Ranges order. Each element should represent desired position of the range after
+	 * sorting. For example: `[2, 3, 1]` means that first range will be placed as second, second as third and third as first.
+	 * @returns {Array.<engine.treeView.Range>} Array with ranges found.
+	 */
+	parse( node, order ) {
 		this._positions = [];
-	}
 
-	parse( node, order ) {
+		// Remove all range brackets from view nodes and save their positions.
 		this._getPositions( node );
+
+		// Create ranges using gathered positions.
 		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 array contains ${ order.length } elements.`
+					`Parse error - there are ${ ranges.length} ranges found, but ranges order array contains ${ order.length } elements.`
 				);
 			}
 
@@ -135,25 +210,17 @@ class RangeParser {
 		return ranges;
 	}
 
-	_sortRanges( ranges, rangesOrder ) {
-		const sortedRanges = [];
-		let index = 0;
-
-		for ( let newPosition of rangesOrder ) {
-			if ( ranges[ newPosition - 1 ] === undefined ) {
-				throw new Error( 'Provided ranges order is invalid.' );
-			}
-
-			sortedRanges[ newPosition - 1] = ranges[ index ];
-			index++;
-		}
-
-		return sortedRanges;
-	}
-
+	/**
+	 * Gathers positions of brackets inside view tree starting from provided node. Method will remove all occurrences of
+	 * `{`, `}`, `[` and `]` from found text nodes. If text node is empty after the process - it will be removed
+	 * too.
+	 *
+	 * @private
+	 * @param {engine.treeView.Node} node Staring node.
+	 */
 	_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
+			// Copy elements into the array, when nodes will be removed from parent node this array will still have all the
 			// items needed for iteration.
 			const children = Array.from( node.getChildren() );
 
@@ -206,7 +273,7 @@ class RangeParser {
 					} 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.` );
+							throw new Error( `Parse 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.
@@ -220,7 +287,7 @@ class RangeParser {
 					}
 				} 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. ` );
+						throw new Error( `Parse error - text range delimiter '${ item.bracket }' is placed inside empty text node. ` );
 					}
 
 					// Store information about element range delimiter.
@@ -233,6 +300,38 @@ class RangeParser {
 		}
 	}
 
+	/**
+	 * Sort ranges in given order. Ranges order should be an array, each element should represent desired position
+	 * of the range after sorting.
+	 * For example: `[2, 3, 1]` means that first range will be placed as second, second as third and third as first.
+	 *
+	 * @private
+	 * @param {Array.<engine.treeView.Range>} ranges Ranges to sort.
+	 * @param {Array.<Number>} rangesOrder Array with new ranges order.
+	 * @returns {Array} Sorted ranges array.
+	 */
+	_sortRanges( ranges, rangesOrder ) {
+		const sortedRanges = [];
+		let index = 0;
+
+		for ( let newPosition of rangesOrder ) {
+			if ( ranges[ newPosition - 1 ] === undefined ) {
+				throw new Error( 'Parse error - provided ranges order is invalid.' );
+			}
+
+			sortedRanges[ newPosition - 1] = ranges[ index ];
+			index++;
+		}
+
+		return sortedRanges;
+	}
+
+	/**
+	 * Uses all found bracket positions to create ranges from them.
+	 *
+	 * @private
+	 * @returns {Array.<engine.treeView.Range}
+	 */
 	_createRanges() {
 		const ranges = [];
 		let range = null;
@@ -240,13 +339,13 @@ class RangeParser {
 		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.` );
+				throw new Error( `Parse 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.` );
+				throw new Error( `Parse 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 ) {
@@ -260,21 +359,43 @@ class RangeParser {
 
 		// Check if all ranges have proper ending.
 		if ( range !== null ) {
-			throw new Error( 'Range was started but no end delimiter was found.' );
+			throw new Error( 'Parse error - range was started but no end delimiter was found.' );
 		}
 
 		return ranges;
 	}
 }
 
+/**
+ * Private helper class used that converts given HTML-like string to view tree.
+ *
+ * @private
+ */
 class ViewParser {
+
+	/**
+	 * Parses HTML-like string to view tree elements.
+	 *
+	 * @param {string} data
+	 * @returns {engine.treeView.Node|engine.treeView.DocumentFragment}
+	 */
 	parse( data ) {
 		const htmlProcessor = new HtmlDataProcessor();
+
+		// Convert HTML string to DOM.
 		const domRoot = htmlProcessor.toDom( data );
 
+		// Convert DOM to View.
 		return this._walkDom( domRoot );
 	}
 
+	/**
+	 * Walks through DOM elements and converts them to tree view elements.
+	 *
+	 * @private
+	 * @param {Node} domNode
+	 * @returns {engine.treeView.Node|engine.treeView.DocumentFragment}
+	 */
 	_walkDom( domNode ) {
 		const isDomElement = domNode instanceof DomElement;
 
@@ -306,6 +427,14 @@ class ViewParser {
 		return new ViewText( domNode.textContent );
 	}
 
+	/**
+	 * Converts DOM Element to {engine.treeView.Element view Element}.
+	 *
+	 * @param {Element} domElement DOM element to convert.
+	 * @returns {engine.treeView.Element|engine.treeView.AttributeElement|engine.treeView.ContainerElement} Tree view
+	 * element converted from DOM element.
+	 * @private
+	 */
 	_convertElement( domElement ) {
 		const info = this._convertElementName( domElement );
 		let viewElement;
@@ -333,6 +462,18 @@ class ViewParser {
 		return viewElement;
 	}
 
+	/**
+	 * Converts DOM element tag name to information needed for creating {@link engine.treeView.Element view Element} instance.
+	 * Name can be provided in couple formats: as a simple element's name (`div`), as a type and name (`container:div`,
+	 * `attribute:span`), as a name and priority (`span:12`) and as a type, priority, name trio (`attribute:span:12`);
+	 *
+	 * @private
+	 * @param {Element} element DOM Element which tag name should be converted.
+	 * @returns {Object} info Object with parsed information.
+	 * @returns {String} info.name Parsed name of the element.
+	 * @returns {String|null} info.type Parsed type of the element, can be `attribute` or `container`.
+	 * @returns {Number|null} info.priority Parsed priority of the element.
+	 */
 	_convertElementName( element ) {
 		const parts = element.tagName.toLowerCase().split( ':' );
 
@@ -367,7 +508,7 @@ class ViewParser {
 				};
 			}
 
-			throw new Error( `Cannot parse element's tag name: ${ element.tagName.toLowerCase() }.` );
+			throw new Error( `Parse error - cannot parse element's tag name: ${ element.tagName.toLowerCase() }.` );
 		}
 
 		// Check if name is in format type:name:priority.
@@ -384,9 +525,16 @@ class ViewParser {
 			}
 		}
 
-		throw new Error( `Cannot parse element's tag name: ${ element.tagName.toLowerCase() }.` );
+		throw new Error( `Parse error - cannot parse element's tag name: ${ element.tagName.toLowerCase() }.` );
 	}
 
+	/**
+	 * Checks if element's type is allowed. Returns `attribute`, `container` or `null`.
+	 *
+	 * @private
+	 * @param {String} type
+	 * @returns {String|null}
+	 */
 	_convertType( type ) {
 		if ( type == 'container' || type == 'attribute' ) {
 			return type;
@@ -395,6 +543,13 @@ class ViewParser {
 		return null;
 	}
 
+	/**
+	 * Checks if given priority is allowed. Returns null if priority cannot be converted.
+	 *
+	 * @private
+	 * @param {String} priorityString
+	 * @returns {Number|Null}
+	 */
 	_convertPriority( priorityString ) {
 		const priority = parseInt( priorityString, 10 );
 
@@ -649,7 +804,6 @@ class ViewStringify {
 	_stringifyElementAttributes( element ) {
 		const attributes = [];
 
-		// TODO: Maybe attributes should be put in alphabetical order, it might be easier to write expected string.
 		for ( let attribute of element.getAttributeKeys() ) {
 			attributes.push( `${ attribute }="${ element.getAttribute( attribute ) }"` );
 		}