From 3352105c068b5bf00b0d4a3bed235cf4d718c6a5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Jul 2026 18:54:58 +0000 Subject: [PATCH 1/2] Initial plan From ea24f6542292db2796d419d1aca94c5e52aca76f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Jul 2026 19:06:17 +0000 Subject: [PATCH 2/2] Fix format state showing relative em font size in heading elements --- .../editing/retrieveModelFormatState.ts | 25 ++++++ .../editing/retrieveModelFormatStateTest.ts | 84 +++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/retrieveModelFormatState.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/retrieveModelFormatState.ts index 47c404f68046..bf8622a9a97e 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/retrieveModelFormatState.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/retrieveModelFormatState.ts @@ -167,6 +167,31 @@ export function retrieveModelFormatState( ); if (formatState.fontSize) { + const fontSize = formatState.fontSize; + + // If the font size is a relative value (e.g. 1.5em from an h2 default style), resolve it + // against the container's computed font size so that the format state always returns an + // absolute value that reflects the actual rendered size. + if ( + (fontSize.endsWith('em') && !fontSize.endsWith('rem')) || + fontSize.endsWith('%') + ) { + if (!containerFormat) { + containerFormat = + domHelper?.getContainerFormat(isInDarkMode, colorHandler) ?? modelFormat; + } + + const containerFontSizePx = parseValueWithUnit(containerFormat?.fontSize ?? ''); + + if (containerFontSizePx > 0) { + const resolvedPx = parseValueWithUnit(fontSize, containerFontSizePx); + + if (resolvedPx > 0) { + formatState.fontSize = resolvedPx + 'px'; + } + } + } + formatState.fontSize = px2Pt(formatState.fontSize); } } diff --git a/packages/roosterjs-content-model-dom/test/modelApi/editing/retrieveModelFormatStateTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/editing/retrieveModelFormatStateTest.ts index 427e64b34524..50f712988dc0 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/editing/retrieveModelFormatStateTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/editing/retrieveModelFormatStateTest.ts @@ -166,6 +166,90 @@ describe('retrieveModelFormatState', () => { }); }); + it('Single selection with heading and em font size from decorator, with domHelper', () => { + const model = createContentModelDocument(); + const result: ContentModelFormatState = {}; + // Simulate h2 decorator format with a relative em-based font size (default browser style for h2) + const para = createParagraph(false, undefined, undefined, { + format: { fontSize: '1.5em', fontWeight: 'bold' }, + tagName: 'h2', + }); + // Segment has no explicit fontSize override + const marker = createSelectionMarker({ + fontFamily: 'Arial', + textColor: 'green', + }); + + spyOn(iterateSelections, 'iterateSelections').and.callFake((path: any, callback) => { + callback([path], undefined, para, [marker]); + return false; + }); + + const domHelper: DOMHelper = { + getContainerFormat: () => ({ fontFamily: 'Arial', fontSize: '16px', textColor: 'green' }), + } as any; + + retrieveModelFormatState(model, null, result, 'remove', domHelper); + + // 1.5em * 16px = 24px = 18pt + expect(result.fontSize).toBe('18pt'); + expect(result.headingLevel).toBe(2); + }); + + it('Single selection with heading and em font size from decorator, without domHelper', () => { + const model = createContentModelDocument(); + const result: ContentModelFormatState = {}; + // Simulate h2 decorator format with a relative em-based font size + const para = createParagraph(false, undefined, undefined, { + format: { fontSize: '1.5em', fontWeight: 'bold' }, + tagName: 'h2', + }); + // Segment has no explicit fontSize override + const marker = createSelectionMarker({ + fontFamily: 'Arial', + textColor: 'green', + }); + + spyOn(iterateSelections, 'iterateSelections').and.callFake((path: any, callback) => { + callback([path], undefined, para, [marker]); + return false; + }); + + // No domHelper - relative font size cannot be resolved, should remain as-is + retrieveModelFormatState(model, null, result); + + expect(result.fontSize).toBe('1.5em'); + expect(result.headingLevel).toBe(2); + }); + + it('Single selection with heading and percentage font size from decorator, with domHelper', () => { + const model = createContentModelDocument(); + const result: ContentModelFormatState = {}; + const para = createParagraph(false, undefined, undefined, { + format: { fontSize: '150%', fontWeight: 'bold' }, + tagName: 'h2', + }); + const marker = createSelectionMarker({ + fontFamily: 'Arial', + textColor: 'green', + }); + + spyOn(iterateSelections, 'iterateSelections').and.callFake((path: any, callback) => { + callback([path], undefined, para, [marker]); + return false; + }); + + const domHelper: DOMHelper = { + getContainerFormat: () => ({ fontFamily: 'Arial', fontSize: '16px', textColor: 'green' }), + } as any; + + retrieveModelFormatState(model, null, result, 'remove', domHelper); + + // 150% * 16px = 24px = 18pt + expect(result.fontSize).toBe('18pt'); + expect(result.headingLevel).toBe(2); + }); + it('Single selection with margin format', () => { const model = createContentModelDocument(); const result: ContentModelFormatState = {};