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

Paste from word should work for legal list only #15982

Merged
37 changes: 24 additions & 13 deletions packages/ckeditor5-paste-from-office/src/filters/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ import {
*/
export function transformListItemLikeElementsIntoLists(
documentFragment: ViewDocumentFragment,
stylesString: string
stylesString: string,
hasMultiLevelListPlugin: boolean
): void {
if ( !documentFragment.childCount ) {
return;
Expand Down Expand Up @@ -61,12 +62,12 @@ export function transformListItemLikeElementsIntoLists(
const listStyle = detectListStyle( itemLikeElement, stylesString );

if ( !currentList ) {
currentList = insertNewEmptyList( listStyle, itemLikeElement.element, writer );
currentList = insertNewEmptyList( listStyle, itemLikeElement.element, writer, hasMultiLevelListPlugin );
} else if ( itemLikeElement.indent > currentIndentation ) {
const lastListItem = currentList.getChild( currentList.childCount - 1 ) as ViewElement;
const lastListItemChild = lastListItem!.getChild( lastListItem.childCount - 1 ) as ViewElement;

currentList = insertNewEmptyList( listStyle, lastListItemChild, writer );
currentList = insertNewEmptyList( listStyle, lastListItemChild, writer, hasMultiLevelListPlugin );
currentIndentation += 1;
} else if ( itemLikeElement.indent < currentIndentation ) {
const differentIndentation = currentIndentation - itemLikeElement.indent;
Expand Down Expand Up @@ -178,9 +179,18 @@ function detectListStyle( listLikeItem: ListLikeElement, stylesString: string )
const listStyleRegexp = new RegExp( `@list l${ listLikeItem.id }:level${ listLikeItem.indent }\\s*({[^}]*)`, 'gi' );
const listStyleTypeRegex = /mso-level-number-format:([^;]{0,100});/gi;
const listStartIndexRegex = /mso-level-start-at:\s{0,100}([0-9]{0,10})\s{0,100};/gi;
const legalListTypeRegex = new RegExp( `@list l${ listLikeItem.id }:level\\d\\s*{[^{]*mso-level-text:"%\\d\\\\.`, 'gi' );
const legalStyleListRegex = new RegExp( `@list\\s{1,}l${ listLikeItem.id }:level\\d\\s*{[^{]*mso-level-text:"%\\d\\\\.`, 'gi' );
const multiLevelNumberFormatTypeRegex = new RegExp( `@list l${ listLikeItem.id }:level\\d\\s*{[^{]*mso-level-number-format:`, 'gi' );

const legalListMatch = legalListTypeRegex.exec( stylesString );
const legalStyleListMatch = legalStyleListRegex.exec( stylesString );
const multiLevelNumberFormatMatch = multiLevelNumberFormatTypeRegex.exec( stylesString );

/**
* Multi level lists in Word have mso-level-number-format attribute except legal lists,
* so we used that. If list has legal list match and doesn't has mso-level-number-format
* then this is legal-list.
*/
const islegalStyleList = legalStyleListMatch && !multiLevelNumberFormatMatch;

const listStyleMatch = listStyleRegexp.exec( stylesString );

Expand Down Expand Up @@ -214,16 +224,16 @@ function detectListStyle( listLikeItem: ListLikeElement, stylesString: string )
}
}

if ( legalListMatch ) {
// type = 'ol'; // TODO: while working on the task: PFW should work for legal only
if ( islegalStyleList ) {
type = 'ol';
}
}

return {
type,
startIndex,
style: mapListStyleDefinition( listStyleType )
// isLegal: !!legalListMatch // TODO: while working on the task: PFW should work for legal only
style: mapListStyleDefinition( listStyleType ),
isLegalStyleList: islegalStyleList
};
}

Expand Down Expand Up @@ -324,7 +334,8 @@ function mapListStyleDefinition( value: string ) {
function insertNewEmptyList(
listStyle: ReturnType<typeof detectListStyle>,
element: ViewElement,
writer: UpcastWriter
writer: UpcastWriter,
hasMultiLevelListPlugin: boolean
) {
const parent = element.parent!;
const list = writer.createElement( listStyle.type );
Expand All @@ -342,9 +353,9 @@ function insertNewEmptyList(
writer.setAttribute( 'start', listStyle.startIndex, list );
}

// if ( listStyle.isLegal ) { // TODO: while working on the task: PFW should work for legal only
// writer.addClass( 'legal-list', list );
// }
if ( listStyle.isLegalStyleList && hasMultiLevelListPlugin ) {
writer.addClass( 'legal-list', list );
}

return list;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@ const msWordMatch2 = /xmlns:o="urn:schemas-microsoft-com/i;
export default class MSWordNormalizer implements Normalizer {
public readonly document: ViewDocument;

public readonly hasMultiLevelListPlugin: boolean;

/**
* Creates a new `MSWordNormalizer` instance.
*
* @param document View document.
*/
constructor( document: ViewDocument ) {
constructor( document: ViewDocument, hasMultiLevelListPlugin: boolean ) {
this.document = document;
this.hasMultiLevelListPlugin = hasMultiLevelListPlugin;
}

/**
Expand All @@ -44,7 +47,7 @@ export default class MSWordNormalizer implements Normalizer {
public execute( data: NormalizerData ): void {
const { body: documentFragment, stylesString } = data._parsedData;

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

Expand Down
3 changes: 2 additions & 1 deletion packages/ckeditor5-paste-from-office/src/pastefromoffice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ export default class PasteFromOffice extends Plugin {
const clipboardPipeline: ClipboardPipeline = editor.plugins.get( 'ClipboardPipeline' );
const viewDocument = editor.editing.view.document;
const normalizers: Array<Normalizer> = [];
const hasMultiLevelListPlugin = this.editor.plugins.has( 'MultiLevelList' );

normalizers.push( new MSWordNormalizer( viewDocument ) );
normalizers.push( new MSWordNormalizer( viewDocument, hasMultiLevelListPlugin ) );
normalizers.push( new GoogleDocsNormalizer( viewDocument ) );
normalizers.push( new GoogleSheetsNormalizer( viewDocument ) );

Expand Down
138 changes: 138 additions & 0 deletions packages/ckeditor5-paste-from-office/tests/filters/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/html
import { stringify } from '@ckeditor/ckeditor5-engine/src/dev-utils/view.js';
import Document from '@ckeditor/ckeditor5-engine/src/view/document.js';
import UpcastWriter from '@ckeditor/ckeditor5-engine/src/view/upcastwriter.js';
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils.js';

import {
transformListItemLikeElementsIntoLists,
Expand All @@ -15,6 +16,8 @@ import {
import { StylesProcessor } from '@ckeditor/ckeditor5-engine/src/view/stylesmap.js';

describe( 'PasteFromOffice - filters', () => {
testUtils.createSinonSandbox();

describe( 'list - paste from MS Word', () => {
const htmlDataProcessor = new HtmlDataProcessor( new Document( new StylesProcessor() ) );

Expand Down Expand Up @@ -104,6 +107,140 @@ describe( 'PasteFromOffice - filters', () => {
);
} );

describe( 'legal list detecting', () => {
it( 'handles "legal-list" when multi-level-list is loaded', () => {
const level1 = 'style="mso-list:l0 level1 lfo0"';
const styles = '@list l0:level1\n' +
'{mso-level-text:"%1\\.%2\\.";}';

const html = `<p ${ level1 }>Foo</p>`;
const view = htmlDataProcessor.toView( html );
const hasMultiLevelListPluginLoaded = true;

transformListItemLikeElementsIntoLists( view, styles, hasMultiLevelListPluginLoaded );

expect( stringify( view ) ).to.equal(
`<ol class="legal-list"><li ${ level1 }>Foo</li></ol>`
);
} );

it( 'detect "legal-list" with double new line', () => {
const level1 = 'style="mso-list:l0 level1 lfo0"';
const styles = '@list l0:level1\n\n' +
'{mso-level-text:"%1\\.%2\\.";}';

const html = `<p ${ level1 }>Foo</p>`;
const view = htmlDataProcessor.toView( html );
const hasMultiLevelListPluginLoaded = true;

transformListItemLikeElementsIntoLists( view, styles, hasMultiLevelListPluginLoaded );

expect( stringify( view ) ).to.equal(
`<ol class="legal-list"><li ${ level1 }>Foo</li></ol>`
);
} );

it( 'detect "legal-list" with double spaces', () => {
const level1 = 'style="mso-list:l0 level1 lfo0"';
const styles = '@list l0:level1\n\n' +
'{mso-level-text:"%1\\.%2\\.";}';

const html = `<p ${ level1 }>Foo</p>`;
const view = htmlDataProcessor.toView( html );
const hasMultiLevelListPluginLoaded = true;

transformListItemLikeElementsIntoLists( view, styles, hasMultiLevelListPluginLoaded );

expect( stringify( view ) ).to.equal(
`<ol class="legal-list"><li ${ level1 }>Foo</li></ol>`
);
} );

it( 'detect "legal-list" with another css attribute', () => {
const level1 = 'style="mso-list:l0 level1 lfo0"';
const styles = '@list l0:level1\n\n' +
'{mso-level-text:"%1\\.%2\\.";' +
'another-css-attribute: value;}';

const html = `<p ${ level1 }>Foo</p>`;
const view = htmlDataProcessor.toView( html );
const hasMultiLevelListPluginLoaded = true;

transformListItemLikeElementsIntoLists( view, styles, hasMultiLevelListPluginLoaded );

expect( stringify( view ) ).to.equal(
`<ol class="legal-list"><li ${ level1 }>Foo</li></ol>`
);
} );

it( 'detect "legal-list" with another css attribute and another order', () => {
const level1 = 'style="mso-list:l0 level1 lfo0"';
const styles = '@list l0:level1\n\n' +
'{another-css-attribute: value;' +
'mso-level-text:"%1\\.%2\\.";}';

const html = `<p ${ level1 }>Foo</p>`;
const view = htmlDataProcessor.toView( html );
const hasMultiLevelListPluginLoaded = true;

transformListItemLikeElementsIntoLists( view, styles, hasMultiLevelListPluginLoaded );

expect( stringify( view ) ).to.equal(
`<ol class="legal-list"><li ${ level1 }>Foo</li></ol>`
);
} );

it( 'handles "legal-list" with wrong id', () => {
const level1 = 'style="mso-list:l0 level1 lfo0"';
const styles = '@list l1:level1\n\n' +
'{another-css-attribute: value;' +
'mso-level-text:"%1\\.%2\\.";}';

const html = `<p ${ level1 }>Foo</p>`;
const view = htmlDataProcessor.toView( html );
const hasMultiLevelListPluginLoaded = true;

transformListItemLikeElementsIntoLists( view, styles, hasMultiLevelListPluginLoaded );

expect( stringify( view ) ).to.equal(
`<ol><li ${ level1 }>Foo</li></ol>`
);
} );

it( 'handles legal-list when multi-level-list is not loaded', () => {
const level1 = 'style="mso-list:l0 level1 lfo0"';
const styles = '@list l0:level1\n' +
'{mso-level-text:"%1\\.%2\\.";}';

const html = `<p ${ level1 }>Foo</p>`;
const view = htmlDataProcessor.toView( html );
const hasMultiLevelListPluginLoaded = false;

transformListItemLikeElementsIntoLists( view, styles, hasMultiLevelListPluginLoaded );

expect( stringify( view ) ).to.equal(
`<ol><li ${ level1 }>Foo</li></ol>`
);
} );

it( 'handles legal-list when multi-level-list with mso-level-number-format attribute', () => {
const level1 = 'style="mso-list:l0 level1 lfo0"';
const styles = '@list l0:level1\n' +
'{mso-level-text:"%1\\.%2\\.";' +
'mso-level-number-format: bullet;}';

const html = `<p ${ level1 }>Foo</p>`;
const view = htmlDataProcessor.toView( html );
const hasMultiLevelListPluginLoaded = false;

transformListItemLikeElementsIntoLists( view, styles, hasMultiLevelListPluginLoaded );

expect( stringify( view ) ).to.equal(
`<ul><li ${ level1 }>Foo</li></ul>`
);
} );
} );

describe( 'nesting', () => {
const level1 = 'style="mso-list:l0 level1 lfo0"';
const level2 = 'style="mso-list:l0 level2 lfo0"';
Expand Down Expand Up @@ -279,6 +416,7 @@ describe( 'PasteFromOffice - filters', () => {
`<ol style="list-style-type:lower-alpha"><li ${ level1 }>Foo</li></ol>`
);
} );

it( 'converts "roman-upper" style to proper CSS attribute', () => {
const styles = '@list l0:level1\n' +
'{mso-level-number-format:roman-upper;}';
Expand Down