来自 google 文档的 ckeditor 粘贴删除了样式

ckeditor pasting from google docs removes the styles

当我从 google 文档复制文本并使用 paste from word 按钮将其粘贴到 ckeditor 时,它删除了所有样式(粗体、斜体……)。
我该如何解决?

重现步骤:
从 google 文档中复制一个带下划线和斜体的单词 将其粘贴到编辑器中。
预期结果:
该词以斜体和下划线粘贴。
实际结果:
这个词是粗体的,没有下划线或斜体。

config.js中添加以下行:

config.pasteFromWordRemoveFontStyles = false;
config.pasteFromWordRemoveStyles = false;

然后使用"Paste from Word"按钮(如下标记),不要直接使用CTRL+VCMD+V粘贴。

我正在发布我的解决方案以防它对某人有帮助。 我在从 googleDocs 粘贴时遇到了同样的问题。 我发现有人在 github (13877) 中创建了 CKEditor 的一个分支来纠正这个问题。

因为我需要它与我的 4.5.6 版一起使用,所以我将它改编成一个插件。 我这样做是为了与 Processwire 一起工作,我无法更改 CKEditor 版本,并且只在这种情况下对其进行了测试。

  • pasteFromGoogleDoc(文件夹)
    • plugin.js

plugin.js的内容:

            /**
         * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
         * For licensing, see LICENSE.md or http://ckeditor.com/license

         * 2017-07-05
         * the code originally from Frederico Knabben, written in ckeditor-dev-t-13877 branch
         * (https://github.com/cksource/ckeditor-dev/tree/t/13877)
         * has been adapted in a plugin by Rpapier for use in Processwire. It hasn't been tested elsewhere.

         * DESCRIPTION
         * Filter to paste from Google Doc and keep style (bold, italic, underline)
         * Those style must be present in the toolbar for it to show, otherwise, it will bypass it

         * For processwire :
         * you must edit the field that has CKEditor and make sure that :
         *  - ACF is On
         *  - pasteFromGoogleDoc plugin is enabled
         *  - CKEditor toolbar configuration contains Bold, Italic and Underline
         *      - e.g : Format, Styles, -, Bold, Italic, Underline, -, RemoveFormat
         *      if Underline is not in the toolbar for example, it will be bypassed by the filter. 
         */

        ( function() {
            'use strict';

            CKEDITOR.plugins.add( 'pasteFromGoogleDoc', {
                requires: ['clipboard'],

                init: function( editor ) {
                    // === arteractive hack for pasteFromGoogleDoc

                    var filterType,
                        filtersFactory = filtersFactoryFactory();

                    if ( editor.config.forcePasteAsPlainText ) {
                        filterType = 'plain-text';
                    } else if ( editor.config.pasteFilter ) {
                        filterType = editor.config.pasteFilter;
                    }
                    // On Webkit the pasteFilter defaults to 'webkit-default-filter' because pasted data is so terrible
                    // that it must be always filtered. (#13877)
                    else if ( CKEDITOR.env.webkit && !( 'pasteFilter' in editor.config ) ) {
                        filterType = 'webkit-default-filter';
                    }   

                    //console.log(filterType);
                    editor.pasteFilter = filtersFactory.get( filterType );



                    editor.on( 'paste', function( evt ) {
                        // Init `dataTransfer` if `paste` event was fired without it, so it will be always available.
                        if ( !evt.data.dataTransfer ) {
                            evt.data.dataTransfer = new CKEDITOR.plugins.clipboard.dataTransfer();
                        }

                        // If dataValue is already set (manually or by paste bin), so do not override it.
                        if ( evt.data.dataValue ) {
                            return;
                        }

                        var dataTransfer = evt.data.dataTransfer,
                            // IE support only text data and throws exception if we try to get html data.
                            // This html data object may also be empty if we drag content of the textarea.
                            value = dataTransfer.getData( 'text/html' );

                        if ( value ) {
                            evt.data.dataValue = value;
                            evt.data.type = 'html';
                        } else {
                            // Try to get text data otherwise.
                            value = dataTransfer.getData( 'text/plain' );

                            if ( value ) {
                                evt.data.dataValue = editor.editable().transformPlainTextToHtml( value );
                                evt.data.type = 'text';
                            }
                        }
                    }, null, null, 1 );


                    editor.on( 'paste', function( evt ) {
                        var dataObj = evt.data,
                            type = dataObj.type,
                            data = dataObj.dataValue,
                            trueType,
                            // Default is 'html'.
                            defaultType = editor.config.clipboard_defaultContentType || 'html',
                            transferType = dataObj.dataTransfer.getTransferType( editor );

                        // If forced type is 'html' we don't need to know true data type.
                        if ( type == 'html' || dataObj.preSniffing == 'html' ) {
                            trueType = 'html';
                        } else {
                            trueType = recogniseContentType( data );
                        }

                        // Unify text markup.
                        if ( trueType == 'htmlifiedtext' ) {
                            data = htmlifiedTextHtmlification( editor.config, data );
                        }

                        // Strip presentational markup & unify text markup.
                        // Forced plain text (dialog or forcePAPT).
                        // Note: we do not check dontFilter option in this case, because forcePAPT was implemented
                        // before pasteFilter and pasteFilter is automatically used on Webkit&Blink since 4.5, so
                        // forcePAPT should have priority as it had before 4.5.
                        if ( type == 'text' && trueType == 'html' ) {
                            data = filterContent( editor, data, filtersFactory.get( 'plain-text' ) );
                        }
                        // External paste and pasteFilter exists and filtering isn't disabled.
                        else if ( transferType == CKEDITOR.DATA_TRANSFER_EXTERNAL && editor.pasteFilter && !dataObj.dontFilter ) {

                            // 2017-07-05 comment out this filter because it is already processsed somewhere...
                            //data = filterContent( editor, data, editor.pasteFilter );
                            //console.log(data);
                        }

                        if ( dataObj.startsWithEOL ) {
                            data = '<br data-cke-eol="1">' + data;
                        }
                        if ( dataObj.endsWithEOL ) {
                            data += '<br data-cke-eol="1">';
                        }

                        if ( type == 'auto' ) {
                            type = ( trueType == 'html' || defaultType == 'html' ) ? 'html' : 'text';
                        }

                        dataObj.type = type;
                        dataObj.dataValue = data;
                        delete dataObj.preSniffing;
                        delete dataObj.startsWithEOL;
                        delete dataObj.endsWithEOL;




                    /*  evt.data.dataValue = data;

                        evt.data.dataValue = evt.data.dataValue
                            .replace( /zooterkins/gi, 'z********s' )
                            .replace( /gadzooks/gi, 'g******s' );*/

                    }, null, null, 6 );             

                }
            } );    

            function filterContent( editor, data, filter ) {
                var fragment = CKEDITOR.htmlParser.fragment.fromHtml( data ),
                    writer = new CKEDITOR.htmlParser.basicWriter();

                filter.applyTo( fragment, true, false, editor.activeEnterMode );
                fragment.writeHtml( writer );

                return writer.getHtml();
            }



            // Returns:
            // * 'htmlifiedtext' if content looks like transformed by browser from plain text.
            //      See clipboard/paste.html TCs for more info.
            // * 'html' if it is not 'htmlifiedtext'.
            function recogniseContentType( data ) {
                if ( CKEDITOR.env.webkit ) {
                    // Plain text or ( <div><br></div> and text inside <div> ).
                    if ( !data.match( /^[^<]*$/g ) && !data.match( /^(<div><br( ?\/)?><\/div>|<div>[^<]*<\/div>)*$/gi ) )
                        return 'html';
                } else if ( CKEDITOR.env.ie ) {
                    // Text and <br> or ( text and <br> in <p> - paragraphs can be separated by new \r\n ).
                    if ( !data.match( /^([^<]|<br( ?\/)?>)*$/gi ) && !data.match( /^(<p>([^<]|<br( ?\/)?>)*<\/p>|(\r\n))*$/gi ) )
                        return 'html';
                } else if ( CKEDITOR.env.gecko ) {
                    // Text or <br>.
                    if ( !data.match( /^([^<]|<br( ?\/)?>)*$/gi ) )
                        return 'html';
                } else {
                    return 'html';
                }

                return 'htmlifiedtext';
            }


            // This function transforms what browsers produce when
            // pasting plain text into editable element (see clipboard/paste.html TCs
            // for more info) into correct HTML (similar to that produced by text2Html).
            function htmlifiedTextHtmlification( config, data ) {
                function repeatParagraphs( repeats ) {
                    // Repeat blocks floor((n+1)/2) times.
                    // Even number of repeats - add <br> at the beginning of last <p>.
                    return CKEDITOR.tools.repeat( '</p><p>', ~~( repeats / 2 ) ) + ( repeats % 2 == 1 ? '<br>' : '' );
                }

                    // Replace adjacent white-spaces (EOLs too - Fx sometimes keeps them) with one space.
                data = data.replace( /\s+/g, ' ' )
                    // Remove spaces from between tags.
                    .replace( /> +</g, '><' )
                    // Normalize XHTML syntax and upper cased <br> tags.
                    .replace( /<br ?\/>/gi, '<br>' );

                // IE - lower cased tags.
                data = data.replace( /<\/?[A-Z]+>/g, function( match ) {
                    return match.toLowerCase();
                } );

                // Don't touch single lines (no <br|p|div>) - nothing to do here.
                if ( data.match( /^[^<]$/ ) )
                    return data;

                // Webkit.
                if ( CKEDITOR.env.webkit && data.indexOf( '<div>' ) > -1 ) {
                        // One line break at the beginning - insert <br>
                    data = data.replace( /^(<div>(<br>|)<\/div>)(?!$|(<div>(<br>|)<\/div>))/g, '<br>' )
                        // Two or more - reduce number of new lines by one.
                        .replace( /^(<div>(<br>|)<\/div>){2}(?!$)/g, '<div></div>' );

                    // Two line breaks create one paragraph in Webkit.
                    if ( data.match( /<div>(<br>|)<\/div>/ ) ) {
                        data = '<p>' + data.replace( /(<div>(<br>|)<\/div>)+/g, function( match ) {
                            return repeatParagraphs( match.split( '</div><div>' ).length + 1 );
                        } ) + '</p>';
                    }

                    // One line break create br.
                    data = data.replace( /<\/div><div>/g, '<br>' );

                    // Remove remaining divs.
                    data = data.replace( /<\/?div>/g, '' );
                }

                // Opera and Firefox and enterMode != BR.
                if ( CKEDITOR.env.gecko && config.enterMode != CKEDITOR.ENTER_BR ) {
                    // Remove bogus <br> - Fx generates two <brs> for one line break.
                    // For two line breaks it still produces two <brs>, but it's better to ignore this case than the first one.
                    if ( CKEDITOR.env.gecko )
                        data = data.replace( /^<br><br>$/, '<br>' );

                    // This line satisfy edge case when for Opera we have two line breaks
                    //data = data.replace( /)

                    if ( data.indexOf( '<br><br>' ) > -1 ) {
                        // Two line breaks create one paragraph, three - 2, four - 3, etc.
                        data = '<p>' + data.replace( /(<br>){2,}/g, function( match ) {
                            return repeatParagraphs( match.length / 4 );
                        } ) + '</p>';
                    }
                }

                return switchEnterMode( config, data );
            }


            function filtersFactoryFactory() {
                var filters = {};

                // GDocs generates many spans and divs, therefore `all` parameter is used
                // to create default filter in Webkit/Blink. (#13877)
                function setUpTags( all ) {
                    var tags = {};

                    for ( var tag in CKEDITOR.dtd ) {
                        if ( tag.charAt( 0 ) != '$' && ( all || tag != 'div' && tag != 'span') ) {
                            tags[ tag ] = 1;
                        }
                    }

                    return tags;
                }

                // Checks if content is pasted from Google Docs.
                // Google Docs wraps everything in element with [id^=docs-internal-guid-],
                // so that function just checks if such element exists. (#13877)
                function isPastedFromGDocs( element ) {
                    if ( element.attributes.id && element.attributes.id.match( /^docs\-internal\-guid\-/ ) ) {
                        return true;
                    } else if ( element.parent && element.parent.name ) {
                        return isPastedFromGDocs( element.parent );
                    }

                    return false;
                }

                // Process data from Google Docs:
                // * turns `*[id^=docs-internal-guid-]` into `span`;
                // * turns `span(text-decoration=underline)` into `u`;
                // * turns `span(font-style=italic)` into `em`
                // * turns `span(font-style=italic)(text-decoration=underline)` into `u > em`. (#13877)
                // 
                function processDataFromGDocs( element ) {

                    var styles = element.attributes.style && CKEDITOR.tools.parseCssText( element.attributes.style );

                    if ( element.attributes.id && element.attributes.id.match( /^docs\-internal\-guid\-/ ) ) {
                        return element.name = 'span';               
                    }

                    if ( !styles ) {
                        return;
                    }

                    if ( styles[ 'font-style' ] == 'italic' && styles[ 'text-decoration' ] == 'underline' ) {
                        element.name = 'em';
                        element.wrapWith( new CKEDITOR.htmlParser.element( 'u' ) );     
                        if (styles[ 'font-weight' ] > 400) {
                            element.wrapWith( new CKEDITOR.htmlParser.element( 'strong' ) );    
                        }                   
                    } else if ( styles[ 'text-decoration' ] == 'underline' ) {
                        element.name = 'u';     
                        if (styles[ 'font-weight' ] > 400) {
                            element.wrapWith( new CKEDITOR.htmlParser.element( 'strong' ) );                        
                        }                   
                    } else if ( styles[ 'font-style' ] == 'italic' ) {
                        element.name = 'em';    
                        if (styles[ 'font-weight' ] > 400) {
                            element.wrapWith( new CKEDITOR.htmlParser.element( 'strong' ) );                        
                        }               
                    }
                }

                function createSemanticContentFilter() {
                    var filter = new CKEDITOR.filter();

                    filter.allow( {
                        : {
                            elements: setUpTags(),
                            attributes: true,
                            styles: false,
                            classes: false
                        }
                    } );

                    return filter;
                }

                function createWebkitDefaultFilter() {
                    var filter = createSemanticContentFilter();

                    // Preserves formatting while pasting from Google Docs in Webkit/Blink
                    // with default paste filter. (#13877)
                    filter.allow( {
                        : {
                            elements: setUpTags( true ),
                            attributes: true,
                            styles: true,
                            match: function( element ) {
                                return isPastedFromGDocs( element );
                            }
                        }
                    } );

                    filter.addElementCallback( processDataFromGDocs );

                    return filter;
                }

                return {
                    get: function( type ) {
                        if ( type == 'plain-text' ) {
                            // Does this look confusing to you? Did we forget about enter mode?
                            // It is a trick that let's us creating one filter for edidtor, regardless of its
                            // activeEnterMode (which as the name indicates can change during runtime).
                            //
                            // How does it work?
                            // The active enter mode is passed to the filter.applyTo method.
                            // The filter first marks all elements except <br> as disallowed and then tries to remove
                            // them. However, it cannot remove e.g. a <p> element completely, because it's a basic structural element,
                            // so it tries to replace it with an element created based on the active enter mode, eventually doing nothing.
                            //
                            // Now you can sleep well.
                            return filters.plainText || ( filters.plainText = new CKEDITOR.filter( 'br' ) );
                        } else if ( type == 'semantic-content' ) {
                            return filters.semanticContent || ( filters.semanticContent = createSemanticContentFilter() );
                        } else if ( type == 'webkit-default-filter' ) {
                            // Webkit based browsers need semantic filter, because they produce terrible HTML without it.
                            // However original `'semantic-content'` filer is too strict and prevents pasting styled contents
                            // from many sources (e.g. Google Docs). Therefore that type extends original `'semantic-content'` filter. (#13877)
                            return filters.webkitDefaultFilter || ( filters.webkitDefaultFilter = createWebkitDefaultFilter() );
                        } else if ( type ) {
                            // Create filter based on rules (string or object).
                            return new CKEDITOR.filter( type );
                        }

                        return null;
                    }
                };
            }


            function switchEnterMode( config, data ) {
                if ( config.enterMode == CKEDITOR.ENTER_BR ) {
                    data = data.replace( /(<\/p><p>)+/g, function( match ) {
                        return CKEDITOR.tools.repeat( '<br>', match.length / 7 * 2 );
                    } ).replace( /<\/?p>/g, '' );
                } else if ( config.enterMode == CKEDITOR.ENTER_DIV ) {
                    data = data.replace( /<(\/)?p>/g, '<div>' );
                }

                return data;
            }


        } )();