diff --git a/packages/roosterjs-content-model-dom/lib/domToModel/processors/listItemProcessor.ts b/packages/roosterjs-content-model-dom/lib/domToModel/processors/listItemProcessor.ts index 07d3f1c178a0..61ccf92bd380 100644 --- a/packages/roosterjs-content-model-dom/lib/domToModel/processors/listItemProcessor.ts +++ b/packages/roosterjs-content-model-dom/lib/domToModel/processors/listItemProcessor.ts @@ -1,4 +1,5 @@ import { createListItem } from '../../modelApi/creators/createListItem'; +import { createListLevel } from '../../modelApi/creators/createListLevel'; import { parseFormat } from '../utils/parseFormat'; import { stackFormat } from '../utils/stackFormat'; import type { ElementProcessor } from 'roosterjs-content-model-types'; @@ -8,8 +9,21 @@ import type { ElementProcessor } from 'roosterjs-content-model-types'; */ export const listItemProcessor: ElementProcessor = (group, element, context) => { const { listFormat } = context; + const originalListParent = listFormat.listParent; + let shouldPopListLevel = false; + + try { + listFormat.listParent = listFormat.listParent ?? group; + + const listParent = listFormat.listParent; + + if (listFormat.levels.length == 0) { + listFormat.levels.push( + createListLevel(listFormat.potentialListType || 'UL', context.blockFormat) + ); + shouldPopListLevel = true; + } - if (listFormat.listParent && listFormat.levels.length > 0) { stackFormat( context, { @@ -31,7 +45,7 @@ export const listItemProcessor: ElementProcessor = (group, elemen context ); - listFormat.listParent!.blocks.push(listItem); + listParent.blocks.push(listItem); parseFormat( element, @@ -54,14 +68,11 @@ export const listItemProcessor: ElementProcessor = (group, elemen } } ); - } else { - const currentBlocks = listFormat.listParent?.blocks; - const lastItem = currentBlocks?.[currentBlocks?.length - 1]; + } finally { + if (shouldPopListLevel) { + listFormat.levels.pop(); + } - context.elementProcessors['*']( - lastItem?.blockType == 'BlockGroup' ? lastItem : group, - element, - context - ); + listFormat.listParent = originalListParent; } }; diff --git a/packages/roosterjs-content-model-dom/lib/domToModel/processors/listProcessor.ts b/packages/roosterjs-content-model-dom/lib/domToModel/processors/listProcessor.ts index 92517b7e51bc..e6334346ae51 100644 --- a/packages/roosterjs-content-model-dom/lib/domToModel/processors/listProcessor.ts +++ b/packages/roosterjs-content-model-dom/lib/domToModel/processors/listProcessor.ts @@ -18,10 +18,8 @@ export const listProcessor: ElementProcessor { - const level: ContentModelListLevel = createListLevel( - element.tagName as 'OL' | 'UL', - context.blockFormat - ); + const tagName = element.tagName as 'OL' | 'UL'; + const level: ContentModelListLevel = createListLevel(tagName, context.blockFormat); const { listFormat } = context; parseFormat(element, context.formatParsers.dataset, level.dataset, context); @@ -31,6 +29,7 @@ export const listProcessor: ElementProcessor { levels: [], listParent: undefined, threadItemCounts: [1], + potentialListType: 'OL', }); }); diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/listItemProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/listItemProcessorTest.ts index a39fc7173e7e..2811d3c7859a 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/listItemProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/listItemProcessorTest.ts @@ -26,10 +26,21 @@ describe('listItemProcessor', () => { blocks: [ { blockType: 'BlockGroup', - blockGroupType: 'General', - element: li, + blockGroupType: 'ListItem', blocks: [], format: {}, + levels: [ + { + listType: 'UL', + format: {}, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, }, ], }); @@ -381,10 +392,21 @@ describe('listItemProcessor without format handlers', () => { blocks: [ { blockType: 'BlockGroup', - blockGroupType: 'General', - element: li, + blockGroupType: 'ListItem', blocks: [], format: {}, + levels: [ + { + listType: 'UL', + format: {}, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, }, ], }); diff --git a/packages/roosterjs-content-model-dom/test/endToEndTest.ts b/packages/roosterjs-content-model-dom/test/endToEndTest.ts index 27fc73fa576f..8d55177d6929 100644 --- a/packages/roosterjs-content-model-dom/test/endToEndTest.ts +++ b/packages/roosterjs-content-model-dom/test/endToEndTest.ts @@ -3027,4 +3027,41 @@ describe('End to end test for DOM => Model => DOM/TEXT', () => { 'www.bing.com' ); }); + + it('LI without UL followed by other blocks', () => { + runTest( + '
  • test
  • other
    ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'test', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'other', format: {} }], + format: {}, + }, + ], + }, + 'test\r\nother', + '
    • test
    other
    ' + ); + }); }); diff --git a/packages/roosterjs-content-model-plugins/test/paste/word/processPastedContentFromWacTest.ts b/packages/roosterjs-content-model-plugins/test/paste/word/processPastedContentFromWacTest.ts index 2a046e3a208a..d254b7198d6a 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/word/processPastedContentFromWacTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/word/processPastedContentFromWacTest.ts @@ -2043,17 +2043,16 @@ describe('wordOnlineHandler', () => { // .test // .test // .test - it('shuold process html properly, when list items are not in side ul tag', () => { + it('should process html properly, when list items are not inside ul tag', () => { runTest( '
    • test

    • test

    • test

    • ', - '
    • test

    • test

    • test

    • ', + '
      • test

      • test

      • test

      ', { blockGroupType: 'Document', blocks: [ { blockType: 'BlockGroup', - blockGroupType: 'General', - element: jasmine.anything() as any, + blockGroupType: 'ListItem', blocks: [ { blockType: 'Paragraph', @@ -2064,12 +2063,17 @@ describe('wordOnlineHandler', () => { decorator: { tagName: 'p', format: {} }, }, ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, format: {}, }, { blockType: 'BlockGroup', - blockGroupType: 'General', - element: jasmine.anything() as any, + blockGroupType: 'ListItem', blocks: [ { blockType: 'Paragraph', @@ -2080,12 +2084,17 @@ describe('wordOnlineHandler', () => { decorator: { tagName: 'p', format: {} }, }, ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, format: {}, }, { blockType: 'BlockGroup', - blockGroupType: 'General', - element: jasmine.anything() as any, + blockGroupType: 'ListItem', blocks: [ { blockType: 'Paragraph', @@ -2096,6 +2105,12 @@ describe('wordOnlineHandler', () => { decorator: { tagName: 'p', format: {} }, }, ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, format: {}, }, ], @@ -2345,17 +2360,16 @@ describe('wordOnlineHandler', () => { // result: // 1. text // 2. text - it('shuold process html properly, if list item in a ListContainerWrapper are not inside ol ', () => { + it('should process html properly, if list item in a ListContainerWrapper are not inside ol ', () => { runTest( '
      1. test

      2. test

      3. test

      4. ', - '
      5. test

      6. test

      7. test

      8. ', + '
        1. test

        2. test

        3. test

        ', { blockGroupType: 'Document', blocks: [ { blockType: 'BlockGroup', - blockGroupType: 'General', - element: jasmine.anything() as any, + blockGroupType: 'ListItem', blocks: [ { blockType: 'Paragraph', @@ -2366,12 +2380,17 @@ describe('wordOnlineHandler', () => { decorator: { tagName: 'p', format: {} }, }, ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, format: {}, }, { blockType: 'BlockGroup', - blockGroupType: 'General', - element: jasmine.anything() as any, + blockGroupType: 'ListItem', blocks: [ { blockType: 'Paragraph', @@ -2382,12 +2401,17 @@ describe('wordOnlineHandler', () => { decorator: { tagName: 'p', format: {} }, }, ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, format: {}, }, { blockType: 'BlockGroup', - blockGroupType: 'General', - element: jasmine.anything() as any, + blockGroupType: 'ListItem', blocks: [ { blockType: 'Paragraph', @@ -2398,6 +2422,12 @@ describe('wordOnlineHandler', () => { decorator: { tagName: 'p', format: {} }, }, ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, format: {}, }, ], diff --git a/packages/roosterjs-content-model-types/lib/context/DomToModelFormatContext.ts b/packages/roosterjs-content-model-types/lib/context/DomToModelFormatContext.ts index dbe1884ab1ef..df71a8e58dca 100644 --- a/packages/roosterjs-content-model-types/lib/context/DomToModelFormatContext.ts +++ b/packages/roosterjs-content-model-types/lib/context/DomToModelFormatContext.ts @@ -24,6 +24,12 @@ export interface DomToModelListFormat { * Current list type stack */ levels: ContentModelListLevel[]; + + /** + * This is used for handling an abnormal case where list items are not inside a ul or ol tag + * It is not common and against the HTML specification, but we need to handle it for robustness + */ + potentialListType?: 'OL' | 'UL'; } /**