浏览代码

List filter refactoring.

Krzysztof Krztoń 7 年之前
父节点
当前提交
20c002603e

+ 107 - 93
packages/ckeditor5-paste-from-office/src/filters/list.js

@@ -9,7 +9,7 @@
 
 import Element from '@ckeditor/ckeditor5-engine/src/view/element';
 import Matcher from '@ckeditor/ckeditor5-engine/src/view/matcher';
-import Position from '@ckeditor/ckeditor5-engine/src/view/position';
+import Range from '@ckeditor/ckeditor5-engine/src/view/range';
 import TreeWalker from '@ckeditor/ckeditor5-engine/src/view/treewalker';
 import UpcastWriter from '@ckeditor/ckeditor5-engine/src/view/upcastwriter';
 
@@ -21,50 +21,62 @@ import UpcastWriter from '@ckeditor/ckeditor5-engine/src/view/upcastwriter';
  *		<p class=MsoListParagraphCxSpFirst style='mso-list:l1 level1 lfo1'>...</p> // Paragraph based list.
  *		<h1 style='mso-list:l0 level1 lfo1'>...</h1> // Heading 1 based list.
  *
- * @param {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} bodyView The view
- * structure which to transform.
- * @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} The view
- * structure instance with list-like elements transformed into semantic lists.
+ * @param {module:engine/view/documentfragment~DocumentFragment} documentFragment The view structure which to transform.
+ * @param {String} stylesString Styles from which list-like elements styling will be extracted.
  */
-export function transformParagraphsToLists( bodyView, stylesString ) {
-	const firstChild = bodyView.getChild( 0 );
-
-	if ( firstChild ) {
-		const listNodes = findAllListNodes( Position.createBefore( firstChild ) );
-		createLists( listNodes, stylesString );
+export function transformListItemLikeElementsIntoLists( documentFragment, stylesString ) {
+	if ( !documentFragment.childCount ) {
+		return;
 	}
 
-	return bodyView;
+	const listLikeItems = findAllListItemLikeElements( documentFragment );
+
+	if ( listLikeItems.length ) {
+		const writer = new UpcastWriter();
+
+		let currentList = null;
+
+		for ( let i = 0; i < listLikeItems.length; i++ ) {
+			if ( !currentList || listLikeItems[ i - 1 ].id !== listLikeItems[ i ].id ) {
+				const listStyle = findListStyle( listLikeItems[ i ], stylesString );
+				currentList = insertEmptyList( listStyle, listLikeItems[ i ].element, writer );
+			}
+
+			const listItem = transformElementIntoListItem( listLikeItems[ i ].element, writer );
+
+			writer.appendChild( listItem, currentList );
+		}
+	}
 }
 
-// Finds all list-like nodes starting from a given position.
+// Finds all list-like elements in a given document fragment.
 //
-// @param {module:engine/src/view/position~Position} startPosition Position from which to start looking.
-// @returns {Array.<Object>} Array of found list items. Each item is an object containing:
+// @param {module:engine/view/documentfragment~DocumentFragment} documentFragment Document fragment
+// in which to look for list-like nodes.
+// @returns {Array.<Object>} Array of found list-like items. Each item is an object containing:
 //
 //		* {module:engine/src/view/element~Element} element List-like element.
 //		* {Number} id List item id parsed from `mso-list` style (see `getListItemData()` function).
 //		* {Number} order List item creation order parsed from `mso-list` style (see `getListItemData()` function).
 //		* {Number} indent List item indentation level parsed from `mso-list` style (see `getListItemData()` function).
-function findAllListNodes( startPosition ) {
-	const treeWalker = new TreeWalker( { startPosition, ignoreElementEnd: true } );
+function findAllListItemLikeElements( documentFragment ) {
+	const treeWalker = new TreeWalker( { boundaries: Range.createIn( documentFragment ), ignoreElementEnd: true } );
 
 	// Matcher for finding list-like elements.
-	const listMatcher = new Matcher( {
+	const listItemLikeElementsMatcher = new Matcher( {
 		name: /^p|h\d+$/,
 		styles: {
 			'mso-list': /.*/
 		}
 	} );
 
-	// Find all list nodes.
-	const listNodes = [];
+	const listLikeItems = [];
 
 	for ( const value of treeWalker ) {
-		if ( value.type === 'elementStart' && listMatcher.match( value.item ) ) {
+		if ( value.type === 'elementStart' && listItemLikeElementsMatcher.match( value.item ) ) {
 			const itemData = getListItemData( value.item );
 
-			listNodes.push( {
+			listLikeItems.push( {
 				element: value.item,
 				id: itemData.id,
 				order: itemData.order,
@@ -73,41 +85,79 @@ function findAllListNodes( startPosition ) {
 		}
 	}
 
-	return listNodes;
+	return listLikeItems;
 }
 
-// Transforms given list-like nodes into semantic lists. As the function operates on a provided
-// {module:engine/src/view/element~Element elements}, it will modify the view structure to which those list elements belongs.
+// Checks list item style based on a provided CSS.
 //
-// @param {Array.<Object>} listItems Array containing list items data. Usually it is the output of `findAllListNodes()` function.
-// @param {String} styles CSS styles which may contain additional data about lists format.
-function createLists( listItems, styles ) {
-	if ( listItems.length ) {
-		const writer = new UpcastWriter();
+// List item style is extracted from CSS stylesheet. Each list with its specific style attribute
+// value (`mso-list:l1 level1 lfo1`) has its dedicated properties in a CSS stylesheet defined with a selector like:
+//
+// 		@list l1:level1 { ... }
+//
+// It contains `mso-level-number-format` property which defines list numbering/bullet style. If this property
+// is not defined it means default `decimal` numbering.
+//
+// Here CSS string representation is used as `mso-level-number-format` property is an invalid CSS property
+// and will be removed during CSS parsing.
+//
+// @param {Object} listLikeItem List-like item for which list style will be searched for. Usually
+// a result of `findAllListItemLikeElements()` function.
+// @param {String} stylesString CSS stylesheet.
+// @returns {Object} result
+// @returns {String} result.type List type, could be `ul` or `ol`.
+// @returns {String} result.style List style, for example: `decimal`, `lower-roman`, etc. It is extracted
+// directly from Word stylesheet without further processing and may be not compatible
+// with CSS `list-style-type` property accepted values.
+function findListStyle( listLikeItem, stylesString ) {
+	const listStyleRegexp = new RegExp( `@list l${ listLikeItem.id }:level${ listLikeItem.indent }\\s*({[^}]*)`, 'gi' );
+	const listStyleTypeRegex = /mso-level-number-format:([^;]*);/gi;
 
-		let currentList = null;
-		let previousListItem = null;
+	const listStyleMatch = listStyleRegexp.exec( stylesString );
 
-		for ( const listItem of listItems ) {
-			const listNode = listItem.element;
+	let listStyleType = 'decimal'; // Decimal is default one.
+	if ( listStyleMatch && listStyleMatch[ 1 ] ) {
+		const listStyleTypeMatch = listStyleTypeRegex.exec( listStyleMatch[ 1 ] );
 
-			if ( !previousListItem || previousListItem.id !== listItem.id ) {
-				const listStyle = findListType( listItem, styles );
-				currentList = new Element( listStyle.type );
-				writer.insertChild( listNode.parent.getChildIndex( listNode ), currentList, listNode.parent );
-			}
+		if ( listStyleTypeMatch && listStyleTypeMatch[ 1 ] ) {
+			listStyleType = listStyleTypeMatch[ 1 ].trim();
+		}
+	}
 
-			removeBulletElement( listNode, writer );
+	return {
+		type: listStyleType !== 'bullet' && listStyleType !== 'image' ? 'ol' : 'ul',
+		style: listStyleType
+	};
+}
 
-			writer.appendChild( listNode, currentList );
-			writer.rename( 'li', listNode );
+// Creates empty list of a given type and inserts it after a specified element.
+//
+// @param {Object} listStyle List style object which determines the type of newly created list.
+// Usually a result of `findListStyle()` function.
+// @param {module:engine/view/element~Element} element Element before which list is inserted.
+// @param {module:engine/view/upcastwriter~UpcastWriter} writer
+// @returns {module:engine/view/element~Element} Newly created list element.
+function insertEmptyList( listStyle, element, writer ) {
+	const list = new Element( listStyle.type );
+	const position = element.parent.getChildIndex( element );
 
-			previousListItem = listItem;
-		}
-	}
+	writer.insertChild( position, list, element.parent );
+
+	return list;
+}
+
+// Transforms given element into a semantic list item. As the function operates on a provided
+// {module:engine/src/view/element~Element element} it will modify the view structure to which this element belongs.
+//
+// @param {module:engine/view/element~Element} element Element which will be transformed into list item.
+// @param {module:engine/view/upcastwriter~UpcastWriter} writer
+function transformElementIntoListItem( element, writer ) {
+	removeBulletElement( element, writer );
+
+	return writer.rename( 'li', element );
 }
 
-// Extracts list information from Word specific list style like:
+// Extracts list item information from Word specific list-like element style:
 //
 //		`style="mso-list:l1 level1 lfo1"`
 //
@@ -117,11 +167,11 @@ function createLists( listItems, styles ) {
 //		* `level1` is a list item indentation level,
 //		* `lfo1` is a list insertion order in a document.
 //
-// @param {module:engine/view/element~Element} element List-like element from which data is extracted.
+// @param {module:engine/view/element~Element} element Element from which style data is extracted.
 // @returns {Object} result
-// @returns {Number} result.id List id.
-// @returns {Number} result.order List creation order.
-// @returns {Number} result.indent List indentation level.
+// @returns {Number} result.id Parent list id.
+// @returns {Number} result.order List item creation order.
+// @returns {Number} result.indent List item indentation level.
 function getListItemData( element ) {
 	const data = {};
 	const listStyle = element.getStyle( 'mso-list' );
@@ -135,50 +185,11 @@ function getListItemData( element ) {
 	return data;
 }
 
-// Checks list item style based on a provided CSS.
-//
-// List item style is extracted from CSS stylesheet. Each list with its specific style attribute value (`mso-list:l1 level1 lfo1`)
-// has its dedicated properties in a CSS stylesheet defined with a selector like:
+// Removes span with a numbering/bullet from a given element.
 //
-// 		@list l1:level1 { ... }
-//
-// It contains `mso-level-number-format` property which defines list numbering/bullet style. If this property
-// is not defined it means default `decimal` numbering.
-//
-// Here CSS string representation is used as `mso-level-number-format` is invalid CSS property which gets removed during parsing.
-//
-// @param {Object} listItem List item for which list style will be searched for.
-// @param {String} styles CSS stylesheet.
-// @returns {Object} result
-// @returns {String} result.type Type of the list, could be `ul` or `ol`.
-// @returns {String} result.style List style like `decimal`, `lower-roman`, etc. It is passed directly from Word stylesheet
-// so may be not compatible with CSS `list-style-type` accepted values.
-function findListType( listItem, styles ) {
-	const listStyleRegexp = new RegExp( `@list l${ listItem.id }:level${ listItem.indent }\\s*({[^}]*)`, 'gi' );
-	const listStyleTypeRegex = /mso-level-number-format:([^;]*);/gi;
-
-	const listStyleMatch = listStyleRegexp.exec( styles );
-
-	let listStyleType = 'decimal'; // Decimal is default one.
-	if ( listStyleMatch && listStyleMatch[ 1 ] ) {
-		const listStyleTypeMatch = listStyleTypeRegex.exec( listStyleMatch[ 1 ] );
-
-		if ( listStyleTypeMatch && listStyleTypeMatch[ 1 ] ) {
-			listStyleType = listStyleTypeMatch[ 1 ].trim();
-		}
-	}
-
-	return {
-		type: listStyleType !== 'bullet' && listStyleType !== 'image' ? 'ol' : 'ul',
-		style: listStyleType
-	};
-}
-
-// Removes span with a numbering/bullet from the given list element.
-//
-// @param {module:engine/view/element~Element} listElement
+// @param {module:engine/view/element~Element} element
 // @param {module:engine/view/upcastwriter~UpcastWriter} writer
-function removeBulletElement( listElement, writer ) {
+function removeBulletElement( element, writer ) {
 	// Matcher for finding `span` elements holding lists numbering/bullets.
 	const bulletMatcher = new Matcher( {
 		name: 'span',
@@ -187,7 +198,10 @@ function removeBulletElement( listElement, writer ) {
 		}
 	} );
 
-	const treeWalker = new TreeWalker( { startPosition: Position.createBefore( listElement.getChild( 0 ) ), ignoreElementEnd: true } );
+	const treeWalker = new TreeWalker( {
+		boundaries: Range.createIn( element ),
+		ignoreElementEnd: true
+	} );
 
 	for ( const value of treeWalker ) {
 		if ( value.type === 'elementStart' && bulletMatcher.match( value.item ) ) {

+ 3 - 3
packages/ckeditor5-paste-from-office/src/pastefromoffice.js

@@ -11,7 +11,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
 
 import { parseHtml } from './filters/utils';
-import { transformParagraphsToLists } from './filters/list';
+import { transformListItemLikeElementsIntoLists } from './filters/list';
 
 /**
  * The Paste from Office plugin.
@@ -57,9 +57,9 @@ export default class PasteFromOffice extends Plugin {
 	 */
 	_normalizeWordInput( input ) {
 		const { body, stylesString } = parseHtml( input );
-		const normalizedInput = transformParagraphsToLists( body, stylesString );
+		transformListItemLikeElementsIntoLists( body, stylesString );
 
-		return normalizedInput;
+		return body;
 	}
 }
 

+ 20 - 20
packages/ckeditor5-paste-from-office/tests/filters/list.js

@@ -6,62 +6,62 @@
 import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
 import { stringify } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 
-import { transformParagraphsToLists } from '../../src/filters/list';
+import { transformListItemLikeElementsIntoLists } from '../../src/filters/list';
 
 describe( 'Filters – list', () => {
 	const htmlDataProcessor = new HtmlDataProcessor();
 
-	describe( 'transformParagraphsToLists', () => {
+	describe( 'transformListItemLikeElementsIntoLists', () => {
 		it( 'replaces list-like elements with semantic lists', () => {
 			const html = '<p style="mso-list:l0 level1 lfo0"><span style="mso-list:Ignore">1.</span>Item 1</p>';
 			const view = htmlDataProcessor.toView( html );
 
-			const result = transformParagraphsToLists( view, '' );
+			transformListItemLikeElementsIntoLists( view, '' );
 
-			expect( result.childCount ).to.equal( 1 );
-			expect( result.getChild( 0 ).name ).to.equal( 'ol' );
-			expect( stringify( result ) ).to.equal( '<ol><li style="mso-list:l0 level1 lfo0">Item 1</li></ol>' );
+			expect( view.childCount ).to.equal( 1 );
+			expect( view.getChild( 0 ).name ).to.equal( 'ol' );
+			expect( stringify( view ) ).to.equal( '<ol><li style="mso-list:l0 level1 lfo0">Item 1</li></ol>' );
 		} );
 
 		it( 'replaces list-like elements with semantic lists with proper bullet type based on styles', () => {
 			const html = '<p style="mso-list:l0 level1 lfo0"><span style="mso-list:Ignore">1.</span>Item 1</p>';
 			const view = htmlDataProcessor.toView( html );
 
-			const result = transformParagraphsToLists( view, '@list l0:level1 { mso-level-number-format: bullet; }' );
+			transformListItemLikeElementsIntoLists( view, '@list l0:level1 { mso-level-number-format: bullet; }' );
 
-			expect( result.childCount ).to.equal( 1 );
-			expect( result.getChild( 0 ).name ).to.equal( 'ul' );
-			expect( stringify( result ) ).to.equal( '<ul><li style="mso-list:l0 level1 lfo0">Item 1</li></ul>' );
+			expect( view.childCount ).to.equal( 1 );
+			expect( view.getChild( 0 ).name ).to.equal( 'ul' );
+			expect( stringify( view ) ).to.equal( '<ul><li style="mso-list:l0 level1 lfo0">Item 1</li></ul>' );
 		} );
 
 		it( 'does not modify the view if there are no list-like elements', () => {
 			const html = '<h1>H1</h1><p>Foo Bar</p>';
 			const view = htmlDataProcessor.toView( html );
 
-			const result = transformParagraphsToLists( view, '' );
+			transformListItemLikeElementsIntoLists( view, '' );
 
-			expect( result.childCount ).to.equal( 2 );
-			expect( stringify( result ) ).to.equal( html );
+			expect( view.childCount ).to.equal( 2 );
+			expect( stringify( view ) ).to.equal( html );
 		} );
 
 		it( 'handles empty `mso-list` style correctly', () => {
 			const html = '<p style="mso-list:"><span style="mso-list:Ignore">1.</span>Item 1</p>';
 			const view = htmlDataProcessor.toView( html );
 
-			const result = transformParagraphsToLists( view, '' );
+			transformListItemLikeElementsIntoLists( view, '' );
 
-			expect( result.childCount ).to.equal( 1 );
-			expect( result.getChild( 0 ).name ).to.equal( 'ol' );
-			expect( stringify( result ) ).to.equal( '<ol><li style="mso-list:">Item 1</li></ol>' );
+			expect( view.childCount ).to.equal( 1 );
+			expect( view.getChild( 0 ).name ).to.equal( 'ol' );
+			expect( stringify( view ) ).to.equal( '<ol><li style="mso-list:">Item 1</li></ol>' );
 		} );
 
 		it( 'handles empty body correctly', () => {
 			const view = htmlDataProcessor.toView( '' );
 
-			const result = transformParagraphsToLists( view, '' );
+			transformListItemLikeElementsIntoLists( view, '' );
 
-			expect( result.childCount ).to.equal( 0 );
-			expect( stringify( result ) ).to.equal( '' );
+			expect( view.childCount ).to.equal( 0 );
+			expect( stringify( view ) ).to.equal( '' );
 		} );
 	} );
 } );