8
0
Просмотр исходного кода

Common filters combined as one 'parseHtml()' function.

Krzysztof Krztoń 7 лет назад
Родитель
Сommit
ffffe58355

+ 0 - 137
packages/ckeditor5-paste-from-office/src/filters/common.js

@@ -1,137 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
-
-const htmlDataProcessor = new HtmlDataProcessor();
-
-/**
- * Extracts `body` tag contents from the provided HTML string.
- *
- * @param {Object} data
- * @param {String} data.html HTML string from which `body` contents will be extracted.
- * @returns {Object} result
- * @returns {String|null} result.body Extracted `body` contents. If `body` tag was not present or empty, `null` is returned.
- */
-export function extractBody( data ) {
-	const bodyRegexp = /<body[^>]*>([\s*|\S*]*?)<\/body>/i;
-	const bodyMatch = data.html.match( bodyRegexp );
-
-	data.body = bodyMatch && bodyMatch[ 1 ] ? bodyMatch[ 1 ] : null;
-
-	return data;
-}
-
-/**
- * Parses provided HTML string to {@link module:engine/view/node~Node}
- * or {@link module:engine/view/documentfragment~DocumentFragment} instance.
- *
- * @param {Object} data
- * @param {String} data.body HTML string which should be parsed.
- * @returns {Object} result
- * @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|null} result.view
- * The {@link module:engine/view/node~Node} or {@link module:engine/view/documentfragment~DocumentFragment} class
- * instance created based on provided HTML string. Returns `null` if `data.body` parameter was empty.
- */
-export function bodyToView( data ) {
-	data.view = data.body ? htmlDataProcessor.toView( data.body ) : null;
-
-	return data;
-}
-
-/**
- * Extracts `style` tags content from provided HTML string and combines into one string.
- *
- * @param {Object} data
- * @param {String} data.html HTML string from which `style` tags content will be extracted.
- * @returns {Object} result
- * @returns {String|null} result.styles Extracted `style` tags content. If there were no `style` tags, `null` is returned.
- */
-export function extractStyles( data ) {
-	const styleRegexp = /<style[^>]*>([\s*|\S*]*?)<\/style>/gi;
-
-	let styles = '';
-
-	let styleMatch;
-	while ( ( styleMatch = styleRegexp.exec( data.html ) ) !== null ) {
-		if ( styleMatch && styleMatch[ 1 ] ) {
-			styles += styleMatch[ 1 ];
-		}
-	}
-
-	data.styles = styles.length ? styles : null;
-
-	return data;
-}
-
-/**
- * Parses given styles string returning native `CSSStyleSheet` object.
- *
- * **Important**: during parsing, the browser will remove all invalid properties (e.g. all `mso-something-something: ...`)
- * and all invalid selectors (like `@list l1:level1`).
- *
- * @param {Object} data
- * @param {String} data.styles Styles to be parse.
- * @param {Document} domDocument Document used to create helper element in which stylesheet will be injected.
- * @returns {Object} result
- * @returns {CSSStyleSheet|null} result.stylesheet Native `CSSStyleSheet` object containing parsed styles
- * or `null` if `data.styles` or `domDocument` were not provided.
- */
-export function stylesToStylesheet( data, domDocument ) {
-	data.stylesheet = null;
-
-	if ( data.styles && domDocument ) {
-		// Replace invalid CSS selectors so they can be correctly parsed:
-		//		* `@list lX:levelX` with `at_list lX_levelX`
-		//		* `@list lX` with `at_list lX`
-		const styles = data.styles
-			.replace( /@list\s+l(\d+):level(\d+)/g, 'at_list l$1_level$2' )
-			.replace( /@list /g, 'at_list ' );
-
-		data.stylesheet = parseCSS( styles, domDocument );
-	}
-
-	return data;
-}
-
-// Parses provided CSS string creating native `CSSStyleSheet` object.
-//
-// If available this function uses shadow DOM element to parse CSS. If not, it fallbacks to the iframe element.
-//
-// @param {String} cssString String containing CSS rules/stylsheet to be parsed.
-// @param {Document} domDocument Document used to create helper element in which stylesheet will be injected.
-// @returns {CSSStyleSheet} Native `CSSStyleSheet` object containing parsed styles.
-function parseCSS( cssString, domDocument ) {
-	const style = domDocument.createElement( 'style' );
-
-	let wrapper = null;
-
-	if ( domDocument.head && domDocument.head.attachShadow ) {
-		// Use shadow DOM if available.
-		wrapper = domDocument.createElement( 'div' );
-		const shadowRoot = wrapper.attachShadow( { mode: 'open' } );
-
-		wrapper.hidden = true;
-
-		domDocument.body.appendChild( wrapper );
-		shadowRoot.appendChild( style );
-	} else {
-		// Use iframe element.
-		wrapper = domDocument.createElement( 'iframe' );
-
-		wrapper.hidden = true;
-
-		domDocument.body.appendChild( wrapper );
-		wrapper.contentDocument.documentElement.appendChild( style );
-	}
-
-	style.textContent = cssString;
-
-	const stylesheet = style.sheet;
-
-	wrapper.remove();
-
-	return stylesheet;
-}

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

@@ -3,6 +3,10 @@
  * For licensing, see LICENSE.md.
  */
 
+/**
+ * @module pastefromoffice/filters/list
+ */
+
 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';
@@ -17,24 +21,20 @@ 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 {Object} data
- * @param {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} data.view The view
+ * @param {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} bodyView The view
  * structure which to transform.
- * @returns {Object} result
- * @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|null} result.view The view
+ * @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} The view
  * structure instance with list-like elements transformed into semantic lists.
  */
-export function paragraphsToLists( data ) {
-	if ( data.view ) {
-		const firstChild = data.view.getChild( 0 );
+export function paragraphsToLists( bodyView, stylesString ) {
+	const firstChild = bodyView.getChild( 0 );
 
-		if ( firstChild ) {
-			const listNodes = findAllListNodes( Position.createBefore( firstChild ) );
-			createLists( listNodes, data.styles );
-		}
+	if ( firstChild ) {
+		const listNodes = findAllListNodes( Position.createBefore( firstChild ) );
+		createLists( listNodes, stylesString );
 	}
 
-	return data;
+	return bodyView;
 }
 
 // Writer used for View elements manipulation.
@@ -172,7 +172,7 @@ function findListType( listItem, styles ) {
 		const listStyleTypeMatch = listStyleTypeRegex.exec( listStyleMatch[ 1 ] );
 
 		if ( listStyleTypeMatch && listStyleTypeMatch[ 1 ] ) {
-			listStyleType = listStyleTypeMatch[ 1 ];
+			listStyleType = listStyleTypeMatch[ 1 ].trim();
 		}
 	}
 

+ 88 - 0
packages/ckeditor5-paste-from-office/src/filters/utils.js

@@ -0,0 +1,88 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module pastefromoffice/filters/utils
+ */
+
+/* globals DOMParser */
+
+import DomConverter from '@ckeditor/ckeditor5-engine/src/view/domconverter';
+import { NBSP_FILLER } from '@ckeditor/ckeditor5-engine/src/view/filler';
+
+const domParser = new DOMParser();
+const domConverter = new DomConverter( { blockFiller: NBSP_FILLER } );
+
+/**
+ * Parses provided HTML extracting contents of `body` and `style` tags.
+ *
+ * @param {String} htmlString HTML string to be parsed.
+ * @returns {Object} result
+ * @returns {module:engine/view/documentfragment~DocumentFragment} result.body Parsed body
+ * content as a traversable structure.
+ * @returns {String} result.bodyString Entire body content as a string.
+ * @returns {Array.<CSSStyleSheet>} result.styles Array of native `CSSStyleSheet` objects, each representing
+ * separate `style` tag from the source HTML.
+ * @returns {String} result.stylesString All `style` tags contents combined in the order of occurrence into one string.
+ */
+export function parseHtml( htmlString ) {
+	// Parse htmlString as native Document object.
+	const htmlDocument = domParser.parseFromString( htmlString, 'text/html' );
+
+	// Get `innerHTML` first as transforming to View modifies the source document.
+	const bodyString = htmlDocument.body.innerHTML;
+
+	// Transform document.body to View.
+	const bodyView = documentToView( htmlDocument );
+
+	// Extract stylesheets.
+	const stylesObject = extractStyles( htmlDocument );
+
+	return {
+		body: bodyView,
+		bodyString,
+		styles: stylesObject.styles,
+		stylesString: stylesObject.stylesString
+	};
+}
+
+// Transforms native `Document` object into {@link module:engine/view/documentfragment~DocumentFragment}.
+//
+// @param {Document} htmlDocument Native `Document` object to be transformed.
+// @returns {module:engine/view/documentfragment~DocumentFragment}
+function documentToView( htmlDocument ) {
+	const fragment = htmlDocument.createDocumentFragment();
+	const nodes = htmlDocument.body.childNodes;
+
+	while ( nodes.length > 0 ) {
+		fragment.appendChild( nodes[ 0 ] );
+	}
+
+	return domConverter.domToView( fragment );
+}
+
+// Extracts both `CSSStyleSheet` and string representation from all `style` elements available in a provided `htmlDocument`.
+//
+// @param {Document} htmlDocument Native `Document` object from which styles will be extracted.
+// @returns {Object} result
+// @returns {Array.<CSSStyleSheet>} result.styles Array of native `CSSStyleSheet` object, each representing
+// separate `style` tag from the source object.
+// @returns {String} result.stylesString All `style` tags contents combined in the order of occurrence as one string.
+function extractStyles( htmlDocument ) {
+	const styles = [];
+	const stylesString = [];
+
+	for ( const el of htmlDocument.all ) {
+		if ( el.tagName.toLowerCase() === 'style' && el.sheet && el.sheet.rules && el.sheet.rules.length ) {
+			styles.push( el.sheet );
+			stylesString.push( el.innerHTML );
+		}
+	}
+
+	return {
+		styles,
+		stylesString: stylesString.join( ' ' )
+	};
+}

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

@@ -8,9 +8,8 @@
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import DocumentFragment from '@ckeditor/ckeditor5-engine/src/view/documentfragment';
 
-import { extractBody, bodyToView, extractStyles } from './filters/common';
+import { parseHtml } from './filters/utils';
 import { paragraphsToLists } from './filters/list';
 
 /**
@@ -40,9 +39,9 @@ export default class PasteFromOffice extends Plugin {
 			if ( isWordInput( html ) ) {
 				evt.stop();
 
-				const normalizedInput = this._normalizeWordInput( html, editor );
-
-				editor.plugins.get( 'Clipboard' ).fire( 'inputTransformation', { content: normalizedInput } );
+				editor.plugins.get( 'Clipboard' ).fire( 'inputTransformation', {
+					content: this._normalizeWordInput( html )
+				} );
 			}
 		} );
 	}
@@ -50,22 +49,17 @@ export default class PasteFromOffice extends Plugin {
 	/**
 	 * Normalizes input pasted from Word to format suitable for editor {@link module:engine/model/model~Model}.
 	 *
-	 * @private
-	 * @param {module:core/editor/editor~Editor} editor Editor instance.
+	 * **Notice**: this function was extracted mainly for testing purposes and should not be called directly.
+	 *
+	 * @protected
 	 * @param {String} input Word input.
-	 * @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} view Normalized input.
+	 * @returns {module:engine/view/documentfragment~DocumentFragment} Normalized input.
 	 */
-	_normalizeWordInput( input, editor ) {
-		const editorDocument = editor.editing.view.getDomRoot();
-		const ownerDocument = editorDocument ? editorDocument.ownerDocument : null;
-
-		const transofrmedInput = transformInput( input, ownerDocument,
-			extractBody,
-			bodyToView,
-			extractStyles,
-			paragraphsToLists );
+	_normalizeWordInput( input ) {
+		const { body, stylesString } = parseHtml( input );
+		const normalizedInput = paragraphsToLists( body, stylesString );
 
-		return transofrmedInput.view || new DocumentFragment();
+		return normalizedInput;
 	}
 }
 
@@ -73,27 +67,6 @@ export default class PasteFromOffice extends Plugin {
 //
 // @param {String} html HTML string to test.
 // @returns {Boolean} True if given HTML string is a Word HTML.
-//
 function isWordInput( html ) {
 	return !!( html && html.match( /<meta\s*name="?generator"?\s*content="?microsoft\s*word\s*\d+"?\/?>/gi ) );
 }
-
-// Transforms given HTML string by provided filter functions.
-//
-// @param {String} html HTML string to transform.
-// @param {Document} domDocument Editor owner document.
-// @param {Array.<Function>} filters Functions which are used filter/transform given HTML. Filters are executed
-// in an order they where provided to this function.
-// @returns {Object} data Object containing transformed parts of an input HTML string in a different formats. The number
-// and type of formats depends on a provided filters as each filter function can create separate format or change existing one.
-// @returns {String} data.html Input HTML string.
-// @returns {*} data.* Any type of data created by filter functions. It directly depends on provided filter functions.
-function transformInput( html, domDocument, ...filters ) {
-	let transformedData = { html };
-
-	for ( const filter of filters ) {
-		transformedData = filter( transformedData, domDocument );
-	}
-
-	return transformedData;
-}