Bladeren bron

Docs and small improvment for content noralizer filters.

Mateusz Samsel 6 jaren geleden
bovenliggende
commit
4818faa831

+ 121 - 6
packages/ckeditor5-paste-from-office/src/contentnormalizer.js

@@ -7,33 +7,148 @@
  * @module paste-from-office/contentnormalizer
  */
 
+import UpcastWriter from '@ckeditor/ckeditor5-engine/src/view/upcastwriter';
+
+/**
+ * Content Normalizer class provides a mechanism to transform input data send through
+ * an {@link module:clipboard/clipboard~Clipboard#event:inputTransformation inputTransformation event}. It fixes an input content,
+ * which has a source in applications like: MS Word, Google Docs, etc. These applications generate content which frequently
+ * is an invalid HTML. Content normalizers transform it, what later might be properly upcast to {@link module:engine/model/model~Model}.
+ *
+ * Content Normalizers are registered by {@link module:paste-from-office/pastefromoffice~PasteFromOffice} plugin. Each instance is
+ * initialized with an activation trigger. Activation trigger is a function which gets content of `text/html` dataTransfer (String) and
+ * returns `true` or `false`. Based on this result normalizer applies filters to given data.
+ *
+ * Filters are function, which are run sequentially, as they were added. Each filter gets data transformed by the previous one.
+ *
+ * Example definition:
+ *
+ * 	const normalizer = new ContentNormalizer( contentHtml =>
+ * 		contentHtml.includes( 'docs-internal-guid' )
+ * 	);
+ *
+ * 	normalizer.addFilter( ( { data } ) => {
+ * 		removeBoldTagWrapper( data.content );
+ * 	} )
+ *
+ * 	normalizer.addFilter( ( { data } ) => {
+ * 		// ...
+ * 		// another modification of data's content
+ * 	} );
+ *
+ * Normalizers are stored inside Paste from Office plugin and are run on
+ * {@link module:clipboard/clipboard~Clipboard#event:inputTransformation inputTransformation event}. Below example is simplified and show
+ * how to call normalizer directly on clipboard event.
+ *
+ * 	editor.plugins.get( 'Clipboard' ).on( 'inputTransformation', ( evt, data ) => {
+ * 		normalizer.transform( data );
+ * 	} );
+ *
+ * @class
+ */
 export default class ContentNormalizer {
-	constructor( { activationTrigger } ) {
-		this.activationTrigger = activationTrigger;
+	/**
+	 * Initialize Content Normalizer.
+	 *
+	 * @param {Function} activationTrigger The function which checks for what content should be applied this normalizer.
+	 * It takes an HTML string from the `text/html` dataTarnsfer as an argument and have to return a boolean value
+	 */
+	constructor( activationTrigger ) {
+		/**
+		 * Keeps a reference to the activation trigger function. The function is used to check if current Content Normalizer instance
+		 * should be applied for given input data. Check is made during the {@link #transform}.
+		 *
+		 * @private
+		 * @type {Function}
+		 */
+		this._activationTrigger = activationTrigger;
 
+		/**
+		 * Keeps a reference to registered filters with {@link #addFilter} method.
+		 *
+		 * @private
+		 * @type {Set}
+		 */
 		this._filters = new Set();
 	}
 
+	/**
+	 * Method checks if passed data should have applied {@link #_filters} registerd in this Content Normalizer.
+	 * If yes, then data are transformed and marked with a flag `isTransformedWithPasteFromOffice = true`.
+	 * In other case data are not modified.
+	 *
+	 * Please notice that presence of `isTransformedWithPasteFromOffice` flag in input data prevent transformation.
+	 * This forbid of running the same normalizer twice or running multiple normalizers over the same data.
+	 *
+	 * @param data input data object it should preserve structure defined in
+	 * {@link module:clipboard/clipboard~Clipboard#event:inputTransformation Clipboard#inputTransformation event}.
+	 */
 	transform( data ) {
 		const html = data.dataTransfer && data.dataTransfer.getData( 'text/html' );
 		const dataReadFirstTime = data.isTransformedWithPasteFromOffice === undefined;
 		const hasHtmlData = !!html;
 
-		if ( hasHtmlData && dataReadFirstTime && this.activationTrigger( html ) ) {
+		if ( hasHtmlData && dataReadFirstTime && this._activationTrigger( html ) ) {
 			this._applyFilters( data );
 			data.isTransformedWithPasteFromOffice = true;
 		}
-
-		return this;
 	}
 
+	/**
+	 * Adds filter function to Content Normalizer.
+	 * Function is called with configuration object where `data` key keeps reference to input data obtained from
+	 * {@link module:clipboard/clipboard~Clipboard#event:inputTransformation Clipboard#inputTransformation event}
+	 *
+	 * See also: {@link module:paste-from-office/contentnormalizer~FilterFunction}
+	 *
+	 * @param {module:paste-from-office/contentnormalizer~FilterFunction} filterFn
+	 */
 	addFilter( filterFn ) {
 		this._filters.add( filterFn );
 	}
 
+	/**
+	 * Applies filters stored in {@link #_filters} to currently processed data.
+	 *
+	 * @private
+	 * @param {Object} data input data object it should preserve structure defined in
+	 * {@link module:clipboard/clipboard~Clipboard#event:inputTransformation Clipboard#inputTransformation event}.
+	 */
 	_applyFilters( data ) {
+		const writer = new UpcastWriter();
+		const documentFragment = data.content;
+
 		for ( const filter of this._filters ) {
-			filter( { data } );
+			filter( { data, documentFragment, writer } );
 		}
 	}
 }
+
+/**
+ * Filter function which is used to transform data of
+ * {@link module:clipboard/clipboard~Clipboard#event:inputTransformation Clipboard#inputTransformation event}.
+ *
+ * Filters are used by {@link module:paste-from-office/contentnormalizer~ContentNormalizer}.
+ *
+ * Example:
+ *
+ * 	function removeBoldTagWrapper( { documentFragment, writer } ) {
+ * 		for ( const childWithWrapper of documentFragment.getChildren() ) {
+ * 			if ( childWithWrapper.is( 'b' ) && childWithWrapper.getStyle( 'font-weight' ) === 'normal' ) {
+ * 				const childIndex = documentFragment.getChildIndex( childWithWrapper );
+ * 				const removedElement = writer.remove( childWithWrapper )[ 0 ];
+ *
+ * 				writer.insertChild( childIndex, removedElement.getChildren(), documentFragment );
+ * 			}
+ * 		}
+ * 	}
+ *
+ * @callback module:paste-from-office/contentnormalizer~FilterFunction
+ * @param {Object} config
+ * @param {Object} config.data input data object it should preserve structure defined in
+ * {@link module:clipboard/clipboard~Clipboard#event:inputTransformation Clipboard#inputTransformation event}.
+ * @param {module:engine/view/upcastwriter~UpcastWriter} config.writer upcast writer which can be used to manipulate
+ * with document fragment.
+ * @param {module:engine/view/documentfragment~DocumentFragment} config.documentFragment the `data.content` obtained from
+ * {@link module:clipboard/clipboard~Clipboard#event:inputTransformation Clipboard#inputTransformation event}
+ */

+ 3 - 6
packages/ckeditor5-paste-from-office/src/filters/common.js

@@ -11,17 +11,14 @@
  * The filter removes `<b>` tag wrapper added by Google Docs for copied content.
  *
  * @param {module:engine/view/documentfragment~DocumentFragment} documentFragment
- * @returns {module:engine/view/documentfragment~DocumentFragment}
  */
-export function removeBoldTagWrapper( documentFragment ) {
+export function removeBoldTagWrapper( { documentFragment, writer } ) {
 	for ( const childWithWrapper of documentFragment.getChildren() ) {
 		if ( childWithWrapper.is( 'b' ) && childWithWrapper.getStyle( 'font-weight' ) === 'normal' ) {
 			const childIndex = documentFragment.getChildIndex( childWithWrapper );
+			const removedElement = writer.remove( childWithWrapper )[ 0 ];
 
-			documentFragment._removeChildren( childIndex );
-			documentFragment._insertChild( childIndex, childWithWrapper.getChildren() );
+			writer.insertChild( childIndex, removedElement.getChildren(), documentFragment );
 		}
 	}
-
-	return documentFragment;
 }

+ 10 - 6
packages/ckeditor5-paste-from-office/src/normalizers/googledocs.js

@@ -10,14 +10,18 @@
 import ContentNormalizer from '../contentnormalizer';
 import { removeBoldTagWrapper } from '../filters/common';
 
+/**
+ * {@link module:paste-from-office/contentnormalizer~ContentNormalizer} instance dedicated to transforming data obtained from Google Docs.
+ * It stores filters which fix quirks detected in Google Docs content.
+ *
+ * @type {module:paste-from-office/contentnormalizer~ContentNormalizer}
+ */
 export const googleDocsNormalizer = ( () => {
-	const normalizer = new ContentNormalizer( {
-		activationTrigger: contentString => /id=("|')docs-internal-guid-[-0-9a-f]+("|')/.test( contentString )
-	} );
+	const normalizer = new ContentNormalizer( contentString =>
+		/id=("|')docs-internal-guid-[-0-9a-f]+("|')/.test( contentString )
+	);
 
-	normalizer.addFilter( ( { data } ) => {
-		removeBoldTagWrapper( data.content );
-	} );
+	normalizer.addFilter( removeBoldTagWrapper );
 
 	return normalizer;
 } )();

+ 10 - 5
packages/ckeditor5-paste-from-office/src/normalizers/msword.js

@@ -12,12 +12,17 @@ import { parseHtml } from '../filters/parse';
 import { transformListItemLikeElementsIntoLists } from '../filters/list';
 import { replaceImagesSourceWithBase64 } from '../filters/image';
 
+/**
+ * {@link module:paste-from-office/contentnormalizer~ContentNormalizer} instance dedicated to transforming data obtained from MS Word.
+ * It stores filters which fix quirks detected in MS Word content.
+ *
+ * @type {module:paste-from-office/contentnormalizer~ContentNormalizer}
+ */
 export const mswordNormalizer = ( () => {
-	const normalizer = new ContentNormalizer( {
-		activationTrigger: contentString =>
-			/<meta\s*name="?generator"?\s*content="?microsoft\s*word\s*\d+"?\/?>/i.test( contentString ) ||
-			/xmlns:o="urn:schemas-microsoft-com/i.test( contentString )
-	} );
+	const normalizer = new ContentNormalizer( contentString =>
+		/<meta\s*name="?generator"?\s*content="?microsoft\s*word\s*\d+"?\/?>/i.test( contentString ) ||
+		/xmlns:o="urn:schemas-microsoft-com/i.test( contentString )
+	);
 
 	normalizer.addFilter( ( { data } ) => {
 		const html = data.dataTransfer.getData( 'text/html' );

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

@@ -16,9 +16,14 @@ 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/contentnormalizer~ContentNormalizer}.
+ * Currently, there are included followed normalizers:
+ *   * {@link module:paste-from-office/normalizer.mswordNormalizer MS Word normalizer}
+ *   * {@link module:paste-from-office/normalizer.googleDocsNormalizer Google Docs normalizer}
+ *
  * For more information about this feature check the {@glink api/paste-from-office package page}.
  *
  * @extends module:core/plugin~Plugin
@@ -48,8 +53,7 @@ export default class PasteFromOffice extends Plugin {
 		normalizers.add( mswordNormalizer );
 		normalizers.add( googleDocsNormalizer );
 
-		this.listenTo(
-			editor.plugins.get( 'Clipboard' ),
+		editor.plugins.get( 'Clipboard' ).on(
 			'inputTransformation',
 			( evt, data ) => {
 				for ( const normalizer of normalizers ) {

+ 15 - 10
packages/ckeditor5-paste-from-office/tests/contentnormalizer.js

@@ -6,6 +6,7 @@
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import ContentNormalizer from '../src/contentnormalizer';
 import { createDataTransfer } from './_utils/utils';
+import UpcastWriter from '@ckeditor/ckeditor5-engine/src/view/upcastwriter';
 
 describe( 'ContentNormalizer', () => {
 	let normalizer, sinonTrigger;
@@ -20,15 +21,13 @@ describe( 'ContentNormalizer', () => {
 	beforeEach( () => {
 		sinonTrigger = sinon.fake.returns( true );
 
-		normalizer = new ContentNormalizer( {
-			activationTrigger: sinonTrigger
-		} );
+		normalizer = new ContentNormalizer( sinonTrigger );
 	} );
 
 	describe( 'constructor()', () => {
 		it( 'should have assigned activation trigger', () => {
-			expect( normalizer.activationTrigger ).to.be.a( 'function' );
-			expect( normalizer.activationTrigger ).to.equal( sinonTrigger );
+			expect( normalizer._activationTrigger ).to.be.a( 'function' );
+			expect( normalizer._activationTrigger ).to.equal( sinonTrigger );
 		} );
 	} );
 
@@ -59,12 +58,20 @@ describe( 'ContentNormalizer', () => {
 
 			it( 'should execute filters over data', () => {
 				const filter = sinon.fake();
+				const writer = new UpcastWriter();
+				const documentFragment = writer.createDocumentFragment();
+
+				data.content = documentFragment;
 
 				normalizer.addFilter( filter );
 				normalizer.transform( data );
 
 				sinon.assert.calledOnce( filter );
-				sinon.assert.calledWith( filter, { data } );
+				sinon.assert.calledWithMatch( filter, {
+					documentFragment,
+					data,
+					writer: sinon.match.instanceOf( UpcastWriter )
+				} );
 			} );
 
 			it( 'should not process again already transformed data', () => {
@@ -86,7 +93,7 @@ describe( 'ContentNormalizer', () => {
 			beforeEach( () => {
 				sinonTrigger = sinon.fake.returns( false );
 
-				normalizer = new ContentNormalizer( { activationTrigger: sinonTrigger } );
+				normalizer = new ContentNormalizer( sinonTrigger );
 			} );
 
 			it( 'should not change data content', () => {
@@ -112,9 +119,7 @@ describe( 'ContentNormalizer', () => {
 		let filter;
 
 		beforeEach( () => {
-			filter = {
-				exec: () => {}
-			};
+			filter = () => {};
 
 			normalizer.addFilter( filter );
 		} );

+ 10 - 3
packages/ckeditor5-paste-from-office/tests/filters/common.js

@@ -5,18 +5,25 @@
 
 import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
 import { removeBoldTagWrapper } from '../../src/filters/common';
+import UpcastWriter from '@ckeditor/ckeditor5-engine/src/view/upcastwriter';
 
 describe( 'PasteFromOffice/filters', () => {
 	const htmlDataProcessor = new HtmlDataProcessor();
 	describe( 'common', () => {
 		describe( 'removeBoldTagWrapper', () => {
+			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 );
 
-				removeBoldTagWrapper( documentFragment );
+				removeBoldTagWrapper( { documentFragment, writer } );
 
 				expect( htmlDataProcessor.toData( documentFragment ) ).to.equal( '<p>Hello world</p>' );
 			} );
@@ -25,7 +32,7 @@ describe( 'PasteFromOffice/filters', () => {
 				const inputData = '<p id="docs-internal-guid-e4b9bad6-7fff-c086-3135-1234567890ab">Hello world</p>';
 				const documentFragment = htmlDataProcessor.toView( inputData );
 
-				removeBoldTagWrapper( documentFragment );
+				removeBoldTagWrapper( { documentFragment, writer } );
 
 				expect( htmlDataProcessor.toData( documentFragment ) ).to.equal(
 					'<p id="docs-internal-guid-e4b9bad6-7fff-c086-3135-1234567890ab">Hello world</p>' );
@@ -35,7 +42,7 @@ describe( 'PasteFromOffice/filters', () => {
 				const inputData = '<b>Hello world</b>';
 				const documentFragment = htmlDataProcessor.toView( inputData );
 
-				removeBoldTagWrapper( documentFragment );
+				removeBoldTagWrapper( { documentFragment, writer } );
 
 				expect( htmlDataProcessor.toData( documentFragment ) ).to.equal(
 					'<b>Hello world</b>' );