diff --git a/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/processWordLists.ts b/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/processWordLists.ts index eab556f3b711..d546e6b4d23a 100644 --- a/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/processWordLists.ts +++ b/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/processWordLists.ts @@ -85,8 +85,14 @@ export function processWordList( setupListFormat(listType, element, context, wordLevel, listFormat, group, [ wordListPaddingParser, ]); - (listFormat.levels[listFormat.levels.length - 1] - .format as WordListFormat).wordList = wordList; + const currentLevelFormat = listFormat.levels[listFormat.levels.length - 1] + .format as WordListFormat; + currentLevelFormat.wordList = wordList; + + if (wordLevel > 1) { + delete currentLevelFormat.paddingLeft; + delete currentLevelFormat.paddingRight; + } const bullet = getBulletFromMetadata(listMetadata, listType); const listFormatMetadata = bullet @@ -103,6 +109,11 @@ export function processWordList( listFormatMetadata, getBulletElement(element), listItem => { + if (wordLevel > 1) { + delete listItem.format.marginLeft; + delete listItem.format.marginRight; + } + if (listType == 'OL') { setStartNumber(listItem, context, listMetadata, element); } diff --git a/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/removeListParagraphMargins.ts b/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/removeListParagraphMargins.ts index 44f2e1e5f49e..666dac3b4bab 100644 --- a/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/removeListParagraphMargins.ts +++ b/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/removeListParagraphMargins.ts @@ -18,36 +18,117 @@ const WORD_LIST_PARAGRAPH_SELECTORS = new Set([ /** * @internal - * Strips all margin-* properties from a CSS property string. + * Strips horizontal margin properties from a CSS property string while preserving + * vertical values from the margin shorthand. * Empty tokens produced by a trailing semicolon are preserved so that the * resulting string still ends with ";" and remains safe to concatenate. - * For example, "margin-top: 0pt; color: red;" becomes " color: red;". + * For example, "margin: 1pt 2pt 3pt 4pt; color: red;" becomes + * "margin-top: 1pt; margin-bottom: 3pt; color: red;". */ -function removeMarginProperties(cssText: string): string { - return cssText - .split(';') - .filter(prop => { - const name = prop.split(':')[0].trim().toLowerCase(); - // Keep empty tokens (the trailing ';' produces one) and any non-margin property. - return !name || !/^margin/.test(name); - }) - .join(';'); +function removeHorizontalMarginProperties(cssText: string): string { + const result: string[] = []; + + cssText.split(';').forEach(prop => { + const separatorIndex = prop.indexOf(':'); + const name = prop + .substring(0, separatorIndex < 0 ? prop.length : separatorIndex) + .trim() + .toLowerCase(); + + if ( + name == 'margin-left' || + name == 'margin-right' || + name == 'margin-inline' || + name == 'margin-inline-start' || + name == 'margin-inline-end' + ) { + return; + } + + if (name == 'margin' && separatorIndex >= 0) { + const value = prop.substring(separatorIndex + 1).trim(); + const importantMatch = value.match(/\s*!important\s*$/i); + const important = importantMatch ? ' !important' : ''; + const values = splitCssValue(value.substring(0, importantMatch?.index ?? value.length)); + + if (values.length >= 1 && values.length <= 4) { + const leadingWhitespace = prop.match(/^\s*/)?.[0] || ''; + const top = values[0]; + const bottom = values.length >= 3 ? values[2] : values[0]; + + result.push(`${leadingWhitespace}margin-top: ${top}${important}`); + result.push(` margin-bottom: ${bottom}${important}`); + return; + } + } + + result.push(prop); + }); + + return result.join(';'); +} + +function splitCssValue(value: string): string[] { + const result: string[] = []; + let current = ''; + let quote: string | null = null; + let parenthesisDepth = 0; + + for (let i = 0; i < value.length; i++) { + const char = value[i]; + + if (quote) { + current += char; + + if (char == '\\') { + i++; + current += value[i] || ''; + } else if (char == quote) { + quote = null; + } + } else if (char == '"' || char == "'") { + current += char; + quote = char; + } else if (char == '(') { + current += char; + parenthesisDepth++; + } else if (char == ')') { + current += char; + parenthesisDepth = Math.max(0, parenthesisDepth - 1); + } else if (parenthesisDepth == 0 && /\s/.test(char)) { + if (current) { + result.push(current); + current = ''; + } + } else { + current += char; + } + } + + if (current) { + result.push(current); + } + + return result; } /** * @internal - * Removes margin properties from global CSS rules that target Word list paragraph - * classes (p.MsoListParagraph, p.MsoListParagraphCxSpFirst, etc.). + * Removes horizontal margin properties from global CSS rules that target Word list + * paragraph classes (p.MsoListParagraph, p.MsoListParagraphCxSpFirst, etc.). * * Word Desktop pastes a global stylesheet that typically includes rules like: * p.MsoListParagraph { margin: 0in; margin-bottom: .0001pt; ... } - * These margins conflict with RoosterJS's own list indentation, causing double - * indentation when the CSS is converted to inline styles via convertInlineCss. + * Horizontal margins conflict with RoosterJS's own list indentation, causing double + * indentation when the CSS is converted to inline styles via convertInlineCss. The + * vertical margins are preserved so Word's margin-top: 0 is not replaced by the + * browser's default paragraph margin when the list level is created. * - * When a rule's selectors are exclusively list paragraph classes the margins are - * removed in place. When a rule groups list paragraph classes with other selectors - * the rule is split: the non-list selectors keep the original text, and a new rule - * is inserted for the list paragraph selectors with margins stripped. + * When a rule's selectors are exclusively list paragraph classes the horizontal + * margins are removed in place. When a rule groups list paragraph classes with other + * selectors the rule is split: the non-list selectors keep the original text, and a + * new rule is inserted for the list paragraph selectors with horizontal margins + * stripped. * * The array is mutated in place so the changes are reflected when convertInlineCss * subsequently processes the same array reference. @@ -67,16 +148,15 @@ export function removeListParagraphMargins(globalCssRules: CssRule[]): void { ); if (nonMatchingSelectors.length === 0) { - // All selectors target list paragraphs — strip margins directly. - rule.text = removeMarginProperties(rule.text); + // All selectors target list paragraphs — strip horizontal margins directly. + rule.text = removeHorizontalMarginProperties(rule.text); } else { // Mixed rule: keep the non-list selectors on the original entry, then - // insert a new entry immediately after for the list paragraph selectors - // with margins removed. + // insert a new entry immediately after for the list paragraph selectors. rule.selectors = nonMatchingSelectors; globalCssRules.splice(i + 1, 0, { selectors: matchingSelectors, - text: removeMarginProperties(rule.text), + text: removeHorizontalMarginProperties(rule.text), }); } } diff --git a/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts b/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts index dcc813b5a13b..4088d3b073c4 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts @@ -863,14 +863,14 @@ describe(ID, () => { listType: 'OL', format: { startNumberOverride: 1, - marginTop: '1em', + marginTop: '0cm', listStyleType: 'decimal', }, dataset: { editingInfo: '{"orderedStyleType":1}' }, }, ], blockType: 'BlockGroup', - format: { lineHeight: '115%' }, + format: { marginTop: '0cm', marginBottom: '0cm', lineHeight: '115%' }, blockGroupType: 'ListItem', blocks: [ { @@ -909,12 +909,12 @@ describe(ID, () => { levels: [ { listType: 'OL', - format: { marginTop: '1em', listStyleType: 'decimal' }, + format: { marginTop: '0cm', listStyleType: 'decimal' }, dataset: { editingInfo: '{"orderedStyleType":1}' }, }, ], blockType: 'BlockGroup', - format: { lineHeight: '115%' }, + format: { marginTop: '0cm', marginBottom: '0cm', lineHeight: '115%' }, blockGroupType: 'ListItem', blocks: [ { @@ -953,12 +953,12 @@ describe(ID, () => { levels: [ { listType: 'OL', - format: { marginTop: '1em', listStyleType: 'decimal' }, + format: { marginTop: '0cm', listStyleType: 'decimal' }, dataset: { editingInfo: '{"orderedStyleType":1}' }, }, ], blockType: 'BlockGroup', - format: { lineHeight: '115%' }, + format: { marginTop: '0cm', marginBottom: '0cm', lineHeight: '115%' }, blockGroupType: 'ListItem', blocks: [ { @@ -997,22 +997,25 @@ describe(ID, () => { levels: [ { listType: 'OL', - format: { marginTop: '1em', listStyleType: 'decimal' }, + format: { marginTop: '0cm', listStyleType: 'decimal' }, dataset: { editingInfo: '{"orderedStyleType":1}' }, }, { listType: 'OL', format: { startNumberOverride: 1, - marginTop: '1em', - paddingLeft: '0px', + marginTop: '0cm', listStyleType: 'lower-alpha', }, dataset: { editingInfo: '{"orderedStyleType":5}' }, }, ], blockType: 'BlockGroup', - format: { lineHeight: '115%', marginLeft: '72pt' }, + format: { + marginTop: '0cm', + marginBottom: '0cm', + lineHeight: '115%', + }, blockGroupType: 'ListItem', blocks: [ { @@ -1051,21 +1054,24 @@ describe(ID, () => { levels: [ { listType: 'OL', - format: { marginTop: '1em', listStyleType: 'decimal' }, + format: { marginTop: '0cm', listStyleType: 'decimal' }, dataset: { editingInfo: '{"orderedStyleType":1}' }, }, { listType: 'OL', format: { - marginTop: '1em', - paddingLeft: '0px', + marginTop: '0cm', listStyleType: 'lower-alpha', }, dataset: { editingInfo: '{"orderedStyleType":5}' }, }, ], blockType: 'BlockGroup', - format: { lineHeight: '115%', marginLeft: '72pt' }, + format: { + marginTop: '0cm', + marginBottom: '0cm', + lineHeight: '115%', + }, blockGroupType: 'ListItem', blocks: [ { @@ -1104,12 +1110,12 @@ describe(ID, () => { levels: [ { listType: 'OL', - format: { marginTop: '1em', listStyleType: 'decimal' }, + format: { marginTop: '0cm', listStyleType: 'decimal' }, dataset: { editingInfo: '{"orderedStyleType":1}' }, }, ], blockType: 'BlockGroup', - format: { lineHeight: '115%' }, + format: { marginTop: '0cm', marginBottom: '8pt', lineHeight: '115%' }, blockGroupType: 'ListItem', blocks: [ { diff --git a/packages/roosterjs-content-model-plugins/test/paste/e2e/wordNestedListMarginTest.ts b/packages/roosterjs-content-model-plugins/test/paste/e2e/wordNestedListMarginTest.ts new file mode 100644 index 000000000000..2d8513d52217 --- /dev/null +++ b/packages/roosterjs-content-model-plugins/test/paste/e2e/wordNestedListMarginTest.ts @@ -0,0 +1,109 @@ +import * as wordFile from '../../../lib/paste/WordDesktop/processPastedContentFromWordDesktop'; +import { initEditor } from './testUtils'; +import { itChromeOnly } from 'roosterjs-content-model-dom/test/testUtils'; +import { paste } from 'roosterjs-content-model-core'; +import type { ClipboardData, ContentModelListItem, IEditor } from 'roosterjs-content-model-types'; + +const ID = 'Word_Nested_List_Margin'; + +describe(ID, () => { + let editor: IEditor; + + beforeEach(() => { + editor = initEditor(ID); + spyOn(wordFile, 'processPastedContentFromWordDesktop').and.callThrough(); + }); + + afterEach(() => { + document.getElementById(ID)?.remove(); + }); + + itChromeOnly('does not add top margins to nested Word list levels', () => { + const rawHtml = ` +
+ + + + + ++ · Desktop Outlook +
++ o Compose window +
++ o Received message +
++ § Expanded reading pane +
++ § Pop-out window +
+ + +`; + const clipboardData = { + types: ['text/plain', 'text/html'], + text: '', + image: null, + files: [], + rawHtml, + customValues: {}, + pasteNativeEvent: true, + } as ClipboardData; + + paste(editor, clipboardData); + + expect(wordFile.processPastedContentFromWordDesktop).toHaveBeenCalled(); + + const listItems = editor + .getContentModelCopy('disconnected') + .blocks.filter( + (block): block is ContentModelListItem => + block.blockType == 'BlockGroup' && block.blockGroupType == 'ListItem' + ); + + expect(listItems.length).toBe(5); + expect(listItems[1].levels[1].format.marginTop).toBe('0in'); + expect(listItems[3].levels[2].format.marginTop).toBe('0in'); + }); +}); diff --git a/packages/roosterjs-content-model-plugins/test/paste/word/processPastedContentFromWordDesktopTest.ts b/packages/roosterjs-content-model-plugins/test/paste/word/processPastedContentFromWordDesktopTest.ts index 20e8533dcd27..80c48165ede6 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/word/processPastedContentFromWordDesktopTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/word/processPastedContentFromWordDesktopTest.ts @@ -1254,9 +1254,7 @@ describe('processPastedContentFromWordDesktopTest', () => { }, ], blockType: 'BlockGroup', - format: { - marginLeft: '0in', - }, + format: {}, blockGroupType: 'ListItem', blocks: [ { @@ -1302,7 +1300,6 @@ describe('processPastedContentFromWordDesktopTest', () => { listType: 'OL', format: { marginTop: '1em', - paddingLeft: '0px', wordList: 'l0', }, dataset: { @@ -1311,9 +1308,7 @@ describe('processPastedContentFromWordDesktopTest', () => { }, ], blockType: 'BlockGroup', - format: { - marginLeft: '104px', - }, + format: {}, blockGroupType: 'ListItem', blocks: [ { @@ -1359,7 +1354,6 @@ describe('processPastedContentFromWordDesktopTest', () => { listType: 'OL', format: { marginTop: '1em', - paddingLeft: '0px', wordList: 'l0', }, dataset: { @@ -1378,9 +1372,7 @@ describe('processPastedContentFromWordDesktopTest', () => { }, ], blockType: 'BlockGroup', - format: { - marginLeft: '0in', - }, + format: {}, blockGroupType: 'ListItem', blocks: [ { @@ -2789,7 +2781,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l1', startNumberOverride: 1, }, @@ -2804,9 +2795,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { lineHeight: '116%', marginTop: '0in', - marginRight: '0in', marginBottom: '0in', - marginLeft: '1in', }, }, { @@ -2848,7 +2837,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}' }, @@ -2858,7 +2846,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l2', startNumberOverride: 1, }, @@ -2873,9 +2860,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { lineHeight: '116%', marginTop: '0in', - marginRight: '0in', marginBottom: '0in', - marginLeft: '1.5in', }, }, { @@ -2917,7 +2902,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}' }, @@ -2927,7 +2911,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l2', }, dataset: { editingInfo: '{"orderedStyleType":13}' }, @@ -2937,7 +2920,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', startNumberOverride: 1, }, @@ -2952,9 +2934,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { lineHeight: '116%', marginTop: '0in', - marginRight: '0in', marginBottom: '0in', - marginLeft: '2in', }, }, { @@ -2996,7 +2976,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}' }, @@ -3006,7 +2985,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l2', }, dataset: { editingInfo: '{"orderedStyleType":13}' }, @@ -3016,7 +2994,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":1}' }, @@ -3026,7 +3003,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', startNumberOverride: 1, }, @@ -3041,9 +3017,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { lineHeight: '116%', marginTop: '0in', - marginRight: '0in', marginBottom: '0in', - marginLeft: '2.5in', }, }, { @@ -3085,7 +3059,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}' }, @@ -3095,7 +3068,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l2', }, dataset: { editingInfo: '{"orderedStyleType":13}' }, @@ -3105,7 +3077,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":1}' }, @@ -3115,7 +3086,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":3}' }, @@ -3125,7 +3095,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', startNumberOverride: 1, }, @@ -3140,9 +3109,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { lineHeight: '116%', marginTop: '0in', - marginRight: '0in', marginBottom: '0in', - marginLeft: '3in', }, }, { @@ -3184,7 +3151,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}' }, @@ -3194,7 +3160,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l2', }, dataset: { editingInfo: '{"orderedStyleType":13}' }, @@ -3204,7 +3169,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":1}' }, @@ -3214,7 +3178,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":3}' }, @@ -3224,7 +3187,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":13}' }, @@ -3234,7 +3196,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', startNumberOverride: 1, }, @@ -3249,9 +3210,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { lineHeight: '116%', marginTop: '0in', - marginRight: '0in', marginBottom: '0in', - marginLeft: '3.5in', }, }, { @@ -3293,7 +3252,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}' }, @@ -3303,7 +3261,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l2', }, dataset: { editingInfo: '{"orderedStyleType":13}' }, @@ -3313,7 +3270,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":1}' }, @@ -3323,7 +3279,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":3}' }, @@ -3333,7 +3288,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":13}' }, @@ -3343,7 +3297,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":13}' }, @@ -3353,7 +3306,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', startNumberOverride: 500, }, @@ -3368,9 +3320,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { lineHeight: '116%', marginTop: '0in', - marginRight: '0in', marginBottom: '0in', - marginLeft: '4in', }, }, { @@ -3412,7 +3362,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}' }, @@ -3422,7 +3371,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l2', }, dataset: { editingInfo: '{"orderedStyleType":13}' }, @@ -3432,7 +3380,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":1}' }, @@ -3442,7 +3389,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":3}' }, @@ -3452,7 +3398,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":13}' }, @@ -3462,7 +3407,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":13}' }, @@ -3472,7 +3416,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":5}' }, @@ -3482,7 +3425,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', startNumberOverride: 6, }, @@ -3497,9 +3439,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { lineHeight: '116%', marginTop: '0in', - marginRight: '0in', marginBottom: '0in', - marginLeft: '4.5in', }, }, { @@ -3541,7 +3481,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}' }, @@ -3551,7 +3490,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l2', }, dataset: { editingInfo: '{"orderedStyleType":13}' }, @@ -3561,7 +3499,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":1}' }, @@ -3571,7 +3508,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":3}' }, @@ -3581,7 +3517,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":13}' }, @@ -3591,7 +3526,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":13}' }, @@ -3601,7 +3535,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":5}' }, @@ -3615,9 +3548,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { lineHeight: '116%', marginTop: '0in', - marginRight: '0in', marginBottom: '0in', - marginLeft: '4in', }, }, { @@ -3659,7 +3590,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}' }, @@ -3669,7 +3599,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l2', }, dataset: { editingInfo: '{"orderedStyleType":13}' }, @@ -3679,7 +3608,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":1}' }, @@ -3689,7 +3617,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":3}' }, @@ -3699,7 +3626,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":13}' }, @@ -3709,7 +3635,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":13}' }, @@ -3723,9 +3648,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { lineHeight: '116%', marginTop: '0in', - marginRight: '0in', marginBottom: '0in', - marginLeft: '3.5in', }, }, { @@ -3767,7 +3690,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}' }, @@ -3777,7 +3699,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l2', }, dataset: { editingInfo: '{"orderedStyleType":13}' }, @@ -3787,7 +3708,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":1}' }, @@ -3797,7 +3717,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":3}' }, @@ -3807,7 +3726,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":13}' }, @@ -3821,9 +3739,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { lineHeight: '116%', marginTop: '0in', - marginRight: '0in', marginBottom: '0in', - marginLeft: '3in', }, }, { @@ -3865,7 +3781,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}' }, @@ -3875,7 +3790,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l2', }, dataset: { editingInfo: '{"orderedStyleType":13}' }, @@ -3885,7 +3799,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":1}' }, @@ -3895,7 +3808,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":3}' }, @@ -3909,9 +3821,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { lineHeight: '116%', marginTop: '0in', - marginRight: '0in', marginBottom: '0in', - marginLeft: '2.5in', }, }, { @@ -3953,7 +3863,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}' }, @@ -3963,7 +3872,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l2', }, dataset: { editingInfo: '{"orderedStyleType":13}' }, @@ -3973,7 +3881,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":1}' }, @@ -3987,9 +3894,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { lineHeight: '116%', marginTop: '0in', - marginRight: '0in', marginBottom: '0in', - marginLeft: '2in', }, }, { @@ -4031,7 +3936,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}' }, @@ -4041,7 +3945,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":13}' }, @@ -4055,9 +3958,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { lineHeight: '116%', marginTop: '0in', - marginRight: '0in', marginBottom: '0in', - marginLeft: '1.5in', }, }, { @@ -4099,7 +4000,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":5}' }, @@ -4113,9 +4013,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { lineHeight: '116%', marginTop: '0in', - marginRight: '0in', marginBottom: '0in', - marginLeft: '1in', }, }, { @@ -4333,7 +4231,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l2', }, dataset: {}, @@ -4347,9 +4244,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { lineHeight: '107%', marginTop: '0in', - marginRight: '0in', marginBottom: '8pt', - marginLeft: '1in', }, }, { @@ -4424,7 +4319,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l2', }, dataset: {}, @@ -4438,9 +4332,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { lineHeight: '107%', marginTop: '0in', - marginRight: '0in', marginBottom: '8pt', - marginLeft: '1in', }, }, { @@ -4476,7 +4368,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l2', }, dataset: {}, @@ -4486,7 +4377,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l1', }, dataset: {}, @@ -4500,9 +4390,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { lineHeight: '107%', marginTop: '0in', - marginRight: '0in', marginBottom: '8pt', - marginLeft: '117pt', }, }, { @@ -4577,7 +4465,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l2', }, dataset: {}, @@ -4591,9 +4478,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { lineHeight: '107%', marginTop: '0in', - marginRight: '0in', marginBottom: '8pt', - marginLeft: '1in', }, }, { @@ -4725,7 +4610,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l0', }, dataset: {}, @@ -4739,9 +4623,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { lineHeight: '105%', marginTop: '0in', - marginRight: '0in', marginBottom: '8pt', - marginLeft: '1in', }, }, { @@ -4821,7 +4703,6 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - paddingLeft: '0px', wordList: 'l0', }, dataset: {}, @@ -4835,9 +4716,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { lineHeight: '105%', marginTop: '0in', - marginRight: '0in', marginBottom: '8pt', - marginLeft: '1in', }, }, ], @@ -5056,7 +4935,6 @@ describe('processPastedContentFromWordDesktopTest', () => { listType: 'OL', format: { marginTop: '1em', - paddingLeft: '0px', wordList: 'l0', startNumberOverride: 1, }, @@ -5070,9 +4948,7 @@ describe('processPastedContentFromWordDesktopTest', () => { isSelected: false, format: {}, }, - format: { - marginLeft: '1in', - }, + format: {}, }, { blockType: 'Paragraph', @@ -5125,7 +5001,6 @@ describe('processPastedContentFromWordDesktopTest', () => { listType: 'OL', format: { marginTop: '1em', - paddingLeft: '0px', wordList: 'l0', }, dataset: { @@ -5136,7 +5011,6 @@ describe('processPastedContentFromWordDesktopTest', () => { listType: 'OL', format: { marginTop: '1em', - paddingLeft: '0px', wordList: 'l0', startNumberOverride: 1, }, @@ -5150,9 +5024,7 @@ describe('processPastedContentFromWordDesktopTest', () => { isSelected: false, format: {}, }, - format: { - marginLeft: '1.5in', - }, + format: {}, }, { blockType: 'Paragraph', diff --git a/packages/roosterjs-content-model-plugins/test/paste/word/removeListParagraphMarginsTest.ts b/packages/roosterjs-content-model-plugins/test/paste/word/removeListParagraphMarginsTest.ts index d33c27c96d0e..3d2324568437 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/word/removeListParagraphMarginsTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/word/removeListParagraphMarginsTest.ts @@ -14,7 +14,7 @@ describe('removeListParagraphMargins', () => { runTest(rules, [{ selectors: ['div.SomeClass'], text: 'margin: 0pt; color: red;' }]); }); - it('all selectors are list paragraph classes — margins removed', () => { + it('all selectors are list paragraph classes — vertical margins preserved', () => { const rules: CssRule[] = [ { selectors: ['p.MsoListParagraph', 'p.MsoListParagraphCxSpFirst'], @@ -24,12 +24,12 @@ describe('removeListParagraphMargins', () => { runTest(rules, [ { selectors: ['p.MsoListParagraph', 'p.MsoListParagraphCxSpFirst'], - text: ' color: red; font-size: 11pt;', + text: 'margin-top: 0pt; margin-bottom: 8pt; color: red; font-size: 11pt;', }, ]); }); - it('single list paragraph selector — margins removed', () => { + it('single list paragraph selector — shorthand preserves vertical margins', () => { const rules: CssRule[] = [ { selectors: ['p.MsoListParagraphCxSpMiddle'], @@ -39,7 +39,7 @@ describe('removeListParagraphMargins', () => { runTest(rules, [ { selectors: ['p.MsoListParagraphCxSpMiddle'], - text: ' font-family: Calibri;', + text: 'margin-top: 0; margin-bottom: 0; font-family: Calibri;', }, ]); }); @@ -131,8 +131,8 @@ describe('removeListParagraphMargins', () => { { selectors: ['ul'], text: 'margin-bottom: 0in;' }, ]; - const noMarginListText = - ' font-size: 10pt; font-family: Aptos, sans-serif; color: rgb(25, 25, 25);'; + const noHorizontalMarginListText = + 'margin-top: 0in; margin-bottom: 0in; font-size: 10pt; font-family: Aptos, sans-serif; color: rgb(25, 25, 25);'; runTest(rules, [ { @@ -150,7 +150,10 @@ describe('removeListParagraphMargins', () => { text: 'margin: 0in 0in 0in 0.5in; font-size: 10pt; font-family: Aptos, sans-serif; color: rgb(25, 25, 25);', }, - { selectors: ['p.MsoListParagraph', 'div.MsoListParagraph'], text: noMarginListText }, + { + selectors: ['p.MsoListParagraph', 'div.MsoListParagraph'], + text: noHorizontalMarginListText, + }, { selectors: ['li.MsoListParagraphCxSpFirst'], text: @@ -158,7 +161,7 @@ describe('removeListParagraphMargins', () => { }, { selectors: ['p.MsoListParagraphCxSpFirst', 'div.MsoListParagraphCxSpFirst'], - text: noMarginListText, + text: noHorizontalMarginListText, }, { selectors: ['li.MsoListParagraphCxSpMiddle'], @@ -167,7 +170,7 @@ describe('removeListParagraphMargins', () => { }, { selectors: ['p.MsoListParagraphCxSpMiddle', 'div.MsoListParagraphCxSpMiddle'], - text: noMarginListText, + text: noHorizontalMarginListText, }, { selectors: ['li.MsoListParagraphCxSpLast'], @@ -176,7 +179,7 @@ describe('removeListParagraphMargins', () => { }, { selectors: ['p.MsoListParagraphCxSpLast', 'div.MsoListParagraphCxSpLast'], - text: noMarginListText, + text: noHorizontalMarginListText, }, { selectors: ['span.Heading1Char'], @@ -203,9 +206,44 @@ describe('removeListParagraphMargins', () => { { selectors: ['span.Normal'], text: 'margin: 5pt; color: black;' }, { selectors: ['p.MsoListParagraph'], - text: ' font-size: 11pt;', + text: 'margin-top: 0pt; margin-bottom: 0pt; font-size: 11pt;', }, { selectors: ['div.Section'], text: 'margin-right: 2pt; padding: 4pt;' }, ]); }); + + it('preserves top and bottom from two-, three-, and four-value shorthands', () => { + const rules: CssRule[] = [ + { + selectors: ['p.MsoListParagraph'], + text: 'margin: 1pt 2pt; color: red; margin: 3pt 4pt 5pt; margin: 6pt 7pt 8pt 9pt;', + }, + ]; + + runTest(rules, [ + { + selectors: ['p.MsoListParagraph'], + text: + 'margin-top: 1pt; margin-bottom: 1pt; color: red; margin-top: 3pt; margin-bottom: 5pt; margin-top: 6pt; margin-bottom: 8pt;', + }, + ]); + }); + + it('preserves CSS function values in shorthand margins', () => { + const rules: CssRule[] = [ + { + selectors: ['p.MsoListParagraph'], + text: + 'margin: calc(100% - 2px) 4px; margin: var(--top-margin, 1px 2px) 4px var(--bottom-margin, 3px 4px) 8px !important;', + }, + ]; + + runTest(rules, [ + { + selectors: ['p.MsoListParagraph'], + text: + 'margin-top: calc(100% - 2px); margin-bottom: calc(100% - 2px); margin-top: var(--top-margin, 1px 2px) !important; margin-bottom: var(--bottom-margin, 3px 4px) !important;', + }, + ]); + }); });