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
·
+A1
1)
+B2
·
+C4
2.
+D3
·
+E1
1)
+F2
· A1
§ B3
2) C2
Foo Bar...
a. A2
2. B1
a. C2
· A1
1) B2
· C1
·
+A1
§
+B3
2)
+C2
Foo Bar...
a.
+A2
2.
+B1
a.
+C2
·
+A1
1)
+B2
·
+C1
Foo Bar...
Foo Bar...
1. A1
2. B1
a. C2
1. D4
b. E2
i. F3
3. G1
1.
+A1
2.
+B1
a.
+C2
1.
+D4
b.
+E2
+i. F3
3.
+G1
Foo
Bar
Baz
`; + const view = htmlDataProcessor.toView( html ); + + transformListItemLikeElementsIntoLists( view, '' ); + + expect( view.childCount ).to.equal( 1 ); + expect( stringify( view ) ).to.equal( + `Foo
Bar
Baz
`; + const view = htmlDataProcessor.toView( html ); + + transformListItemLikeElementsIntoLists( view, '' ); + + expect( view.childCount ).to.equal( 1 ); + expect( stringify( view ) ).to.equal( + `Foo
Bar
Baz
` + + `Bax
123
`; + const view = htmlDataProcessor.toView( html ); + + transformListItemLikeElementsIntoLists( view, '' ); + + expect( view.childCount ).to.equal( 1 ); + expect( stringify( view ) ).to.equal( + `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( + `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( + `Foo
Bar
Baz
`; + const view = htmlDataProcessor.toView( html ); + + transformListItemLikeElementsIntoLists( view, '' ); + + expect( view.childCount ).to.equal( 1 ); + expect( stringify( view ) ).to.equal( + `