diff --git a/packages/roosterjs-content-model-core/lib/override/listMetadataApplier.ts b/packages/roosterjs-content-model-core/lib/override/listMetadataApplier.ts index adf1c06a1a0d..d4f7321394c0 100644 --- a/packages/roosterjs-content-model-core/lib/override/listMetadataApplier.ts +++ b/packages/roosterjs-content-model-core/lib/override/listMetadataApplier.ts @@ -47,10 +47,10 @@ export const listItemMetadataApplier: MetadataApplier< > = { metadataDefinition: ListMetadataDefinition, applierFunction: (metadata, format, context) => { - const depth = context.listFormat.nodeStack.length - 2; // Minus two for the parent element and convert length to index + const depth = context.listFormat.currentLevel; if (depth >= 0) { - const listType = context.listFormat.nodeStack[depth + 1].listType ?? 'OL'; + const listType = context.listFormat.nodeStack[depth + 1]?.listType ?? 'OL'; const listStyleType = getAutoListStyleType(listType, metadata ?? {}, depth); if (listStyleType !== undefined) { @@ -77,10 +77,10 @@ export const listLevelMetadataApplier: MetadataApplier< > = { metadataDefinition: ListMetadataDefinition, applierFunction: (metadata, format, context) => { - const depth = context.listFormat.nodeStack.length - 2; // Minus two for the parent element and convert length to index + const depth = context.listFormat.currentLevel; if (depth >= 0) { - const listType = context.listFormat.nodeStack[depth + 1].listType ?? 'OL'; + const listType = context.listFormat.nodeStack[depth + 1]?.listType ?? 'OL'; const listStyleType = getAutoListStyleType(listType, metadata ?? {}, depth); if (listStyleType !== undefined) { diff --git a/packages/roosterjs-content-model-core/test/overrides/listMetadataApplierTest.ts b/packages/roosterjs-content-model-core/test/overrides/listMetadataApplierTest.ts index 651fa4472081..1ffe016815d1 100644 --- a/packages/roosterjs-content-model-core/test/overrides/listMetadataApplierTest.ts +++ b/packages/roosterjs-content-model-core/test/overrides/listMetadataApplierTest.ts @@ -25,6 +25,7 @@ describe('listItemMetadataApplier', () => { expect(context.listFormat).toEqual({ threadItemCounts: [], nodeStack: [], + currentLevel: 0, }); }); @@ -61,6 +62,7 @@ describe('listItemMetadataApplier', () => { refNode: null, }, ], + currentLevel: 0, }); expect(format).toEqual({}); }); @@ -100,6 +102,7 @@ describe('listItemMetadataApplier', () => { refNode: null, }, ], + currentLevel: 0, }); expect(format).toEqual({}); }); @@ -138,6 +141,7 @@ describe('listItemMetadataApplier', () => { refNode: null, }, ], + currentLevel: 0, }); expect(format).toEqual({ listStyleType: '"(2) "', @@ -177,6 +181,7 @@ describe('listItemMetadataApplier', () => { refNode: null, }, ], + currentLevel: 0, }); expect(format).toEqual({}); }); @@ -216,6 +221,7 @@ describe('listItemMetadataApplier', () => { refNode: null, }, ], + currentLevel: 0, }); expect(format).toEqual({}); }); @@ -255,6 +261,7 @@ describe('listItemMetadataApplier', () => { refNode: null, }, ], + currentLevel: 0, }); expect(format).toEqual({ listStyleType: 'test', @@ -296,6 +303,7 @@ describe('listItemMetadataApplier', () => { refNode: null, }, ], + currentLevel: 0, }); expect(format).toEqual({ listStyleType: '"➢ "', @@ -318,8 +326,14 @@ describe('listItemMetadataApplier', () => { listType: 'OL', refNode: null, }, + { + node: {} as Node, + listType: 'OL', + refNode: null, + }, ]; context.listFormat.threadItemCounts = [2, 3, 4]; + context.listFormat.currentLevel = 1; listItemMetadataApplier.applierFunction( { @@ -347,7 +361,13 @@ describe('listItemMetadataApplier', () => { listType: 'OL', refNode: null, }, + { + node: {} as Node, + listType: 'OL', + refNode: null, + }, ], + currentLevel: 1, }); expect(format).toEqual({ listStyleType: '"(3) "', @@ -547,6 +567,7 @@ describe('listLevelMetadataApplier', () => { expect(context.listFormat).toEqual({ threadItemCounts: [], nodeStack: [], + currentLevel: 0, }); }); @@ -583,6 +604,7 @@ describe('listLevelMetadataApplier', () => { refNode: null, }, ], + currentLevel: 0, }); expect(format).toEqual({ listStyleType: 'lower-roman', @@ -624,6 +646,7 @@ describe('listLevelMetadataApplier', () => { refNode: null, }, ], + currentLevel: 0, }); expect(format).toEqual({ listStyleType: 'lower-roman', @@ -664,6 +687,7 @@ describe('listLevelMetadataApplier', () => { refNode: null, }, ], + currentLevel: 0, }); expect(format).toEqual({}); }); @@ -701,6 +725,7 @@ describe('listLevelMetadataApplier', () => { refNode: null, }, ], + currentLevel: 0, }); expect(format).toEqual({ listStyleType: 'decimal', @@ -742,6 +767,7 @@ describe('listLevelMetadataApplier', () => { refNode: null, }, ], + currentLevel: 0, }); expect(format).toEqual({ listStyleType: 'decimal', @@ -783,6 +809,7 @@ describe('listLevelMetadataApplier', () => { refNode: null, }, ], + currentLevel: 0, }); expect(format).toEqual({ listStyleType: 'test', @@ -824,6 +851,7 @@ describe('listLevelMetadataApplier', () => { refNode: null, }, ], + currentLevel: 0, }); expect(format).toEqual({}); }); @@ -874,6 +902,7 @@ describe('listLevelMetadataApplier', () => { refNode: null, }, ], + currentLevel: 0, }); expect(format).toEqual({}); }); diff --git a/packages/roosterjs-content-model-dom/lib/modelToDom/context/createModelToDomContext.ts b/packages/roosterjs-content-model-dom/lib/modelToDom/context/createModelToDomContext.ts index 46eade22b82d..ebcb0cfecd00 100644 --- a/packages/roosterjs-content-model-dom/lib/modelToDom/context/createModelToDomContext.ts +++ b/packages/roosterjs-content-model-dom/lib/modelToDom/context/createModelToDomContext.ts @@ -66,6 +66,7 @@ function createModelToDomFormatContext(): ModelToDomFormatContext { listFormat: { threadItemCounts: [], nodeStack: [], + currentLevel: 0, }, implicitFormat: {}, }; diff --git a/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleList.ts b/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleList.ts index 90105c7401fc..6ed248c629f6 100644 --- a/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleList.ts +++ b/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleList.ts @@ -34,6 +34,8 @@ export const handleList: ContentModelBlockHandler = ( const stackLevel = nodeStack[layer + 1]; const itemLevel = listItem.levels[layer]; + context.listFormat.currentLevel = layer; + if ( stackLevel.listType != itemLevel.listType || stackLevel.dataset?.editingInfo != itemLevel.dataset.editingInfo || @@ -79,6 +81,8 @@ export const handleList: ContentModelBlockHandler = ( let isNewlyCreated = false; const levelRefNode = nodeStack[layer].refNode ?? null; + context.listFormat.currentLevel = layer; + if (context.allowCacheListItem && level.cachedElement) { newList = level.cachedElement; diff --git a/packages/roosterjs-content-model-dom/test/formatHandlers/list/listItemThreadFormatHandlerTest.ts b/packages/roosterjs-content-model-dom/test/formatHandlers/list/listItemThreadFormatHandlerTest.ts index 4b96d3316943..d956b1a43f8a 100644 --- a/packages/roosterjs-content-model-dom/test/formatHandlers/list/listItemThreadFormatHandlerTest.ts +++ b/packages/roosterjs-content-model-dom/test/formatHandlers/list/listItemThreadFormatHandlerTest.ts @@ -217,6 +217,7 @@ describe('listItemThreadFormatHandler.parse', () => { expect(context.listFormat).toEqual({ threadItemCounts: [], nodeStack: [], + currentLevel: 0, }); }); @@ -232,6 +233,7 @@ describe('listItemThreadFormatHandler.parse', () => { expect(context.listFormat).toEqual({ threadItemCounts: [], nodeStack: [], + currentLevel: 0, }); }); @@ -267,6 +269,7 @@ describe('listItemThreadFormatHandler.parse', () => { refNode: null, }, ], + currentLevel: 0, }); }); @@ -302,6 +305,7 @@ describe('listItemThreadFormatHandler.parse', () => { refNode: null, }, ], + currentLevel: 0, }); }); @@ -339,6 +343,7 @@ describe('listItemThreadFormatHandler.parse', () => { refNode: null, }, ], + currentLevel: 0, }); }); @@ -384,6 +389,7 @@ describe('listItemThreadFormatHandler.parse', () => { refNode: null, }, ], + currentLevel: 0, }); }); @@ -414,6 +420,7 @@ describe('listItemThreadFormatHandler.parse', () => { refNode: null, }, ], + currentLevel: 0, }); }); }); diff --git a/packages/roosterjs-content-model-dom/test/formatHandlers/list/listLevelThreadFormatHandlerTest.ts b/packages/roosterjs-content-model-dom/test/formatHandlers/list/listLevelThreadFormatHandlerTest.ts index ee7c4e606b11..a99b2fa5c65f 100644 --- a/packages/roosterjs-content-model-dom/test/formatHandlers/list/listLevelThreadFormatHandlerTest.ts +++ b/packages/roosterjs-content-model-dom/test/formatHandlers/list/listLevelThreadFormatHandlerTest.ts @@ -190,6 +190,7 @@ describe('listLevelThreadFormatHandler.parse', () => { expect(context.listFormat).toEqual({ threadItemCounts: [], nodeStack: [], + currentLevel: 0, }); }); @@ -205,6 +206,7 @@ describe('listLevelThreadFormatHandler.parse', () => { expect(context.listFormat).toEqual({ threadItemCounts: [0], nodeStack: [parent1, parent2], + currentLevel: 0, }); }); @@ -221,6 +223,7 @@ describe('listLevelThreadFormatHandler.parse', () => { expect(context.listFormat).toEqual({ threadItemCounts: [0], nodeStack: [parent1, parent2], + currentLevel: 0, }); }); @@ -238,6 +241,7 @@ describe('listLevelThreadFormatHandler.parse', () => { expect(context.listFormat).toEqual({ threadItemCounts: [1, 2], nodeStack: [parent1, parent2, parent3], + currentLevel: 0, }); }); @@ -257,6 +261,7 @@ describe('listLevelThreadFormatHandler.parse', () => { expect(context.listFormat).toEqual({ threadItemCounts: [1, 3], nodeStack: [parent1, parent2, parent3], + currentLevel: 0, }); }); @@ -273,6 +278,7 @@ describe('listLevelThreadFormatHandler.parse', () => { expect(context.listFormat).toEqual({ threadItemCounts: [1], nodeStack: [parent], + currentLevel: 0, }); }); @@ -290,6 +296,7 @@ describe('listLevelThreadFormatHandler.parse', () => { expect(context.listFormat).toEqual({ threadItemCounts: [1], nodeStack: [parent], + currentLevel: 0, }); }); }); diff --git a/packages/roosterjs-content-model-dom/test/modelToDom/context/createModelToDomContextTest.ts b/packages/roosterjs-content-model-dom/test/modelToDom/context/createModelToDomContextTest.ts index 901a2816cecd..c8033449511c 100644 --- a/packages/roosterjs-content-model-dom/test/modelToDom/context/createModelToDomContextTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelToDom/context/createModelToDomContextTest.ts @@ -21,6 +21,7 @@ describe('createModelToDomContext', () => { listFormat: { threadItemCounts: [], nodeStack: [], + currentLevel: 0, }, implicitFormat: {}, modelHandlers: defaultContentModelHandlers, @@ -54,6 +55,7 @@ describe('createModelToDomContext', () => { listFormat: { threadItemCounts: [], nodeStack: [], + currentLevel: 0, }, implicitFormat: {}, modelHandlers: defaultContentModelHandlers, @@ -122,6 +124,7 @@ describe('createModelToDomContext', () => { listFormat: { threadItemCounts: [], nodeStack: [], + currentLevel: 0, }, implicitFormat: {}, modelHandlers: { diff --git a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListItemTest.ts b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListItemTest.ts index 21ac026c324d..a473c59606fb 100644 --- a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListItemTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListItemTest.ts @@ -66,6 +66,7 @@ describe('handleListItem without format handler', () => { refNode: null, }, ], + currentLevel: 0, }); expect(handleListSpy).toHaveBeenCalledTimes(1); expect(handleListSpy).toHaveBeenCalledWith(document, parent, listItem, context, null); @@ -119,6 +120,7 @@ describe('handleListItem without format handler', () => { refNode: null, }, ], + currentLevel: 0, }); expect(handleListSpy).toHaveBeenCalledTimes(1); expect(handleListSpy).toHaveBeenCalledWith(document, parent, listItem, context, null); @@ -190,6 +192,7 @@ describe('handleListItem without format handler', () => { refNode: null, }, ], + currentLevel: 0, }); expect(handleListSpy).toHaveBeenCalledTimes(1); expect(handleListSpy).toHaveBeenCalledWith(document, parent, listItem, context, null); @@ -503,6 +506,7 @@ describe('handleListItem with cache', () => { refNode: null, }, ], + currentLevel: 0, }); expect(handleListSpy).toHaveBeenCalledTimes(1); expect(handleListSpy).toHaveBeenCalledWith(document, parent, listItem, context, null); @@ -574,6 +578,7 @@ describe('handleListItem with cache', () => { refNode: null, }, ], + currentLevel: 0, }); expect(handleListSpy).toHaveBeenCalledTimes(1); expect(handleListSpy).toHaveBeenCalledWith(document, parent, listItem, context, null); @@ -651,6 +656,7 @@ describe('handleListItem with cache', () => { refNode: cachedLI.nextSibling, }, ], + currentLevel: 0, }); expect(handleListSpy).toHaveBeenCalledTimes(1); @@ -730,6 +736,7 @@ describe('handleListItem with cache', () => { refNode: cachedLI.nextSibling, }, ], + currentLevel: 0, }); expect(handleListSpy).toHaveBeenCalledTimes(1); @@ -799,6 +806,7 @@ describe('handleListItem with cache', () => { refNode: null, }, ], + currentLevel: 1, }); expect(handleListSpy).toHaveBeenCalledTimes(1); diff --git a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListItemWithMetadataTest.ts b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListItemWithMetadataTest.ts index a4577d751a2d..36160724dbbd 100644 --- a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListItemWithMetadataTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListItemWithMetadataTest.ts @@ -61,6 +61,7 @@ describe('handleListItem with metadata', () => { refNode: null, }, ], + currentLevel: 0, }); expect(handleList).toHaveBeenCalledTimes(1); expect(handleList).toHaveBeenCalledWith(document, parent, listItem, context, null); @@ -114,6 +115,7 @@ describe('handleListItem with metadata', () => { refNode: null, }, ], + currentLevel: 0, }); expect(handleList).toHaveBeenCalledTimes(1); expect(handleList).toHaveBeenCalledWith(document, parent, listItem, context, null); @@ -180,6 +182,7 @@ describe('handleListItem with metadata', () => { refNode: null, }, ], + currentLevel: 0, }); expect(handleList).toHaveBeenCalledTimes(1); expect(handleList).toHaveBeenCalledWith(document, parent, listItem, context, null); @@ -231,6 +234,7 @@ describe('handleListItem with metadata', () => { refNode: br, }, ], + currentLevel: 0, }); expect(handleList).toHaveBeenCalledTimes(1); expect(handleList).toHaveBeenCalledWith(document, parent, listItem, context, br); @@ -273,6 +277,7 @@ describe('handleListItem with metadata', () => { refNode: null, }, ], + currentLevel: 0, }); expect(handleList).toHaveBeenCalledTimes(1); expect(handleList).toHaveBeenCalledWith(document, parent, listItem, context, null); diff --git a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListTest.ts b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListTest.ts index 8e449af30cc2..4cf5be44d043 100644 --- a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListTest.ts @@ -39,6 +39,7 @@ describe('handleList without format handlers', () => { refNode: null, }, ], + currentLevel: 0, }); }); @@ -60,6 +61,7 @@ describe('handleList without format handlers', () => { refNode: null, }, ], + currentLevel: 0, }); }); @@ -89,6 +91,7 @@ describe('handleList without format handlers', () => { refNode: null, }, ], + currentLevel: 0, }); }); @@ -120,6 +123,7 @@ describe('handleList without format handlers', () => { refNode: null, }, ], + currentLevel: 0, }); }); @@ -155,6 +159,7 @@ describe('handleList without format handlers', () => { refNode: null, }, ], + currentLevel: 0, }); }); @@ -196,6 +201,7 @@ describe('handleList without format handlers', () => { refNode: null, }, ], + currentLevel: 1, }); }); @@ -239,6 +245,7 @@ describe('handleList without format handlers', () => { refNode: null, }, ], + currentLevel: 1, }); }); @@ -273,6 +280,7 @@ describe('handleList without format handlers', () => { refNode: null, }, ], + currentLevel: 0, }); }); @@ -315,6 +323,7 @@ describe('handleList without format handlers', () => { refNode: null, }, ], + currentLevel: 0, }); }); @@ -361,6 +370,7 @@ describe('handleList without format handlers', () => { refNode: null, }, ], + currentLevel: 1, }); }); }); @@ -469,6 +479,7 @@ describe('handleList handles metadata', () => { refNode: null, }, ], + currentLevel: 0, }); }); @@ -512,6 +523,7 @@ describe('handleList handles metadata', () => { refNode: null, }, ], + currentLevel: 1, }); expect(result).toBe(br); }); @@ -611,6 +623,7 @@ describe('handleList handles metadata', () => { refNode: null, }, ], + currentLevel: 0, }); expect(listItem.levels[0].format.listStyleType).toBe('disc'); }); @@ -701,6 +714,7 @@ describe('handleList handles metadata', () => { refNode: null, }, ], + currentLevel: 0, }); expect(listItem.levels[0].format.listStyleType).toBe('disc'); }); @@ -748,6 +762,7 @@ describe('handleList with cache', () => { refNode: null, }, ], + currentLevel: 0, }); expect(reuseCachedElementSpy).not.toHaveBeenCalled(); expect(listItem.levels[0].cachedElement).toBe(parent.firstChild as any); @@ -789,6 +804,7 @@ describe('handleList with cache', () => { refNode: null, }, ], + currentLevel: 0, }); expect(reuseCachedElementSpy).toHaveBeenCalledWith( parent, @@ -836,6 +852,7 @@ describe('handleList with cache', () => { refNode: null, }, ], + currentLevel: 0, }); expect(reuseCachedElementSpy).toHaveBeenCalledWith( parent, @@ -884,6 +901,7 @@ describe('handleList with cache', () => { refNode: null, }, ], + currentLevel: 0, }); expect(listItem.levels[0].cachedElement).toBe(cachedUL); @@ -939,6 +957,7 @@ describe('handleList with cache', () => { refNode: null, }, ], + currentLevel: 1, }); expect(listItem.levels[0].cachedElement).toBe(cachedOL); expect(listItem.levels[1].cachedElement).toBe(cachedUL); @@ -992,6 +1011,7 @@ describe('handleList with cache', () => { refNode: null, }, ], + currentLevel: 1, }); expect(listItem.levels[0].cachedElement).toBe(parent.firstChild as any); expect(listItem.levels[1].cachedElement).toBe(cachedUL); @@ -1041,6 +1061,7 @@ describe('handleList with cache', () => { refNode: existingOL2, }, ], + currentLevel: 0, }); expect(newRefNode).toBeNull(); }); @@ -1094,6 +1115,7 @@ describe('handleList with cache', () => { refNode: null, }, ], + currentLevel: 0, }); expect(newRefNode).toBeNull(); }); diff --git a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListWithMetadataTest.ts b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListWithMetadataTest.ts index af7951c88345..80ecd1e8c8ee 100644 --- a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListWithMetadataTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListWithMetadataTest.ts @@ -39,6 +39,7 @@ describe('handleList with metadata', () => { refNode: null, }, ], + currentLevel: 0, }); }); @@ -63,6 +64,7 @@ describe('handleList with metadata', () => { refNode: null, }, ], + currentLevel: 0, }); }); @@ -88,6 +90,7 @@ describe('handleList with metadata', () => { refNode: null, }, ], + currentLevel: 0, }); }); @@ -119,6 +122,7 @@ describe('handleList with metadata', () => { refNode: null, }, ], + currentLevel: 0, }); }); @@ -156,6 +160,7 @@ describe('handleList with metadata', () => { refNode: null, }, ], + currentLevel: 0, }); }); @@ -199,6 +204,7 @@ describe('handleList with metadata', () => { refNode: null, }, ], + currentLevel: 1, }); }); @@ -253,6 +259,7 @@ describe('handleList with metadata', () => { refNode: null, }, ], + currentLevel: 1, }); }); @@ -287,6 +294,7 @@ describe('handleList with metadata', () => { refNode: null, }, ], + currentLevel: 0, }); }); @@ -325,6 +333,7 @@ describe('handleList with metadata', () => { refNode: null, }, ], + currentLevel: 0, }); }); @@ -374,6 +383,7 @@ describe('handleList with metadata', () => { refNode: null, }, ], + currentLevel: 1, }); }); diff --git a/packages/roosterjs-content-model-types/lib/context/ModelToDomFormatContext.ts b/packages/roosterjs-content-model-types/lib/context/ModelToDomFormatContext.ts index f9517ba8e6b1..9e15411cda1c 100644 --- a/packages/roosterjs-content-model-types/lib/context/ModelToDomFormatContext.ts +++ b/packages/roosterjs-content-model-types/lib/context/ModelToDomFormatContext.ts @@ -30,6 +30,13 @@ export interface ModelToDomListContext { * A stack of current list element chain, start from the parent node of top level list */ nodeStack: ModelToDomListStackItem[]; + + /** + * Current level of list item being processed, start from 0 + * This is used by metadata applier to determine which level of list item is being processed, + * so it can apply correct list style for that level. It will be updated by list handler before processing each list item, and reset to 0 when processing a new list. + */ + currentLevel: number; } /**