diff --git a/src/filters/removeboldwrapper.js b/src/filters/removeboldwrapper.js new file mode 100644 index 0000000..673c2ea --- /dev/null +++ b/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 `` 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 ); + } + } +} diff --git a/src/normalizer.jsdoc b/src/normalizer.jsdoc new file mode 100644 index 0000000..51f6bb3 --- /dev/null +++ b/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}. + */ diff --git a/src/normalizers/googledocsnormalizer.js b/src/normalizers/googledocsnormalizer.js new file mode 100644 index 0000000..9599e72 --- /dev/null +++ b/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 ); + } +} diff --git a/src/normalizers/mswordnormalizer.js b/src/normalizers/mswordnormalizer.js new file mode 100644 index 0000000..1bb8a7a --- /dev/null +++ b/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 = //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; + } +} diff --git a/src/pastefromoffice.js b/src/pastefromoffice.js index ff92d3f..bdd89d6 100644 --- a/src/pastefromoffice.js +++ b/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 @@ -31,49 +36,40 @@ export default class PasteFromOffice extends Plugin { return 'PasteFromOffice'; } + /** + * @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( //gi ) || - html.match( /xmlns:o="urn:schemas-microsoft-com/gi ) ) ); -} diff --git a/tests/_data/paste-from-google-docs/bold-wrapper/index.js b/tests/_data/paste-from-google-docs/bold-wrapper/index.js new file mode 100644 index 0000000..61a3818 --- /dev/null +++ b/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 + } + } +}; diff --git a/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/input.firefox.html b/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/input.firefox.html new file mode 100644 index 0000000..5187a76 --- /dev/null +++ b/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/input.firefox.html @@ -0,0 +1,4 @@ + +

Hello world

+ + diff --git a/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/input.html b/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/input.html new file mode 100644 index 0000000..50e8d90 --- /dev/null +++ b/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/input.html @@ -0,0 +1,5 @@ + + +

Hello world


+ + diff --git a/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/model.html b/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/model.html new file mode 100644 index 0000000..5d87fd8 --- /dev/null +++ b/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/model.html @@ -0,0 +1 @@ +Hello world diff --git a/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/normalized.firefox.html b/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/normalized.firefox.html new file mode 100644 index 0000000..c207b2c --- /dev/null +++ b/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/normalized.firefox.html @@ -0,0 +1 @@ +

Hello world

diff --git a/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/normalized.html b/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/normalized.html new file mode 100644 index 0000000..fb099a7 --- /dev/null +++ b/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/normalized.html @@ -0,0 +1 @@ +

Hello world


diff --git a/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/simple-text-windows.html b/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/simple-text-windows.html new file mode 100644 index 0000000..8f73e66 --- /dev/null +++ b/tests/_data/paste-from-google-docs/bold-wrapper/simple-text-windows/simple-text-windows.html @@ -0,0 +1 @@ +

Hello world

diff --git a/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/input.firefox.html b/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/input.firefox.html new file mode 100644 index 0000000..e698d5e --- /dev/null +++ b/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/input.firefox.html @@ -0,0 +1 @@ +

Hello world

diff --git a/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/input.html b/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/input.html new file mode 100644 index 0000000..297defb --- /dev/null +++ b/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/input.html @@ -0,0 +1 @@ +

Hello world


diff --git a/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/model.html b/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/model.html new file mode 100644 index 0000000..5d87fd8 --- /dev/null +++ b/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/model.html @@ -0,0 +1 @@ +Hello world diff --git a/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/normalized.firefox.html b/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/normalized.firefox.html new file mode 100644 index 0000000..491774a --- /dev/null +++ b/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/normalized.firefox.html @@ -0,0 +1 @@ +

Hello world

diff --git a/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/normalized.html b/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/normalized.html new file mode 100644 index 0000000..7ed6144 --- /dev/null +++ b/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/normalized.html @@ -0,0 +1 @@ +

Hello world


diff --git a/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/simple-text.html b/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/simple-text.html new file mode 100644 index 0000000..7459856 --- /dev/null +++ b/tests/_data/paste-from-google-docs/bold-wrapper/simple-text/simple-text.html @@ -0,0 +1 @@ +

Hello world

diff --git a/tests/_utils/fixtures.js b/tests/_utils/fixtures.js index efca05d..f41007a 100644 --- a/tests/_utils/fixtures.js +++ b/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 }; diff --git a/tests/_utils/utils.js b/tests/_utils/utils.js index 81a1614..8fad203 100644 --- a/tests/_utils/utils.js +++ b/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 `

`. // // @param {String} title Tests group title. // @param {Object} fixtures Object containing fixtures. @@ -131,32 +137,39 @@ function groupFixturesByBrowsers( browsers, fixturesGroup, skipBrowsers ) { // @param {Array.} 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 ] ); } ); diff --git a/tests/data/integration.js b/tests/data/integration.js index 2145846..7c00a1e 100644 --- a/tests/data/integration.js +++ b/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 ] + } + } ); } ); diff --git a/tests/data/normalization.js b/tests/data/normalization.js index c8452c2..48210ff 100644 --- a/tests/data/normalization.js +++ b/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 + } ); } ); diff --git a/tests/filters/image.js b/tests/filters/image.js index 79979de..f28391c 100644 --- a/tests/filters/image.js +++ b/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 = '

Foo

'; - 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 = '

Foo

'; - 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 = '

Foo

'; - 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 = '

Foo

'; + 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 = '

Foo

'; + 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 = '

Foo

'; + 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 ); } ); } ); } ); diff --git a/tests/filters/list.js b/tests/filters/list.js index a8d7617..5c3fb01 100644 --- a/tests/filters/list.js +++ b/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 = '

1.Item 1

'; - const view = htmlDataProcessor.toView( html ); + describe( 'transformListItemLikeElementsIntoLists()', () => { + it( 'replaces list-like elements with semantic lists', () => { + const html = '

1.Item 1

'; + 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( '
  1. Item 1
' ); - } ); + expect( view.childCount ).to.equal( 1 ); + expect( view.getChild( 0 ).name ).to.equal( 'ol' ); + expect( stringify( view ) ).to.equal( '
  1. Item 1
' ); + } ); - it( 'replaces list-like elements with semantic lists with proper bullet type based on styles', () => { - const html = '

1.Item 1

'; - const view = htmlDataProcessor.toView( html ); + it( 'replaces list-like elements with semantic lists with proper bullet type based on styles', () => { + const html = '

1.Item 1

'; + 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( '
  • Item 1
' ); - } ); + expect( view.childCount ).to.equal( 1 ); + expect( view.getChild( 0 ).name ).to.equal( 'ul' ); + expect( stringify( view ) ).to.equal( '
  • Item 1
' ); + } ); - it( 'does not modify the view if there are no list-like elements', () => { - const html = '

H1

Foo Bar

'; - const view = htmlDataProcessor.toView( html ); + it( 'does not modify the view if there are no list-like elements', () => { + const html = '

H1

Foo Bar

'; + 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 = '

1.Item 1

'; - const view = htmlDataProcessor.toView( html ); + it( 'handles empty `mso-list` style correctly', () => { + const html = '

1.Item 1

'; + 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( '
  1. Item 1
' ); - } ); + expect( view.childCount ).to.equal( 1 ); + expect( view.getChild( 0 ).name ).to.equal( 'ol' ); + expect( stringify( view ) ).to.equal( '
  1. Item 1
' ); + } ); - 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( '' ); } ); } ); } ); diff --git a/tests/filters/parse.js b/tests/filters/parse.js index b7e61f9..4f35a7d 100644 --- a/tests/filters/parse.js +++ b/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 = '

Foo Bar

'; - 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 = '

Foo Bar

'; + 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( '

Foo Bar

' ); + expect( bodyString ).to.equal( '

Foo Bar

' ); - 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 = '

Foo Bar

'; - const { body, bodyString, styles, stylesString } = parseHtml( html ); + it( 'correctly parses HTML with body contents only', () => { + const html = '

Foo Bar

'; + 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( '

Foo Bar

' ); + expect( bodyString ).to.equal( '

Foo Bar

' ); - 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 = ''; - const { body, bodyString, styles, stylesString } = parseHtml( html ); + it( 'correctly parses HTML with no body and multiple style tags', () => { + const 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 = ''; - const { body, bodyString, styles, stylesString } = parseHtml( html ); + it( 'correctly parses HTML with no body and no style tags', () => { + const 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 = '

Foo Bar

'; - const { body, bodyString, styles, stylesString } = parseHtml( html ); + it( 'correctly parses HTML with body contents and empty style tag', () => { + const html = '

Foo Bar

'; + 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( '

Foo Bar

' ); + expect( bodyString ).to.equal( '

Foo Bar

' ); - 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 = '

Foo Bar

Ba'; - const { body, bodyString, styles, stylesString } = parseHtml( html ); + it( 'should remove any content after body closing tag - plain', () => { + const html = '

Foo Bar

Ba'; + 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( '

Foo Bar

' ); + expect( bodyString ).to.equal( '

Foo Bar

' ); - 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 = '

Foo Bar

Fo'; - const { body, bodyString, styles, stylesString } = parseHtml( html ); + it( 'should remove any content after body closing tag - inline', () => { + const html = '

Foo Bar

Fo'; + 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( '

Foo Bar

' ); + expect( bodyString ).to.equal( '

Foo Bar

' ); - 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 = '

Foo Bar

ar

'; - const { body, bodyString, styles, stylesString } = parseHtml( html ); + it( 'should remove any content after body closing tag - block', () => { + const html = '

Foo Bar

ar

'; + 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( '

Foo Bar

' ); + expect( bodyString ).to.equal( '

Foo Bar

' ); - 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 = '

Foo Bar

oo'; - const { body, bodyString, styles, stylesString } = parseHtml( html ); + it( 'should remove any content after body closing tag - no html tag', () => { + const html = '

Foo Bar

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( '

Foo Bar

' ); + expect( bodyString ).to.equal( '

Foo Bar

' ); - 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 = '

Foo Bar

Baz'; - const { body, bodyString, styles, stylesString } = parseHtml( html ); + it( 'should not remove any content if no body tag', () => { + const html = '

Foo Bar

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( '

Foo Bar

Baz' ); + expect( bodyString ).to.equal( '

Foo Bar

Baz' ); - expect( styles.length ).to.equal( 0 ); + expect( styles.length ).to.equal( 0 ); - expect( stylesString ).to.equal( '' ); - } ); + expect( stylesString ).to.equal( '' ); } ); } ); } ); diff --git a/tests/filters/reomoveboldwrapper.js b/tests/filters/reomoveboldwrapper.js new file mode 100644 index 0000000..db35333 --- /dev/null +++ b/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 = '' + + '

Hello world

' + + '
'; + const documentFragment = htmlDataProcessor.toView( inputData ); + + removeBoldWrapper( documentFragment, writer ); + + expect( htmlDataProcessor.toData( documentFragment ) ).to.equal( '

Hello world

' ); + } ); + + it( 'should not remove non-bold tag with google id', () => { + const inputData = '

Hello world

'; + const documentFragment = htmlDataProcessor.toView( inputData ); + + removeBoldWrapper( documentFragment, writer ); + + expect( htmlDataProcessor.toData( documentFragment ) ).to.equal( + '

Hello world

' ); + } ); + + it( 'should not remove bold tag without google id', () => { + const inputData = 'Hello world'; + const documentFragment = htmlDataProcessor.toView( inputData ); + + removeBoldWrapper( documentFragment, writer ); + + expect( htmlDataProcessor.toData( documentFragment ) ).to.equal( + 'Hello world' ); + } ); + } ); +} ); diff --git a/tests/filters/space.js b/tests/filters/space.js index 93a8607..8898d03 100644 --- a/tests/filters/space.js +++ b/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 = '

Foo

Bar Baz

'; - const expected = '

Foo\u00A0

Bar \u00A0 Baz\u00A0

'; - 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 = '

Foo

Bar Baz

'; + const expected = '

Foo\u00A0

Bar \u00A0 Baz\u00A0

'; - it( 'should replace last space before special "o:p" tag with NBSP', () => { - const input = '

Foo Bar

'; - const expected = '

Foo \u00A0\u00A0 Bar

'; + 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 = '

Foo Bar

'; + const expected = '

Foo \u00A0\u00A0 Bar

'; - it( 'should remove newlines from spacerun spans #1', () => { - const input = ' \n'; - const expected = ' \u00A0'; + expect( normalizeSpacing( input ) ).to.equal( expected ); + } ); - expect( normalizeSpacing( input ) ).to.equal( expected ); - } ); + it( 'should remove newlines from spacerun spans #1', () => { + const input = ' \n'; + const expected = ' \u00A0'; - it( 'should remove newlines from spacerun spans #2', () => { - const input = ' \r\n'; - const expected = '\u00A0'; + expect( normalizeSpacing( input ) ).to.equal( expected ); + } ); - expect( normalizeSpacing( input ) ).to.equal( expected ); - } ); + it( 'should remove newlines from spacerun spans #2', () => { + const input = ' \r\n'; + const expected = '\u00A0'; - it( 'should remove newlines from spacerun spans #3', () => { - const input = ' \r\n\n '; - const expected = ' \u00A0'; + expect( normalizeSpacing( input ) ).to.equal( expected ); + } ); - expect( normalizeSpacing( input ) ).to.equal( expected ); - } ); + it( 'should remove newlines from spacerun spans #3', () => { + const input = ' \r\n\n '; + const expected = ' \u00A0'; - it( 'should remove newlines from spacerun spans #4', () => { - const input = '\n\n\n '; - const expected = ' \u00A0'; + expect( normalizeSpacing( input ) ).to.equal( expected ); + } ); - expect( normalizeSpacing( input ) ).to.equal( expected ); - } ); + it( 'should remove newlines from spacerun spans #4', () => { + const input = '\n\n\n '; + const expected = ' \u00A0'; - it( 'should remove newlines from spacerun spans #5', () => { - const input = '\n\n'; - const expected = ''; + expect( normalizeSpacing( input ) ).to.equal( expected ); + } ); - expect( normalizeSpacing( input ) ).to.equal( expected ); - } ); + it( 'should remove newlines from spacerun spans #5', () => { + const input = '\n\n'; + const expected = ''; - it( 'should remove multiline sequences of whitespaces', () => { - const input = '

Foo

\n\n \n

Bar

\r\n\r\n

Baz

'; - const expected = '

Foo

Bar

Baz

'; + expect( normalizeSpacing( input ) ).to.equal( expected ); + } ); - expect( normalizeSpacing( input ) ).to.equal( expected ); - } ); + it( 'should remove multiline sequences of whitespaces', () => { + const input = '

Foo

\n\n \n

Bar

\r\n\r\n

Baz

'; + const expected = '

Foo

Bar

Baz

'; - it( 'should normalize Safari "space spans"', () => { - const input = '

Foo Baz

'; - const expected = '

Foo \u00A0 \u00A0 Baz \u00A0\u00A0

'; + expect( normalizeSpacing( input ) ).to.equal( expected ); + } ); - expect( normalizeSpacing( input ) ).to.equal( expected ); - } ); + it( 'should normalize Safari "space spans"', () => { + const input = '

Foo Baz

'; + const expected = '

Foo \u00A0 \u00A0 Baz \u00A0\u00A0

'; - it( 'should normalize nested Safari "space spans"', () => { - const input = - '

Foo Baz

'; + expect( normalizeSpacing( input ) ).to.equal( expected ); + } ); - const expected = '

Foo \u00A0 \u00A0 \u00A0 Baz

'; + it( 'should normalize nested Safari "space spans"', () => { + const input = + '

Foo Baz

'; - expect( normalizeSpacing( input ) ).to.equal( expected ); - } ); + const expected = '

Foo \u00A0 \u00A0 \u00A0 Baz

'; + + expect( normalizeSpacing( input ) ).to.equal( expected ); } ); + } ); - describe( 'normalizeSpacerunSpans()', () => { - it( 'should normalize spaces inside special "span.spacerun" elements', () => { - const input = '

Foo

' + - '

Baz

'; + describe( 'normalizeSpacerunSpans()', () => { + it( 'should normalize spaces inside special "span.spacerun" elements', () => { + const input = '

Foo

' + + '

Baz

'; - const expected = '

   Foo

' + - '

Baz      

'; + const expected = '

   Foo

' + + '

Baz      

'; - 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 ); } ); } ); } ); diff --git a/tests/manual/integration.html b/tests/manual/integration.html index 15955a7..09375e5 100644 --- a/tests/manual/integration.html +++ b/tests/manual/integration.html @@ -1,3 +1,8 @@ +

Paste here:

@@ -10,11 +15,11 @@

Paste here:

-Clipboard HTML +

Clipboard HTML

-Clipboard text +

Clipboard text

-Clipboard HTML after transformation +

Clipboard normalized HTML view after transformation

diff --git a/tests/normalizers/googledocsnormalizer.js b/tests/normalizers/googledocsnormalizer.js new file mode 100644 index 0000000..58b9309 --- /dev/null +++ b/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( '

' ) ).to.be.true; + } ); + + it( 'should return false for microsoft word content', () => { + expect( normalizer.isActive( '

Foo bar

' ) ).to.be.false; + } ); + + it( 'should return false for content form other sources', () => { + expect( normalizer.isActive( '

foo

' ) ).to.be.false; + } ); + } ); +} ); diff --git a/tests/normalizers/mswordnormalizer.js b/tests/normalizers/mswordnormalizer.js new file mode 100644 index 0000000..db72e02 --- /dev/null +++ b/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( '

Foo bar

' ) ).to.be.true; + } ); + + it( 'should return true for microsoft word content - safari', () => { + expect( normalizer.isActive( '' ) ).to.be.true; + } ); + + it( 'should return false for google docs content', () => { + expect( normalizer.isActive( '

' ) ).to.be.false; + } ); + + it( 'should return false for content fromother sources', () => { + expect( normalizer.isActive( '

foo

' ) ).to.be.false; + } ); + } ); +} ); diff --git a/tests/pastefromoffice.js b/tests/pastefromoffice.js index a2692e5..f69f6b2 100644 --- a/tests/pastefromoffice.js +++ b/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': '' - } ); - - 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': '' - } ); - - 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': '' - } ); + describe( 'isTransformedWithPasteFromOffice - flag', () => { + describe( 'data which should be marked with flag', () => { + it( 'should process data with microsoft word header', () => { + checkCorrectData( '' ); + } ); - editor.plugins.get( 'Clipboard' ).fire( 'inputTransformation', { content, dataTransfer } ); + it( 'should process data with nested microsoft header', () => { + checkCorrectData( '' ); + } ); - expect( normalizeSpy.called ).to.false; - } ); + it( 'should process data from google docs', () => { + checkCorrectData( '

' ); + } ); + + 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': '' + 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( '

Hello world

' ); + } ); - evt.stop(); + it( 'should not process data with similar headers to MS Word', () => { + checkInvalidData( '' ); + } ); + + 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( '' + + '

Hello world

' ); + } ); + + it( 'should not process again google docs data containing a flag', () => { + checkAlreadyProcessedData( '

Hello world

' ); + } ); + + 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; + } } );