Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add htmlNormalization event to ClipboardPipeline. #17696

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 57 additions & 8 deletions packages/ckeditor5-clipboard/src/clipboardpipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,15 +224,17 @@ export default class ClipboardPipeline extends Plugin {
if ( data.content ) {
content = data.content;
} else {
let contentData = '';

if ( dataTransfer.getData( 'text/html' ) ) {
contentData = normalizeClipboardHtml( dataTransfer.getData( 'text/html' ) );
} else if ( dataTransfer.getData( 'text/plain' ) ) {
contentData = plainTextToHtml( dataTransfer.getData( 'text/plain' ) );
const htmlOrView = this.fire<ClipboardInputHTMLNormalizationEvent>( 'inputHtmlNormalization', {
html: dataTransfer.getData( 'text/html' ),
dataTransfer,
method: data.method
} ) || '';

if ( typeof htmlOrView === 'string' ) {
content = this.editor.data.htmlProcessor.toView( htmlOrView );
} else {
content = htmlOrView;
}

content = this.editor.data.htmlProcessor.toView( contentData );
}

const eventInfo = new EventInfo( this, 'inputTransformation' );
Expand Down Expand Up @@ -287,6 +289,18 @@ export default class ClipboardPipeline extends Plugin {
this.listenTo<ClipboardContentInsertionEvent>( this, 'contentInsertion', ( evt, data ) => {
data.resultRange = clipboardMarkersUtils._pasteFragmentWithMarkers( data.content );
}, { priority: 'low' } );

this.listenTo<ClipboardInputHTMLNormalizationEvent>( this, 'inputHtmlNormalization', ( evt, { dataTransfer } ) => {
if ( evt.return ) {
return;
}

if ( dataTransfer.getData( 'text/html' ) ) {
evt.return = normalizeClipboardHtml( dataTransfer.getData( 'text/html' ) );
} else if ( dataTransfer.getData( 'text/plain' ) ) {
evt.return = plainTextToHtml( dataTransfer.getData( 'text/plain' ) );
}
}, { priority: 'low' } );
}

/**
Expand Down Expand Up @@ -391,6 +405,41 @@ export interface ClipboardInputTransformationData {
method: 'paste' | 'drop';
}

/**
* Fired when the HTML content from the clipboard is being normalized, just before it is processed by the input pipeline and
* converted to a view document fragment.
*
* It is a part of the {@glink framework/deep-dive/clipboard#input-pipeline clipboard input pipeline}.
*
* **Note**: Your should not stop this event if you want to change the input data. You should modify the `return` property instead.
*/
export type ClipboardInputHTMLNormalizationEvent = {
name: 'inputHtmlNormalization';
args: [ data: ClipboardInputHTMLNormalizationData ];
return: string | ViewDocumentFragment;
};

/**
* The data of 'inputHtmlNormalization' event.
*/
export interface ClipboardInputHTMLNormalizationData {

/**
* The data transfer instance.
*/
dataTransfer: DataTransfer;

/**
* The HTML content from the clipboard.
*/
html: string;

/**
* Whether the event was triggered by a paste or a drop operation.
*/
method: 'paste' | 'drop';
}

/**
* Fired with the `content`, `dataTransfer`, `method`, and `targetRanges` properties:
*
Expand Down
1 change: 1 addition & 0 deletions packages/ckeditor5-clipboard/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export {
default as ClipboardPipeline,
type ClipboardContentInsertionEvent,
type ClipboardContentInsertionData,
type ClipboardInputHTMLNormalizationEvent,
type ClipboardInputTransformationEvent,
type ClipboardInputTransformationData,
type ClipboardOutputTransformationEvent,
Expand Down
104 changes: 104 additions & 0 deletions packages/ckeditor5-clipboard/tests/clipboardpipeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,110 @@ describe( 'ClipboardPipeline feature', () => {
expect( spy.callCount ).to.equal( 1 );
} );

describe( 'html normalization', () => {
it( 'should allow chaining html normalization', () => {
const dataTransferMock = createDataTransfer( { 'text/html': '<p>foo</p>', 'text/plain': 'foo' } );
const preventDefaultSpy = sinon.spy();

clipboardPlugin.on( 'inputHtmlNormalization', ( evt, data ) => {
evt.return = data.html.replace( 'foo', 'bar' );
}, { priority: 'high' } );

clipboardPlugin.on( 'inputTransformation', ( evt, data ) => {
expect( stringifyView( data.content ) ).to.equal( '<p>bar</p>' );
} );

viewDocument.fire( 'paste', {
stopPropagation: () => {},
dataTransfer: dataTransferMock,
preventDefault: preventDefaultSpy
} );
} );

it( 'should allow chaining html normalization (pick content from data transfer)', () => {
const dataTransferMock = createDataTransfer( { 'text/html': '<p>foo</p>', 'text/plain': 'foo' } );
const preventDefaultSpy = sinon.spy();

clipboardPlugin.on( 'inputHtmlNormalization', ( evt, { dataTransfer } ) => {
evt.return = dataTransfer.getData( 'text/html' ).replace( 'foo', 'bar' );
}, { priority: 'high' } );

clipboardPlugin.on( 'inputTransformation', ( evt, data ) => {
expect( stringifyView( data.content ) ).to.equal( '<p>bar</p>' );
} );

viewDocument.fire( 'paste', {
stopPropagation: () => {},
dataTransfer: dataTransferMock,
preventDefault: preventDefaultSpy
} );
} );

it( 'should allow stopping html normalization chain', () => {
const dataTransferMock = createDataTransfer( { 'text/html': '<p>foo</p>', 'text/plain': 'foo' } );
const preventDefaultSpy = sinon.spy();

clipboardPlugin.on( 'inputHtmlNormalization', ( evt, { html } ) => {
evt.return = html.replace( 'foo', 'bar' );
evt.stop();
}, { priority: 'high' } );

clipboardPlugin.on( 'inputHtmlNormalization', evt => {
// This handler should not be called
evt.return = '<p>baz</p>';
}, { priority: 'normal' } );

clipboardPlugin.on( 'inputTransformation', ( _, data ) => {
expect( stringifyView( data.content ) ).to.equal( '<p>bar</p>' );
} );

viewDocument.fire( 'paste', {
stopPropagation: () => {},
dataTransfer: dataTransferMock,
preventDefault: preventDefaultSpy
} );
} );

it( 'should call default normalizer if no custom handlers provided HTML content', () => {
const dataTransferMock = createDataTransfer( { 'text/html': '<p>foo</p>', 'text/plain': 'foo' } );
const preventDefaultSpy = sinon.spy();

clipboardPlugin.on( 'inputHtmlNormalization', () => {
// This handler does not provide any content
}, { priority: 'high' } );

clipboardPlugin.on( 'inputTransformation', ( _, data ) => {
expect( stringifyView( data.content ) ).to.equal( '<p>foo</p>' );
} );

viewDocument.fire( 'paste', {
stopPropagation: () => {},
dataTransfer: dataTransferMock,
preventDefault: preventDefaultSpy
} );
} );

it( 'should be possible to return view document fragment from html normalization', () => {
const dataTransferMock = createDataTransfer( { 'text/html': '<p>foo</p>', 'text/plain': 'foo' } );
const preventDefaultSpy = sinon.spy();

clipboardPlugin.on( 'inputHtmlNormalization', ( evt, { html } ) => {
evt.return = parseView( html );
}, { priority: 'high' } );

clipboardPlugin.on( 'inputTransformation', ( _, data ) => {
expect( data.content ).not.be.instanceOf( String );
expect( stringifyView( data.content ) ).to.equal( '<p>foo</p>' );
} );

viewDocument.fire( 'paste', {
stopPropagation: () => {},
dataTransfer: dataTransferMock,
preventDefault: preventDefaultSpy
} );
} );
} );

function createDataTransfer( data ) {
return {
getData( type ) {
Expand Down
2 changes: 1 addition & 1 deletion packages/ckeditor5-paste-from-office/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

export { default as PasteFromOffice } from './pastefromoffice.js';
export type { Normalizer, NormalizerData } from './normalizer.js';
export type { Normalizer } from './normalizer.js';
export { default as MSWordNormalizer } from './normalizers/mswordnormalizer.js';
export { parseHtml } from './filters/parse.js';

Expand Down
10 changes: 2 additions & 8 deletions packages/ckeditor5-paste-from-office/src/normalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
* @module paste-from-office/normalizer
*/

import type { ClipboardInputTransformationData } from 'ckeditor5/src/clipboard.js';
import type { ParseHtmlResult } from './filters/parse.js';
import type { DataTransfer, ViewDocumentFragment } from 'ckeditor5/src/engine.js';

/**
* Interface defining a content transformation pasted from an external editor.
Expand All @@ -27,10 +26,5 @@ export interface Normalizer {
/**
* Executes the normalization of a given data.
*/
execute( data: NormalizerData ): void;
}

export interface NormalizerData extends ClipboardInputTransformationData {
_isTransformedWithPasteFromOffice?: boolean;
_parsedData: ParseHtmlResult;
execute( data: DataTransfer ): ViewDocumentFragment;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@
* @module paste-from-office/normalizers/googledocsnormalizer
*/

import { UpcastWriter, type ViewDocument } from 'ckeditor5/src/engine.js';
import {
UpcastWriter,
type ViewDocument,
type DataTransfer,
type ViewDocumentFragment
} from 'ckeditor5/src/engine.js';

import removeBoldWrapper from '../filters/removeboldwrapper.js';
import transformBlockBrsToParagraphs from '../filters/br.js';
import { unwrapParagraphInListItem } from '../filters/list.js';
import type { Normalizer, NormalizerData } from '../normalizer.js';
import type { Normalizer } from '../normalizer.js';
import { parseHtml } from '../filters/parse.js';

const googleDocsMatch = /id=("|')docs-internal-guid-[-0-9a-f]+("|')/i;

Expand Down Expand Up @@ -41,14 +47,17 @@ export default class GoogleDocsNormalizer implements Normalizer {
/**
* @inheritDoc
*/
public execute( data: NormalizerData ): void {
public execute( dataTransfer: DataTransfer ): ViewDocumentFragment {
const writer = new UpcastWriter( this.document );
const { body: documentFragment } = data._parsedData;
const { body: documentFragment } = parseHtml(
dataTransfer.getData( 'text/html' ),
this.document.stylesProcessor
);

removeBoldWrapper( documentFragment, writer );
unwrapParagraphInListItem( documentFragment, writer );
transformBlockBrsToParagraphs( documentFragment, writer );

data.content = documentFragment;
return documentFragment;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@
* @module paste-from-office/normalizers/googlesheetsnormalizer
*/

import { UpcastWriter, type ViewDocument } from 'ckeditor5/src/engine.js';
import {
UpcastWriter,
type ViewDocument,
type DataTransfer,
type ViewDocumentFragment
} from 'ckeditor5/src/engine.js';

import removeXmlns from '../filters/removexmlns.js';
import removeGoogleSheetsTag from '../filters/removegooglesheetstag.js';
import removeInvalidTableWidth from '../filters/removeinvalidtablewidth.js';
import removeStyleBlock from '../filters/removestyleblock.js';
import type { Normalizer, NormalizerData } from '../normalizer.js';
import type { Normalizer } from '../normalizer.js';
import { parseHtml } from '../filters/parse.js';

const googleSheetsMatch = /<google-sheets-html-origin/i;

Expand Down Expand Up @@ -42,15 +48,18 @@ export default class GoogleSheetsNormalizer implements Normalizer {
/**
* @inheritDoc
*/
public execute( data: NormalizerData ): void {
public execute( dataTransfer: DataTransfer ): ViewDocumentFragment {
const writer = new UpcastWriter( this.document );
const { body: documentFragment } = data._parsedData;
const { body: documentFragment } = parseHtml(
dataTransfer.getData( 'text/html' ),
this.document.stylesProcessor
);

removeGoogleSheetsTag( documentFragment, writer );
removeXmlns( documentFragment, writer );
removeInvalidTableWidth( documentFragment, writer );
removeStyleBlock( documentFragment, writer );

data.content = documentFragment;
return documentFragment;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@ import transformBookmarks from '../filters/bookmark.js';
import { transformListItemLikeElementsIntoLists } from '../filters/list.js';
import { replaceImagesSourceWithBase64 } from '../filters/image.js';
import removeMSAttributes from '../filters/removemsattributes.js';
import { UpcastWriter, type ViewDocument } from 'ckeditor5/src/engine.js';
import type { Normalizer, NormalizerData } from '../normalizer.js';
import {
UpcastWriter,
type ViewDocumentFragment,
type DataTransfer,
type ViewDocument
} from 'ckeditor5/src/engine.js';
import type { Normalizer } from '../normalizer.js';
import { parseHtml } from '../filters/parse.js';

const msWordMatch1 = /<meta\s*name="?generator"?\s*content="?microsoft\s*word\s*\d+"?\/?>/i;
const msWordMatch2 = /xmlns:o="urn:schemas-microsoft-com/i;
Expand Down Expand Up @@ -45,15 +51,18 @@ export default class MSWordNormalizer implements Normalizer {
/**
* @inheritDoc
*/
public execute( data: NormalizerData ): void {
public execute( dataTransfer: DataTransfer ): ViewDocumentFragment {
const writer = new UpcastWriter( this.document );
const { body: documentFragment, stylesString } = data._parsedData;
const { body: documentFragment, stylesString } = parseHtml(
dataTransfer.getData( 'text/html' ),
this.document.stylesProcessor
);

transformBookmarks( documentFragment, writer );
transformListItemLikeElementsIntoLists( documentFragment, stylesString, this.hasMultiLevelListPlugin );
replaceImagesSourceWithBase64( documentFragment, data.dataTransfer.getData( 'text/rtf' ) );
replaceImagesSourceWithBase64( documentFragment, dataTransfer.getData( 'text/rtf' ) );
removeMSAttributes( documentFragment );

data.content = documentFragment;
return documentFragment;
}
}
Loading
Loading