diff --git a/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts b/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts index 6d3ce769f570..db30bd32aac9 100644 --- a/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts +++ b/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts @@ -192,7 +192,7 @@ describe('paste with content model & paste plugin', () => { paste(editor!, clipboardData); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(2); - expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 7); + expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 8); expect(WordDesktopFile.processPastedContentFromWordDesktop).toHaveBeenCalledTimes(1); }); @@ -248,7 +248,7 @@ describe('paste with content model & paste plugin', () => { paste(editor!, clipboardData, 'asPlainText'); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(2); - expect(addParserF.addParser).toHaveBeenCalledTimes(12); + expect(addParserF.addParser).toHaveBeenCalledTimes(13); expect(WordDesktopFile.processPastedContentFromWordDesktop).toHaveBeenCalledTimes(1); }); diff --git a/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts b/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts index 9fc9f592bf9f..77b2f05a7aed 100644 --- a/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts +++ b/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts @@ -11,6 +11,7 @@ import { setProcessor } from '../utils/setProcessor'; import { wordContainerParser } from '../parsers/wordContainerParser'; import { wordTableParser } from '../parsers/wordTableParser'; import { removeListParagraphMargins } from './removeListParagraphMargins'; +import { wordBorderParser } from './wordBorderParser'; import type { WordMetadata } from './WordMetadata'; import type { CssRule, DomToModelOption, ElementProcessor } from 'roosterjs-content-model-types'; @@ -38,6 +39,7 @@ export function processPastedContentFromWordDesktop( addParser(domToModelOption, 'listLevel', listLevelParser); addParser(domToModelOption, 'container', wordContainerParser); addParser(domToModelOption, 'table', wordTableParser); + addParser(domToModelOption, 'tableBorder', wordBorderParser); } const wordDesktopElementProcessor = ( diff --git a/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/wordBorderParser.ts b/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/wordBorderParser.ts new file mode 100644 index 000000000000..8767b5f5b6b8 --- /dev/null +++ b/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/wordBorderParser.ts @@ -0,0 +1,55 @@ +import { BorderKeys } from 'roosterjs-content-model-dom'; +import type { + BorderFormat, + ContentModelTableFormat, + FormatParser, +} from 'roosterjs-content-model-types'; + +const BorderStyleNames = new Map([ + ['border-top', 'borderTop'], + ['border-right', 'borderRight'], + ['border-bottom', 'borderBottom'], + ['border-left', 'borderLeft'], +]); + +/** + * @internal + */ +export const wordBorderParser: FormatParser = (format, element) => { + const style = element.getAttribute('style'); + + if (style) { + const noBorder = new Map(); + + style.split(';').forEach(declaration => { + const separatorIndex = declaration.indexOf(':'); + + if (separatorIndex < 0) { + return; + } + + const name = declaration.substring(0, separatorIndex).trim().toLowerCase(); + const value = declaration + .substring(separatorIndex + 1) + .trim() + .toLowerCase(); + const isNone = /^none(?:\s|$|!important)/.test(value); + + if (name == 'border') { + BorderKeys.forEach(key => noBorder.set(key, isNone)); + } else { + const key = BorderStyleNames.get(name); + + if (key) { + noBorder.set(key, isNone); + } + } + }); + + noBorder.forEach((isNone, key) => { + if (isNone) { + format[key] = 'none'; + } + }); + } +}; diff --git a/packages/roosterjs-content-model-plugins/test/paste/plugin/ContentModelPastePluginTest.ts b/packages/roosterjs-content-model-plugins/test/paste/plugin/ContentModelPastePluginTest.ts index b3dd98241be7..7848b60501f6 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/plugin/ContentModelPastePluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/plugin/ContentModelPastePluginTest.ts @@ -59,7 +59,7 @@ describe('Content Model Paste Plugin Test', () => { plugin.initialize(editor); plugin.onPluginEvent(event); - expect(addParser.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 7); + expect(addParser.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 8); expect(setProcessor.setProcessor).toHaveBeenCalledTimes(2); }); diff --git a/packages/roosterjs-content-model-plugins/test/paste/word/wordBorderParserTest.ts b/packages/roosterjs-content-model-plugins/test/paste/word/wordBorderParserTest.ts new file mode 100644 index 000000000000..6b73092d15c4 --- /dev/null +++ b/packages/roosterjs-content-model-plugins/test/paste/word/wordBorderParserTest.ts @@ -0,0 +1,79 @@ +import { wordBorderParser } from '../../../lib/paste/WordDesktop/wordBorderParser'; +import type { ContentModelTableFormat, DomToModelContext } from 'roosterjs-content-model-types'; + +describe('wordBorderParser', () => { + let format: ContentModelTableFormat; + let element: HTMLElement; + let context: DomToModelContext; + + beforeEach(() => { + format = {}; + element = document.createElement('td'); + context = {} as any; + }); + + it('clears all border formats when style attribute has border none', () => { + format.borderTop = '1px solid'; + format.borderRight = '1px solid'; + format.borderBottom = '1px solid'; + format.borderLeft = '1px solid'; + element.setAttribute('style', 'width: 100px; border:none; padding: 0in 5.4pt 0in 5.4pt;'); + + wordBorderParser(format, element, context, {}); + + expect(format).toEqual({ + borderTop: 'none', + borderRight: 'none', + borderBottom: 'none', + borderLeft: 'none', + }); + }); + + it('clears side border formats when style attribute has side border none', () => { + format.borderTop = '1px solid'; + format.borderRight = '1px solid'; + format.borderBottom = '1px solid'; + format.borderLeft = '1px solid'; + element.setAttribute( + 'style', + 'border-top:none; border-right:1px solid red; border-bottom: none !important;' + ); + + wordBorderParser(format, element, context, {}); + + expect(format).toEqual({ + borderTop: 'none', + borderRight: '1px solid', + borderBottom: 'none', + borderLeft: '1px solid', + }); + }); + + it('respects later visible side border declarations after border none', () => { + format.borderTop = '1px solid'; + format.borderRight = '1px solid'; + format.borderBottom = '1px solid'; + format.borderLeft = '1px solid'; + element.setAttribute('style', 'border:none; border-top: 1px solid red;'); + + wordBorderParser(format, element, context, {}); + + expect(format).toEqual({ + borderTop: '1px solid', + borderRight: 'none', + borderBottom: 'none', + borderLeft: 'none', + }); + }); + + it('does not update border formats without border none in style attribute', () => { + format.borderTop = '1px solid'; + element.setAttribute('style', 'border-top: 1px solid red;'); + + wordBorderParser(format, element, context, {}); + + expect(format).toEqual({ + borderTop: '1px solid', + }); + }); +});