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 86caf67d625..157452ae203 100644 --- a/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts +++ b/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts @@ -1,452 +1,452 @@ -import * as addParserF from 'roosterjs-content-model-plugins/lib/paste/utils/addParser'; -import * as domToContentModel from 'roosterjs-content-model-dom/lib/domToModel/domToContentModel'; -import * as ExcelF from 'roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel'; -import * as getPasteSourceF from 'roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/getPasteSource'; -import * as getSelectedSegmentsF from 'roosterjs-content-model-dom/lib/modelApi/selection/collectSelections'; -import * as mergeModelFile from 'roosterjs-content-model-dom/lib/modelApi/editing/mergeModel'; -import * as PPT from 'roosterjs-content-model-plugins/lib/paste/PowerPoint/processPastedContentFromPowerPoint'; -import * as setProcessorF from 'roosterjs-content-model-plugins/lib/paste/utils/setProcessor'; -import * as WacComponents from 'roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents'; -import * as WordDesktopFile from 'roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop'; -import { Editor } from '../../../lib/editor/Editor'; -import { expectEqual, initEditor } from 'roosterjs-content-model-plugins/test/paste/e2e/testUtils'; -import { paste } from '../../../lib/command/paste/paste'; -import { PastePlugin } from 'roosterjs-content-model-plugins/lib/paste/PastePlugin'; -import { - ClipboardData, - ContentModelDocument, - IEditor, - BeforePasteEvent, - PluginEvent, -} from 'roosterjs-content-model-types'; - -let clipboardData: ClipboardData; - -const DEFAULT_TIMES_ADD_PARSER_CALLED = 4; - -describe('Paste ', () => { - let editor: IEditor; - let createContentModel: jasmine.Spy; - let focus: jasmine.Spy; - let mockedModel: ContentModelDocument; - let mockedMergeModel: ContentModelDocument; - let getVisibleViewport: jasmine.Spy; - - let div: HTMLDivElement; - - beforeEach(() => { - spyOn(domToContentModel, 'domToContentModel').and.callThrough(); - clipboardData = { - types: ['image/png', 'text/html'], - text: '', - image: null!, - rawHtml: '\r\nteststringteststring\r\n', - customValues: {}, - imageDataUri: null!, - }; - div = document.createElement('div'); - document.body.appendChild(div); - mockedModel = { - blockGroupType: 'Document', - blocks: [], - } as ContentModelDocument; - - mockedMergeModel = ({} as any) as ContentModelDocument; - - createContentModel = jasmine.createSpy('createContentModel').and.returnValue(mockedModel); - focus = jasmine.createSpy('focus'); - getVisibleViewport = jasmine.createSpy('getVisibleViewport'); - spyOn(mergeModelFile, 'mergeModel').and.callFake(() => { - mockedModel = mockedMergeModel; - return null; - }); - spyOn(getSelectedSegmentsF, 'getSelectedSegments').and.returnValue([ - { - format: { - fontSize: '1pt', - fontFamily: 'Arial', - }, - } as any, - ]); - - editor = new Editor(div, { - plugins: [new PastePlugin()], - coreApiOverride: { - focus, - createContentModel, - getVisibleViewport, - }, - }); - - spyOn(editor, 'getDocument').and.callThrough(); - spyOn(editor, 'triggerEvent').and.callThrough(); - }); - - afterEach(() => { - document.body.removeChild(div); - div = null!; - }); - - it('Execute', () => { - paste(editor, clipboardData); - - expect(mockedModel).toEqual(mockedMergeModel); - }); - - it('Execute | As plain text', () => { - paste(editor, clipboardData, 'asPlainText'); - - expect(mockedModel).toEqual(mockedMergeModel); - }); -}); - -describe('paste with content model & paste plugin', () => { - let editor: Editor | undefined; - let div: HTMLDivElement | undefined; - - beforeEach(() => { - div = document.createElement('div'); - document.body.appendChild(div); - editor = new Editor(div, { - plugins: [new PastePlugin()], - }); - spyOn(addParserF, 'addParser').and.callThrough(); - spyOn(setProcessorF, 'setProcessor').and.callThrough(); - clipboardData = { - types: ['image/png', 'text/html'], - text: '', - image: null!, - rawHtml: '\r\nteststringteststring\r\n', - customValues: {}, - imageDataUri: null!, - }; - }); - - afterEach(() => { - editor?.dispose(); - editor = undefined; - div?.remove(); - div = undefined; - }); - - it('Word Desktop', () => { - spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('wordDesktop'); - spyOn(WordDesktopFile, 'processPastedContentFromWordDesktop').and.callThrough(); - - paste(editor!, clipboardData); - - expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(1); - expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 5); - expect(WordDesktopFile.processPastedContentFromWordDesktop).toHaveBeenCalledTimes(1); - }); - - it('Word Online', () => { - spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('wacComponents'); - spyOn(WacComponents, 'processPastedContentWacComponents').and.callThrough(); - - paste(editor!, clipboardData); - - expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(2); - expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 6); - expect(WacComponents.processPastedContentWacComponents).toHaveBeenCalledTimes(1); - }); - - it('Excel Online', () => { - spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('excelOnline'); - spyOn(ExcelF, 'processPastedContentFromExcel').and.callThrough(); - - paste(editor!, clipboardData); - - expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(1); - expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 1); - expect(ExcelF.processPastedContentFromExcel).toHaveBeenCalledTimes(1); - }); - - it('Excel Desktop', () => { - spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('excelDesktop'); - spyOn(ExcelF, 'processPastedContentFromExcel').and.callThrough(); - - paste(editor!, clipboardData); - - expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(1); - expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 1); - expect(ExcelF.processPastedContentFromExcel).toHaveBeenCalledTimes(1); - }); - - it('PowerPoint', () => { - spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('powerPointDesktop'); - spyOn(PPT, 'processPastedContentFromPowerPoint').and.callThrough(); - - paste(editor!, clipboardData); - - expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); - expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED); - expect(PPT.processPastedContentFromPowerPoint).toHaveBeenCalledTimes(1); - }); - - // Plain Text - it('Word Desktop | Plain Text', () => { - spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('wordDesktop'); - spyOn(WordDesktopFile, 'processPastedContentFromWordDesktop').and.callThrough(); - - paste(editor!, clipboardData, 'asPlainText'); - - expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); - expect(addParserF.addParser).toHaveBeenCalledTimes(0); - expect(WordDesktopFile.processPastedContentFromWordDesktop).toHaveBeenCalledTimes(0); - }); - - it('Word Online | Plain Text', () => { - spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('wacComponents'); - spyOn(WacComponents, 'processPastedContentWacComponents').and.callThrough(); - - paste(editor!, clipboardData, 'asPlainText'); - - expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); - expect(addParserF.addParser).toHaveBeenCalledTimes(0); - expect(WacComponents.processPastedContentWacComponents).toHaveBeenCalledTimes(0); - }); - - it('Excel Online | Plain Text', () => { - spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('excelOnline'); - spyOn(ExcelF, 'processPastedContentFromExcel').and.callThrough(); - - paste(editor!, clipboardData, 'asPlainText'); - - expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); - expect(addParserF.addParser).toHaveBeenCalledTimes(0); - expect(ExcelF.processPastedContentFromExcel).toHaveBeenCalledTimes(0); - }); - - it('Excel Desktop | Plain Text', () => { - spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('excelDesktop'); - spyOn(ExcelF, 'processPastedContentFromExcel').and.callThrough(); - - paste(editor!, clipboardData, 'asPlainText'); - - expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); - expect(addParserF.addParser).toHaveBeenCalledTimes(0); - expect(ExcelF.processPastedContentFromExcel).toHaveBeenCalledTimes(0); - }); - - it('PowerPoint | Plain Text', () => { - spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('powerPointDesktop'); - spyOn(PPT, 'processPastedContentFromPowerPoint').and.callThrough(); - - paste(editor!, clipboardData, 'asPlainText'); - - expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); - expect(addParserF.addParser).toHaveBeenCalledTimes(0); - expect(PPT.processPastedContentFromPowerPoint).toHaveBeenCalledTimes(0); - }); - - it('Verify the event data is not lost', () => { - clipboardData = { - types: ['image/png', 'text/plain', 'text/html'], - text: 'Flight\tDescription\r\n', - image: {}, - files: [], - rawHtml: - '\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n \r\n \r\n \r\n\r\n \r\n \r\n\r\n \r\n
FlightDescription
\r\n\r\n\r\n\r\n\r\n', - customValues: {}, - pasteNativeEvent: true, - imageDataUri: '', - }; - - let eventChecker: BeforePasteEvent = {}; - editor = new Editor(div!, { - plugins: [ - { - initialize: () => {}, - dispose: () => {}, - getName: () => 'test', - onPluginEvent(event: PluginEvent) { - if (event.eventType === 'beforePaste') { - eventChecker = event; - } - }, - }, - ], - }); - - paste(editor!, clipboardData); - - expect(eventChecker?.clipboardData).toEqual(clipboardData); - expect(eventChecker?.htmlBefore).toBeTruthy(); - expect(eventChecker?.htmlAfter).toBeTruthy(); - expect(eventChecker?.pasteType).toEqual('normal'); - }); -}); - -describe('Paste with clipboardData', () => { - let editor: IEditor = undefined!; - const ID = 'EDITOR_ID'; - - beforeEach(() => { - editor = initEditor(ID); - clipboardData = ({ - types: ['text/plain', 'text/html'], - text: 'Test\r\nasdsad\r\n', - image: null, - files: [], - rawHtml: '', - customValues: {}, - htmlFirstLevelChildTags: ['P', 'P'], - html: '', - }); - }); - - afterEach(() => { - editor.dispose(); - document.getElementById(ID)?.remove(); - }); - - it('Replace windowtext with set black font color from clipboardContent', () => { - clipboardData.rawHtml = - '

Test

'; - - paste(editor, clipboardData); - - const model = editor.getContentModelCopy('connected'); - - expectEqual(model, { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'Test', - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: { - textColor: '', - backgroundColor: '', - fontFamily: '', - fontSize: '', - fontWeight: '', - italic: false, - letterSpacing: '', - lineHeight: '', - strikethrough: false, - superOrSubScriptSequence: '', - underline: false, - }, - }, - ], - format: { - marginTop: '1em', - marginBottom: '1em', - }, - decorator: { - tagName: 'p', - format: {}, - }, - }, - ], - format: {}, - }); - }); - - it('Remove unsupported url of link from clipboardContent', () => { - clipboardData.rawHtml = - 'Link'; - - paste(editor, clipboardData); - - const model = editor.getContentModelCopy('connected'); - - expectEqual(model, { - blockGroupType: 'Document', - blocks: [ - { - segments: [ - { text: 'Link', segmentType: 'Text', format: {} }, - { - isSelected: true, - segmentType: 'SelectionMarker', - format: { - backgroundColor: '', - fontFamily: '', - fontSize: '', - fontWeight: '', - italic: false, - letterSpacing: '', - lineHeight: '', - strikethrough: false, - superOrSubScriptSequence: '', - textColor: '', - underline: false, - }, - }, - ], - blockType: 'Paragraph', - format: {}, - }, - ], - format: {}, - }); - }); - - it('Keep supported url of link from clipboardContent', () => { - clipboardData.rawHtml = - 'Link'; - - paste(editor, clipboardData); - - const model = editor.getContentModelCopy('connected'); - - expectEqual(model, { - blockGroupType: 'Document', - blocks: [ - { - segments: [ - { - text: 'Link', - segmentType: 'Text', - format: {}, - link: { - format: { - underline: true, - href: 'https://github.com/microsoft/roosterjs', - }, - dataset: {}, - }, - }, - { - isSelected: true, - segmentType: 'SelectionMarker', - format: { - backgroundColor: '', - fontFamily: '', - fontSize: '', - fontWeight: '', - italic: false, - letterSpacing: '', - lineHeight: '', - strikethrough: false, - superOrSubScriptSequence: '', - textColor: '', - underline: false, - }, - link: { - format: { - underline: true, - href: 'https://github.com/microsoft/roosterjs', - }, - dataset: {}, - }, - }, - ], - blockType: 'Paragraph', - format: {}, - }, - ], - format: {}, - }); - }); -}); +import * as addParserF from 'roosterjs-content-model-plugins/lib/paste/utils/addParser'; +import * as domToContentModel from 'roosterjs-content-model-dom/lib/domToModel/domToContentModel'; +import * as ExcelF from 'roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel'; +import * as getPasteSourceF from 'roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/getPasteSource'; +import * as getSelectedSegmentsF from 'roosterjs-content-model-dom/lib/modelApi/selection/collectSelections'; +import * as mergeModelFile from 'roosterjs-content-model-dom/lib/modelApi/editing/mergeModel'; +import * as PPT from 'roosterjs-content-model-plugins/lib/paste/PowerPoint/processPastedContentFromPowerPoint'; +import * as setProcessorF from 'roosterjs-content-model-plugins/lib/paste/utils/setProcessor'; +import * as WacComponents from 'roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents'; +import * as WordDesktopFile from 'roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop'; +import { Editor } from '../../../lib/editor/Editor'; +import { expectEqual, initEditor } from 'roosterjs-content-model-plugins/test/paste/e2e/testUtils'; +import { paste } from '../../../lib/command/paste/paste'; +import { PastePlugin } from 'roosterjs-content-model-plugins/lib/paste/PastePlugin'; +import { + ClipboardData, + ContentModelDocument, + IEditor, + BeforePasteEvent, + PluginEvent, +} from 'roosterjs-content-model-types'; + +let clipboardData: ClipboardData; + +const DEFAULT_TIMES_ADD_PARSER_CALLED = 4; + +describe('Paste ', () => { + let editor: IEditor; + let createContentModel: jasmine.Spy; + let focus: jasmine.Spy; + let mockedModel: ContentModelDocument; + let mockedMergeModel: ContentModelDocument; + let getVisibleViewport: jasmine.Spy; + + let div: HTMLDivElement; + + beforeEach(() => { + spyOn(domToContentModel, 'domToContentModel').and.callThrough(); + clipboardData = { + types: ['image/png', 'text/html'], + text: '', + image: null!, + rawHtml: '\r\nteststringteststring\r\n', + customValues: {}, + imageDataUri: null!, + }; + div = document.createElement('div'); + document.body.appendChild(div); + mockedModel = { + blockGroupType: 'Document', + blocks: [], + } as ContentModelDocument; + + mockedMergeModel = ({} as any) as ContentModelDocument; + + createContentModel = jasmine.createSpy('createContentModel').and.returnValue(mockedModel); + focus = jasmine.createSpy('focus'); + getVisibleViewport = jasmine.createSpy('getVisibleViewport'); + spyOn(mergeModelFile, 'mergeModel').and.callFake(() => { + mockedModel = mockedMergeModel; + return null; + }); + spyOn(getSelectedSegmentsF, 'getSelectedSegments').and.returnValue([ + { + format: { + fontSize: '1pt', + fontFamily: 'Arial', + }, + } as any, + ]); + + editor = new Editor(div, { + plugins: [new PastePlugin()], + coreApiOverride: { + focus, + createContentModel, + getVisibleViewport, + }, + }); + + spyOn(editor, 'getDocument').and.callThrough(); + spyOn(editor, 'triggerEvent').and.callThrough(); + }); + + afterEach(() => { + document.body.removeChild(div); + div = null!; + }); + + it('Execute', () => { + paste(editor, clipboardData); + + expect(mockedModel).toEqual(mockedMergeModel); + }); + + it('Execute | As plain text', () => { + paste(editor, clipboardData, 'asPlainText'); + + expect(mockedModel).toEqual(mockedMergeModel); + }); +}); + +describe('paste with content model & paste plugin', () => { + let editor: Editor | undefined; + let div: HTMLDivElement | undefined; + + beforeEach(() => { + div = document.createElement('div'); + document.body.appendChild(div); + editor = new Editor(div, { + plugins: [new PastePlugin()], + }); + spyOn(addParserF, 'addParser').and.callThrough(); + spyOn(setProcessorF, 'setProcessor').and.callThrough(); + clipboardData = { + types: ['image/png', 'text/html'], + text: '', + image: null!, + rawHtml: '\r\nteststringteststring\r\n', + customValues: {}, + imageDataUri: null!, + }; + }); + + afterEach(() => { + editor?.dispose(); + editor = undefined; + div?.remove(); + div = undefined; + }); + + it('Word Desktop', () => { + spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('wordDesktop'); + spyOn(WordDesktopFile, 'processPastedContentFromWordDesktop').and.callThrough(); + + paste(editor!, clipboardData); + + expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(1); + expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 5); + expect(WordDesktopFile.processPastedContentFromWordDesktop).toHaveBeenCalledTimes(1); + }); + + it('Word Online', () => { + spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('wacComponents'); + spyOn(WacComponents, 'processPastedContentWacComponents').and.callThrough(); + + paste(editor!, clipboardData); + + expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(2); + expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 6); + expect(WacComponents.processPastedContentWacComponents).toHaveBeenCalledTimes(1); + }); + + it('Excel Online', () => { + spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('excelOnline'); + spyOn(ExcelF, 'processPastedContentFromExcel').and.callThrough(); + + paste(editor!, clipboardData); + + expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(1); + expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 1); + expect(ExcelF.processPastedContentFromExcel).toHaveBeenCalledTimes(1); + }); + + it('Excel Desktop', () => { + spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('excelDesktop'); + spyOn(ExcelF, 'processPastedContentFromExcel').and.callThrough(); + + paste(editor!, clipboardData); + + expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(1); + expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 1); + expect(ExcelF.processPastedContentFromExcel).toHaveBeenCalledTimes(1); + }); + + it('PowerPoint', () => { + spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('powerPointDesktop'); + spyOn(PPT, 'processPastedContentFromPowerPoint').and.callThrough(); + + paste(editor!, clipboardData); + + expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); + expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED); + expect(PPT.processPastedContentFromPowerPoint).toHaveBeenCalledTimes(1); + }); + + // Plain Text + it('Word Desktop | Plain Text', () => { + spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('wordDesktop'); + spyOn(WordDesktopFile, 'processPastedContentFromWordDesktop').and.callThrough(); + + paste(editor!, clipboardData, 'asPlainText'); + + expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); + expect(addParserF.addParser).toHaveBeenCalledTimes(0); + expect(WordDesktopFile.processPastedContentFromWordDesktop).toHaveBeenCalledTimes(0); + }); + + it('Word Online | Plain Text', () => { + spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('wacComponents'); + spyOn(WacComponents, 'processPastedContentWacComponents').and.callThrough(); + + paste(editor!, clipboardData, 'asPlainText'); + + expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); + expect(addParserF.addParser).toHaveBeenCalledTimes(0); + expect(WacComponents.processPastedContentWacComponents).toHaveBeenCalledTimes(0); + }); + + it('Excel Online | Plain Text', () => { + spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('excelOnline'); + spyOn(ExcelF, 'processPastedContentFromExcel').and.callThrough(); + + paste(editor!, clipboardData, 'asPlainText'); + + expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); + expect(addParserF.addParser).toHaveBeenCalledTimes(0); + expect(ExcelF.processPastedContentFromExcel).toHaveBeenCalledTimes(0); + }); + + it('Excel Desktop | Plain Text', () => { + spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('excelDesktop'); + spyOn(ExcelF, 'processPastedContentFromExcel').and.callThrough(); + + paste(editor!, clipboardData, 'asPlainText'); + + expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); + expect(addParserF.addParser).toHaveBeenCalledTimes(0); + expect(ExcelF.processPastedContentFromExcel).toHaveBeenCalledTimes(0); + }); + + it('PowerPoint | Plain Text', () => { + spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('powerPointDesktop'); + spyOn(PPT, 'processPastedContentFromPowerPoint').and.callThrough(); + + paste(editor!, clipboardData, 'asPlainText'); + + expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); + expect(addParserF.addParser).toHaveBeenCalledTimes(0); + expect(PPT.processPastedContentFromPowerPoint).toHaveBeenCalledTimes(0); + }); + + it('Verify the event data is not lost', () => { + clipboardData = { + types: ['image/png', 'text/plain', 'text/html'], + text: 'Flight\tDescription\r\n', + image: {}, + files: [], + rawHtml: + '\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n \r\n \r\n \r\n\r\n \r\n \r\n\r\n \r\n
FlightDescription
\r\n\r\n\r\n\r\n\r\n', + customValues: {}, + pasteNativeEvent: true, + imageDataUri: '', + }; + + let eventChecker: BeforePasteEvent = {}; + editor = new Editor(div!, { + plugins: [ + { + initialize: () => {}, + dispose: () => {}, + getName: () => 'test', + onPluginEvent(event: PluginEvent) { + if (event.eventType === 'beforePaste') { + eventChecker = event; + } + }, + }, + ], + }); + + paste(editor!, clipboardData); + + expect(eventChecker?.clipboardData).toEqual(clipboardData); + expect(eventChecker?.htmlBefore).toBeTruthy(); + expect(eventChecker?.htmlAfter).toBeTruthy(); + expect(eventChecker?.pasteType).toEqual('normal'); + }); +}); + +describe('Paste with clipboardData', () => { + let editor: IEditor = undefined!; + const ID = 'EDITOR_ID'; + + beforeEach(() => { + editor = initEditor(ID); + clipboardData = ({ + types: ['text/plain', 'text/html'], + text: 'Test\r\nasdsad\r\n', + image: null, + files: [], + rawHtml: '', + customValues: {}, + htmlFirstLevelChildTags: ['P', 'P'], + html: '', + }); + }); + + afterEach(() => { + editor.dispose(); + document.getElementById(ID)?.remove(); + }); + + it('Replace windowtext with set black font color from clipboardContent', () => { + clipboardData.rawHtml = + '

Test

'; + + paste(editor, clipboardData); + + const model = editor.getContentModelCopy('connected'); + + expectEqual(model, { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: { + textColor: '', + backgroundColor: '', + fontFamily: '', + fontSize: '', + fontWeight: '', + italic: false, + letterSpacing: '', + lineHeight: '', + strikethrough: false, + superOrSubScriptSequence: '', + underline: false, + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + ], + format: {}, + }); + }); + + it('Remove unsupported url of link from clipboardContent', () => { + clipboardData.rawHtml = + 'Link'; + + paste(editor, clipboardData); + + const model = editor.getContentModelCopy('connected'); + + expectEqual(model, { + blockGroupType: 'Document', + blocks: [ + { + segments: [ + { text: 'Link', segmentType: 'Text', format: {} }, + { + isSelected: true, + segmentType: 'SelectionMarker', + format: { + backgroundColor: '', + fontFamily: '', + fontSize: '', + fontWeight: '', + italic: false, + letterSpacing: '', + lineHeight: '', + strikethrough: false, + superOrSubScriptSequence: '', + textColor: '', + underline: false, + }, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + }); + }); + + it('Keep supported url of link from clipboardContent', () => { + clipboardData.rawHtml = + 'Link'; + + paste(editor, clipboardData); + + const model = editor.getContentModelCopy('connected'); + + expectEqual(model, { + blockGroupType: 'Document', + blocks: [ + { + segments: [ + { + text: 'Link', + segmentType: 'Text', + format: {}, + link: { + format: { + underline: true, + href: 'https://github.com/microsoft/roosterjs', + }, + dataset: {}, + }, + }, + { + isSelected: true, + segmentType: 'SelectionMarker', + format: { + backgroundColor: '', + fontFamily: '', + fontSize: '', + fontWeight: '', + italic: false, + letterSpacing: '', + lineHeight: '', + strikethrough: false, + superOrSubScriptSequence: '', + textColor: '', + underline: false, + }, + link: { + format: { + underline: true, + href: 'https://github.com/microsoft/roosterjs', + }, + dataset: {}, + }, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + }); + }); +}); diff --git a/packages/roosterjs-content-model-dom/lib/modelToDom/optimizers/optimize.ts b/packages/roosterjs-content-model-dom/lib/modelToDom/optimizers/optimize.ts index e347c8eb8d1..fa173ade5ca 100644 --- a/packages/roosterjs-content-model-dom/lib/modelToDom/optimizers/optimize.ts +++ b/packages/roosterjs-content-model-dom/lib/modelToDom/optimizers/optimize.ts @@ -1,38 +1,38 @@ -import { enumerateInheritedStyle, removeUnnecessaryStyle } from './removeUnnecessaryStyle'; -import { isEntityElement } from '../../domUtils/entityUtils'; -import { mergeNode } from './mergeNode'; -import { removeUnnecessaryAttribute } from './removeUnnecessaryAttribute'; -import { removeUnnecessarySpan } from './removeUnnecessarySpan'; - -/** - * @internal - */ -export function optimize(root: Node, isRecursive: boolean = false) { - /** - * Do no do any optimization to entity - */ - if (isEntityElement(root)) { - return; - } - if (!isRecursive && root.parentElement != null) { - const computedAttributes = {} as Record; - // html doesn't provide computed attributes, use parent's attributes directly - Array.from(root.parentElement.attributes).forEach(attr => { - computedAttributes[attr.name] = attr; - }); - removeUnnecessaryAttribute(root, computedAttributes); - - const computedStyle = {} as Record>; - enumerateInheritedStyle(root.parentElement, (key, values) => { - computedStyle[key] = values; - }); - removeUnnecessaryStyle(root, computedStyle); - } - - removeUnnecessarySpan(root); - mergeNode(root); - - for (let child = root.firstChild; child; child = child.nextSibling) { - optimize(child, true); - } -} +import { enumerateInheritedStyle, removeUnnecessaryStyle } from './removeUnnecessaryStyle'; +import { isEntityElement } from '../../domUtils/entityUtils'; +import { mergeNode } from './mergeNode'; +import { removeUnnecessaryAttribute } from './removeUnnecessaryAttribute'; +import { removeUnnecessarySpan } from './removeUnnecessarySpan'; + +/** + * @internal + */ +export function optimize(root: Node, isRecursive: boolean = false) { + /** + * Do no do any optimization to entity + */ + if (isEntityElement(root)) { + return; + } + if (!isRecursive && root.parentElement != null) { + const computedAttributes = {} as Record; + // html doesn't provide computed attributes, use parent's attributes directly + Array.from(root.parentElement.attributes).forEach(attr => { + computedAttributes[attr.name] = attr; + }); + removeUnnecessaryAttribute(root, computedAttributes); + + const computedStyle = {} as Record>; + enumerateInheritedStyle(root.parentElement, (key, values) => { + computedStyle[key] = values; + }); + removeUnnecessaryStyle(root, computedStyle); + } + + removeUnnecessarySpan(root); + mergeNode(root); + + for (let child = root.firstChild; child; child = child.nextSibling) { + optimize(child, true); + } +} diff --git a/packages/roosterjs-content-model-dom/lib/modelToDom/optimizers/removeUnnecessaryAttribute.ts b/packages/roosterjs-content-model-dom/lib/modelToDom/optimizers/removeUnnecessaryAttribute.ts index 7a5b81d5eed..efbbabf6a91 100644 --- a/packages/roosterjs-content-model-dom/lib/modelToDom/optimizers/removeUnnecessaryAttribute.ts +++ b/packages/roosterjs-content-model-dom/lib/modelToDom/optimizers/removeUnnecessaryAttribute.ts @@ -1,28 +1,28 @@ -import { isNodeOfType } from '../../domUtils/isNodeOfType'; - -/** - * @internal - */ -export function removeUnnecessaryAttribute(root: Node, computedAttributes: Record) { - if (!isNodeOfType(root, 'ELEMENT_NODE')) { - return; - } - const newComputedAttributes = { - ...computedAttributes, - }; - for (let i = root.attributes.length - 1; i >= 0; i--) { - const attr = root.attributes[i]; - if (attr.name === 'style') { - continue; - } - if (newComputedAttributes[attr.name]?.isEqualNode(attr) ?? false) { - root.removeAttribute(attr.name); - } else { - newComputedAttributes[attr.name] = attr; - } - } - - for (let child = root.firstChild; child; child = child.nextSibling) { - removeUnnecessaryAttribute(child, newComputedAttributes); - } -} +import { isNodeOfType } from '../../domUtils/isNodeOfType'; + +/** + * @internal + */ +export function removeUnnecessaryAttribute(root: Node, computedAttributes: Record) { + if (!isNodeOfType(root, 'ELEMENT_NODE')) { + return; + } + const newComputedAttributes = { + ...computedAttributes, + }; + for (let i = root.attributes.length - 1; i >= 0; i--) { + const attr = root.attributes[i]; + if (attr.name === 'style') { + continue; + } + if (newComputedAttributes[attr.name]?.isEqualNode(attr) ?? false) { + root.removeAttribute(attr.name); + } else { + newComputedAttributes[attr.name] = attr; + } + } + + for (let child = root.firstChild; child; child = child.nextSibling) { + removeUnnecessaryAttribute(child, newComputedAttributes); + } +} diff --git a/packages/roosterjs-content-model-dom/lib/modelToDom/optimizers/removeUnnecessarySpan.ts b/packages/roosterjs-content-model-dom/lib/modelToDom/optimizers/removeUnnecessarySpan.ts index 8a58eb3ee1e..3b0ccb11b88 100644 --- a/packages/roosterjs-content-model-dom/lib/modelToDom/optimizers/removeUnnecessarySpan.ts +++ b/packages/roosterjs-content-model-dom/lib/modelToDom/optimizers/removeUnnecessarySpan.ts @@ -1,37 +1,37 @@ -import { isNodeOfType } from '../../domUtils/isNodeOfType'; - -/** - * @internal - */ -export function removeUnnecessarySpan(root: Node) { - for (let child = root.firstChild; child; ) { - if ( - isNodeOfType(child, 'ELEMENT_NODE') && - child.tagName == 'SPAN' && - child.attributes.length == 0 && - !isImageSpan(child) - ) { - const node = child; - let refNode = child.nextSibling; - child = child.nextSibling; - - while (node.lastChild) { - const newNode = node.lastChild; - root.insertBefore(newNode, refNode); - refNode = newNode; - } - - root.removeChild(node); - } else { - child = child.nextSibling; - } - } -} - -const isImageSpan = (child: HTMLElement) => { - return ( - isNodeOfType(child.firstChild, 'ELEMENT_NODE') && - child.firstChild.tagName == 'IMG' && - child.firstChild == child.lastChild - ); -}; +import { isNodeOfType } from '../../domUtils/isNodeOfType'; + +/** + * @internal + */ +export function removeUnnecessarySpan(root: Node) { + for (let child = root.firstChild; child; ) { + if ( + isNodeOfType(child, 'ELEMENT_NODE') && + child.tagName == 'SPAN' && + child.attributes.length == 0 && + !isImageSpan(child) + ) { + const node = child; + let refNode = child.nextSibling; + child = child.nextSibling; + + while (node.lastChild) { + const newNode = node.lastChild; + root.insertBefore(newNode, refNode); + refNode = newNode; + } + + root.removeChild(node); + } else { + child = child.nextSibling; + } + } +} + +const isImageSpan = (child: HTMLElement) => { + return ( + isNodeOfType(child.firstChild, 'ELEMENT_NODE') && + child.firstChild.tagName == 'IMG' && + child.firstChild == child.lastChild + ); +}; diff --git a/packages/roosterjs-content-model-dom/lib/modelToDom/optimizers/removeUnnecessaryStyle.ts b/packages/roosterjs-content-model-dom/lib/modelToDom/optimizers/removeUnnecessaryStyle.ts index 63073cacb5d..85bfe88a1c4 100644 --- a/packages/roosterjs-content-model-dom/lib/modelToDom/optimizers/removeUnnecessaryStyle.ts +++ b/packages/roosterjs-content-model-dom/lib/modelToDom/optimizers/removeUnnecessaryStyle.ts @@ -1,55 +1,55 @@ -import { INHERITABLE_PROPERTIES } from 'roosterjs-editor-types'; -import { isNodeOfType } from '../../domUtils/isNodeOfType'; - -/** - * @internal - */ -export function enumerateInheritedStyle( - element: HTMLElement, - handler: (key: string, value: Set) => void -) { - element.style.cssText.split(';').forEach(value => { - const [key, valueText] = value.split(':').map(part => part.trim()); - if ( - !key || - !valueText || - (element.tagName == 'A' && key === 'color') || // The color of a link is not inherited - !INHERITABLE_PROPERTIES.includes(key) - ) { - return; - } - const values = new Set(valueText.split(',').map(value => value.trim())); - - handler(key, values); - }); -} - -/** - * @internal - */ -export function removeUnnecessaryStyle(root: Node, computedCSS: Record>) { - if (!isNodeOfType(root, 'ELEMENT_NODE')) { - return; - } - const newComputedCSS = { - ...computedCSS, - }; - enumerateInheritedStyle(root, (key, values) => { - if ( - computedCSS[key]?.size === values.size && - [...computedCSS[key]].every(value => values.has(value)) - ) { - root.style.removeProperty(key); - } else { - newComputedCSS[key] = values; - } - }); - - if (root.style.cssText === '') { - root.removeAttribute('style'); - } - - for (let child = root.firstChild; child; child = child.nextSibling) { - removeUnnecessaryStyle(child, newComputedCSS); - } -} +import { INHERITABLE_PROPERTIES } from 'roosterjs-editor-types'; +import { isNodeOfType } from '../../domUtils/isNodeOfType'; + +/** + * @internal + */ +export function enumerateInheritedStyle( + element: HTMLElement, + handler: (key: string, value: Set) => void +) { + element.style.cssText.split(';').forEach(value => { + const [key, valueText] = value.split(':').map(part => part.trim()); + if ( + !key || + !valueText || + (element.tagName == 'A' && key === 'color') || // The color of a link is not inherited + !INHERITABLE_PROPERTIES.includes(key) + ) { + return; + } + const values = new Set(valueText.split(',').map(value => value.trim())); + + handler(key, values); + }); +} + +/** + * @internal + */ +export function removeUnnecessaryStyle(root: Node, computedCSS: Record>) { + if (!isNodeOfType(root, 'ELEMENT_NODE')) { + return; + } + const newComputedCSS = { + ...computedCSS, + }; + enumerateInheritedStyle(root, (key, values) => { + if ( + computedCSS[key]?.size === values.size && + [...computedCSS[key]].every(value => values.has(value)) + ) { + root.style.removeProperty(key); + } else { + newComputedCSS[key] = values; + } + }); + + if (root.style.cssText === '') { + root.removeAttribute('style'); + } + + for (let child = root.firstChild; child; child = child.nextSibling) { + removeUnnecessaryStyle(child, newComputedCSS); + } +} diff --git a/packages/roosterjs-content-model-dom/package.json b/packages/roosterjs-content-model-dom/package.json index defa6518e48..aa289ff0527 100644 --- a/packages/roosterjs-content-model-dom/package.json +++ b/packages/roosterjs-content-model-dom/package.json @@ -1,11 +1,11 @@ -{ - "name": "roosterjs-content-model-dom", - "description": "Content Model for roosterjs", - "dependencies": { - "tslib": "^2.3.1", - "roosterjs-content-model-types": "", - "roosterjs-editor-types": "" - }, - "version": "0.0.0", - "main": "./lib/index.ts" -} +{ + "name": "roosterjs-content-model-dom", + "description": "Content Model for roosterjs", + "dependencies": { + "tslib": "^2.3.1", + "roosterjs-content-model-types": "", + "roosterjs-editor-types": "" + }, + "version": "0.0.0", + "main": "./lib/index.ts" +} diff --git a/packages/roosterjs-content-model-dom/test/endToEndTest.ts b/packages/roosterjs-content-model-dom/test/endToEndTest.ts index 5cd6e43eb36..72128a15f00 100644 --- a/packages/roosterjs-content-model-dom/test/endToEndTest.ts +++ b/packages/roosterjs-content-model-dom/test/endToEndTest.ts @@ -1,2148 +1,2148 @@ -import * as createGeneralBlock from '../lib/modelApi/creators/createGeneralBlock'; -import { contentModelToDom } from '../lib/modelToDom/contentModelToDom'; -import { contentModelToText, createDomToModelContext, createModelToDomContext } from '../lib'; -import { domToContentModel } from '../lib/domToModel/domToContentModel'; -import { expectHtml } from './testUtils'; -import { - ContentModelBlockFormat, - ContentModelDocument, - ContentModelGeneralBlock, -} from 'roosterjs-content-model-types'; - -describe('End to end test for DOM => Model => DOM/TEXT', () => { - function runTest( - html: string, - expectedModel: ContentModelDocument, - expectedText: string, - ...expectedHtml: string[] - ) { - const div1 = document.createElement('div'); - div1.innerHTML = html; - - const model = domToContentModel(div1, createDomToModelContext()); - - expect(model).toEqual(expectedModel); - - const div2 = document.createElement('div'); - - contentModelToDom(document, div2, model, createModelToDomContext()); - const text = contentModelToText(model); - - expect(text).toBe(expectedText); - expectHtml(div2.innerHTML, expectedHtml); - } - - it('List with margin', () => { - runTest( - '
  • 1
  • 2
', - { - blockGroupType: 'Document', - blocks: [ - { - blockGroupType: 'ListItem', - blockType: 'BlockGroup', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'Text', - text: '1', - format: { - fontFamily: 'Calibri, sans-serif', - fontSize: '11pt', - textColor: 'black', - }, - }, - ], - isImplicit: true, - segmentFormat: { - fontFamily: 'Calibri, sans-serif', - fontSize: '11pt', - textColor: 'black', - }, - }, - ], - levels: [ - { - listType: 'UL', - format: { marginBottom: '0in' }, - dataset: {}, - }, - ], - format: { - marginRight: '0in', - marginLeft: '0in', - }, - formatHolder: { - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Calibri, sans-serif', - fontSize: '11pt', - textColor: 'black', - }, - isSelected: false, - }, - }, - { - blockGroupType: 'ListItem', - blockType: 'BlockGroup', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'Text', - text: '2', - format: { - fontFamily: 'Calibri, sans-serif', - fontSize: '11pt', - textColor: 'black', - }, - }, - ], - isImplicit: true, - segmentFormat: { - fontFamily: 'Calibri, sans-serif', - fontSize: '11pt', - textColor: 'black', - }, - }, - ], - levels: [ - { - listType: 'UL', - format: { marginBottom: '0in' }, - dataset: {}, - }, - ], - format: { - marginRight: '0in', - marginLeft: '0in', - }, - formatHolder: { - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Calibri, sans-serif', - fontSize: '11pt', - textColor: 'black', - }, - isSelected: false, - }, - }, - ], - }, - '1\r\n2', - '
  • 1
  • 2
' - ); - }); - - it('list with dummy item', () => { - runTest( - '
  1. 1
    1. a
  2. b
  3. 2
', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [{ segmentType: 'Text', text: '1', format: {} }], - format: {}, - isImplicit: true, - }, - ], - levels: [{ listType: 'OL', format: {}, dataset: {} }], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, - format: {}, - }, - format: {}, - }, - { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [{ segmentType: 'Text', text: 'a', format: {} }], - format: {}, - isImplicit: true, - }, - ], - levels: [ - { listType: 'OL', format: {}, dataset: {} }, - { listType: 'OL', format: {}, dataset: {} }, - ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, - format: {}, - }, - format: {}, - }, - { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [{ segmentType: 'Text', text: 'b', format: {} }], - format: {}, - isImplicit: true, - }, - ], - levels: [ - { - listType: 'OL', - format: { displayForDummyItem: 'block' }, - dataset: {}, - }, - ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, - format: {}, - }, - format: {}, - }, - { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [{ segmentType: 'Text', text: '2', format: {} }], - format: {}, - isImplicit: true, - }, - ], - levels: [{ listType: 'OL', format: {}, dataset: {} }], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, - format: {}, - }, - format: {}, - }, - ], - }, - '1\r\na\r\nb\r\n2', - '
  1. 1
    1. a
  2. b
  3. 2
' - ); - }); - - it('div with whiteSpace, pre and blockquote', () => { - runTest( - '
aa\nbb
cc\ndd
ee
', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'aa\nbb', - format: {}, - }, - ], - format: { - whiteSpace: 'pre', - }, - }, - { - blockType: 'BlockGroup', - blockGroupType: 'FormatContainer', - tagName: 'pre', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'cc\ndd', - format: { fontFamily: 'monospace' }, - }, - ], - format: { whiteSpace: 'pre' }, - isImplicit: true, - segmentFormat: { fontFamily: 'monospace' }, - }, - ], - format: { - marginTop: '1em', - marginBottom: '1em', - whiteSpace: 'pre', - }, - }, - { - blockType: 'BlockGroup', - blockGroupType: 'FormatContainer', - tagName: 'blockquote', - format: { - marginRight: '40px', - marginLeft: '40px', - marginTop: '1em', - marginBottom: '1em', - }, - blocks: [ - { - blockType: 'Paragraph', - isImplicit: true, - segments: [ - { - segmentType: 'Text', - text: 'ee', - format: {}, - }, - ], - format: {}, - }, - ], - }, - ], - }, - 'aa\nbb\r\ncc\ndd\r\nee', - '
aa\nbb
cc\ndd
ee
' - ); - }); - - it('Two PRE tags', () => { - runTest( - '
test1
test2
', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'BlockGroup', - blockGroupType: 'FormatContainer', - tagName: 'pre', - format: { - marginTop: '1em', - marginBottom: '1em', - whiteSpace: 'pre', - }, - blocks: [ - { - blockType: 'Paragraph', - format: { whiteSpace: 'pre' }, - segments: [ - { - segmentType: 'Text', - text: 'test1', - format: { fontFamily: 'monospace' }, - }, - ], - isImplicit: true, - segmentFormat: { fontFamily: 'monospace' }, - }, - ], - }, - { - blockType: 'BlockGroup', - blockGroupType: 'FormatContainer', - tagName: 'pre', - format: { - marginTop: '1em', - marginBottom: '1em', - whiteSpace: 'pre', - }, - blocks: [ - { - blockType: 'Paragraph', - format: { whiteSpace: 'pre' }, - segments: [ - { - segmentType: 'Text', - text: 'test2', - format: { fontFamily: 'monospace' }, - }, - ], - isImplicit: true, - segmentFormat: { fontFamily: 'monospace' }, - }, - ], - }, - ], - }, - 'test1\r\ntest2', - '
test1
test2
' - ); - }); - - it('Block under styled inline', () => { - runTest( - 'aa
bb
cc
', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'aa', - format: { - fontWeight: 'bold', - backgroundColor: 'red', - }, - }, - ], - format: {}, - isImplicit: true, - }, - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'bb', - format: { - fontWeight: 'bold', - }, - }, - ], - format: {}, - }, - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'cc', - format: { - fontWeight: 'bold', - backgroundColor: 'red', - }, - }, - ], - format: {}, - isImplicit: true, - }, - ], - }, - 'aa\r\nbb\r\ncc', - 'aa
bb
cc' - ); - }); - - it('Table under styled inline', () => { - runTest( - 'aa
bb
cc
', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'aa', - format: { - fontWeight: 'bold', - backgroundColor: 'red', - }, - }, - ], - format: {}, - isImplicit: true, - }, - { - blockType: 'Table', - rows: [ - { - format: {}, - height: 0, - cells: [ - { - blockGroupType: 'TableCell', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'bb', - format: { - fontWeight: 'bold', - }, - }, - ], - format: {}, - isImplicit: true, - }, - ], - format: {}, - spanLeft: false, - spanAbove: false, - isHeader: false, - dataset: {}, - }, - ], - }, - ], - format: {}, - widths: [], - dataset: {}, - }, - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'cc', - format: { - fontWeight: 'bold', - backgroundColor: 'red', - }, - }, - ], - format: {}, - isImplicit: true, - }, - ], - }, - 'aa\r\nbb\r\ncc', - 'aa
bb
cc' - ); - }); - - it('Table under styled block', () => { - runTest( - 'aa
bb
cc
', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'BlockGroup', - blockGroupType: 'FormatContainer', - tagName: 'div', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'aa', - format: { - fontWeight: 'bold', - }, - }, - ], - format: {}, - isImplicit: true, - }, - { - blockType: 'Table', - rows: [ - { - format: {}, - height: 0, - cells: [ - { - blockGroupType: 'TableCell', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'bb', - format: { - fontWeight: 'bold', - }, - }, - ], - format: {}, - isImplicit: true, - }, - ], - format: {}, - spanLeft: false, - spanAbove: false, - isHeader: false, - dataset: {}, - }, - ], - }, - ], - format: {}, - widths: [], - dataset: {}, - }, - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'cc', - format: { - fontWeight: 'bold', - }, - }, - ], - format: {}, - isImplicit: true, - }, - ], - format: { - backgroundColor: 'red', - display: 'block', - }, - }, - ], - }, - 'aa\r\nbb\r\ncc', - '
aa
bb
cc
' - ); - }); - - it('Blockquote with margins', () => { - runTest( - '
aa
aa
', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'BlockGroup', - blockGroupType: 'FormatContainer', - tagName: 'blockquote', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'aa', - format: {}, - }, - ], - format: {}, - isImplicit: true, - }, - ], - format: { - marginTop: '20px', - marginRight: '20px', - marginBottom: '20px', - marginLeft: '20px', - }, - }, - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'aa', - format: {}, - }, - ], - format: { - marginTop: '0px', - marginRight: '20px', - marginBottom: '0px', - marginLeft: '20px', - }, - }, - ], - }, - 'aa\r\naa', - '
aa
aa
' - ); - }); - - it('margin on paragraph', () => { - runTest( - 'aa', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'aa', - format: { - fontWeight: 'bold', - }, - }, - ], - format: { - marginTop: '20px', - marginRight: '20px', - marginBottom: '20px', - marginLeft: '20px', - display: 'block', - } as ContentModelBlockFormat, - isImplicit: false, - }, - ], - }, - 'aa', - '
aa
' - ); - }); - - it('Multiple P tag', () => { - runTest( - '

aaa

bbb

', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'aaa', - format: {}, - }, - ], - format: { - marginTop: '1em', - marginBottom: '1em', - }, - decorator: { - tagName: 'p', - format: {}, - }, - }, - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'bbb', - format: {}, - }, - ], - format: { - marginTop: '1em', - marginBottom: '1em', - }, - decorator: { - tagName: 'p', - format: {}, - }, - }, - ], - }, - 'aaa\r\nbbb', - '

aaa

bbb

' - ); - }); - - it('P tags with margin', () => { - runTest( - '

aaa

bbb

', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'aaa', - format: {}, - }, - ], - format: { - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - marginLeft: '0px', - }, - decorator: { - tagName: 'p', - format: {}, - }, - }, - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'bbb', - format: {}, - }, - ], - format: { - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - marginLeft: '0px', - }, - decorator: { - tagName: 'p', - format: {}, - }, - }, - ], - }, - 'aaa\r\nbbb', - '

aaa

bbb

' - ); - }); - - it('Headers', () => { - runTest( - '

aa

bb

cc

', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'aa', - format: {}, - }, - ], - format: {}, - decorator: { - tagName: 'h1', - format: { - fontSize: '2em', - fontWeight: 'bold', - }, - }, - }, - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'bb', - format: {}, - }, - ], - format: {}, - decorator: { - tagName: 'h2', - format: { - fontSize: '1.5em', - fontWeight: 'bold', - }, - }, - }, - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'cc', - format: {}, - }, - ], - format: { - marginTop: '50px', - marginRight: '50px', - marginBottom: '50px', - marginLeft: '50px', - }, - decorator: { - tagName: 'h3', - format: { - fontSize: '1.17em', - fontWeight: 'bold', - }, - }, - }, - ], - }, - 'aa\r\nbb\r\ncc', - '

aa

bb

cc

' - ); - }); - - it('Header with format from context', () => { - runTest( - '

test

', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'Text', - text: 'test', - format: {}, - }, - ], - decorator: { - tagName: 'h1', - format: { fontSize: '40px', fontWeight: 'bold' }, - }, - }, - ], - }, - 'test', - '

test

' - ); - }); - - it('PREs', () => { - runTest( - '
aaa\nbbb
aaa\nbb
', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'BlockGroup', - blockGroupType: 'FormatContainer', - tagName: 'pre', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'aaa\nbbb', - format: { - fontFamily: 'monospace', - }, - }, - ], - format: { - whiteSpace: 'pre', - }, - isImplicit: true, - segmentFormat: { fontFamily: 'monospace' }, - }, - ], - format: { - whiteSpace: 'pre', - marginTop: '1em', - marginBottom: '1em', - }, - }, - { - blockType: 'BlockGroup', - blockGroupType: 'FormatContainer', - tagName: 'pre', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'aaa\nbb', - format: { - fontFamily: 'monospace', - fontSize: '20px', - }, - }, - ], - format: { - whiteSpace: 'pre', - }, - isImplicit: true, - segmentFormat: { fontFamily: 'monospace', fontSize: '20px' }, - }, - ], - format: { - whiteSpace: 'pre', - marginTop: '1em', - marginBottom: '1em', - }, - }, - ], - }, - 'aaa\nbbb\r\naaa\nbb', - '
aaa\nbbb
aaa\nbb
' - ); - }); - - it('Code and Link', () => { - runTest( - '
aaabbbccc
aaabbbccc
', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'aaa', - format: {}, - }, - { - segmentType: 'Text', - text: 'bbb', - format: {}, - code: { - format: { - fontFamily: 'monospace', - }, - }, - }, - { - segmentType: 'Text', - text: 'ccc', - format: {}, - }, - ], - format: {}, - }, - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'aaa', - format: {}, - }, - { - segmentType: 'Text', - text: 'bbb', - format: {}, - link: { - format: { - underline: true, - href: '#', - }, - dataset: {}, - }, - }, - { - segmentType: 'Text', - text: 'ccc', - format: {}, - }, - ], - format: {}, - }, - ], - }, - 'aaabbbccc\r\naaabbbccc', - '
aaabbbccc
aaabbbccc
' - ); - }); - - it('BlockQuotes', () => { - runTest( - '
aaaa
bbbbbb
cccc
aaaa
bbbbbb
cccc
', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'aaaa', - format: { - textColor: 'red', - }, - }, - ], - format: {}, - segmentFormat: { textColor: 'red' }, - }, - { - blockType: 'BlockGroup', - blockGroupType: 'FormatContainer', - tagName: 'blockquote', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'bbbbbb', - format: { - fontFamily: 'Calibri, Arial, Helvetica, sans-serif', - fontSize: '12pt', - textColor: 'rgb(102, 102, 102)', - }, - }, - ], - format: {}, - segmentFormat: { - fontFamily: 'Calibri, Arial, Helvetica, sans-serif', - fontSize: '12pt', - textColor: 'rgb(102, 102, 102)', - }, - }, - ], - format: { - borderLeft: '3px solid rgb(200, 200, 200)', - marginTop: '1em', - marginRight: '40px', - marginBottom: '1em', - marginLeft: '40px', - paddingLeft: '10px', - }, - }, - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'cccc', - format: { - textColor: 'red', - }, - }, - ], - format: {}, - segmentFormat: { textColor: 'red' }, - }, - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'aaaa', - format: { - textColor: 'red', - }, - }, - ], - format: {}, - segmentFormat: { textColor: 'red' }, - }, - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'bbbbbb', - format: { - fontFamily: 'Calibri, Arial, Helvetica, sans-serif', - fontSize: '12pt', - textColor: 'rgb(102, 102, 102)', - }, - }, - ], - format: { - marginRight: '40px', - marginLeft: '40px', - }, - segmentFormat: { - fontFamily: 'Calibri, Arial, Helvetica, sans-serif', - fontSize: '12pt', - textColor: 'rgb(102, 102, 102)', - }, - }, - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'cccc', - format: { - textColor: 'red', - }, - }, - ], - format: {}, - segmentFormat: { - textColor: 'red', - }, - }, - ], - }, - 'aaaa\r\nbbbbbb\r\ncccc\r\naaaa\r\nbbbbbb\r\ncccc', - '
aaaa
bbbbbb
cccc
aaaa
bbbbbb
cccc
' - ); - }); - - it('margin left and format container', () => { - runTest( - '
aaa
bbb
ccc
', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'aaa', - format: {}, - }, - ], - format: { - marginLeft: '40px', - }, - }, - { - blockType: 'BlockGroup', - blockGroupType: 'FormatContainer', - tagName: 'pre', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'bbb', - format: { - fontFamily: 'monospace', - }, - }, - ], - format: { - whiteSpace: 'pre', - }, - segmentFormat: { - fontFamily: 'monospace', - }, - }, - ], - format: { - whiteSpace: 'pre', - marginTop: '1em', - marginBottom: '1em', - marginLeft: '40px', - }, - }, - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'ccc', - format: {}, - }, - ], - format: { - marginLeft: '40px', - }, - isImplicit: true, - }, - ], - }, - 'aaa\r\nbbb\r\nccc', - '
aaa
bbb
ccc
' - ); - }); - - it('nested margin left', () => { - runTest( - '
aaa
bbb
', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'aaa', - format: {}, - }, - ], - format: { - marginLeft: '40px', - }, - }, - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'bbb', - format: {}, - }, - ], - format: { - marginLeft: '90px', - marginTop: '0px', - marginRight: '50px', - marginBottom: '0px', - }, - }, - ], - }, - 'aaa\r\nbbb', - '
aaa
bbb
' - ); - }); - - it('text after format container', () => { - runTest( - '
test1
test2', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test1', - format: {}, - }, - ], - format: { - htmlAlign: 'center', - backgroundColor: 'red', - }, - isImplicit: false, - }, - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test2', - format: {}, - }, - ], - format: {}, - isImplicit: true, - }, - ], - }, - 'test1\r\ntest2', - '
test1
test2', - '
test1
test2' - ); - }); - - it('Center', () => { - const cloneNodeSpy = jasmine - .createSpy('cloneNode') - .and.returnValue(document.createElement('center')); - const mockedElement = { - name: 'ELEMENT', - cloneNode: cloneNodeSpy, - } as any; - const mockedGeneral: ContentModelGeneralBlock = { - blockType: 'BlockGroup', - blockGroupType: 'General', - element: mockedElement, - blocks: [], - format: {}, - }; - - const createGeneralBlockSpy = spyOn( - createGeneralBlock, - 'createGeneralBlock' - ).and.returnValue(mockedGeneral); - - runTest( - '
test1
test2
test3
', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'BlockGroup', - blockGroupType: 'General', - format: {}, - blocks: [ - { - blockType: 'Paragraph', - format: {}, - isImplicit: true, - segments: [ - { - segmentType: 'Text', - format: {}, - text: 'test1', - }, - ], - }, - { - blockType: 'Table', - format: {}, - rows: [ - { - format: {}, - height: 0, - cells: [ - { - blockGroupType: 'TableCell', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - isImplicit: true, - segments: [ - { - segmentType: 'Text', - format: {}, - text: 'test2', - }, - ], - }, - ], - format: {}, - spanLeft: false, - spanAbove: false, - isHeader: false, - dataset: {}, - }, - ], - }, - ], - widths: [], - dataset: {}, - }, - { - blockType: 'Paragraph', - format: { htmlAlign: 'end' }, - isImplicit: false, - segments: [ - { - segmentType: 'Text', - format: {}, - text: 'test3', - }, - ], - }, - ], - element: mockedElement, - }, - ], - }, - 'test1\r\ntest2\r\ntest3', - '
test1
test2
test3
' - ); - - expect(createGeneralBlockSpy).toHaveBeenCalledTimes(1); - expect(cloneNodeSpy).toHaveBeenCalledTimes(1); - }); - - it('html align overwrite text align from context', () => { - runTest( - '
aaa
bbb
ccc
ddd
eee
', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: { textAlign: 'center' }, - segments: [ - { - segmentType: 'Text', - format: {}, - text: 'aaa', - }, - ], - }, - { - blockType: 'BlockGroup', - blockGroupType: 'FormatContainer', - tagName: 'div', - format: { - htmlAlign: 'end', - }, - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'Text', - format: {}, - text: 'bbb', - }, - ], - isImplicit: true, - }, - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'Text', - format: {}, - text: 'ccc', - }, - ], - }, - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'Text', - format: {}, - text: 'ddd', - }, - ], - isImplicit: true, - }, - ], - }, - { - blockType: 'Paragraph', - format: { - textAlign: 'center', - }, - segments: [ - { - segmentType: 'Text', - format: {}, - text: 'eee', - }, - ], - isImplicit: true, - }, - ], - }, - 'aaa\r\nbbb\r\nccc\r\nddd\r\neee', - '
aaa
bbb
ccc
ddd
eee
' - ); - }); - - it('SUB needs to be put inside S or U if any', () => { - runTest( - '
test
', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'Text', - text: 'test', - format: { - strikethrough: true, - underline: true, - superOrSubScriptSequence: 'sub', - }, - }, - ], - }, - ], - }, - 'test', - '
test
' - ); - }); - - it('Table with margin under "align=center"', () => { - runTest( - '
', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'BlockGroup', - blockGroupType: 'FormatContainer', - format: { htmlAlign: 'center' }, - tagName: 'div', - blocks: [ - { - blockType: 'Table', - format: { - marginBottom: '0px', - marginLeft: '0px', - marginRight: '0px', - marginTop: '0px', - }, - widths: [], - dataset: {}, - rows: [ - { - format: {}, - height: 0, - cells: [ - { - blockGroupType: 'TableCell', - format: {}, - spanAbove: false, - spanLeft: false, - isHeader: false, - dataset: {}, - blocks: [], - }, - ], - }, - ], - }, - ], - }, - ], - }, - '', - '
' - ); - }); - - it('A with display:block', () => { - runTest( - 'test', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - isImplicit: true, - format: {}, - segments: [ - { - segmentType: 'Text', - text: 'test', - format: { textColor: 'red' }, - link: { - format: { - underline: true, - href: '#', - textColor: 'red', - display: 'block', - }, - dataset: {}, - }, - }, - ], - segmentFormat: { textColor: 'red' }, - }, - ], - }, - 'test', - 'test' - ); - }); - - it('Segment format on block', () => { - runTest( - '
test
', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test', - format: { - strikethrough: true, - underline: true, - italic: true, - fontWeight: 'bold', - }, - }, - ], - format: {}, - segmentFormat: { - strikethrough: true, - underline: true, - italic: true, - fontWeight: 'bold', - }, - }, - ], - }, - 'test', - '
test
' - ); - }); - - it('Segment format on block from parent', () => { - runTest( - 'aaabbb
ccc
ddd
eeee', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { segmentType: 'Text', text: 'aaa', format: {} }, - { segmentType: 'Text', text: 'bbb', format: { fontWeight: 'bold' } }, - ], - format: {}, - isImplicit: true, - }, - { - blockType: 'Paragraph', - segments: [ - { segmentType: 'Text', text: 'ccc', format: { fontWeight: 'bold' } }, - ], - format: {}, - }, - { - blockType: 'Paragraph', - segments: [ - { segmentType: 'Text', text: 'ddd', format: { fontWeight: 'bold' } }, - { segmentType: 'Text', text: 'eeee', format: {} }, - ], - format: {}, - isImplicit: true, - }, - ], - }, - 'aaabbb\r\nccc\r\ndddeeee', - 'aaabbb
ccc
dddeeee' - ); - }); - - it('Link inside superscript', () => { - runTest( - '', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'www.bing.com', - format: { superOrSubScriptSequence: 'super' }, - link: { - format: { underline: true, href: 'http://www.bing.com' }, - dataset: {}, - }, - }, - ], - format: {}, - }, - ], - }, - 'www.bing.com', - '' - ); - }); - - it('Multiple P tag with margin-left', () => { - runTest( - '

aaa

bbb

', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: { - marginLeft: '40px', - marginTop: '1em', - marginBottom: '1em', - }, - segments: [ - { - segmentType: 'Text', - format: {}, - text: 'aaa', - }, - ], - decorator: { - format: {}, - tagName: 'p', - }, - }, - { - blockType: 'Paragraph', - format: { - marginLeft: '40px', - marginTop: '1em', - marginBottom: '1em', - }, - segments: [ - { - segmentType: 'Text', - format: {}, - text: 'bbb', - }, - ], - decorator: { - format: {}, - tagName: 'p', - }, - }, - ], - }, - 'aaa\r\nbbb', - '

aaa

bbb

' - ); - }); - - it('SPAN inside link with color', () => { - runTest( - 'beforetestafter', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'before', - format: {}, - link: { - format: { underline: true, href: '#' }, - dataset: {}, - }, - }, - { - segmentType: 'Text', - text: 'test', - format: { textColor: 'red' }, - link: { - format: { underline: true, href: '#', textColor: 'red' }, - dataset: {}, - }, - }, - { - segmentType: 'Text', - text: 'after', - format: {}, - link: { - format: { underline: true, href: '#' }, - dataset: {}, - }, - }, - ], - format: {}, - isImplicit: true, - }, - ], - }, - 'beforetestafter', - 'beforetestafter' - ); - }); - - it('text-indent', () => { - runTest( - '
aa
bb
cc
', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: { textIndent: '20px' }, - segments: [{ segmentType: 'Text', format: {}, text: 'aa' }], - }, - { - blockType: 'Paragraph', - format: { textIndent: '20px' }, - segments: [{ segmentType: 'Text', format: {}, text: 'bb' }], - }, - { - blockType: 'Paragraph', - format: {}, - segments: [{ segmentType: 'Text', format: {}, text: 'cc' }], - isImplicit: true, - }, - ], - }, - 'aa\r\nbb\r\ncc', - '
aa
bb
cc' - ); - }); - - it('text-indent with inner block', () => { - runTest( - '
aa
bb
cc
', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: { textIndent: '20px' }, - segments: [{ segmentType: 'Text', format: {}, text: 'aa' }], - }, - { - blockType: 'Paragraph', - format: {}, - segments: [{ segmentType: 'Text', format: {}, text: 'bb' }], - isImplicit: true, - }, - { - blockType: 'Paragraph', - format: { textIndent: '40px' }, - segments: [{ segmentType: 'Text', format: {}, text: 'cc' }], - }, - ], - }, - 'aa\r\nbb\r\ncc', - '
aa
bb
cc
' - ); - }); - - it('Table with COLGROUP', () => { - runTest( - '
abc
', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Table', - rows: [ - { - cells: [ - { - blockGroupType: 'TableCell', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'a', - format: {}, - }, - ], - format: {}, - isImplicit: true, - }, - ], - dataset: {}, - format: {}, - spanAbove: false, - spanLeft: false, - isHeader: false, - }, - { - blockGroupType: 'TableCell', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'b', - format: {}, - }, - ], - format: {}, - isImplicit: true, - }, - ], - dataset: {}, - format: {}, - spanAbove: false, - spanLeft: false, - isHeader: false, - }, - { - blockGroupType: 'TableCell', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'c', - format: {}, - }, - ], - format: {}, - isImplicit: true, - }, - ], - dataset: {}, - format: {}, - spanAbove: false, - spanLeft: false, - isHeader: false, - }, - ], - format: {}, - height: 0, - }, - ], - dataset: {}, - format: {}, - widths: [100, 150, 120], - }, - ], - }, - 'a\r\nb\r\nc', - '
abc
' - ); - }); - - it('Table with COLGROUP with span', () => { - runTest( - '
abc
', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Table', - rows: [ - { - cells: [ - { - blockGroupType: 'TableCell', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'a', - format: {}, - }, - ], - format: {}, - isImplicit: true, - }, - ], - dataset: {}, - format: {}, - spanAbove: false, - spanLeft: false, - isHeader: false, - }, - { - blockGroupType: 'TableCell', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'b', - format: {}, - }, - ], - format: {}, - isImplicit: true, - }, - ], - dataset: {}, - format: {}, - spanAbove: false, - spanLeft: false, - isHeader: false, - }, - { - blockGroupType: 'TableCell', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'c', - format: {}, - }, - ], - format: {}, - isImplicit: true, - }, - ], - dataset: {}, - format: {}, - spanAbove: false, - spanLeft: false, - isHeader: false, - }, - ], - format: {}, - height: 0, - }, - ], - dataset: {}, - format: {}, - widths: [150, 150, 120], - }, - ], - }, - 'a\r\nb\r\nc', - '
abc
' - ); - }); - - it('list with list style', () => { - runTest( - '
    1. test
', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test', - format: {}, - }, - ], - isImplicit: true, - format: {}, - }, - ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, - format: {}, - }, - levels: [ - { - listType: 'OL', - format: {}, - dataset: {}, - }, - { - listType: 'OL', - format: { listStyleType: '"1) "' }, - dataset: {}, - }, - ], - format: {}, - }, - ], - }, - 'test', - '
    1. test
' - ); - }); - - it('link with color', () => { - runTest( - '', - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'www.bing.com', - format: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: 'rgb(245, 212, 39)', - }, - link: { - format: { - underline: true, - href: 'http://www.bing.com', - textColor: 'rgb(245, 212, 39)', - }, - dataset: {}, - }, - }, - ], - format: {}, - segmentFormat: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: 'rgb(245, 212, 39)', - }, - }, - ], - }, - 'www.bing.com', - '' - ); - }); -}); +import * as createGeneralBlock from '../lib/modelApi/creators/createGeneralBlock'; +import { contentModelToDom } from '../lib/modelToDom/contentModelToDom'; +import { contentModelToText, createDomToModelContext, createModelToDomContext } from '../lib'; +import { domToContentModel } from '../lib/domToModel/domToContentModel'; +import { expectHtml } from './testUtils'; +import { + ContentModelBlockFormat, + ContentModelDocument, + ContentModelGeneralBlock, +} from 'roosterjs-content-model-types'; + +describe('End to end test for DOM => Model => DOM/TEXT', () => { + function runTest( + html: string, + expectedModel: ContentModelDocument, + expectedText: string, + ...expectedHtml: string[] + ) { + const div1 = document.createElement('div'); + div1.innerHTML = html; + + const model = domToContentModel(div1, createDomToModelContext()); + + expect(model).toEqual(expectedModel); + + const div2 = document.createElement('div'); + + contentModelToDom(document, div2, model, createModelToDomContext()); + const text = contentModelToText(model); + + expect(text).toBe(expectedText); + expectHtml(div2.innerHTML, expectedHtml); + } + + it('List with margin', () => { + runTest( + '
  • 1
  • 2
', + { + blockGroupType: 'Document', + blocks: [ + { + blockGroupType: 'ListItem', + blockType: 'BlockGroup', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: '1', + format: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'black', + }, + }, + ], + isImplicit: true, + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'black', + }, + }, + ], + levels: [ + { + listType: 'UL', + format: { marginBottom: '0in' }, + dataset: {}, + }, + ], + format: { + marginRight: '0in', + marginLeft: '0in', + }, + formatHolder: { + segmentType: 'SelectionMarker', + format: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'black', + }, + isSelected: false, + }, + }, + { + blockGroupType: 'ListItem', + blockType: 'BlockGroup', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: '2', + format: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'black', + }, + }, + ], + isImplicit: true, + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'black', + }, + }, + ], + levels: [ + { + listType: 'UL', + format: { marginBottom: '0in' }, + dataset: {}, + }, + ], + format: { + marginRight: '0in', + marginLeft: '0in', + }, + formatHolder: { + segmentType: 'SelectionMarker', + format: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'black', + }, + isSelected: false, + }, + }, + ], + }, + '1\r\n2', + '
  • 1
  • 2
' + ); + }); + + it('list with dummy item', () => { + runTest( + '
  1. 1
    1. a
  2. b
  3. 2
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: '1', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'a', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'OL', format: {}, dataset: {} }, + { listType: 'OL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'b', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { + listType: 'OL', + format: { displayForDummyItem: 'block' }, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: '2', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + '1\r\na\r\nb\r\n2', + '
  1. 1
    1. a
  2. b
  3. 2
' + ); + }); + + it('div with whiteSpace, pre and blockquote', () => { + runTest( + '
aa\nbb
cc\ndd
ee
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'aa\nbb', + format: {}, + }, + ], + format: { + whiteSpace: 'pre', + }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'FormatContainer', + tagName: 'pre', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'cc\ndd', + format: { fontFamily: 'monospace' }, + }, + ], + format: { whiteSpace: 'pre' }, + isImplicit: true, + segmentFormat: { fontFamily: 'monospace' }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + whiteSpace: 'pre', + }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'FormatContainer', + tagName: 'blockquote', + format: { + marginRight: '40px', + marginLeft: '40px', + marginTop: '1em', + marginBottom: '1em', + }, + blocks: [ + { + blockType: 'Paragraph', + isImplicit: true, + segments: [ + { + segmentType: 'Text', + text: 'ee', + format: {}, + }, + ], + format: {}, + }, + ], + }, + ], + }, + 'aa\nbb\r\ncc\ndd\r\nee', + '
aa\nbb
cc\ndd
ee
' + ); + }); + + it('Two PRE tags', () => { + runTest( + '
test1
test2
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'FormatContainer', + tagName: 'pre', + format: { + marginTop: '1em', + marginBottom: '1em', + whiteSpace: 'pre', + }, + blocks: [ + { + blockType: 'Paragraph', + format: { whiteSpace: 'pre' }, + segments: [ + { + segmentType: 'Text', + text: 'test1', + format: { fontFamily: 'monospace' }, + }, + ], + isImplicit: true, + segmentFormat: { fontFamily: 'monospace' }, + }, + ], + }, + { + blockType: 'BlockGroup', + blockGroupType: 'FormatContainer', + tagName: 'pre', + format: { + marginTop: '1em', + marginBottom: '1em', + whiteSpace: 'pre', + }, + blocks: [ + { + blockType: 'Paragraph', + format: { whiteSpace: 'pre' }, + segments: [ + { + segmentType: 'Text', + text: 'test2', + format: { fontFamily: 'monospace' }, + }, + ], + isImplicit: true, + segmentFormat: { fontFamily: 'monospace' }, + }, + ], + }, + ], + }, + 'test1\r\ntest2', + '
test1
test2
' + ); + }); + + it('Block under styled inline', () => { + runTest( + 'aa
bb
cc
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'aa', + format: { + fontWeight: 'bold', + backgroundColor: 'red', + }, + }, + ], + format: {}, + isImplicit: true, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'bb', + format: { + fontWeight: 'bold', + }, + }, + ], + format: {}, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'cc', + format: { + fontWeight: 'bold', + backgroundColor: 'red', + }, + }, + ], + format: {}, + isImplicit: true, + }, + ], + }, + 'aa\r\nbb\r\ncc', + 'aa
bb
cc' + ); + }); + + it('Table under styled inline', () => { + runTest( + 'aa
bb
cc
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'aa', + format: { + fontWeight: 'bold', + backgroundColor: 'red', + }, + }, + ], + format: {}, + isImplicit: true, + }, + { + blockType: 'Table', + rows: [ + { + format: {}, + height: 0, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'bb', + format: { + fontWeight: 'bold', + }, + }, + ], + format: {}, + isImplicit: true, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: {}, + widths: [], + dataset: {}, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'cc', + format: { + fontWeight: 'bold', + backgroundColor: 'red', + }, + }, + ], + format: {}, + isImplicit: true, + }, + ], + }, + 'aa\r\nbb\r\ncc', + 'aa
bb
cc' + ); + }); + + it('Table under styled block', () => { + runTest( + 'aa
bb
cc
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'FormatContainer', + tagName: 'div', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'aa', + format: { + fontWeight: 'bold', + }, + }, + ], + format: {}, + isImplicit: true, + }, + { + blockType: 'Table', + rows: [ + { + format: {}, + height: 0, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'bb', + format: { + fontWeight: 'bold', + }, + }, + ], + format: {}, + isImplicit: true, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: {}, + widths: [], + dataset: {}, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'cc', + format: { + fontWeight: 'bold', + }, + }, + ], + format: {}, + isImplicit: true, + }, + ], + format: { + backgroundColor: 'red', + display: 'block', + }, + }, + ], + }, + 'aa\r\nbb\r\ncc', + '
aa
bb
cc
' + ); + }); + + it('Blockquote with margins', () => { + runTest( + '
aa
aa
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'FormatContainer', + tagName: 'blockquote', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'aa', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + format: { + marginTop: '20px', + marginRight: '20px', + marginBottom: '20px', + marginLeft: '20px', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'aa', + format: {}, + }, + ], + format: { + marginTop: '0px', + marginRight: '20px', + marginBottom: '0px', + marginLeft: '20px', + }, + }, + ], + }, + 'aa\r\naa', + '
aa
aa
' + ); + }); + + it('margin on paragraph', () => { + runTest( + 'aa', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'aa', + format: { + fontWeight: 'bold', + }, + }, + ], + format: { + marginTop: '20px', + marginRight: '20px', + marginBottom: '20px', + marginLeft: '20px', + display: 'block', + } as ContentModelBlockFormat, + isImplicit: false, + }, + ], + }, + 'aa', + '
aa
' + ); + }); + + it('Multiple P tag', () => { + runTest( + '

aaa

bbb

', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'aaa', + format: {}, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'bbb', + format: {}, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + ], + }, + 'aaa\r\nbbb', + '

aaa

bbb

' + ); + }); + + it('P tags with margin', () => { + runTest( + '

aaa

bbb

', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'aaa', + format: {}, + }, + ], + format: { + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '0px', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'bbb', + format: {}, + }, + ], + format: { + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '0px', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + ], + }, + 'aaa\r\nbbb', + '

aaa

bbb

' + ); + }); + + it('Headers', () => { + runTest( + '

aa

bb

cc

', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'aa', + format: {}, + }, + ], + format: {}, + decorator: { + tagName: 'h1', + format: { + fontSize: '2em', + fontWeight: 'bold', + }, + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'bb', + format: {}, + }, + ], + format: {}, + decorator: { + tagName: 'h2', + format: { + fontSize: '1.5em', + fontWeight: 'bold', + }, + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'cc', + format: {}, + }, + ], + format: { + marginTop: '50px', + marginRight: '50px', + marginBottom: '50px', + marginLeft: '50px', + }, + decorator: { + tagName: 'h3', + format: { + fontSize: '1.17em', + fontWeight: 'bold', + }, + }, + }, + ], + }, + 'aa\r\nbb\r\ncc', + '

aa

bb

cc

' + ); + }); + + it('Header with format from context', () => { + runTest( + '

test

', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + ], + decorator: { + tagName: 'h1', + format: { fontSize: '40px', fontWeight: 'bold' }, + }, + }, + ], + }, + 'test', + '

test

' + ); + }); + + it('PREs', () => { + runTest( + '
aaa\nbbb
aaa\nbb
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'FormatContainer', + tagName: 'pre', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'aaa\nbbb', + format: { + fontFamily: 'monospace', + }, + }, + ], + format: { + whiteSpace: 'pre', + }, + isImplicit: true, + segmentFormat: { fontFamily: 'monospace' }, + }, + ], + format: { + whiteSpace: 'pre', + marginTop: '1em', + marginBottom: '1em', + }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'FormatContainer', + tagName: 'pre', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'aaa\nbb', + format: { + fontFamily: 'monospace', + fontSize: '20px', + }, + }, + ], + format: { + whiteSpace: 'pre', + }, + isImplicit: true, + segmentFormat: { fontFamily: 'monospace', fontSize: '20px' }, + }, + ], + format: { + whiteSpace: 'pre', + marginTop: '1em', + marginBottom: '1em', + }, + }, + ], + }, + 'aaa\nbbb\r\naaa\nbb', + '
aaa\nbbb
aaa\nbb
' + ); + }); + + it('Code and Link', () => { + runTest( + '
aaabbbccc
aaabbbccc
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'aaa', + format: {}, + }, + { + segmentType: 'Text', + text: 'bbb', + format: {}, + code: { + format: { + fontFamily: 'monospace', + }, + }, + }, + { + segmentType: 'Text', + text: 'ccc', + format: {}, + }, + ], + format: {}, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'aaa', + format: {}, + }, + { + segmentType: 'Text', + text: 'bbb', + format: {}, + link: { + format: { + underline: true, + href: '#', + }, + dataset: {}, + }, + }, + { + segmentType: 'Text', + text: 'ccc', + format: {}, + }, + ], + format: {}, + }, + ], + }, + 'aaabbbccc\r\naaabbbccc', + '
aaabbbccc
aaabbbccc
' + ); + }); + + it('BlockQuotes', () => { + runTest( + '
aaaa
bbbbbb
cccc
aaaa
bbbbbb
cccc
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'aaaa', + format: { + textColor: 'red', + }, + }, + ], + format: {}, + segmentFormat: { textColor: 'red' }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'FormatContainer', + tagName: 'blockquote', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'bbbbbb', + format: { + fontFamily: 'Calibri, Arial, Helvetica, sans-serif', + fontSize: '12pt', + textColor: 'rgb(102, 102, 102)', + }, + }, + ], + format: {}, + segmentFormat: { + fontFamily: 'Calibri, Arial, Helvetica, sans-serif', + fontSize: '12pt', + textColor: 'rgb(102, 102, 102)', + }, + }, + ], + format: { + borderLeft: '3px solid rgb(200, 200, 200)', + marginTop: '1em', + marginRight: '40px', + marginBottom: '1em', + marginLeft: '40px', + paddingLeft: '10px', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'cccc', + format: { + textColor: 'red', + }, + }, + ], + format: {}, + segmentFormat: { textColor: 'red' }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'aaaa', + format: { + textColor: 'red', + }, + }, + ], + format: {}, + segmentFormat: { textColor: 'red' }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'bbbbbb', + format: { + fontFamily: 'Calibri, Arial, Helvetica, sans-serif', + fontSize: '12pt', + textColor: 'rgb(102, 102, 102)', + }, + }, + ], + format: { + marginRight: '40px', + marginLeft: '40px', + }, + segmentFormat: { + fontFamily: 'Calibri, Arial, Helvetica, sans-serif', + fontSize: '12pt', + textColor: 'rgb(102, 102, 102)', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'cccc', + format: { + textColor: 'red', + }, + }, + ], + format: {}, + segmentFormat: { + textColor: 'red', + }, + }, + ], + }, + 'aaaa\r\nbbbbbb\r\ncccc\r\naaaa\r\nbbbbbb\r\ncccc', + '
aaaa
bbbbbb
cccc
aaaa
bbbbbb
cccc
' + ); + }); + + it('margin left and format container', () => { + runTest( + '
aaa
bbb
ccc
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'aaa', + format: {}, + }, + ], + format: { + marginLeft: '40px', + }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'FormatContainer', + tagName: 'pre', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'bbb', + format: { + fontFamily: 'monospace', + }, + }, + ], + format: { + whiteSpace: 'pre', + }, + segmentFormat: { + fontFamily: 'monospace', + }, + }, + ], + format: { + whiteSpace: 'pre', + marginTop: '1em', + marginBottom: '1em', + marginLeft: '40px', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'ccc', + format: {}, + }, + ], + format: { + marginLeft: '40px', + }, + isImplicit: true, + }, + ], + }, + 'aaa\r\nbbb\r\nccc', + '
aaa
bbb
ccc
' + ); + }); + + it('nested margin left', () => { + runTest( + '
aaa
bbb
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'aaa', + format: {}, + }, + ], + format: { + marginLeft: '40px', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'bbb', + format: {}, + }, + ], + format: { + marginLeft: '90px', + marginTop: '0px', + marginRight: '50px', + marginBottom: '0px', + }, + }, + ], + }, + 'aaa\r\nbbb', + '
aaa
bbb
' + ); + }); + + it('text after format container', () => { + runTest( + '
test1
test2', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test1', + format: {}, + }, + ], + format: { + htmlAlign: 'center', + backgroundColor: 'red', + }, + isImplicit: false, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test2', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + }, + 'test1\r\ntest2', + '
test1
test2', + '
test1
test2' + ); + }); + + it('Center', () => { + const cloneNodeSpy = jasmine + .createSpy('cloneNode') + .and.returnValue(document.createElement('center')); + const mockedElement = { + name: 'ELEMENT', + cloneNode: cloneNodeSpy, + } as any; + const mockedGeneral: ContentModelGeneralBlock = { + blockType: 'BlockGroup', + blockGroupType: 'General', + element: mockedElement, + blocks: [], + format: {}, + }; + + const createGeneralBlockSpy = spyOn( + createGeneralBlock, + 'createGeneralBlock' + ).and.returnValue(mockedGeneral); + + runTest( + '
test1
test2
test3
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'General', + format: {}, + blocks: [ + { + blockType: 'Paragraph', + format: {}, + isImplicit: true, + segments: [ + { + segmentType: 'Text', + format: {}, + text: 'test1', + }, + ], + }, + { + blockType: 'Table', + format: {}, + rows: [ + { + format: {}, + height: 0, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + isImplicit: true, + segments: [ + { + segmentType: 'Text', + format: {}, + text: 'test2', + }, + ], + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + widths: [], + dataset: {}, + }, + { + blockType: 'Paragraph', + format: { htmlAlign: 'end' }, + isImplicit: false, + segments: [ + { + segmentType: 'Text', + format: {}, + text: 'test3', + }, + ], + }, + ], + element: mockedElement, + }, + ], + }, + 'test1\r\ntest2\r\ntest3', + '
test1
test2
test3
' + ); + + expect(createGeneralBlockSpy).toHaveBeenCalledTimes(1); + expect(cloneNodeSpy).toHaveBeenCalledTimes(1); + }); + + it('html align overwrite text align from context', () => { + runTest( + '
aaa
bbb
ccc
ddd
eee
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: { textAlign: 'center' }, + segments: [ + { + segmentType: 'Text', + format: {}, + text: 'aaa', + }, + ], + }, + { + blockType: 'BlockGroup', + blockGroupType: 'FormatContainer', + tagName: 'div', + format: { + htmlAlign: 'end', + }, + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + format: {}, + text: 'bbb', + }, + ], + isImplicit: true, + }, + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + format: {}, + text: 'ccc', + }, + ], + }, + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + format: {}, + text: 'ddd', + }, + ], + isImplicit: true, + }, + ], + }, + { + blockType: 'Paragraph', + format: { + textAlign: 'center', + }, + segments: [ + { + segmentType: 'Text', + format: {}, + text: 'eee', + }, + ], + isImplicit: true, + }, + ], + }, + 'aaa\r\nbbb\r\nccc\r\nddd\r\neee', + '
aaa
bbb
ccc
ddd
eee
' + ); + }); + + it('SUB needs to be put inside S or U if any', () => { + runTest( + '
test
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: { + strikethrough: true, + underline: true, + superOrSubScriptSequence: 'sub', + }, + }, + ], + }, + ], + }, + 'test', + '
test
' + ); + }); + + it('Table with margin under "align=center"', () => { + runTest( + '
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'FormatContainer', + format: { htmlAlign: 'center' }, + tagName: 'div', + blocks: [ + { + blockType: 'Table', + format: { + marginBottom: '0px', + marginLeft: '0px', + marginRight: '0px', + marginTop: '0px', + }, + widths: [], + dataset: {}, + rows: [ + { + format: {}, + height: 0, + cells: [ + { + blockGroupType: 'TableCell', + format: {}, + spanAbove: false, + spanLeft: false, + isHeader: false, + dataset: {}, + blocks: [], + }, + ], + }, + ], + }, + ], + }, + ], + }, + '', + '
' + ); + }); + + it('A with display:block', () => { + runTest( + 'test', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + isImplicit: true, + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: { textColor: 'red' }, + link: { + format: { + underline: true, + href: '#', + textColor: 'red', + display: 'block', + }, + dataset: {}, + }, + }, + ], + segmentFormat: { textColor: 'red' }, + }, + ], + }, + 'test', + 'test' + ); + }); + + it('Segment format on block', () => { + runTest( + '
test
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: { + strikethrough: true, + underline: true, + italic: true, + fontWeight: 'bold', + }, + }, + ], + format: {}, + segmentFormat: { + strikethrough: true, + underline: true, + italic: true, + fontWeight: 'bold', + }, + }, + ], + }, + 'test', + '
test
' + ); + }); + + it('Segment format on block from parent', () => { + runTest( + 'aaabbb
ccc
ddd
eeee', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'aaa', format: {} }, + { segmentType: 'Text', text: 'bbb', format: { fontWeight: 'bold' } }, + ], + format: {}, + isImplicit: true, + }, + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'ccc', format: { fontWeight: 'bold' } }, + ], + format: {}, + }, + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'ddd', format: { fontWeight: 'bold' } }, + { segmentType: 'Text', text: 'eeee', format: {} }, + ], + format: {}, + isImplicit: true, + }, + ], + }, + 'aaabbb\r\nccc\r\ndddeeee', + 'aaabbb
ccc
dddeeee' + ); + }); + + it('Link inside superscript', () => { + runTest( + '', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'www.bing.com', + format: { superOrSubScriptSequence: 'super' }, + link: { + format: { underline: true, href: 'http://www.bing.com' }, + dataset: {}, + }, + }, + ], + format: {}, + }, + ], + }, + 'www.bing.com', + '' + ); + }); + + it('Multiple P tag with margin-left', () => { + runTest( + '

aaa

bbb

', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: { + marginLeft: '40px', + marginTop: '1em', + marginBottom: '1em', + }, + segments: [ + { + segmentType: 'Text', + format: {}, + text: 'aaa', + }, + ], + decorator: { + format: {}, + tagName: 'p', + }, + }, + { + blockType: 'Paragraph', + format: { + marginLeft: '40px', + marginTop: '1em', + marginBottom: '1em', + }, + segments: [ + { + segmentType: 'Text', + format: {}, + text: 'bbb', + }, + ], + decorator: { + format: {}, + tagName: 'p', + }, + }, + ], + }, + 'aaa\r\nbbb', + '

aaa

bbb

' + ); + }); + + it('SPAN inside link with color', () => { + runTest( + 'beforetestafter', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'before', + format: {}, + link: { + format: { underline: true, href: '#' }, + dataset: {}, + }, + }, + { + segmentType: 'Text', + text: 'test', + format: { textColor: 'red' }, + link: { + format: { underline: true, href: '#', textColor: 'red' }, + dataset: {}, + }, + }, + { + segmentType: 'Text', + text: 'after', + format: {}, + link: { + format: { underline: true, href: '#' }, + dataset: {}, + }, + }, + ], + format: {}, + isImplicit: true, + }, + ], + }, + 'beforetestafter', + 'beforetestafter' + ); + }); + + it('text-indent', () => { + runTest( + '
aa
bb
cc
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: { textIndent: '20px' }, + segments: [{ segmentType: 'Text', format: {}, text: 'aa' }], + }, + { + blockType: 'Paragraph', + format: { textIndent: '20px' }, + segments: [{ segmentType: 'Text', format: {}, text: 'bb' }], + }, + { + blockType: 'Paragraph', + format: {}, + segments: [{ segmentType: 'Text', format: {}, text: 'cc' }], + isImplicit: true, + }, + ], + }, + 'aa\r\nbb\r\ncc', + '
aa
bb
cc' + ); + }); + + it('text-indent with inner block', () => { + runTest( + '
aa
bb
cc
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: { textIndent: '20px' }, + segments: [{ segmentType: 'Text', format: {}, text: 'aa' }], + }, + { + blockType: 'Paragraph', + format: {}, + segments: [{ segmentType: 'Text', format: {}, text: 'bb' }], + isImplicit: true, + }, + { + blockType: 'Paragraph', + format: { textIndent: '40px' }, + segments: [{ segmentType: 'Text', format: {}, text: 'cc' }], + }, + ], + }, + 'aa\r\nbb\r\ncc', + '
aa
bb
cc
' + ); + }); + + it('Table with COLGROUP', () => { + runTest( + '
abc
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + rows: [ + { + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'a', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + dataset: {}, + format: {}, + spanAbove: false, + spanLeft: false, + isHeader: false, + }, + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'b', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + dataset: {}, + format: {}, + spanAbove: false, + spanLeft: false, + isHeader: false, + }, + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'c', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + dataset: {}, + format: {}, + spanAbove: false, + spanLeft: false, + isHeader: false, + }, + ], + format: {}, + height: 0, + }, + ], + dataset: {}, + format: {}, + widths: [100, 150, 120], + }, + ], + }, + 'a\r\nb\r\nc', + '
abc
' + ); + }); + + it('Table with COLGROUP with span', () => { + runTest( + '
abc
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + rows: [ + { + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'a', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + dataset: {}, + format: {}, + spanAbove: false, + spanLeft: false, + isHeader: false, + }, + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'b', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + dataset: {}, + format: {}, + spanAbove: false, + spanLeft: false, + isHeader: false, + }, + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'c', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + dataset: {}, + format: {}, + spanAbove: false, + spanLeft: false, + isHeader: false, + }, + ], + format: {}, + height: 0, + }, + ], + dataset: {}, + format: {}, + widths: [150, 150, 120], + }, + ], + }, + 'a\r\nb\r\nc', + '
abc
' + ); + }); + + it('list with list style', () => { + runTest( + '
    1. test
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + ], + isImplicit: true, + format: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [ + { + listType: 'OL', + format: {}, + dataset: {}, + }, + { + listType: 'OL', + format: { listStyleType: '"1) "' }, + dataset: {}, + }, + ], + format: {}, + }, + ], + }, + 'test', + '
    1. test
' + ); + }); + + it('link with color', () => { + runTest( + '', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'www.bing.com', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(245, 212, 39)', + }, + link: { + format: { + underline: true, + href: 'http://www.bing.com', + textColor: 'rgb(245, 212, 39)', + }, + dataset: {}, + }, + }, + ], + format: {}, + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(245, 212, 39)', + }, + }, + ], + }, + 'www.bing.com', + '' + ); + }); +}); diff --git a/packages/roosterjs-editor-dom/lib/htmlSanitizer/getInheritableStyles.ts b/packages/roosterjs-editor-dom/lib/htmlSanitizer/getInheritableStyles.ts index 68170ae0909..b7e4bbfed20 100644 --- a/packages/roosterjs-editor-dom/lib/htmlSanitizer/getInheritableStyles.ts +++ b/packages/roosterjs-editor-dom/lib/htmlSanitizer/getInheritableStyles.ts @@ -1,16 +1,16 @@ -import { INHERITABLE_PROPERTIES } from 'roosterjs-editor-types'; -import type { StringMap } from 'roosterjs-editor-types'; - -/** - * Get inheritable CSS style values from the given element - * @param element The element to get style from - */ -export default function getInheritableStyles(element: HTMLElement | null): StringMap { - const win = element && element.ownerDocument && element.ownerDocument.defaultView; - const styles = win && element && win.getComputedStyle(element); - const result: StringMap = {}; - INHERITABLE_PROPERTIES.forEach( - name => (result[name] = (styles && styles.getPropertyValue(name)) || '') - ); - return result; -} +import { INHERITABLE_PROPERTIES } from 'roosterjs-editor-types'; +import type { StringMap } from 'roosterjs-editor-types'; + +/** + * Get inheritable CSS style values from the given element + * @param element The element to get style from + */ +export default function getInheritableStyles(element: HTMLElement | null): StringMap { + const win = element && element.ownerDocument && element.ownerDocument.defaultView; + const styles = win && element && win.getComputedStyle(element); + const result: StringMap = {}; + INHERITABLE_PROPERTIES.forEach( + name => (result[name] = (styles && styles.getPropertyValue(name)) || '') + ); + return result; +} diff --git a/packages/roosterjs-editor-types/lib/Constants.ts b/packages/roosterjs-editor-types/lib/Constants.ts index 7dbb41a8e87..f5c6cc9442b 100644 --- a/packages/roosterjs-editor-types/lib/Constants.ts +++ b/packages/roosterjs-editor-types/lib/Constants.ts @@ -1,11 +1,11 @@ -/** - * Inheritable CSS properties - * Ref: https://www.w3.org/TR/CSS21/propidx.html - */ -export const INHERITABLE_PROPERTIES = ( - 'border-spacing,caption-side,color,' + - 'cursor,direction,empty-cells,font-family,font-size,font-style,font-variant,font-weight,' + - 'font,letter-spacing,line-height,list-style-image,list-style-position,list-style-type,' + - 'list-style,orphans,quotes,text-align,text-indent,text-transform,visibility,white-space,' + - 'widows,word-spacing' -).split(','); +/** + * Inheritable CSS properties + * Ref: https://www.w3.org/TR/CSS21/propidx.html + */ +export const INHERITABLE_PROPERTIES = ( + 'border-spacing,caption-side,color,' + + 'cursor,direction,empty-cells,font-family,font-size,font-style,font-variant,font-weight,' + + 'font,letter-spacing,line-height,list-style-image,list-style-position,list-style-type,' + + 'list-style,orphans,quotes,text-align,text-indent,text-transform,visibility,white-space,' + + 'widows,word-spacing' +).split(','); diff --git a/packages/roosterjs-editor-types/lib/type/index.ts b/packages/roosterjs-editor-types/lib/type/index.ts index e1a7f1bd245..c632d66f86b 100644 --- a/packages/roosterjs-editor-types/lib/type/index.ts +++ b/packages/roosterjs-editor-types/lib/type/index.ts @@ -1,27 +1,27 @@ -export { - AttributeCallback, - AttributeCallbackMap, - CssStyleCallback, - CssStyleCallbackMap, - ElementCallback, - StringMap, - ElementCallbackMap, - PredefinedCssMap, -} from './htmlSanitizerCallbackTypes'; -export { DOMEventHandlerFunction, DOMEventHandlerObject, DOMEventHandler } from './domEventHandler'; -export { TrustedHTMLHandler } from './TrustedHTMLHandler'; -export { SizeTransformer } from './SizeTransformer'; -export { - ArrayItemType, - DefinitionBase, - StringDefinition, - NumberDefinition, - BooleanDefinition, - ArrayDefinition, - ObjectDefinition, - ObjectPropertyDefinition, - CustomizeDefinition, - Definition, -} from './Definition'; -export { CoreCreator } from './CoreCreator'; -export { INHERITABLE_PROPERTIES } from '../Constants'; +export { + AttributeCallback, + AttributeCallbackMap, + CssStyleCallback, + CssStyleCallbackMap, + ElementCallback, + StringMap, + ElementCallbackMap, + PredefinedCssMap, +} from './htmlSanitizerCallbackTypes'; +export { DOMEventHandlerFunction, DOMEventHandlerObject, DOMEventHandler } from './domEventHandler'; +export { TrustedHTMLHandler } from './TrustedHTMLHandler'; +export { SizeTransformer } from './SizeTransformer'; +export { + ArrayItemType, + DefinitionBase, + StringDefinition, + NumberDefinition, + BooleanDefinition, + ArrayDefinition, + ObjectDefinition, + ObjectPropertyDefinition, + CustomizeDefinition, + Definition, +} from './Definition'; +export { CoreCreator } from './CoreCreator'; +export { INHERITABLE_PROPERTIES } from '../Constants';