diff --git a/src/filters/list.js b/src/filters/list.js index bd4e2bc..6bd0c36 100644 --- a/src/filters/list.js +++ b/src/filters/list.js @@ -34,12 +34,43 @@ export function transformListItemLikeElementsIntoLists( documentFragment, styles } let currentList = null; + let currentIndentation = 1; itemLikeElements.forEach( ( itemLikeElement, i ) => { - if ( !currentList || isNewListNeeded( itemLikeElements[ i - 1 ], itemLikeElement ) ) { + const isDifferentList = isNewListNeeded( itemLikeElements[ i - 1 ], itemLikeElement ); + const previousItemLikeElement = isDifferentList ? null : itemLikeElements[ i - 1 ]; + const indentationDifference = getIndentationDifference( previousItemLikeElement, itemLikeElement ); + + if ( isDifferentList ) { + currentList = null; + currentIndentation = 1; + } + + if ( !currentList || indentationDifference !== 0 ) { const listStyle = detectListStyle( itemLikeElement, stylesString ); - currentList = insertNewEmptyList( listStyle, itemLikeElement.element, writer ); + if ( !currentList ) { + currentList = insertNewEmptyList( listStyle, itemLikeElement.element, writer ); + } else if ( itemLikeElement.indent > currentIndentation ) { + const lastListItem = currentList.getChild( currentList.childCount - 1 ); + const lastListItemChild = lastListItem.getChild( lastListItem.childCount - 1 ); + + currentList = insertNewEmptyList( listStyle, lastListItemChild, writer ); + + currentIndentation += 1; + } else if ( itemLikeElement.indent < currentIndentation ) { + const differentIndentation = currentIndentation - itemLikeElement.indent; + + currentList = findParentListAtLevel( currentList, differentIndentation ); + + currentIndentation = parseInt( itemLikeElement.indent ); + } + + if ( itemLikeElement.indent <= currentIndentation ) { + if ( !currentList.is( listStyle.type ) ) { + currentList = writer.rename( listStyle.type, currentList ); + } + } } const listItem = transformElementIntoListItem( itemLikeElement.element, writer ); @@ -155,14 +186,16 @@ function detectListStyle( listLikeItem, stylesString ) { // // @param {Object} listStyle List style object which determines the type of newly created list. // Usually a result of `detectListStyle()` function. -// @param {module:engine/view/element~Element} element Element before which list is inserted. +// @param {module:engine/view/element~Element} element Element after which list is inserted. // @param {module:engine/view/upcastwriter~UpcastWriter} writer // @returns {module:engine/view/element~Element} Newly created list element. + function insertNewEmptyList( listStyle, element, writer ) { + const parent = element.parent; const list = writer.createElement( listStyle.type ); - const position = element.parent.getChildIndex( element ); + const position = parent.getChildIndex( element ) + 1; - writer.insertChild( position, list, element.parent ); + writer.insertChild( position, list, parent ); return list; } @@ -243,6 +276,10 @@ function removeBulletElement( element, writer ) { // @param {Object} currentItem // @returns {Boolean} function isNewListNeeded( previousItem, currentItem ) { + if ( !previousItem ) { + return true; + } + if ( previousItem.id !== currentItem.id ) { return true; } @@ -260,3 +297,38 @@ function isNewListNeeded( previousItem, currentItem ) { function isList( element ) { return element.is( 'ol' ) || element.is( 'ul' ); } + +// Calculates the indentation difference between two given list items (based on indent attribute +// extracted from `mso-list` style, see #getListItemData). +// +// @param {Object} previousItem +// @param {Object} currentItem +// @returns {Number} +function getIndentationDifference( previousItem, currentItem ) { + return previousItem ? currentItem.indent - previousItem.indent : currentItem.indent - 1; +} + +// Finds parent list element (ul/ol) of a given list element with indentation level lower by a given value. +// +// @param {module:engine/view/element~Element} listElement List element from which to start looking for a parent list. +// @param {Number} indentationDifference Indentation difference between lists. +// @returns {module:engine/view/element~Element} Found list element with indentation level lower by a given value. +function findParentListAtLevel( listElement, indentationDifference ) { + const ancestors = listElement.getAncestors( { parentFirst: true } ); + + let parentList = null; + let levelChange = 0; + + for ( const ancestor of ancestors ) { + if ( ancestor.name === 'ul' || ancestor.name === 'ol' ) { + levelChange++; + } + + if ( levelChange === indentationDifference ) { + parentList = ancestor; + break; + } + } + + return parentList; +} diff --git a/tests/_data/list/index.js b/tests/_data/list/index.js index a4cef47..370a5eb 100644 --- a/tests/_data/list/index.js +++ b/tests/_data/list/index.js @@ -13,6 +13,9 @@ import heading1 from './heading1/input.word2016.html'; import heading3Styled from './heading3-styled/input.word2016.html'; import heading7 from './heading7/input.word2016.html'; import resumeTemplate from './resume-template/input.word2016.html'; +import nested from './nested/input.word2016.html'; +import nestedMixed from './nested-mixed/input.word2016.html'; +import nestedMultiple from './nested-multiple/input.word2016.html'; import simpleNormalized from './simple/normalized.word2016.html'; import styledNormalized from './styled/normalized.word2016.html'; @@ -23,6 +26,9 @@ import heading1Normalized from './heading1/normalized.word2016.html'; import heading3StyledNormalized from './heading3-styled/normalized.word2016.html'; import heading7Normalized from './heading7/normalized.word2016.html'; import resumeTemplateNormalized from './resume-template/normalized.word2016.html'; +import nestedNormalized from './nested/normalized.word2016.html'; +import nestedMixedNormalized from './nested-mixed/normalized.word2016.html'; +import nestedMultipleNormalized from './nested-multiple/normalized.word2016.html'; import simpleModel from './simple/model.word2016.html'; import styledModel from './styled/model.word2016.html'; @@ -33,6 +39,9 @@ import heading1Model from './heading1/model.word2016.html'; import heading3StyledModel from './heading3-styled/model.word2016.html'; import heading7Model from './heading7/model.word2016.html'; import resumeTemplateModel from './resume-template/model.word2016.html'; +import nestedModel from './nested/model.word2016.html'; +import nestedMixedModel from './nested-mixed/model.word2016.html'; +import nestedMultipleModel from './nested-multiple/model.word2016.html'; export const fixtures = { input: { @@ -44,7 +53,10 @@ export const fixtures = { heading1, heading3Styled, heading7, - resumeTemplate + resumeTemplate, + nested, + nestedMixed, + nestedMultiple }, normalized: { simple: simpleNormalized, @@ -55,7 +67,10 @@ export const fixtures = { heading1: heading1Normalized, heading3Styled: heading3StyledNormalized, heading7: heading7Normalized, - resumeTemplate: resumeTemplateNormalized + resumeTemplate: resumeTemplateNormalized, + nested: nestedNormalized, + nestedMixed: nestedMixedNormalized, + nestedMultiple: nestedMultipleNormalized }, model: { simple: simpleModel, @@ -66,7 +81,10 @@ export const fixtures = { heading1: heading1Model, heading3Styled: heading3StyledModel, heading7: heading7Model, - resumeTemplate: resumeTemplateModel + resumeTemplate: resumeTemplateModel, + nested: nestedModel, + nestedMixed: nestedMixedModel, + nestedMultiple: nestedMultipleModel } }; @@ -80,6 +98,9 @@ import heading1Safari from './heading1/input.safari.word2016.html'; import heading3StyledSafari from './heading3-styled/input.safari.word2016.html'; import heading7Safari from './heading7/input.safari.word2016.html'; import resumeTemplateSafari from './resume-template/input.safari.word2016.html'; +import nestedSafari from './nested/input.safari.word2016.html'; +import nestedMixedSafari from './nested-mixed/input.safari.word2016.html'; +import nestedMultipleSafari from './nested-multiple/input.safari.word2016.html'; import simpleNormalizedSafari from './simple/normalized.safari.word2016.html'; import styledNormalizedSafari from './styled/normalized.safari.word2016.html'; @@ -90,6 +111,7 @@ import heading1NormalizedSafari from './heading1/normalized.safari.word2016.html import heading3StyledNormalizedSafari from './heading3-styled/normalized.safari.word2016.html'; import heading7NormalizedSafari from './heading7/normalized.safari.word2016.html'; import resumeTemplateNormalizedSafari from './resume-template/normalized.safari.word2016.html'; +import nestedMultipleNormalizedSafari from './nested-multiple/normalized.safari.word2016.html'; import styledSafariModel from './styled/model.safari.word2016.html'; import resumeTemplateSafariModel from './resume-template/model.safari.word2016.html'; @@ -105,7 +127,10 @@ export const browserFixtures = { heading1: heading1Safari, heading3Styled: heading3StyledSafari, heading7: heading7Safari, - resumeTemplate: resumeTemplateSafari + resumeTemplate: resumeTemplateSafari, + nested: nestedSafari, + nestedMixed: nestedMixedSafari, + nestedMultiple: nestedMultipleSafari }, normalized: { simple: simpleNormalizedSafari, @@ -116,7 +141,10 @@ export const browserFixtures = { heading1: heading1NormalizedSafari, heading3Styled: heading3StyledNormalizedSafari, heading7: heading7NormalizedSafari, - resumeTemplate: resumeTemplateNormalizedSafari + resumeTemplate: resumeTemplateNormalizedSafari, + nested: nestedNormalized, + nestedMixed: nestedMixedNormalized, + nestedMultiple: nestedMultipleNormalizedSafari }, model: { simple: simpleModel, @@ -127,7 +155,10 @@ export const browserFixtures = { heading1: heading1Model, heading3Styled: heading3StyledModel, heading7: heading7Model, - resumeTemplate: resumeTemplateSafariModel + resumeTemplate: resumeTemplateSafariModel, + nested: nestedModel, + nestedMixed: nestedMixedModel, + nestedMultiple: nestedMultipleModel } } }; diff --git a/tests/_data/list/nested-mixed/input.safari.word2016.html b/tests/_data/list/nested-mixed/input.safari.word2016.html new file mode 100644 index 0000000..858e1f3 --- /dev/null +++ b/tests/_data/list/nested-mixed/input.safari.word2016.html @@ -0,0 +1,206 @@ +

· A1

1) B2

· C4

2. D3

· E1

1) F2

diff --git a/tests/_data/list/nested-mixed/input.word2016.html b/tests/_data/list/nested-mixed/input.word2016.html new file mode 100644 index 0000000..79a19cb --- /dev/null +++ b/tests/_data/list/nested-mixed/input.word2016.html @@ -0,0 +1,942 @@ + + + + + + + + + + + + + + + + + + + +

·      +A1

+ +

1)    +B2

+ +

·      +C4

+ +

2.     +D3

+ +

·      +E1

+ +

1)    +F2

+ + + + + diff --git a/tests/_data/list/nested-mixed/model.word2016.html b/tests/_data/list/nested-mixed/model.word2016.html new file mode 100644 index 0000000..3bae02f --- /dev/null +++ b/tests/_data/list/nested-mixed/model.word2016.html @@ -0,0 +1,6 @@ +A1 +B2 +C4 +D3 +E1 +F2 diff --git a/tests/_data/list/nested-mixed/nested-mixed.docx b/tests/_data/list/nested-mixed/nested-mixed.docx new file mode 100644 index 0000000..77e8ff0 Binary files /dev/null and b/tests/_data/list/nested-mixed/nested-mixed.docx differ diff --git a/tests/_data/list/nested-mixed/normalized.word2016.html b/tests/_data/list/nested-mixed/normalized.word2016.html new file mode 100644 index 0000000..610ca67 --- /dev/null +++ b/tests/_data/list/nested-mixed/normalized.word2016.html @@ -0,0 +1,29 @@ + diff --git a/tests/_data/list/nested-multiple/input.safari.word2016.html b/tests/_data/list/nested-multiple/input.safari.word2016.html new file mode 100644 index 0000000..285c507 --- /dev/null +++ b/tests/_data/list/nested-multiple/input.safari.word2016.html @@ -0,0 +1,259 @@ +

· A1

§ B3

2) C2

Foo Bar...

a. A2

2. B1

a. C2

· A1

1) B2

· C1

diff --git a/tests/_data/list/nested-multiple/input.word2016.html b/tests/_data/list/nested-multiple/input.word2016.html new file mode 100644 index 0000000..3eb8f8a --- /dev/null +++ b/tests/_data/list/nested-multiple/input.word2016.html @@ -0,0 +1,1017 @@ + + + + + + + + + + + + + + + + + + + +

·      +A1

+ +

§  +B3

+ +

2)    +C2

+ +

 

+ +

Foo Bar...

+ +

 

+ +

a.     +A2

+ +

2.     +B1

+ +

a.     +C2

+ +

 

+ +

·      +A1

+ +

1)    +B2

+ +

·      +C1

+ + + + + diff --git a/tests/_data/list/nested-multiple/model.word2016.html b/tests/_data/list/nested-multiple/model.word2016.html new file mode 100644 index 0000000..55cb156 --- /dev/null +++ b/tests/_data/list/nested-multiple/model.word2016.html @@ -0,0 +1,17 @@ +A1 +B3 +C2 + + +Foo Bar... + + +A2 +B1 +C2 + + + +A1 +B2 +C1 diff --git a/tests/_data/list/nested-multiple/nested-multiple.docx b/tests/_data/list/nested-multiple/nested-multiple.docx new file mode 100644 index 0000000..60cbb08 Binary files /dev/null and b/tests/_data/list/nested-multiple/nested-multiple.docx differ diff --git a/tests/_data/list/nested-multiple/normalized.safari.word2016.html b/tests/_data/list/nested-multiple/normalized.safari.word2016.html new file mode 100644 index 0000000..9a3efa3 --- /dev/null +++ b/tests/_data/list/nested-multiple/normalized.safari.word2016.html @@ -0,0 +1,51 @@ + + +

+

Foo Bar...

+

+ +
    +
  1. + A2 +
  2. + +
  3. + B1 +
      +
    1. + C2 +
    2. +
    +
  4. +
+ +

+ + diff --git a/tests/_data/list/nested-multiple/normalized.word2016.html b/tests/_data/list/nested-multiple/normalized.word2016.html new file mode 100644 index 0000000..d7cc3bb --- /dev/null +++ b/tests/_data/list/nested-multiple/normalized.word2016.html @@ -0,0 +1,50 @@ + + +

+

Foo Bar...

+

+ +
    +
  1. + A2 +
  2. + +
  3. + B1 +
      +
    1. + C2 +
    2. +
    +
  4. +
+ +

+ + diff --git a/tests/_data/list/nested/input.safari.word2016.html b/tests/_data/list/nested/input.safari.word2016.html new file mode 100644 index 0000000..b92631b --- /dev/null +++ b/tests/_data/list/nested/input.safari.word2016.html @@ -0,0 +1,191 @@ +

1. A1

2. B1

a. C2

1. D4

b. E2

i. F3

3. G1

diff --git a/tests/_data/list/nested/input.word2016.html b/tests/_data/list/nested/input.word2016.html new file mode 100644 index 0000000..a468d93 --- /dev/null +++ b/tests/_data/list/nested/input.word2016.html @@ -0,0 +1,928 @@ + + + + + + + + + + + + + + + + + + + +

1.     +A1

+ +

2.     +B1

+ +

a.     +C2

+ +

1.     +D4

+ +

b.     +E2

+ +

                                               +i.     F3

+ +

3.     +G1

+ + + + + diff --git a/tests/_data/list/nested/model.word2016.html b/tests/_data/list/nested/model.word2016.html new file mode 100644 index 0000000..002aee3 --- /dev/null +++ b/tests/_data/list/nested/model.word2016.html @@ -0,0 +1,7 @@ +A1 +B1 +C2 +D4 +E2 +F3 +G1 diff --git a/tests/_data/list/nested/nested.docx b/tests/_data/list/nested/nested.docx new file mode 100644 index 0000000..6b45eab Binary files /dev/null and b/tests/_data/list/nested/nested.docx differ diff --git a/tests/_data/list/nested/normalized.word2016.html b/tests/_data/list/nested/normalized.word2016.html new file mode 100644 index 0000000..aeb4b50 --- /dev/null +++ b/tests/_data/list/nested/normalized.word2016.html @@ -0,0 +1,32 @@ +
    +
  1. + A1 +
  2. + +
  3. + B1 +
      +
    1. + C2 +
        +
      1. + D4 +
      2. +
      +
    2. + +
    3. + E2 +
        +
      1. + F3 +
      2. +
      +
    4. +
    +
  4. + +
  5. + G1 +
  6. +
diff --git a/tests/filters/list.js b/tests/filters/list.js index eee5334..1618709 100644 --- a/tests/filters/list.js +++ b/tests/filters/list.js @@ -81,6 +81,98 @@ describe( 'PasteFromOffice - filters', () => { expect( view.childCount ).to.equal( 0 ); expect( stringify( view ) ).to.equal( '' ); } ); + + describe( 'Nesting', () => { + const level1 = 'style="mso-list:l0 level1 lfo0"'; + const level2 = 'style="mso-list:l0 level2 lfo0"'; + const level3 = 'style="mso-list:l0 level3 lfo0"'; + const level4 = 'style="mso-list:l0 level4 lfo0"'; + + it( 'handles simple indentation', () => { + const html = `

Foo

Bar

Baz

`; + const view = htmlDataProcessor.toView( html ); + + transformListItemLikeElementsIntoLists( view, '' ); + + expect( view.childCount ).to.equal( 1 ); + expect( stringify( view ) ).to.equal( + `
  1. Foo` + + `
    1. Bar` + + `
      1. Baz
` ); + } ); + + it( 'handles non-linear indentation', () => { + const html = `

Foo

Bar

Baz

`; + const view = htmlDataProcessor.toView( html ); + + transformListItemLikeElementsIntoLists( view, '' ); + + expect( view.childCount ).to.equal( 1 ); + expect( stringify( view ) ).to.equal( + `
  1. Foo` + + `
    1. Bar` + + `
      1. Baz
` ); + } ); + + it( 'handles indentation in both directions', () => { + const html = `

Foo

Bar

Baz

` + + `

Bax

123

`; + const view = htmlDataProcessor.toView( html ); + + transformListItemLikeElementsIntoLists( view, '' ); + + expect( view.childCount ).to.equal( 1 ); + expect( stringify( view ) ).to.equal( + `
  1. Foo` + + `
    1. Bar` + + `
      1. Baz
      ` + + `
    2. Bax
    ` + + `
  2. 123
` ); + } ); + + it( 'handles different list styles #1', () => { + const html = `

Foo

Bar

Baz

`; + const view = htmlDataProcessor.toView( html ); + + transformListItemLikeElementsIntoLists( view, '@list l0:level1 { mso-level-number-format: bullet; }' + + '@list l0:level3 { mso-level-number-format: bullet; }' ); + + expect( view.childCount ).to.equal( 1 ); + expect( stringify( view ) ).to.equal( + `` ); + } ); + + it( 'handles different list styles #2', () => { + const html = `

Foo

Bar

Baz

`; + const view = htmlDataProcessor.toView( html ); + + transformListItemLikeElementsIntoLists( view, '@list l0:level1 { mso-level-number-format: bullet; }' + + '@list l0:level2 { mso-level-number-format: bullet; }' ); + + expect( view.childCount ).to.equal( 1 ); + + expect( stringify( view ) ).to.equal( + `' ); + } ); + + it( 'handles indentation in the first list element', () => { + const html = `

Foo

Bar

Baz

`; + const view = htmlDataProcessor.toView( html ); + + transformListItemLikeElementsIntoLists( view, '' ); + + expect( view.childCount ).to.equal( 1 ); + expect( stringify( view ) ).to.equal( + `
  1. Foo` + + `
  2. Bar` + + `
    1. Baz
` ); + } ); + } ); } ); } );