Przeglądaj źródła

Image pasting filter refactoring.

Krzysztof Krztoń 7 lat temu
rodzic
commit
47ee39a4c3

+ 122 - 16
packages/ckeditor5-paste-from-office/src/filters/image.js

@@ -13,32 +13,128 @@ import UpcastWriter from '@ckeditor/ckeditor5-engine/src/view/upcastwriter';
 
 import { convertHexToBase64 } from './utils';
 
-export function transformImages( documentFragment, dataTransfer ) {
+/**
+ * Replaces source attribute of all `<img>` elements representing regular
+ * images (not the Word shapes) with inlined base64 image representation extracted from RTF data.
+ *
+ * @param {module:engine/view/documentfragment~DocumentFragment} documentFragment Document fragment on which transform images.
+ * @param {String} rtfData The RTF data from which images representation will be used.
+ */
+export function replaceImagesSourceWithBase64( documentFragment, rtfData ) {
 	if ( !documentFragment.childCount ) {
 		return;
 	}
 
+	const upcastWriter = new UpcastWriter();
+	const shapesIds = findAllShapesIds( documentFragment );
+
+	removeAllImgElementsRepresentingShapes( shapesIds, documentFragment, upcastWriter );
+	removeAllShapeElements( documentFragment, upcastWriter );
+
 	const imageElements = findAllImageElements( documentFragment );
 
 	if ( imageElements.length ) {
-		const upcastWriter = new UpcastWriter();
-		const imageData = extractImageDataFromRtf( dataTransfer.getData( 'text/rtf' ) );
+		const imageData = extractImageDataFromRtf( rtfData );
+
+		replaceImagesSourceWithInlineRepresentation( imageElements, imageData, upcastWriter );
+	}
+}
+
+// Finds all shapes (`<v:*>...</v:*>`) ids. Shapes can represent images (canvas) or Word shapes (which does not have RTF representation).
+//
+// @param {module:engine/view/documentfragment~DocumentFragment} documentFragment Document fragment from which to extract shape ids.
+// @returns {Array.<String>} Array of shape ids.
+function findAllShapesIds( documentFragment ) {
+	const range = Range.createIn( documentFragment );
+
+	const shapeElementsMatcher = new Matcher( {
+		name: /v:(.+)/
+	} );
+
+	const shapesIds = [];
+
+	for ( const value of range ) {
+		const el = value.item;
+		const prevSiblingName = el.previousSibling && el.previousSibling.name || null;
+
+		// If shape element have 'o:gfxdata' attribute and is not directly before `<v:shapetype>` element it means it represent Word shape.
+		if ( shapeElementsMatcher.match( el ) && el.getAttribute( 'o:gfxdata' ) && prevSiblingName !== 'v:shapetype' ) {
+			shapesIds.push( value.item.getAttribute( 'id' ) );
+		}
+	}
+
+	return shapesIds;
+}
+
+// Removes all `<img>` elements which represents Word shapes and not regular images.
+//
+// @param {Array.<String>} shapesIds Shape ids which will be checked against `<img>` elements.
+// @param {module:engine/view/documentfragment~DocumentFragment} documentFragment Document fragment from which to remove `<img>` elements.
+// @param {module:engine/view/upcastwriter~UpcastWriter} writer
+function removeAllImgElementsRepresentingShapes( shapesIds, documentFragment, writer ) {
+	const range = Range.createIn( documentFragment );
+
+	const imageElementsMatcher = new Matcher( {
+		name: 'img'
+	} );
+
+	const imgs = [];
+
+	for ( const value of range ) {
+		if ( imageElementsMatcher.match( value.item ) ) {
+			const el = value.item;
+			const shapes = el.getAttribute( 'v:shapes' ) ? el.getAttribute( 'v:shapes' ).split( ' ' ) : [];
+
+			if ( shapes.length && shapes.every( shape => shapesIds.indexOf( shape ) > -1 ) ) {
+				imgs.push( el );
+			}
+		}
+	}
 
-		replaceImageSourceWithInlineData( imageElements, imageData, upcastWriter );
+	for ( const img of imgs ) {
+		writer.remove( img );
 	}
 }
 
+// Removes all shape elements (`<v:*>...</v:*>`) so they do not pollute the output structure.
+//
+// @param {module:engine/view/documentfragment~DocumentFragment} documentFragment Document fragment from which to remove shape elements.
+// @param {module:engine/view/upcastwriter~UpcastWriter} writer
+function removeAllShapeElements( documentFragment, writer ) {
+	const range = Range.createIn( documentFragment );
+
+	const shapeElementsMatcher = new Matcher( {
+		name: /v:(.+)/
+	} );
+
+	const shapes = [];
+
+	for ( const value of range ) {
+		if ( shapeElementsMatcher.match( value.item ) ) {
+			shapes.push( value.item );
+		}
+	}
+
+	for ( const shape of shapes ) {
+		writer.remove( shape );
+	}
+}
+
+// Finds all `<img>` elements in a given document fragment.
+//
+// @param {module:engine/view/documentfragment~DocumentFragment} documentFragment Document fragment in which to look for `<img>` elements.
+// @returns {Array.<module:engine/view/element~Element>} Array of found `<img>` elements.
 function findAllImageElements( documentFragment ) {
 	const range = Range.createIn( documentFragment );
 
-	const listItemLikeElementsMatcher = new Matcher( {
+	const imageElementsMatcher = new Matcher( {
 		name: 'img'
 	} );
 
 	const imgs = [];
 
 	for ( const value of range ) {
-		if ( value.type === 'elementStart' && listItemLikeElementsMatcher.match( value.item ) ) {
+		if ( imageElementsMatcher.match( value.item ) ) {
 			imgs.push( value.item );
 		}
 	}
@@ -46,6 +142,13 @@ function findAllImageElements( documentFragment ) {
 	return imgs;
 }
 
+// Extracts all images HEX representations from a given RTF data.
+//
+// @param {String} rtfData The RTF data from which to extract images HEX representation.
+// @returns {Array.<Object>} Array of found HEX representations. Each array item is an object containing:
+//
+// 		* {String} hex Image representation in HEX format.
+// 		* {string} type Type of image, `image/png` or `image/jpeg`.
 function extractImageDataFromRtf( rtfData ) {
 	if ( !rtfData ) {
 		return [];
@@ -80,18 +183,21 @@ function extractImageDataFromRtf( rtfData ) {
 	return result;
 }
 
-function replaceImageSourceWithInlineData( imageElements, imagesRtfData, upcastWriter ) {
-	// Assuming there is equal amount of Images in RTF and HTML source, so we can match them accordingly to the existing order.
-	if ( imageElements.length === imagesRtfData.length ) {
+// Replaces `src` attribute value of all given images with the corresponding base64 image representation.
+//
+// @param {Array.<module:engine/view/element~Element>} imageElements Array of image elements which will have its source replaced.
+// @param {Array.<Object>} imagesHexSources Array of images hex sources (usually the result of `extractImageDataFromRtf()` function).
+// The array should be the same length as `imageElements` parameter.
+// @param {module:engine/view/upcastwriter~UpcastWriter} upcastWriter
+function replaceImagesSourceWithInlineRepresentation( imageElements, imagesHexSources, upcastWriter ) {
+	// Assume there is an equal amount of image elements and images HEX sources so they can be matched accordingly based on existing order.
+	if ( imageElements.length === imagesHexSources.length ) {
 		for ( let i = 0; i < imageElements.length; i++ ) {
-			// Replace only `file` urls of images (shapes get newSrcValue with null).
-			if ( ( imageElements[ i ].getAttribute( 'src' ).indexOf( 'file://' ) === 0 ) && imagesRtfData[ i ] ) {
-				upcastWriter.setAttribute( 'src', createSrcWithBase64( imagesRtfData[ i ] ), imageElements[ i ] );
+			// Replace only `file` urls of images (online images are also represented with local `file://` path).
+			if ( imageElements[ i ].getAttribute( 'src' ).indexOf( 'file://' ) === 0 && imagesHexSources[ i ] ) {
+				const newSrc = `data:${ imagesHexSources[ i ].type };base64,${ convertHexToBase64( imagesHexSources[ i ].hex ) }`;
+				upcastWriter.setAttribute( 'src', newSrc, imageElements[ i ] );
 			}
 		}
 	}
 }
-
-function createSrcWithBase64( img ) {
-	return img.type ? 'data:' + img.type + ';base64,' + convertHexToBase64( img.hex ) : null;
-}

+ 9 - 4
packages/ckeditor5-paste-from-office/src/filters/utils.js

@@ -27,6 +27,9 @@ import { NBSP_FILLER } from '@ckeditor/ckeditor5-engine/src/view/filler';
 export function parseHtml( htmlString ) {
 	const domParser = new DOMParser();
 
+	// Remove Word specific "if comments" so content inside is not omitted by the parser.
+	htmlString = htmlString.replace( /<!--\[if gte vml 1]>/g, '' );
+
 	// Parse htmlString as native Document object.
 	const htmlDocument = domParser.parseFromString( normalizeSpacing( htmlString ), 'text/html' );
 
@@ -102,16 +105,18 @@ function extractStyles( htmlDocument ) {
 	};
 }
 
-// Replaces last space preceding elements closing tag with `&nbsp;`. Such operation prevents spaces from being removed
-// during further DOM/View processing (see especially {@link module:engine/view/domconverter~DomConverter#_processDataFromDomText}).
-// This method also takes into account Word specific `<o:p></o:p>` empty tags.
+// Replaces last space preceding elements closing tag and Word specific empty `<o:p></o:p>` tags with `&nbsp;`.
+// Such operation prevents spaces from being removed during further DOM/View processing (see
+// especially {@link module:engine/view/domconverter~DomConverter#_processDataFromDomText}).
+// Also multiline sequences of spaces and new lines between tags are removed.
 //
 // @param {String} htmlString HTML string in which spacing should be normalized.
 // @returns {String} Input HTML with spaces normalized.
 function normalizeSpacing( htmlString ) {
 	return normalizeSafariSpaceSpans( normalizeSafariSpaceSpans( htmlString ) ) // Run normalization two times to cover nested spans.
 		.replace( / <\//g, '\u00A0</' )
-		.replace( / <o:p><\/o:p>/g, '\u00A0<o:p></o:p>' );
+		.replace( / <o:p><\/o:p>/g, '\u00A0<o:p></o:p>' )
+		.replace( />(\s*(\r\n?|\n)\s*)+</g, '><' );
 }
 
 // Normalizes specific spacing generated by Safari when content pasted from Word (`<span class="Apple-converted-space"> </span>`)

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

@@ -12,7 +12,7 @@ import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
 
 import { parseHtml } from './filters/utils';
 import { transformListItemLikeElementsIntoLists } from './filters/list';
-import { transformImages } from './filters/image';
+import { replaceImagesSourceWithBase64 } from './filters/image';
 
 /**
  * The Paste from Office plugin.
@@ -61,7 +61,7 @@ export default class PasteFromOffice extends Plugin {
 		const { body, stylesString } = parseHtml( input );
 
 		transformListItemLikeElementsIntoLists( body, stylesString );
-		transformImages( body, dataTransfer );
+		replaceImagesSourceWithBase64( body, dataTransfer.getData( 'text/rtf' ) );
 
 		return body;
 	}

+ 8 - 10
packages/ckeditor5-paste-from-office/tests/_utils/utils.js

@@ -224,21 +224,19 @@ function generateIntegrationTests( title, fixtures, editorConfig, skip ) {
 // 	because tab preceding `03` text will be treated as formatting character and will be removed.
 //
 // @param {module:engine/view/text~Text|module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment}
-// actual Actual HTML.
-// @param {String} expected Expected HTML.
-function expectNormalized( actual, expected ) {
-	const expectedInlined = inlineData( expected );
-
+// actualView Actual HTML.
+// @param {String} expectedHtml Expected HTML.
+function expectNormalized( actualView, expectedHtml ) {
 	// We are ok with both spaces and non-breaking spaces in the actual content.
 	// Replace `&nbsp;` with regular spaces to align with expected content.
-	const actualNormalized = stringifyView( actual ).replace( /\u00A0/g, ' ' );
-	const expectedNormalized = normalizeHtml( expectedInlined );
+	const actualNormalized = stringifyView( actualView ).replace( /\u00A0/g, ' ' );
+	const expectedNormalized = normalizeHtml( inlineData( expectedHtml ) );
 
 	// Extract base64 images so they do not pollute HTML diff and can be compared separately.
-	const { data: actualSimplified, images: actualImages } = extractBase64Srcs( actualNormalized );
-	const { data: expectedSimplified, images: expectedImages } = extractBase64Srcs( expectedNormalized );
+	const { data: actual, images: actualImages } = extractBase64Srcs( actualNormalized );
+	const { data: expected, images: expectedImages } = extractBase64Srcs( expectedNormalized );
 
-	expect( actualSimplified ).to.equal( expectedSimplified );
+	expect( actual ).to.equal( expected );
 
 	if ( actualImages.length > 0 && expectedImages.length > 0 ) {
 		expect( actualImages.length ).to.equal( expectedImages.length );