Sfoglia il codice sorgente

Merge pull request #62 from ckeditor/t/61

Feature: Prevent of bolding entire content pasted from Google Docs. Closes #61.
Maciej 6 anni fa
parent
commit
9fbbcede08
31 ha cambiato i file con 770 aggiunte e 394 eliminazioni
  1. 25 0
      packages/ckeditor5-paste-from-office/src/filters/removeboldwrapper.js
  2. 34 0
      packages/ckeditor5-paste-from-office/src/normalizer.jsdoc
  3. 36 0
      packages/ckeditor5-paste-from-office/src/normalizers/googledocsnormalizer.js
  4. 41 0
      packages/ckeditor5-paste-from-office/src/normalizers/mswordnormalizer.js
  5. 34 38
      packages/ckeditor5-paste-from-office/src/pastefromoffice.js
  6. 51 0
      packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/index.js
  7. 4 0
      packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/input.firefox.html
  8. 5 0
      packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/input.html
  9. 1 0
      packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/model.html
  10. 1 0
      packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/normalized.firefox.html
  11. 1 0
      packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/normalized.html
  12. 1 0
      packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/simple-text-windows.html
  13. 1 0
      packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/input.firefox.html
  14. 1 0
      packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/input.html
  15. 1 0
      packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/model.html
  16. 1 0
      packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/normalized.firefox.html
  17. 1 0
      packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/normalized.html
  18. 1 0
      packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/simple-text.html
  19. 6 2
      packages/ckeditor5-paste-from-office/tests/_utils/fixtures.js
  20. 19 6
      packages/ckeditor5-paste-from-office/tests/_utils/utils.js
  21. 10 1
      packages/ckeditor5-paste-from-office/tests/data/integration.js
  22. 8 1
      packages/ckeditor5-paste-from-office/tests/data/normalization.js
  23. 75 77
      packages/ckeditor5-paste-from-office/tests/filters/image.js
  24. 40 42
      packages/ckeditor5-paste-from-office/tests/filters/list.js
  25. 102 104
      packages/ckeditor5-paste-from-office/tests/filters/parse.js
  26. 50 0
      packages/ckeditor5-paste-from-office/tests/filters/reomoveboldwrapper.js
  27. 66 67
      packages/ckeditor5-paste-from-office/tests/filters/space.js
  28. 8 3
      packages/ckeditor5-paste-from-office/tests/manual/integration.html
  29. 25 0
      packages/ckeditor5-paste-from-office/tests/normalizers/googledocsnormalizer.js
  30. 32 0
      packages/ckeditor5-paste-from-office/tests/normalizers/mswordnormalizer.js
  31. 89 53
      packages/ckeditor5-paste-from-office/tests/pastefromoffice.js

+ 25 - 0
packages/ckeditor5-paste-from-office/src/filters/removeboldwrapper.js

@@ -0,0 +1,25 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module paste-from-office/filters/removeboldwrapper
+ */
+
+/**
+ * Removes `<b>` tag wrapper added by Google Docs to a copied content.
+ *
+ * @param {module:engine/view/documentfragment~DocumentFragment} documentFragment element `data.content` obtained from clipboard
+ * @param {module:engine/view/upcastwriter~UpcastWriter} writer
+ */
+export default function removeBoldWrapper( documentFragment, writer ) {
+	for ( const child of documentFragment.getChildren() ) {
+		if ( child.is( 'b' ) && child.getStyle( 'font-weight' ) === 'normal' ) {
+			const childIndex = documentFragment.getChildIndex( child );
+
+			writer.remove( child );
+			writer.insertChild( childIndex, child.getChildren(), documentFragment );
+		}
+	}
+}

+ 34 - 0
packages/ckeditor5-paste-from-office/src/normalizer.jsdoc

@@ -0,0 +1,34 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module paste-from-office/normalizer
+ */
+
+/**
+ * Interface defining a content transformation pasted from an external editor.
+ *
+ * Normalizers are registered by the {@link module:paste-from-office/pastefromoffice~PasteFromOffice} plugin and run on
+ * {@link module:clipboard/clipboard~Clipboard#event:inputTransformation inputTransformation event}. They detect environment-specific
+ * quirks and transform it into a form compatible with other CKEditor features.
+ *
+ * @interface Normalizer
+ */
+
+/**
+ * Must return `true` if the `htmlString` contains content which this normalizer can transform.
+ *
+ * @method #isActive
+ * @param {String} htmlString full content of `dataTransfer.getData( 'text/html' )`
+ * @returns {Boolean}
+ */
+
+/**
+ * Executes the normalization of a given data.
+ *
+ * @method #execute
+ * @param {Object} data object obtained from
+ * {@link module:clipboard/clipboard~Clipboard#event:inputTransformation inputTransformation event}.
+ */

+ 36 - 0
packages/ckeditor5-paste-from-office/src/normalizers/googledocsnormalizer.js

@@ -0,0 +1,36 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module paste-from-office/normalizers/googledocsnormalizer
+ */
+
+import removeBoldWrapper from '../filters/removeboldwrapper';
+import UpcastWriter from '@ckeditor/ckeditor5-engine/src/view/upcastwriter';
+
+const googleDocsMatch = /id=("|')docs-internal-guid-[-0-9a-f]+("|')/i;
+
+/**
+ * Normalizer for the content pasted from Google Docs.
+ *
+ * @implements module:paste-from-office/normalizer~Normalizer
+ */
+export default class GoogleDocsNormalizer {
+	/**
+	 * @inheritDoc
+	 */
+	isActive( htmlString ) {
+		return googleDocsMatch.test( htmlString );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	execute( data ) {
+		const writer = new UpcastWriter();
+
+		removeBoldWrapper( data.content, writer );
+	}
+}

+ 41 - 0
packages/ckeditor5-paste-from-office/src/normalizers/mswordnormalizer.js

@@ -0,0 +1,41 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module paste-from-office/normalizers/mswordnormalizer
+ */
+
+import { parseHtml } from '../filters/parse';
+import { transformListItemLikeElementsIntoLists } from '../filters/list';
+import { replaceImagesSourceWithBase64 } from '../filters/image';
+
+const msWordMatch1 = /<meta\s*name="?generator"?\s*content="?microsoft\s*word\s*\d+"?\/?>/i;
+const msWordMatch2 = /xmlns:o="urn:schemas-microsoft-com/i;
+
+/**
+ * Normalizer for the content pasted from Microsoft Word.
+ *
+ * @implements module:paste-from-office/normalizer~Normalizer
+ */
+export default class MSWordNormalizer {
+	/**
+	 * @inheritDoc
+	 */
+	isActive( htmlString ) {
+		return msWordMatch1.test( htmlString ) || msWordMatch2.test( htmlString );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	execute( data ) {
+		const { body, stylesString } = parseHtml( data.dataTransfer.getData( 'text/html' ) );
+
+		transformListItemLikeElementsIntoLists( body, stylesString );
+		replaceImagesSourceWithBase64( body, data.dataTransfer.getData( 'text/rtf' ) );
+
+		data.content = body;
+	}
+}

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

@@ -9,16 +9,21 @@
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 
-import { parseHtml } from './filters/parse';
-import { transformListItemLikeElementsIntoLists } from './filters/list';
-import { replaceImagesSourceWithBase64 } from './filters/image';
+import GoogleDocsNormalizer from './normalizers/googledocsnormalizer';
+import MSWordNormalizer from './normalizers/mswordnormalizer';
+import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
 
 /**
  * The Paste from Office plugin.
  *
- * This plugin handles content pasted from Office apps (for now only Word) and transforms it (if necessary)
+ * This plugin handles content pasted from Office apps and transforms it (if necessary)
  * to a valid structure which can then be understood by the editor features.
  *
+ * Transformation is made by a set of predefined {@link module:paste-from-office/normalizer~Normalizer normalizers}.
+ * This plugin includes following normalizers:
+ *   * {@link module:paste-from-office/normalizer/mswordnormalizer~MSWordNormalizer Microsoft Word normalizer}
+ *   * {@link module:paste-from-office/normalizer/googledocsnormalizer~GoogleDocsNormalizer Google Docs normalizer}
+ *
  * For more information about this feature check the {@glink api/paste-from-office package page}.
  *
  * @extends module:core/plugin~Plugin
@@ -34,46 +39,37 @@ export default class PasteFromOffice extends Plugin {
 	/**
 	 * @inheritDoc
 	 */
+	static get requires() {
+		return [ Clipboard ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
 	init() {
 		const editor = this.editor;
+		const normalizers = [];
 
-		this.listenTo( editor.plugins.get( 'Clipboard' ), 'inputTransformation', ( evt, data ) => {
-			const html = data.dataTransfer.getData( 'text/html' );
+		normalizers.push( new MSWordNormalizer() );
+		normalizers.push( new GoogleDocsNormalizer() );
 
-			if ( data.pasteFromOfficeProcessed !== true && isWordInput( html ) ) {
-				data.content = this._normalizeWordInput( html, data.dataTransfer );
+		editor.plugins.get( 'Clipboard' ).on(
+			'inputTransformation',
+			( evt, data ) => {
+				if ( data.isTransformedWithPasteFromOffice ) {
+					return;
+				}
 
-				// Set the flag so if `inputTransformation` is re-fired, PFO will not process it again (#44).
-				data.pasteFromOfficeProcessed = true;
-			}
-		}, { priority: 'high' } );
-	}
-
-	/**
-	 * Normalizes input pasted from Word to format suitable for editor {@link module:engine/model/model~Model}.
-	 *
-	 * **Note**: this function was exposed mainly for testing purposes and should not be called directly.
-	 *
-	 * @protected
-	 * @param {String} input Word input.
-	 * @param {module:clipboard/datatransfer~DataTransfer} dataTransfer Data transfer instance.
-	 * @returns {module:engine/view/documentfragment~DocumentFragment} Normalized input.
-	 */
-	_normalizeWordInput( input, dataTransfer ) {
-		const { body, stylesString } = parseHtml( input );
+				const htmlString = data.dataTransfer.getData( 'text/html' );
+				const activeNormalizer = normalizers.find( normalizer => normalizer.isActive( htmlString ) );
 
-		transformListItemLikeElementsIntoLists( body, stylesString );
-		replaceImagesSourceWithBase64( body, dataTransfer.getData( 'text/rtf' ) );
+				if ( activeNormalizer ) {
+					activeNormalizer.execute( data );
 
-		return body;
+					data.isTransformedWithPasteFromOffice = true;
+				}
+			},
+			{ priority: 'high' }
+		);
 	}
 }
-
-// Checks if given HTML string is a result of pasting content from Word.
-//
-// @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 ) ||
-		html.match( /xmlns:o="urn:schemas-microsoft-com/gi ) ) );
-}

+ 51 - 0
packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/index.js

@@ -0,0 +1,51 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import simpleText from './simple-text/input.html';
+import simpleTextWindows from './simple-text-windows/input.html';
+
+import simpleTextNormalized from './simple-text/normalized.html';
+import simpleTextWindowsNormalized from './simple-text-windows/normalized.html';
+
+import simpleTextModel from './simple-text/model.html';
+import simpleTextWindowsModel from './simple-text-windows/model.html';
+
+export const fixtures = {
+	input: {
+		simpleText,
+		simpleTextWindows
+	},
+	normalized: {
+		simpleText: simpleTextNormalized,
+		simpleTextWindows: simpleTextWindowsNormalized
+	},
+	model: {
+		simpleText: simpleTextModel,
+		simpleTextWindows: simpleTextWindowsModel
+	}
+};
+
+import simpleTextFirefox from './simple-text/input.firefox.html';
+import simpleTextWindowsFirefox from './simple-text-windows/input.firefox.html';
+
+import simpleTextNormalizedFirefox from './simple-text/normalized.firefox.html';
+import simpleTextWindowsNormalizedFirefox from './simple-text-windows/normalized.firefox.html';
+
+export const browserFixtures = {
+	firefox: {
+		input: {
+			simpleText: simpleTextFirefox,
+			simpleTextWindows: simpleTextWindowsFirefox
+		},
+		normalized: {
+			simpleText: simpleTextNormalizedFirefox,
+			simpleTextWindows: simpleTextWindowsNormalizedFirefox
+		},
+		model: {
+			simpleText: simpleTextModel,
+			simpleTextWindows: simpleTextWindowsModel
+		}
+	}
+};

+ 4 - 0
packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/input.firefox.html

@@ -0,0 +1,4 @@
+<html><body>
+<!--StartFragment--><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;" id="docs-internal-guid-f8b26bf1-7fff-40c0-af18-1234567890ab"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Hello world</span></p><!--EndFragment-->
+</body>
+</html>

+ 5 - 0
packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/input.html

@@ -0,0 +1,5 @@
+<html>
+<body>
+<!--StartFragment--><b style="font-weight:normal;" id="docs-internal-guid-0954d9f2-7fff-2b3c-4978-1234567890ab"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Hello world</span></p></b><br class="Apple-interchange-newline"><!--EndFragment-->
+</body>
+</html>

+ 1 - 0
packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/model.html

@@ -0,0 +1 @@
+<paragraph>Hello world</paragraph>

+ 1 - 0
packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/normalized.firefox.html

@@ -0,0 +1 @@
+<p dir="ltr" id="docs-internal-guid-f8b26bf1-7fff-40c0-af18-1234567890ab" style="line-height:1.38;margin-bottom:0pt;margin-top:0pt"><span style="background-color:transparent;color:#000000;font-family:Arial;font-size:11pt;font-style:normal;font-variant:normal;font-weight:400;text-decoration:none;vertical-align:baseline;white-space:pre-wrap">Hello world</span></p>

+ 1 - 0
packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/normalized.html

@@ -0,0 +1 @@
+<p dir="ltr" style="line-height:1.38;margin-bottom:0pt;margin-top:0pt"><span style="background-color:transparent;color:#000000;font-family:Arial;font-size:11pt;font-style:normal;font-variant:normal;font-weight:400;text-decoration:none;vertical-align:baseline;white-space:pre-wrap">Hello world</span></p><br class="Apple-interchange-newline">

File diff suppressed because it is too large
+ 1 - 0
packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/simple-text-windows.html


+ 1 - 0
packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/input.firefox.html

@@ -0,0 +1 @@
+<meta charset="utf-8"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;" id="docs-internal-guid-b205dcb1-7fff-2468-bad5-1234567890ab"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Hello world</span></p>

File diff suppressed because it is too large
+ 1 - 0
packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/input.html


+ 1 - 0
packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/model.html

@@ -0,0 +1 @@
+<paragraph>Hello world</paragraph>

+ 1 - 0
packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/normalized.firefox.html

@@ -0,0 +1 @@
+<p dir="ltr" id="docs-internal-guid-b205dcb1-7fff-2468-bad5-1234567890ab" style="line-height:1.38;margin-bottom:0pt;margin-top:0pt"><span style="background-color:transparent;color:#000000;font-family:Arial;font-size:11pt;font-style:normal;font-variant:normal;font-weight:400;text-decoration:none;vertical-align:baseline;white-space:pre-wrap">Hello world</span></p>

+ 1 - 0
packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/normalized.html

@@ -0,0 +1 @@
+<p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Hello world</span></p><br class="Apple-interchange-newline">

File diff suppressed because it is too large
+ 1 - 0
packages/ckeditor5-paste-from-office/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/simple-text.html


+ 6 - 2
packages/ckeditor5-paste-from-office/tests/_utils/fixtures.js

@@ -9,6 +9,8 @@ import { fixtures as image, browserFixtures as imageBrowser } from '../_data/ima
 import { fixtures as link, browserFixtures as linkBrowser } from '../_data/link/index.js';
 import { fixtures as list, browserFixtures as listBrowser } from '../_data/list/index.js';
 import { fixtures as spacing, browserFixtures as spacingBrowser } from '../_data/spacing/index.js';
+import { fixtures as googleDocsBoldWrapper, browserFixtures as googleDocsBoldWrapperBrowser }
+	from '../_data/paste-from-google-docs/bold-wrapper/index';
 
 // Generic fixtures.
 export const fixtures = {
@@ -16,7 +18,8 @@ export const fixtures = {
 	image,
 	link,
 	list,
-	spacing
+	spacing,
+	'google-docs-bold-wrapper': googleDocsBoldWrapper
 };
 
 // Browser specific fixtures.
@@ -25,5 +28,6 @@ export const browserFixtures = {
 	image: imageBrowser,
 	link: linkBrowser,
 	list: listBrowser,
-	spacing: spacingBrowser
+	spacing: spacingBrowser,
+	'google-docs-bold-wrapper': googleDocsBoldWrapperBrowser
 };

+ 19 - 6
packages/ckeditor5-paste-from-office/tests/_utils/utils.js

@@ -8,12 +8,16 @@
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 
+import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
+import normalizeClipboardData from '@ckeditor/ckeditor5-clipboard/src/utils/normalizeclipboarddata';
 import normalizeHtml from '@ckeditor/ckeditor5-utils/tests/_utils/normalizehtml';
 import { setData, stringify as stringifyModel } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { stringify as stringifyView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 
 import { fixtures, browserFixtures } from './fixtures';
 
+const htmlDataProcessor = new HtmlDataProcessor();
+
 /**
  * Mocks dataTransfer object which can be used for simulating paste.
  *
@@ -124,6 +128,8 @@ function groupFixturesByBrowsers( browsers, fixturesGroup, skipBrowsers ) {
 }
 
 // Generates normalization tests based on a provided fixtures. For each input fixture one test is generated.
+// Please notice that normalization compares generated Views, not DOM. That's why there might appear some not familiar structures,
+// like closing tags for void tags, for example `<br></br>`.
 //
 // @param {String} title Tests group title.
 // @param {Object} fixtures Object containing fixtures.
@@ -131,32 +137,39 @@ function groupFixturesByBrowsers( browsers, fixturesGroup, skipBrowsers ) {
 // @param {Array.<String>} skip Array of fixtures names which tests should be skipped.
 function generateNormalizationTests( title, fixtures, editorConfig, skip ) {
 	describe( title, () => {
-		let editor, pasteFromOfficePlugin;
+		let editor;
 
 		beforeEach( () => {
 			return VirtualTestEditor
 				.create( editorConfig )
 				.then( newEditor => {
 					editor = newEditor;
-
-					pasteFromOfficePlugin = editor.plugins.get( 'PasteFromOffice' );
 				} );
 		} );
 
 		afterEach( () => {
 			editor.destroy();
-
-			pasteFromOfficePlugin = null;
 		} );
 
 		for ( const name of Object.keys( fixtures.input ) ) {
 			( skip.indexOf( name ) !== -1 ? it.skip : it )( name, () => {
+				// Simulate data from Clipboard event
+				const clipboardPlugin = editor.plugins.get( 'Clipboard' );
+				const content = htmlDataProcessor.toView( normalizeClipboardData( fixtures.input[ name ] ) );
 				const dataTransfer = createDataTransfer( {
+					'text/html': fixtures.input[ name ],
 					'text/rtf': fixtures.inputRtf && fixtures.inputRtf[ name ]
 				} );
 
+				// data.content might be completely overwritten with a new object, so we need obtain final result for comparison.
+				clipboardPlugin.on( 'inputTransformation', ( evt, data ) => {
+					evt.return = data.content;
+				}, { priority: 'lowest' } );
+
+				const transformedContent = clipboardPlugin.fire( 'inputTransformation', { content, dataTransfer } );
+
 				expectNormalized(
-					pasteFromOfficePlugin._normalizeWordInput( fixtures.input[ name ], dataTransfer ),
+					transformedContent,
 					fixtures.normalized[ name ]
 				);
 			} );

+ 10 - 1
packages/ckeditor5-paste-from-office/tests/data/integration.js

@@ -22,7 +22,7 @@ import { generateTests } from '../_utils/utils';
 
 const browsers = [ 'chrome', 'firefox', 'safari', 'edge' ];
 
-describe( 'Paste from Office', () => {
+describe( 'PasteFromOffice - integration', () => {
 	generateTests( {
 		input: 'basic-styles',
 		type: 'integration',
@@ -82,4 +82,13 @@ describe( 'Paste from Office', () => {
 			plugins: [ Clipboard, Paragraph, Bold, Italic, Underline, PasteFromOffice ]
 		}
 	} );
+
+	generateTests( {
+		input: 'google-docs-bold-wrapper',
+		type: 'integration',
+		browsers,
+		editorConfig: {
+			plugins: [ Clipboard, Paragraph, Bold, PasteFromOffice ]
+		}
+	} );
 } );

+ 8 - 1
packages/ckeditor5-paste-from-office/tests/data/normalization.js

@@ -14,7 +14,7 @@ const editorConfig = {
 	plugins: [ Clipboard, PasteFromOffice ]
 };
 
-describe( 'Paste from Office', () => {
+describe( 'PasteFromOffice - normalization', () => {
 	generateTests( {
 		input: 'basic-styles',
 		type: 'normalization',
@@ -49,4 +49,11 @@ describe( 'Paste from Office', () => {
 		browsers,
 		editorConfig
 	} );
+
+	generateTests( {
+		input: 'google-docs-bold-wrapper',
+		type: 'normalization',
+		browsers,
+		editorConfig
+	} );
 } );

+ 75 - 77
packages/ckeditor5-paste-from-office/tests/filters/image.js

@@ -12,98 +12,96 @@ import { parseHtml } from '../../src/filters/parse';
 import { replaceImagesSourceWithBase64, _convertHexToBase64 } from '../../src/filters/image';
 import { browserFixtures } from '../_data/image/index';
 
-describe( 'Paste from Office', () => {
-	describe( 'Filters', () => {
-		describe( 'image', () => {
-			let editor;
-
-			describe( 'replaceImagesSourceWithBase64()', () => {
-				describe( 'with RTF', () => {
-					beforeEach( () => {
-						return VirtualTestEditor
-							.create( {} )
-							.then( editorInstance => {
-								editor = editorInstance;
-							} );
-					} );
-
-					afterEach( () => {
-						editor.destroy();
-					} );
-
-					it( 'should handle correctly empty RTF data', () => {
-						const input = '<p>Foo <img src="file://test.jpg" /></p>';
-						const rtfString = '';
-						const { body } = parseHtml( input );
-
-						replaceImagesSourceWithBase64( body, rtfString, editor.editing.model );
-
-						expect( stringifyView( body ) ).to.equal( normalizeHtml( input ) );
-					} );
-
-					it( 'should not change image with "http://" source', () => {
-						const input = '<p>Foo <img src="http://ckeditor.com/logo.jpg" /></p>';
-						const rtfString = browserFixtures.chrome.inputRtf.onlineOffline;
-						const { body } = parseHtml( input );
-
-						replaceImagesSourceWithBase64( body, rtfString, editor.editing.model );
-
-						expect( stringifyView( body ) ).to.equal( normalizeHtml( input ) );
-					} );
-
-					it( 'should not change image with "file://" source if not images in RTF data', () => {
-						const input = '<p>Foo <img src="file://test.jpg" /></p>';
-						const rtfString = '{\\rtf1\\adeflang1025\\ansi\\ansicpg1252\\uc1\\adeff31507}';
-						const { body } = parseHtml( input );
-
-						replaceImagesSourceWithBase64( body, rtfString, editor.editing.model );
-
-						expect( stringifyView( body ) ).to.equal( normalizeHtml( input ) );
-					} );
+describe( 'PasteFromOffice - filters', () => {
+	describe( 'image', () => {
+		let editor;
+
+		describe( 'replaceImagesSourceWithBase64()', () => {
+			describe( 'with RTF', () => {
+				beforeEach( () => {
+					return VirtualTestEditor
+						.create( {} )
+						.then( editorInstance => {
+							editor = editorInstance;
+						} );
 				} );
-			} );
-
-			describe( '_convertHexToBase64()', () => {
-				it( '#1', () => {
-					const hex = '48656c6c6f20576f726c6421';
-					const base64 = 'SGVsbG8gV29ybGQh';
 
-					expect( _convertHexToBase64( hex ) ).to.equal( base64 );
+				afterEach( () => {
+					editor.destroy();
 				} );
 
-				it( '#2', () => {
-					const hex = '466f6f204261722042617a';
-					const base64 = 'Rm9vIEJhciBCYXo=';
+				it( 'should handle correctly empty RTF data', () => {
+					const input = '<p>Foo <img src="file://test.jpg" /></p>';
+					const rtfString = '';
+					const { body } = parseHtml( input );
+
+					replaceImagesSourceWithBase64( body, rtfString, editor.editing.model );
 
-					expect( _convertHexToBase64( hex ) ).to.equal( base64 );
+					expect( stringifyView( body ) ).to.equal( normalizeHtml( input ) );
 				} );
 
-				it( '#3', () => {
-					const hex = '687474703a2f2f636b656469746f722e636f6d';
-					const base64 = 'aHR0cDovL2NrZWRpdG9yLmNvbQ==';
+				it( 'should not change image with "http://" source', () => {
+					const input = '<p>Foo <img src="http://ckeditor.com/logo.jpg" /></p>';
+					const rtfString = browserFixtures.chrome.inputRtf.onlineOffline;
+					const { body } = parseHtml( input );
 
-					expect( _convertHexToBase64( hex ) ).to.equal( base64 );
+					replaceImagesSourceWithBase64( body, rtfString, editor.editing.model );
+
+					expect( stringifyView( body ) ).to.equal( normalizeHtml( input ) );
 				} );
 
-				it( '#4', () => {
-					const hex = '434B456469746F72203520697320746865206265737421';
-					const base64 = 'Q0tFZGl0b3IgNSBpcyB0aGUgYmVzdCE=';
+				it( 'should not change image with "file://" source if not images in RTF data', () => {
+					const input = '<p>Foo <img src="file://test.jpg" /></p>';
+					const rtfString = '{\\rtf1\\adeflang1025\\ansi\\ansicpg1252\\uc1\\adeff31507}';
+					const { body } = parseHtml( input );
+
+					replaceImagesSourceWithBase64( body, rtfString, editor.editing.model );
 
-					expect( _convertHexToBase64( hex ) ).to.equal( base64 );
+					expect( stringifyView( body ) ).to.equal( normalizeHtml( input ) );
 				} );
+			} );
+		} );
 
-				it( '#5', () => {
-					const hex = '496E74726F6475636564204D6564696120656D6265642C20626C6F636B20636F6E74656E7420696E207461626' +
-						'C657320616E6420696E746567726174696F6E73207769746820416E67756C617220322B20616E642052656163742E2046' +
-						'696E64206F7574206D6F726520696E2074686520434B456469746F722035207631312E312E302072656C6561736564206' +
-						'26C6F6720706F73742E';
+		describe( '_convertHexToBase64()', () => {
+			it( '#1', () => {
+				const hex = '48656c6c6f20576f726c6421';
+				const base64 = 'SGVsbG8gV29ybGQh';
 
-					const base64 = 'SW50cm9kdWNlZCBNZWRpYSBlbWJlZCwgYmxvY2sgY29udGVudCBpbiB0YWJsZXMgYW5kIGludGVncmF0aW9ucy' +
-						'B3aXRoIEFuZ3VsYXIgMisgYW5kIFJlYWN0LiBGaW5kIG91dCBtb3JlIGluIHRoZSBDS0VkaXRvciA1IHYxMS4xLjAgcmVsZWF' +
-						'zZWQgYmxvZyBwb3N0Lg==';
+				expect( _convertHexToBase64( hex ) ).to.equal( base64 );
+			} );
 
-					expect( _convertHexToBase64( hex ) ).to.equal( base64 );
-				} );
+			it( '#2', () => {
+				const hex = '466f6f204261722042617a';
+				const base64 = 'Rm9vIEJhciBCYXo=';
+
+				expect( _convertHexToBase64( hex ) ).to.equal( base64 );
+			} );
+
+			it( '#3', () => {
+				const hex = '687474703a2f2f636b656469746f722e636f6d';
+				const base64 = 'aHR0cDovL2NrZWRpdG9yLmNvbQ==';
+
+				expect( _convertHexToBase64( hex ) ).to.equal( base64 );
+			} );
+
+			it( '#4', () => {
+				const hex = '434B456469746F72203520697320746865206265737421';
+				const base64 = 'Q0tFZGl0b3IgNSBpcyB0aGUgYmVzdCE=';
+
+				expect( _convertHexToBase64( hex ) ).to.equal( base64 );
+			} );
+
+			it( '#5', () => {
+				const hex = '496E74726F6475636564204D6564696120656D6265642C20626C6F636B20636F6E74656E7420696E207461626' +
+					'C657320616E6420696E746567726174696F6E73207769746820416E67756C617220322B20616E642052656163742E2046' +
+					'696E64206F7574206D6F726520696E2074686520434B456469746F722035207631312E312E302072656C6561736564206' +
+					'26C6F6720706F73742E';
+
+				const base64 = 'SW50cm9kdWNlZCBNZWRpYSBlbWJlZCwgYmxvY2sgY29udGVudCBpbiB0YWJsZXMgYW5kIGludGVncmF0aW9ucy' +
+					'B3aXRoIEFuZ3VsYXIgMisgYW5kIFJlYWN0LiBGaW5kIG91dCBtb3JlIGluIHRoZSBDS0VkaXRvciA1IHYxMS4xLjAgcmVsZWF' +
+					'zZWQgYmxvZyBwb3N0Lg==';
+
+				expect( _convertHexToBase64( hex ) ).to.equal( base64 );
 			} );
 		} );
 	} );

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

@@ -9,63 +9,61 @@ import View from '@ckeditor/ckeditor5-engine/src/view/view';
 
 import { transformListItemLikeElementsIntoLists } from '../../src/filters/list';
 
-describe( 'Paste from Office', () => {
-	describe( 'Filters', () => {
-		describe( 'list', () => {
-			const htmlDataProcessor = new HtmlDataProcessor();
+describe( 'PasteFromOffice - filters', () => {
+	describe( 'list', () => {
+		const htmlDataProcessor = new HtmlDataProcessor();
 
-			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 );
+		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 );
 
-					transformListItemLikeElementsIntoLists( view, '', new View() );
+				transformListItemLikeElementsIntoLists( view, '', new View() );
 
-					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>' );
-				} );
+				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 );
+			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 );
 
-					transformListItemLikeElementsIntoLists( view, '@list l0:level1 { mso-level-number-format: bullet; }', new View() );
+				transformListItemLikeElementsIntoLists( view, '@list l0:level1 { mso-level-number-format: bullet; }', new View() );
 
-					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>' );
-				} );
+				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 );
+			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 );
 
-					transformListItemLikeElementsIntoLists( view, '', new View() );
+				transformListItemLikeElementsIntoLists( view, '', new View() );
 
-					expect( view.childCount ).to.equal( 2 );
-					expect( stringify( view ) ).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 );
+			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 );
 
-					transformListItemLikeElementsIntoLists( view, '', new View() );
+				transformListItemLikeElementsIntoLists( view, '', new View() );
 
-					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>' );
-				} );
+				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( '' );
+			it( 'handles empty body correctly', () => {
+				const view = htmlDataProcessor.toView( '' );
 
-					transformListItemLikeElementsIntoLists( view, '', new View() );
+				transformListItemLikeElementsIntoLists( view, '', new View() );
 
-					expect( view.childCount ).to.equal( 0 );
-					expect( stringify( view ) ).to.equal( '' );
-				} );
+				expect( view.childCount ).to.equal( 0 );
+				expect( stringify( view ) ).to.equal( '' );
 			} );
 		} );
 	} );

+ 102 - 104
packages/ckeditor5-paste-from-office/tests/filters/parse.js

@@ -9,159 +9,157 @@ import DocumentFragment from '@ckeditor/ckeditor5-engine/src/view/documentfragme
 
 import { parseHtml } from '../../src/filters/parse';
 
-describe( 'Paste from Office', () => {
-	describe( 'Filters', () => {
-		describe( 'parse', () => {
-			describe( 'parseHtml()', () => {
-				it( 'correctly parses HTML with body and one style tag', () => {
-					const html = '<head><style>p { color: red; } a { font-size: 12px; }</style></head><body><p>Foo Bar</p></body>';
-					const { body, bodyString, styles, stylesString } = parseHtml( html );
+describe( 'PasteFromOffice - filters', () => {
+	describe( 'parse', () => {
+		describe( 'parseHtml()', () => {
+			it( 'correctly parses HTML with body and one style tag', () => {
+				const html = '<head><style>p { color: red; } a { font-size: 12px; }</style></head><body><p>Foo Bar</p></body>';
+				const { body, bodyString, styles, stylesString } = parseHtml( html );
 
-					expect( body ).to.instanceof( DocumentFragment );
-					expect( body.childCount ).to.equal( 1, 'body.childCount' );
+				expect( body ).to.instanceof( DocumentFragment );
+				expect( body.childCount ).to.equal( 1, 'body.childCount' );
 
-					expect( bodyString ).to.equal( '<p>Foo Bar</p>' );
+				expect( bodyString ).to.equal( '<p>Foo Bar</p>' );
 
-					expect( styles.length ).to.equal( 1, 'styles.length' );
-					expect( styles[ 0 ] ).to.instanceof( CSSStyleSheet );
-					expect( styles[ 0 ].cssRules.length ).to.equal( 2 );
-					expect( styles[ 0 ].cssRules[ 0 ].style.color ).to.equal( 'red' );
-					expect( styles[ 0 ].cssRules[ 1 ].style[ 'font-size' ] ).to.equal( '12px' );
+				expect( styles.length ).to.equal( 1, 'styles.length' );
+				expect( styles[ 0 ] ).to.instanceof( CSSStyleSheet );
+				expect( styles[ 0 ].cssRules.length ).to.equal( 2 );
+				expect( styles[ 0 ].cssRules[ 0 ].style.color ).to.equal( 'red' );
+				expect( styles[ 0 ].cssRules[ 1 ].style[ 'font-size' ] ).to.equal( '12px' );
 
-					expect( stylesString ).to.equal( 'p { color: red; } a { font-size: 12px; }' );
-				} );
+				expect( stylesString ).to.equal( 'p { color: red; } a { font-size: 12px; }' );
+			} );
 
-				it( 'correctly parses HTML with body contents only', () => {
-					const html = '<p>Foo Bar</p>';
-					const { body, bodyString, styles, stylesString } = parseHtml( html );
+			it( 'correctly parses HTML with body contents only', () => {
+				const html = '<p>Foo Bar</p>';
+				const { body, bodyString, styles, stylesString } = parseHtml( html );
 
-					expect( body ).to.instanceof( DocumentFragment );
-					expect( body.childCount ).to.equal( 1 );
+				expect( body ).to.instanceof( DocumentFragment );
+				expect( body.childCount ).to.equal( 1 );
 
-					expect( bodyString ).to.equal( '<p>Foo Bar</p>' );
+				expect( bodyString ).to.equal( '<p>Foo Bar</p>' );
 
-					expect( styles.length ).to.equal( 0 );
+				expect( styles.length ).to.equal( 0 );
 
-					expect( stylesString ).to.equal( '' );
-				} );
+				expect( stylesString ).to.equal( '' );
+			} );
 
-				it( 'correctly parses HTML with no body and multiple style tags', () => {
-					const html = '<html><head><style>p { color: blue; }</style><style>a { color: green; }</style></head></html>';
-					const { body, bodyString, styles, stylesString } = parseHtml( html );
+			it( 'correctly parses HTML with no body and multiple style tags', () => {
+				const html = '<html><head><style>p { color: blue; }</style><style>a { color: green; }</style></head></html>';
+				const { body, bodyString, styles, stylesString } = parseHtml( html );
 
-					expect( body ).to.instanceof( DocumentFragment );
-					expect( body.childCount ).to.equal( 0 );
+				expect( body ).to.instanceof( DocumentFragment );
+				expect( body.childCount ).to.equal( 0 );
 
-					expect( bodyString ).to.equal( '' );
+				expect( bodyString ).to.equal( '' );
 
-					expect( styles.length ).to.equal( 2 );
-					expect( styles[ 0 ] ).to.instanceof( CSSStyleSheet );
-					expect( styles[ 1 ] ).to.instanceof( CSSStyleSheet );
-					expect( styles[ 0 ].cssRules.length ).to.equal( 1 );
-					expect( styles[ 1 ].cssRules.length ).to.equal( 1 );
-					expect( styles[ 0 ].cssRules[ 0 ].style.color ).to.equal( 'blue' );
-					expect( styles[ 1 ].cssRules[ 0 ].style.color ).to.equal( 'green' );
+				expect( styles.length ).to.equal( 2 );
+				expect( styles[ 0 ] ).to.instanceof( CSSStyleSheet );
+				expect( styles[ 1 ] ).to.instanceof( CSSStyleSheet );
+				expect( styles[ 0 ].cssRules.length ).to.equal( 1 );
+				expect( styles[ 1 ].cssRules.length ).to.equal( 1 );
+				expect( styles[ 0 ].cssRules[ 0 ].style.color ).to.equal( 'blue' );
+				expect( styles[ 1 ].cssRules[ 0 ].style.color ).to.equal( 'green' );
 
-					expect( stylesString ).to.equal( 'p { color: blue; } a { color: green; }' );
-				} );
+				expect( stylesString ).to.equal( 'p { color: blue; } a { color: green; }' );
+			} );
 
-				it( 'correctly parses HTML with no body and no style tags', () => {
-					const html = '<html><head><meta name="Foo" content="Bar"></head></html>';
-					const { body, bodyString, styles, stylesString } = parseHtml( html );
+			it( 'correctly parses HTML with no body and no style tags', () => {
+				const html = '<html><head><meta name="Foo" content="Bar"></head></html>';
+				const { body, bodyString, styles, stylesString } = parseHtml( html );
 
-					expect( body ).to.instanceof( DocumentFragment );
-					expect( body.childCount ).to.equal( 0 );
+				expect( body ).to.instanceof( DocumentFragment );
+				expect( body.childCount ).to.equal( 0 );
 
-					expect( bodyString ).to.equal( '' );
+				expect( bodyString ).to.equal( '' );
 
-					expect( styles.length ).to.equal( 0 );
+				expect( styles.length ).to.equal( 0 );
 
-					expect( stylesString ).to.equal( '' );
-				} );
+				expect( stylesString ).to.equal( '' );
+			} );
 
-				it( 'correctly parses HTML with body contents and empty style tag', () => {
-					const html = '<head><style></style></head><body><p>Foo Bar</p></body>';
-					const { body, bodyString, styles, stylesString } = parseHtml( html );
+			it( 'correctly parses HTML with body contents and empty style tag', () => {
+				const html = '<head><style></style></head><body><p>Foo Bar</p></body>';
+				const { body, bodyString, styles, stylesString } = parseHtml( html );
 
-					expect( body ).to.instanceof( DocumentFragment );
-					expect( body.childCount ).to.equal( 1 );
+				expect( body ).to.instanceof( DocumentFragment );
+				expect( body.childCount ).to.equal( 1 );
 
-					expect( bodyString ).to.equal( '<p>Foo Bar</p>' );
+				expect( bodyString ).to.equal( '<p>Foo Bar</p>' );
 
-					expect( styles.length ).to.equal( 0 );
+				expect( styles.length ).to.equal( 0 );
 
-					expect( stylesString ).to.equal( '' );
-				} );
+				expect( stylesString ).to.equal( '' );
+			} );
 
-				it( 'should remove any content after body closing tag - plain', () => {
-					const html = '<html><head></head><body><p>Foo Bar</p></body>Ba</html>';
-					const { body, bodyString, styles, stylesString } = parseHtml( html );
+			it( 'should remove any content after body closing tag - plain', () => {
+				const html = '<html><head></head><body><p>Foo Bar</p></body>Ba</html>';
+				const { body, bodyString, styles, stylesString } = parseHtml( html );
 
-					expect( body ).to.instanceof( DocumentFragment );
-					expect( body.childCount ).to.equal( 1, 'body.childCount' );
+				expect( body ).to.instanceof( DocumentFragment );
+				expect( body.childCount ).to.equal( 1, 'body.childCount' );
 
-					expect( bodyString ).to.equal( '<p>Foo Bar</p>' );
+				expect( bodyString ).to.equal( '<p>Foo Bar</p>' );
 
-					expect( styles.length ).to.equal( 0 );
+				expect( styles.length ).to.equal( 0 );
 
-					expect( stylesString ).to.equal( '' );
-				} );
+				expect( stylesString ).to.equal( '' );
+			} );
 
-				it( 'should remove any content after body closing tag - inline', () => {
-					const html = '<html><head></head><body><p>Foo Bar</p></body><span>Fo</span></html>';
-					const { body, bodyString, styles, stylesString } = parseHtml( html );
+			it( 'should remove any content after body closing tag - inline', () => {
+				const html = '<html><head></head><body><p>Foo Bar</p></body><span>Fo</span></html>';
+				const { body, bodyString, styles, stylesString } = parseHtml( html );
 
-					expect( body ).to.instanceof( DocumentFragment );
-					expect( body.childCount ).to.equal( 1, 'body.childCount' );
+				expect( body ).to.instanceof( DocumentFragment );
+				expect( body.childCount ).to.equal( 1, 'body.childCount' );
 
-					expect( bodyString ).to.equal( '<p>Foo Bar</p>' );
+				expect( bodyString ).to.equal( '<p>Foo Bar</p>' );
 
-					expect( styles.length ).to.equal( 0 );
+				expect( styles.length ).to.equal( 0 );
 
-					expect( stylesString ).to.equal( '' );
-				} );
+				expect( stylesString ).to.equal( '' );
+			} );
 
-				it( 'should remove any content after body closing tag - block', () => {
-					const html = '<html><head></head><body><p>Foo Bar</p></body><p>ar</p></html>';
-					const { body, bodyString, styles, stylesString } = parseHtml( html );
+			it( 'should remove any content after body closing tag - block', () => {
+				const html = '<html><head></head><body><p>Foo Bar</p></body><p>ar</p></html>';
+				const { body, bodyString, styles, stylesString } = parseHtml( html );
 
-					expect( body ).to.instanceof( DocumentFragment );
-					expect( body.childCount ).to.equal( 1, 'body.childCount' );
+				expect( body ).to.instanceof( DocumentFragment );
+				expect( body.childCount ).to.equal( 1, 'body.childCount' );
 
-					expect( bodyString ).to.equal( '<p>Foo Bar</p>' );
+				expect( bodyString ).to.equal( '<p>Foo Bar</p>' );
 
-					expect( styles.length ).to.equal( 0 );
+				expect( styles.length ).to.equal( 0 );
 
-					expect( stylesString ).to.equal( '' );
-				} );
+				expect( stylesString ).to.equal( '' );
+			} );
 
-				it( 'should remove any content after body closing tag - no html tag', () => {
-					const html = '<head></head><body><p>Foo Bar</p></body>oo';
-					const { body, bodyString, styles, stylesString } = parseHtml( html );
+			it( 'should remove any content after body closing tag - no html tag', () => {
+				const html = '<head></head><body><p>Foo Bar</p></body>oo';
+				const { body, bodyString, styles, stylesString } = parseHtml( html );
 
-					expect( body ).to.instanceof( DocumentFragment );
-					expect( body.childCount ).to.equal( 1, 'body.childCount' );
+				expect( body ).to.instanceof( DocumentFragment );
+				expect( body.childCount ).to.equal( 1, 'body.childCount' );
 
-					expect( bodyString ).to.equal( '<p>Foo Bar</p>' );
+				expect( bodyString ).to.equal( '<p>Foo Bar</p>' );
 
-					expect( styles.length ).to.equal( 0 );
+				expect( styles.length ).to.equal( 0 );
 
-					expect( stylesString ).to.equal( '' );
-				} );
+				expect( stylesString ).to.equal( '' );
+			} );
 
-				it( 'should not remove any content if no body tag', () => {
-					const html = '<p>Foo Bar</p>Baz';
-					const { body, bodyString, styles, stylesString } = parseHtml( html );
+			it( 'should not remove any content if no body tag', () => {
+				const html = '<p>Foo Bar</p>Baz';
+				const { body, bodyString, styles, stylesString } = parseHtml( html );
 
-					expect( body ).to.instanceof( DocumentFragment );
-					expect( body.childCount ).to.equal( 2, 'body.childCount' );
+				expect( body ).to.instanceof( DocumentFragment );
+				expect( body.childCount ).to.equal( 2, 'body.childCount' );
 
-					expect( bodyString ).to.equal( '<p>Foo Bar</p>Baz' );
+				expect( bodyString ).to.equal( '<p>Foo Bar</p>Baz' );
 
-					expect( styles.length ).to.equal( 0 );
+				expect( styles.length ).to.equal( 0 );
 
-					expect( stylesString ).to.equal( '' );
-				} );
+				expect( stylesString ).to.equal( '' );
 			} );
 		} );
 	} );

+ 50 - 0
packages/ckeditor5-paste-from-office/tests/filters/reomoveboldwrapper.js

@@ -0,0 +1,50 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
+import removeBoldWrapper from '../../src/filters/removeboldwrapper';
+import UpcastWriter from '@ckeditor/ckeditor5-engine/src/view/upcastwriter';
+
+describe( 'PasteFromOffice - filters', () => {
+	const htmlDataProcessor = new HtmlDataProcessor();
+	describe( 'removeBoldWrapper', () => {
+		let writer;
+
+		before( () => {
+			writer = new UpcastWriter();
+		} );
+
+		it( 'should remove bold wrapper added by google docs', () => {
+			const inputData = '<b style="font-weight:normal;" id="docs-internal-guid-45309eee-7fff-33a3-6dbd-1234567890ab">' +
+				'<p>Hello world</p>' +
+				'</b>';
+			const documentFragment = htmlDataProcessor.toView( inputData );
+
+			removeBoldWrapper( documentFragment, writer );
+
+			expect( htmlDataProcessor.toData( documentFragment ) ).to.equal( '<p>Hello world</p>' );
+		} );
+
+		it( 'should not remove non-bold tag with google id', () => {
+			const inputData = '<p id="docs-internal-guid-e4b9bad6-7fff-c086-3135-1234567890ab">Hello world</p>';
+			const documentFragment = htmlDataProcessor.toView( inputData );
+
+			removeBoldWrapper( documentFragment, writer );
+
+			expect( htmlDataProcessor.toData( documentFragment ) ).to.equal(
+				'<p id="docs-internal-guid-e4b9bad6-7fff-c086-3135-1234567890ab">Hello world</p>' );
+		} );
+
+		it( 'should not remove bold tag without google id', () => {
+			const inputData = '<b>Hello world</b>';
+			const documentFragment = htmlDataProcessor.toView( inputData );
+
+			removeBoldWrapper( documentFragment, writer );
+
+			expect( htmlDataProcessor.toData( documentFragment ) ).to.equal(
+				'<b>Hello world</b>' );
+		} );
+	} );
+} );

+ 66 - 67
packages/ckeditor5-paste-from-office/tests/filters/space.js

@@ -6,100 +6,99 @@
 /* globals DOMParser */
 
 import { normalizeSpacing, normalizeSpacerunSpans } from '../../src/filters/space';
-describe( 'Paste from Office', () => {
-	describe( 'Filters', () => {
-		describe( 'space', () => {
-			describe( 'normalizeSpacing()', () => {
-				it( 'should replace last space before closing tag with NBSP', () => {
-					const input = '<p>Foo </p><p><span> Bar  </span> Baz </p>';
-					const expected = '<p>Foo\u00A0</p><p><span> Bar \u00A0</span> Baz\u00A0</p>';
 
-					expect( normalizeSpacing( input ) ).to.equal( expected );
-				} );
+describe( 'PasteFromOffice - filters', () => {
+	describe( 'space', () => {
+		describe( 'normalizeSpacing()', () => {
+			it( 'should replace last space before closing tag with NBSP', () => {
+				const input = '<p>Foo </p><p><span> Bar  </span> Baz </p>';
+				const expected = '<p>Foo\u00A0</p><p><span> Bar \u00A0</span> Baz\u00A0</p>';
 
-				it( 'should replace last space before special "o:p" tag with NBSP', () => {
-					const input = '<p>Foo  <o:p></o:p><span> <o:p></o:p> Bar</span></p>';
-					const expected = '<p>Foo \u00A0<o:p></o:p><span>\u00A0<o:p></o:p> Bar</span></p>';
+				expect( normalizeSpacing( input ) ).to.equal( expected );
+			} );
 
-					expect( normalizeSpacing( input ) ).to.equal( expected );
-				} );
+			it( 'should replace last space before special "o:p" tag with NBSP', () => {
+				const input = '<p>Foo  <o:p></o:p><span> <o:p></o:p> Bar</span></p>';
+				const expected = '<p>Foo \u00A0<o:p></o:p><span>\u00A0<o:p></o:p> Bar</span></p>';
 
-				it( 'should remove newlines from spacerun spans #1', () => {
-					const input = '<span style=\'mso-spacerun:yes\'>  \n</span>';
-					const expected = '<span style=\'mso-spacerun:yes\'> \u00A0</span>';
+				expect( normalizeSpacing( input ) ).to.equal( expected );
+			} );
 
-					expect( normalizeSpacing( input ) ).to.equal( expected );
-				} );
+			it( 'should remove newlines from spacerun spans #1', () => {
+				const input = '<span style=\'mso-spacerun:yes\'>  \n</span>';
+				const expected = '<span style=\'mso-spacerun:yes\'> \u00A0</span>';
 
-				it( 'should remove newlines from spacerun spans #2', () => {
-					const input = '<span style=\'mso-spacerun:yes\'> \r\n</span>';
-					const expected = '<span style=\'mso-spacerun:yes\'>\u00A0</span>';
+				expect( normalizeSpacing( input ) ).to.equal( expected );
+			} );
 
-					expect( normalizeSpacing( input ) ).to.equal( expected );
-				} );
+			it( 'should remove newlines from spacerun spans #2', () => {
+				const input = '<span style=\'mso-spacerun:yes\'> \r\n</span>';
+				const expected = '<span style=\'mso-spacerun:yes\'>\u00A0</span>';
 
-				it( 'should remove newlines from spacerun spans #3', () => {
-					const input = '<span style=\'mso-spacerun:yes\'>  \r\n\n  </span>';
-					const expected = '<span style=\'mso-spacerun:yes\'>   \u00A0</span>';
+				expect( normalizeSpacing( input ) ).to.equal( expected );
+			} );
 
-					expect( normalizeSpacing( input ) ).to.equal( expected );
-				} );
+			it( 'should remove newlines from spacerun spans #3', () => {
+				const input = '<span style=\'mso-spacerun:yes\'>  \r\n\n  </span>';
+				const expected = '<span style=\'mso-spacerun:yes\'>   \u00A0</span>';
 
-				it( 'should remove newlines from spacerun spans #4', () => {
-					const input = '<span style=\'mso-spacerun:yes\'>\n\n\n  </span>';
-					const expected = '<span style=\'mso-spacerun:yes\'> \u00A0</span>';
+				expect( normalizeSpacing( input ) ).to.equal( expected );
+			} );
 
-					expect( normalizeSpacing( input ) ).to.equal( expected );
-				} );
+			it( 'should remove newlines from spacerun spans #4', () => {
+				const input = '<span style=\'mso-spacerun:yes\'>\n\n\n  </span>';
+				const expected = '<span style=\'mso-spacerun:yes\'> \u00A0</span>';
 
-				it( 'should remove newlines from spacerun spans #5', () => {
-					const input = '<span style=\'mso-spacerun:yes\'>\n\n</span>';
-					const expected = '';
+				expect( normalizeSpacing( input ) ).to.equal( expected );
+			} );
 
-					expect( normalizeSpacing( input ) ).to.equal( expected );
-				} );
+			it( 'should remove newlines from spacerun spans #5', () => {
+				const input = '<span style=\'mso-spacerun:yes\'>\n\n</span>';
+				const expected = '';
 
-				it( 'should remove multiline sequences of whitespaces', () => {
-					const input = '<p>Foo</p> \n\n   \n<p>Bar</p>   \r\n\r\n  <p>Baz</p>';
-					const expected = '<p>Foo</p><p>Bar</p><p>Baz</p>';
+				expect( normalizeSpacing( input ) ).to.equal( expected );
+			} );
 
-					expect( normalizeSpacing( input ) ).to.equal( expected );
-				} );
+			it( 'should remove multiline sequences of whitespaces', () => {
+				const input = '<p>Foo</p> \n\n   \n<p>Bar</p>   \r\n\r\n  <p>Baz</p>';
+				const expected = '<p>Foo</p><p>Bar</p><p>Baz</p>';
 
-				it( 'should normalize Safari "space spans"', () => {
-					const input = '<p>Foo <span class="Apple-converted-space">   </span> Baz <span>  </span></p>';
-					const expected = '<p>Foo \u00A0 \u00A0 Baz \u00A0\u00A0</p>';
+				expect( normalizeSpacing( input ) ).to.equal( expected );
+			} );
 
-					expect( normalizeSpacing( input ) ).to.equal( expected );
-				} );
+			it( 'should normalize Safari "space spans"', () => {
+				const input = '<p>Foo <span class="Apple-converted-space">   </span> Baz <span>  </span></p>';
+				const expected = '<p>Foo \u00A0 \u00A0 Baz \u00A0\u00A0</p>';
 
-				it( 'should normalize nested Safari "space spans"', () => {
-					const input =
-						'<p> Foo <span class="Apple-converted-space"> <span class="Apple-converted-space">    </span></span> Baz</p>';
+				expect( normalizeSpacing( input ) ).to.equal( expected );
+			} );
 
-					const expected = '<p> Foo \u00A0 \u00A0 \u00A0 Baz</p>';
+			it( 'should normalize nested Safari "space spans"', () => {
+				const input =
+					'<p> Foo <span class="Apple-converted-space"> <span class="Apple-converted-space">    </span></span> Baz</p>';
 
-					expect( normalizeSpacing( input ) ).to.equal( expected );
-				} );
+				const expected = '<p> Foo \u00A0 \u00A0 \u00A0 Baz</p>';
+
+				expect( normalizeSpacing( input ) ).to.equal( expected );
 			} );
+		} );
 
-			describe( 'normalizeSpacerunSpans()', () => {
-				it( 'should normalize spaces inside special "span.spacerun" elements', () => {
-					const input = '<p> <span style=\'mso-spacerun:yes\'>   </span>Foo</p>' +
-						'<p> Baz <span style=\'mso-spacerun:yes\'>      </span></p>';
+		describe( 'normalizeSpacerunSpans()', () => {
+			it( 'should normalize spaces inside special "span.spacerun" elements', () => {
+				const input = '<p> <span style=\'mso-spacerun:yes\'>   </span>Foo</p>' +
+					'<p> Baz <span style=\'mso-spacerun:yes\'>      </span></p>';
 
-					const expected = '<p> <span style="mso-spacerun:yes">&nbsp; &nbsp;</span>Foo</p>' +
-						'<p> Baz <span style="mso-spacerun:yes">&nbsp; &nbsp; &nbsp; </span></p>';
+				const expected = '<p> <span style="mso-spacerun:yes">&nbsp; &nbsp;</span>Foo</p>' +
+					'<p> Baz <span style="mso-spacerun:yes">&nbsp; &nbsp; &nbsp; </span></p>';
 
-					const domParser = new DOMParser();
-					const htmlDocument = domParser.parseFromString( input, 'text/html' );
+				const domParser = new DOMParser();
+				const htmlDocument = domParser.parseFromString( input, 'text/html' );
 
-					expect( htmlDocument.body.innerHTML.replace( /'/g, '"' ).replace( /: /g, ':' ) ).to.not.equal( expected );
+				expect( htmlDocument.body.innerHTML.replace( /'/g, '"' ).replace( /: /g, ':' ) ).to.not.equal( expected );
 
-					normalizeSpacerunSpans( htmlDocument );
+				normalizeSpacerunSpans( htmlDocument );
 
-					expect( htmlDocument.body.innerHTML.replace( /'/g, '"' ).replace( /: /g, ':' ) ).to.equal( expected );
-				} );
+				expect( htmlDocument.body.innerHTML.replace( /'/g, '"' ).replace( /: /g, ':' ) ).to.equal( expected );
 			} );
 		} );
 	} );

+ 8 - 3
packages/ckeditor5-paste-from-office/tests/manual/integration.html

@@ -1,3 +1,8 @@
+<style>
+.clipboard-title {
+	font-size: 1em;
+}
+</style>
 <div id="editor">
 	<h2>Paste here:</h2>
 
@@ -10,11 +15,11 @@
 	</ul>
 </div>
 
-Clipboard HTML
+<h2 class="clipboard-title">Clipboard HTML</h2>
 <div id="html" contenteditable="true" style="width:100%;height:300px;background-color:#EEE;overflow:auto;"></div>
 
-Clipboard text
+<h2 class="clipboard-title">Clipboard text</h2>
 <div id="text" contenteditable="true" style="width:100%;height:100px;background-color:#DDD;overflow:auto;"></div>
 
-Clipboard HTML after transformation
+<h2 class="clipboard-title">Clipboard normalized HTML view after transformation</h2>
 <div id="data" contenteditable="true" style="width:100%;height:100px;background-color:#CCC;overflow:auto;"></div>

+ 25 - 0
packages/ckeditor5-paste-from-office/tests/normalizers/googledocsnormalizer.js

@@ -0,0 +1,25 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import GoogleDocsNormalizer from '../../src/normalizers/googledocsnormalizer';
+
+// `execute()` of the google docs normalizer is tested with autogenerated normalization tests.
+describe( 'GoogleDocsNormalizer', () => {
+	const normalizer = new GoogleDocsNormalizer();
+
+	describe( 'isActive()', () => {
+		it( 'should return true from google docs content', () => {
+			expect( normalizer.isActive( '<p id="docs-internal-guid-12345678-1234-1234-1234-1234567890ab"></p>' ) ).to.be.true;
+		} );
+
+		it( 'should return false for microsoft word content', () => {
+			expect( normalizer.isActive( '<meta name=Generator content="Microsoft Word 15"><p>Foo bar</p>' ) ).to.be.false;
+		} );
+
+		it( 'should return false for content form other sources', () => {
+			expect( normalizer.isActive( '<p>foo</p>' ) ).to.be.false;
+		} );
+	} );
+} );

+ 32 - 0
packages/ckeditor5-paste-from-office/tests/normalizers/mswordnormalizer.js

@@ -0,0 +1,32 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import MSWordNormalizer from '../../src/normalizers/mswordnormalizer';
+
+// `execute()` of the msword normalizer is tested with autogenerated normalization tests.
+describe( 'MSWordNormalizer', () => {
+	const normalizer = new MSWordNormalizer();
+
+	describe( 'isActive()', () => {
+		it( 'should return true for microsoft word content', () => {
+			expect( normalizer.isActive( '<meta name=Generator content="Microsoft Word 15"><p>Foo bar</p>' ) ).to.be.true;
+		} );
+
+		it( 'should return true for microsoft word content - safari', () => {
+			expect( normalizer.isActive( '<html xmlns:o="urn:schemas-microsoft-com:office:office"' +
+				'xmlns:w="urn:schemas-microsoft-com:office:word" ' +
+				'xmlns:m="http://schemas.microsoft.com/office/2004/12/omml" ' +
+				'xmlns="http://www.w3.org/TR/REC-html40">' ) ).to.be.true;
+		} );
+
+		it( 'should return false for google docs content', () => {
+			expect( normalizer.isActive( '<p id="docs-internal-guid-12345678-1234-1234-1234-1234567890ab"></p>' ) ).to.be.false;
+		} );
+
+		it( 'should return false for content fromother sources', () => {
+			expect( normalizer.isActive( '<p>foo</p>' ) ).to.be.false;
+		} );
+	} );
+} );

+ 89 - 53
packages/ckeditor5-paste-from-office/tests/pastefromoffice.js

@@ -3,87 +3,123 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
+import PasteFromOffice from '../src/pastefromoffice';
 import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
-import DocumentFragment from '@ckeditor/ckeditor5-engine/src/view/documentfragment';
-import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
-
-import PasteFromOffice from '../src/pastefromoffice';
+import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
 import { createDataTransfer } from './_utils/utils';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 
-describe( 'Paste from Office plugin', () => {
-	let editor, content, normalizeSpy;
+describe( 'PasteFromOffice', () => {
+	const htmlDataProcessor = new HtmlDataProcessor();
+	let editor, pasteFromOffice, clipboard;
 
 	testUtils.createSinonSandbox();
 
-	before( () => {
-		content = new DocumentFragment();
-	} );
-
 	beforeEach( () => {
-		return VirtualTestEditor
-			.create( {
-				plugins: [ Clipboard, PasteFromOffice ]
-			} )
-			.then( newEditor => {
-				editor = newEditor;
-				normalizeSpy = testUtils.sinon.spy( editor.plugins.get( 'PasteFromOffice' ), '_normalizeWordInput' );
+		return VirtualTestEditor.create( {
+			plugins: [ PasteFromOffice ]
+		} )
+			.then( _editor => {
+				editor = _editor;
+				pasteFromOffice = editor.plugins.get( 'PasteFromOffice' );
+				clipboard = editor.plugins.get( 'Clipboard' );
 			} );
 	} );
 
-	it( 'runs normalizations if Word meta tag detected #1', () => {
-		const dataTransfer = createDataTransfer( {
-			'text/html': '<meta name=Generator content="Microsoft Word 15">'
-		} );
-
-		editor.plugins.get( 'Clipboard' ).fire( 'inputTransformation', { content, dataTransfer } );
-
-		expect( normalizeSpy.calledOnce ).to.true;
+	it( 'should be loaded', () => {
+		expect( pasteFromOffice ).to.be.instanceOf( PasteFromOffice );
 	} );
 
-	it( 'runs normalizations if Word meta tag detected #2', () => {
-		const dataTransfer = createDataTransfer( {
-			'text/html': '<html><head><meta name="Generator"  content=Microsoft Word 15></head></html>'
-		} );
-
-		editor.plugins.get( 'Clipboard' ).fire( 'inputTransformation', { content, dataTransfer } );
+	it( 'has proper name', () => {
+		expect( PasteFromOffice.pluginName ).to.equal( 'PasteFromOffice' );
+	} );
 
-		expect( normalizeSpy.calledOnce ).to.true;
+	it( 'should load Clipboard plugin', () => {
+		expect( editor.plugins.get( Clipboard ) ).to.be.instanceOf( Clipboard );
 	} );
 
-	it( 'does not normalize the content without Word meta tag', () => {
-		const dataTransfer = createDataTransfer( {
-			'text/html': '<meta name=Generator content="Other">'
-		} );
+	describe( 'isTransformedWithPasteFromOffice - flag', () => {
+		describe( 'data which should be marked with flag', () => {
+			it( 'should process data with microsoft word header', () => {
+				checkCorrectData( '<meta name=Generator content="Microsoft Word 15">' );
+			} );
 
-		editor.plugins.get( 'Clipboard' ).fire( 'inputTransformation', { content, dataTransfer } );
+			it( 'should process data with nested microsoft header', () => {
+				checkCorrectData( '<html><head><meta name="Generator"  content=Microsoft Word 15></head></html>' );
+			} );
 
-		expect( normalizeSpy.called ).to.false;
-	} );
+			it( 'should process data from google docs', () => {
+				checkCorrectData( '<p id="docs-internal-guid-12345678-1234-1234-1234-1234567890ab"></p>' );
+			} );
+
+			function checkCorrectData( inputString ) {
+				const data = setUpData( inputString );
+				const getDataSpy = sinon.spy( data.dataTransfer, 'getData' );
 
-	it( 'does not process content many times for the same `inputTransformation` event', () => {
-		const clipboard = editor.plugins.get( 'Clipboard' );
+				clipboard.fire( 'inputTransformation', data );
 
-		const dataTransfer = createDataTransfer( {
-			'text/html': '<html><head><meta name="Generator"  content=Microsoft Word 15></head></html>'
+				expect( data.isTransformedWithPasteFromOffice ).to.be.true;
+				sinon.assert.called( getDataSpy );
+			}
 		} );
 
-		let eventRefired = false;
-		clipboard.on( 'inputTransformation', ( evt, data ) => {
-			if ( !eventRefired ) {
-				eventRefired = true;
+		describe( 'data which should not be marked with flag', () => {
+			it( 'should not process data with regular html', () => {
+				checkInvalidData( '<p>Hello world</p>' );
+			} );
 
-				evt.stop();
+			it( 'should not process data with similar headers to MS Word', () => {
+				checkInvalidData( '<meta name=Generator content="Other">' );
+			} );
+
+			function checkInvalidData( inputString ) {
+				const data = setUpData( inputString );
+				const getDataSpy = sinon.spy( data.dataTransfer, 'getData' );
 
 				clipboard.fire( 'inputTransformation', data );
+
+				expect( data.isTransformedWithPasteFromOffice ).to.be.undefined;
+				sinon.assert.called( getDataSpy );
 			}
+		} );
 
-			expect( data.pasteFromOfficeProcessed ).to.true;
-			expect( normalizeSpy.calledOnce ).to.true;
-		}, { priority: 'low' } );
+		describe( 'data which already have the flag', () => {
+			it( 'should not process again ms word data containing a flag', () => {
+				checkAlreadyProcessedData( '<meta name=Generator content="Microsoft Word 15">' +
+					'<p class="MsoNormal">Hello world<o:p></o:p></p>' );
+			} );
+
+			it( 'should not process again google docs data containing a flag', () => {
+				checkAlreadyProcessedData( '<meta charset="utf-8"><b id="docs-internal-guid-30db46f5-7fff-15a1-e17c-1234567890ab"' +
+					'style="font-weight:normal;"><p dir="ltr">Hello world</p></b>' );
+			} );
+
+			function checkAlreadyProcessedData( inputString ) {
+				const data = setUpData( inputString, true );
+				const getDataSpy = sinon.spy( data.dataTransfer, 'getData' );
 
-		editor.plugins.get( 'Clipboard' ).fire( 'inputTransformation', { content, dataTransfer } );
+				clipboard.fire( 'inputTransformation', data );
 
-		expect( normalizeSpy.calledOnce ).to.true;
+				expect( data.isTransformedWithPasteFromOffice ).to.be.true;
+				sinon.assert.notCalled( getDataSpy );
+			}
+		} );
 	} );
+
+	// @param {String} inputString html to be processed by paste from office
+	// @param {Boolean} [isTransformedWithPasteFromOffice=false] if set, marks output data with isTransformedWithPasteFromOffice flag
+	// @returns {Object} data object simulating content obtained from the clipboard
+	function setUpData( inputString, isTransformedWithPasteFromOffice = false ) {
+		const data = {
+			content: htmlDataProcessor.toView( inputString ),
+			dataTransfer: createDataTransfer( { 'text/html': inputString } )
+		};
+
+		if ( isTransformedWithPasteFromOffice ) {
+			data.isTransformedWithPasteFromOffice = true;
+		}
+
+		return data;
+	}
 } );