es-ckeditor
Version:
CKEditor-based implementation and add some plugins, For example kityformula etc.
601 lines (510 loc) • 19.2 kB
JavaScript
/**
* @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/* globals CKEDITOR */
// This filter could be one day merged to common filter. However currently pasteTools.createFilter doesn't pass additional arguments,
// so it's not possible to pass rtf clipboard to it (#3670).
( function() {
'use strict';
/**
* Filter handling pasting images. In case of missing RTF content images are extracted
* from [Object URLs](https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications#Example_Using_object_URLs_to_display_images).
* In other cases hey are extracted from RTF content.
*
* @private
* @since 4.14.0
* @param {String} html
* @param {CKEDITOR.editor} editor
* @param {String} rtf
* @member CKEDITOR.plugins.pastetools.filters
*/
CKEDITOR.tools.array.unique = function( array ) {
return CKEDITOR.tools.array.filter( array, function( item, index ) {
return index === CKEDITOR.tools.array.indexOf( array, item );
} );
}
CKEDITOR.pasteFilters.image = function( html, editor, rtf ) {
var imgTags;
// If the editor does not allow images, skip embedding.
if ( editor.activeFilter && !editor.activeFilter.check( 'img[src]' ) ) {
return html;
}
imgTags = extractTagsFromHtml( html );
if ( imgTags.length === 0 ) {
return html;
}
if ( rtf ) {
return handleRtfImages( html, rtf, imgTags );
}
return handleBlobImages( editor, html, imgTags );
};
/**
* Parses RTF content to find embedded images.
*
* @private
* @since 4.16.0
* @param {String} rtfContent RTF content to be checked for images.
* @returns {CKEDITOR.plugins.pastetools.filters.image.ImageData[]} An array of images found in the `rtfContent`.
* @member CKEDITOR.plugins.pastetools.filters.image
*/
CKEDITOR.pasteFilters.image.extractFromRtf = extractFromRtf;
/**
* Extracts an array of `src`` attributes in `<img>` tags from the given HTML. `<img>` tags belonging to VML shapes are removed.
*
* ```js
* CKEDITOR.plugins.pastefromword.images.extractTagsFromHtml( html );
* // Returns: [ 'http://example-picture.com/random.png', 'http://example-picture.com/another.png' ]
* ```
*
* @private
* @since 4.16.0
* @param {String} html A string representing HTML code.
* @returns {String[]} An array of strings representing the `src` attribute of the `<img>` tags found in `html`.
* @member CKEDITOR.plugins.pastetools.filters.image
*/
CKEDITOR.pasteFilters.image.extractTagsFromHtml = extractTagsFromHtml;
/**
* Extract image type from its RTF content
*
* @private
* @since 4.16.0
* @param {String} imageContent Image content as RTF string.
* @returns {String} If the image type can be extracted, it is returned in `image/*` format.
* Otherwise, `'unknown'` is returned.
* @member CKEDITOR.plugins.pastetools.filters.image
*/
CKEDITOR.pasteFilters.image.getImageType = getImageType;
/**
* Creates image source as Base64-encoded [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs).
*
* @private
* @since 4.16.0
* @param {CKEDITOR.plugins.pastetools.filters.image.ImageData} img Image data.
* @returns {String} Data URL representing the image.
* @member CKEDITOR.plugins.pastetools.filters.image
*/
CKEDITOR.pasteFilters.image.createSrcWithBase64 = createSrcWithBase64;
/**
* Converts blob url into base64 string. Conversion is happening asynchronously.
* Currently supported file types: `image/png`, `image/jpeg`, `image/gif`.
*
* @private
* @since 4.16.0
* @param {String} blobUrlSrc Address of blob which is going to be converted
* @returns {CKEDITOR.tools.promise.<String/null>} Promise, which resolves to Data URL representing image.
* If image's type is unsupported, promise resolves to `null`.
* @member CKEDITOR.plugins.pastetools.filters.image
*/
CKEDITOR.pasteFilters.image.convertBlobUrlToBase64 = convertBlobUrlToBase64;
/**
* Return file type based on first 4 bytes of given file. Currently recognised file types: `image/png`, `image/jpeg`, `image/gif`.
*
* @private
* @since 4.16.0
* @param {Uint8Array} bytesArray Typed array which will be analysed to obtain file type.
* @returns {String/null} File type recognized from given typed array or null.
* @member CKEDITOR.plugins.pastetools.filters.image
*/
CKEDITOR.pasteFilters.image.getImageTypeFromSignature = getImageTypeFromSignature;
/**
* Array of all supported image formats.
*
* @private
* @since 4.16.0
* @type {String[]}
* @member CKEDITOR.plugins.pastetools.filters.image
*/
CKEDITOR.pasteFilters.image.supportedImageTypes = [
'image/png',
'image/jpeg',
'image/gif'
];
/**
* Recognizable image types with their respective markers.
*
* The recognizing of image type is done by searching for image marker
* inside the RTF image content.
*
* @private
* @since 4.16.0
* @type {CKEDITOR.plugins.pastetools.filters.image.RecognizableImageType[]}
* @member CKEDITOR.plugins.pastetools.filters.image
*/
CKEDITOR.pasteFilters.image.recognizableImageTypes = [
{
marker: /\\pngblip/,
type: 'image/png'
},
{
marker: /\\jpegblip/,
type: 'image/jpeg'
},
{
marker: /\\emfblip/,
type: 'image/emf'
},
{
marker: /\\wmetafile\d/,
type: 'image/wmf'
}
];
/**
* Recognizable image file signatures with their respective types.
*
* The recognizing of image type is done by matching the first bytes
* of the signature represented as hex string.
*
* @private
* @since 4.16.0
* @type {CKEDITOR.plugins.pastetools.filters.image.RecognizableImageSignature[]}
* @member CKEDITOR.plugins.pastetools.filters.image
*/
CKEDITOR.pasteFilters.image.recognizableImageSignatures = [
{
signature: 'ffd8ff',
type: 'image/jpeg'
},
{
signature: '47494638',
type: 'image/gif'
},
{
signature: '89504e47',
type: 'image/png'
}
];
function handleRtfImages( html, rtf, imgTags ) {
var hexImages = extractFromRtf( rtf ),
newSrcValues,
i;
if ( hexImages.length === 0 ) {
return html;
}
newSrcValues = CKEDITOR.tools.array.map( hexImages, function( img ) {
return createSrcWithBase64( img );
}, this );
if ( imgTags.length !== newSrcValues.length ) {
CKEDITOR.error( 'pastetools-failed-image-extraction', {
rtf: hexImages.length,
html: imgTags.length
} );
return html;
}
// Assuming there is equal amount of Images in RTF and HTML source, so we can match them accordingly to the existing order.
for ( i = 0; i < imgTags.length; i++ ) {
// Replace only `file` urls of images ( shapes get newSrcValue with null ).
if ( imgTags[ i ].indexOf( 'file://' ) === 0 ) {
if ( !newSrcValues[ i ] ) {
CKEDITOR.error( 'pastetools-unsupported-image', {
type: hexImages[ i ].type,
index: i
} );
continue;
}
// In Word there is a chance that some of the images are also inserted via VML.
// This regex ensures that we replace only HTML <img> tags.
// Oh, and there are also Windows paths that need to be escaped
// before passing to regex.
var escapedPath = imgTags[ i ].replace( /\\/g, '\\\\' ),
imgRegex = new RegExp( '(<img [^>]*src=["\']?)' + escapedPath );
html = html.replace( imgRegex, '$1' + newSrcValues[ i ] );
}
}
return html;
}
function handleBlobImages( editor, html, imgTags ) {
var blobUrls = CKEDITOR.tools.array.unique( CKEDITOR.tools.array.filter( imgTags, function( imgTag ) {
return imgTag.match( /^blob:/i );
} ) ),
promises = CKEDITOR.tools.array.map( blobUrls, convertBlobUrlToBase64 );
CKEDITOR.tools.promise = Promise;
CKEDITOR.tools.promise.all( promises ).then( function( dataUrls ) {
CKEDITOR.tools.array.forEach( dataUrls, function( dataUrl, i ) {
if ( !dataUrl ) {
CKEDITOR.error( 'pastetools-unsupported-image', {
type: 'blob',
index: i
} );
return;
}
var blob = blobUrls[ i ],
nodeList = editor.editable().find( 'img[src="' + blob + '"]' ).toArray();
CKEDITOR.tools.array.forEach( nodeList, function( element ) {
element.setAttribute( 'src', dataUrl );
element.setAttribute( 'data-cke-saved-src', dataUrl );
}, this );
} );
} );
return html;
}
function extractFromRtf( rtfContent ) {
var filter = CKEDITOR.plugins.pastetools.filters.common.rtf,
ret = [],
wholeImages;
// Remove headers, footers, non-Word images and drawn objects.
// Headers and footers are in \header* and \footer* groups,
// non-Word images are inside \nonshp groups.
// Drawn objects are inside \shprslt and could be e.g. image alignment.
rtfContent = filter.removeGroups( rtfContent, '(?:(?:header|footer)[lrf]?|nonshppict|shprslt)' );
wholeImages = filter.getGroups( rtfContent, 'pict' );
if ( !wholeImages ) {
return ret;
}
for ( var i = 0; i < wholeImages.length; i++ ) {
var currentImage = wholeImages[ i ].content,
imageId = getImageId( currentImage ),
imageType = getImageType( currentImage ),
imageDataIndex = getImageIndex( imageId ),
isAlreadyExtracted = imageDataIndex !== -1 && ret[ imageDataIndex ].hex,
// If the same image is inserted more then once, the same id is used.
isDuplicated = isAlreadyExtracted && ret[ imageDataIndex ].type === imageType,
// Sometimes image is duplicated with another format, especially if
// it's right after the original one (so, in other words, original is the last image extracted).
isAlternateFormat = isAlreadyExtracted && ret[ imageDataIndex ].type !== imageType &&
imageDataIndex === ret.length - 1,
// WordArt shapes are defined using \defshp control word. Thanks to that
// they can be easily filtered.
isWordArtShape = currentImage.indexOf( '\\defshp' ) !== -1,
isSupportedType = CKEDITOR.tools.array.indexOf( CKEDITOR.pasteFilters.image.supportedImageTypes, imageType ) !== -1;
if ( isDuplicated ) {
ret.push( ret[ imageDataIndex ] );
continue;
}
if ( isAlternateFormat || isWordArtShape ) {
continue;
}
var newImageData = {
id: imageId,
hex: isSupportedType ? getImageContent( currentImage ) : null,
type: imageType
};
if ( imageDataIndex !== -1 ) {
ret.splice( imageDataIndex, 1, newImageData );
} else {
ret.push( newImageData );
}
}
return ret;
function getImageIndex( id ) {
// In some cases LibreOffice does not include ids for images.
// In that case, always treat them as unique (not found in the array).
if ( typeof id !== 'string' ) {
return -1;
}
return CKEDITOR.tools.array.indexOf( ret, function( image ) {
return image.id === id;
} );
}
function getImageId( image ) {
var blipUidRegex = /\\blipuid (\w+)\}/,
blipTagRegex = /\\bliptag(-?\d+)/,
blipUidMatch = image.match( blipUidRegex ),
blipTagMatch = image.match( blipTagRegex );
if ( blipUidMatch ) {
return blipUidMatch[ 1 ];
} else if ( blipTagMatch ) {
return blipTagMatch[ 1 ];
}
return null;
}
// Image content is basically \pict group content. However RTF sometimes
// break content into several lines and we don't want any whitespace
// in our images. So we need to get rid of it.
function getImageContent( image ) {
var content = filter.extractGroupContent( image );
return content.replace( /\s/g, '' );
}
}
function extractTagsFromHtml( html ) {
var regexp = /<img[^>]+src="([^"]+)[^>]+/g,
ret = [],
item;
while ( item = regexp.exec( html ) ) {
ret.push( item[ 1 ] );
}
return ret;
}
function getImageType( imageContent ) {
var tests = CKEDITOR.pasteFilters.image.recognizableImageTypes,
extractedType = find( tests, function( test ) {
return test.marker.test( imageContent );
} );
if ( extractedType ) {
return extractedType.type;
}
return 'unknown';
}
function find(array, fn, thisArg) {
var length = array.length,
i = 0;
while ( i < length ) {
if ( fn.call( thisArg, array[ i ], i, array ) ) {
return array[ i ];
}
i++;
}
return undefined;
}
function createSrcWithBase64( img ) {
var isSupportedType = CKEDITOR.tools.array.indexOf( CKEDITOR.pasteFilters.image.supportedImageTypes, img.type ) !== -1,
data = img.hex;
if ( !isSupportedType ) {
return null;
}
if ( typeof data === 'string' ) {
data = convertHexStringToBytes( img.hex );
}
return img.type ? 'data:' + img.type + ';base64,' + convertBytesToBase64( data ) : null;
}
function convertBytesToBase64(bytesArray) {
// Bytes are `8bit` numbers, where base64 use `6bit` to store data. That's why we process 3 Bytes into 4 characters representing base64.
//
// Algorithm:
// 1. Take `3 * 8bit`.
// 2. If there is less than 3 bytes, fill empty bits with zeros.
// 3. Transform `3 * 8bit` into `4 * 6bit` numbers.
// 4. Translate those numbers to proper characters related to base64.
// 5. If extra zero bytes were added fill them with `=` sign.
//
// Example:
// 1. Bytes Array: [ 8, 161, 29, 138, 218, 43 ] -> binary: `0000 1000 1010 0001 0001 1101 1000 1010 1101 1010 0010 1011`.
// 2. Binary: `0000 10|00 1010| 0001 00|01 1101| 1000 10|10 1101| 1010 00|10 1011` ← `|` (pipe) shows where base64 will cut bits during transformation.
// 3. Now we have 6bit numbers (written in decimal values), which are translated to indexes in `base64characters` array.
// Decimal: `2 10 4 29 34 45 40 43` → base64: `CKEditor`.
var base64characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
base64string = '',
bytesArrayLength = bytesArray.length,
i;
for ( i = 0; i < bytesArrayLength; i += 3 ) {
var array3 = bytesArray.slice( i, i + 3 ),
array3length = array3.length,
array4 = [],
j;
if ( array3length < 3 ) {
for ( j = array3length; j < 3; j++ ) {
array3[ j ] = 0;
}
}
// 0xFC -> 11111100 || 0x03 -> 00000011 || 0x0F -> 00001111 || 0xC0 -> 11000000 || 0x3F -> 00111111
array4[ 0 ] = ( array3[ 0 ] & 0xFC ) >> 2;
array4[ 1 ] = ( ( array3[ 0 ] & 0x03 ) << 4 ) | ( array3[ 1 ] >> 4 );
array4[ 2 ] = ( ( array3[ 1 ] & 0x0F ) << 2 ) | ( ( array3[ 2 ] & 0xC0 ) >> 6 );
array4[ 3 ] = array3[ 2 ] & 0x3F;
for ( j = 0; j < 4; j++ ) {
// Example: if array3length == 1, then we need to add 2 equal signs at the end of base64.
// array3[ 0 ] is used to calculate array4[ 0 ] and array4[ 1 ], so there will be regular values,
// next two ones have to be replaced with `=`, because array3[ 1 ] and array3[ 2 ] wasn't present in the input string.
if ( j <= array3length ) {
base64string += base64characters.charAt( array4[ j ] );
} else {
base64string += '=';
}
}
}
return base64string;
}
function convertHexStringToBytes(hexString) {
var bytesArray = [],
bytesArrayLength = hexString.length / 2,
i;
for ( i = 0; i < bytesArrayLength; i++ ) {
bytesArray.push( parseInt( hexString.substr( i * 2, 2 ), 16 ) );
}
return bytesArray;
}
function convertBlobUrlToBase64( blobUrlSrc ) {
return new CKEDITOR.tools.promise( function( resolve ) {
CKEDITOR.ajax.load( blobUrlSrc, function( arrayBuffer ) {
var data = new Uint8Array( arrayBuffer ),
imageType = getImageTypeFromSignature( data ),
base64 = createSrcWithBase64( {
type: imageType,
hex: data
} );
resolve( base64 );
} , 'arraybuffer' );
} );
}
function getImageTypeFromSignature( bytesArray ) {
var fileSignature = bytesArray.subarray( 0, 4 ),
hexSignature = CKEDITOR.tools.array.map( fileSignature, function( signatureByte ) {
return signatureByte.toString( 16 );
} ).join( '' ),
matchedType = CKEDITOR.tools.array.find( CKEDITOR.pasteFilters.image.recognizableImageSignatures,
function( test ) {
return hexSignature.indexOf( test.signature ) === 0;
} );
if ( !matchedType ) {
return null;
}
return matchedType.type;
}
} )();
/**
* Virtual class that illustrates image data
* returned by {@link CKEDITOR.plugins.pastetools.filters.image#extractFromRtf} method.
*
* @since 4.16.0
* @class CKEDITOR.plugins.pastetools.filters.image.ImageData
* @abstract
*/
/**
* Unique id of an image extracted from RTF content.
*
* @property {String} id
* @member CKEDITOR.plugins.pastetools.filters.image.ImageData
*/
/**
* Image content extracted from RTF content as a hexadecimal string.
*
* @property {String} hex
* @member CKEDITOR.plugins.pastetools.filters.image.ImageData
*/
/**
* MIME type of an image extracted from RTF content.
* If the type couldn't be extracted or it's unknown, `'unknown'` value is used.
*
* @property {String} type
* @member CKEDITOR.plugins.pastetools.filters.image.ImageData
*/
/**
* Virtual class that illustrates format of objects in
* {@link CKEDITOR.plugins.pastetools.filters.image#recognizableImageTypes} property.
*
* @since 4.16.0
* @class CKEDITOR.plugins.pastetools.filters.image.RecognizableImageType
* @abstract
*/
/**
* Regular expression that matches the marker unique for the given image type.
*
* @property {RegExp} marker
* @member CKEDITOR.plugins.pastetools.filters.image.RecognizableImageType
*/
/**
* Image MIME type.
*
* @property {String} type
* @member CKEDITOR.plugins.pastetools.filters.image.RecognizableImageType
*/
/**
* Virtual class that illustrates format of objects in
* {@link CKEDITOR.plugins.pastetools.filters.image#recognizableImageSignatures} property.
*
* @since 4.16.0
* @class CKEDITOR.plugins.pastetools.filters.image.RecognizableImageSignature
* @abstract
*/
/**
* File signature as a hex string.
*
* @property {String} signature
* @member CKEDITOR.plugins.pastetools.filters.image.RecognizableImageSignature
*/
/**
* Image MIME type.
*
* @property {String} type
* @member CKEDITOR.plugins.pastetools.filters.image.RecognizableImageSignature
*/