From ce4298ed629df213283f22c12bfdf164005e4737 Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Tue, 3 Dec 2024 18:00:32 -0300 Subject: [PATCH 1/6] refactor trustedHTML --- .../createModelFromHtml.ts | 5 +-- .../lib/command/paste/paste.ts | 13 +++---- .../restoreSnapshotHTML.ts | 5 +-- .../lib/editor/Editor.ts | 16 +++++++- .../lib/editor/core/createEditorCore.ts | 15 +++---- .../lib/utils/domCreator.ts | 37 ++++++++++++++++++ .../restoreSnapshotHTMLTest.ts | 16 +++++--- .../test/editor/core/createEditorCoreTest.ts | 12 +++--- .../test/utils/domCreatorTest.ts | 39 +++++++++++++++++++ .../Excel/processPastedContentFromExcel.ts | 10 ++--- .../lib/paste/PastePlugin.ts | 6 +-- .../processPastedContentFromPowerPoint.ts | 9 ++--- .../lib/paste/WordDesktop/getStyleMetadata.ts | 9 ++--- .../processPastedContentFromWordDesktop.ts | 8 ++-- .../test/paste/ContentModelPastePluginTest.ts | 16 +++++--- .../test/paste/getStyleMetadataTest.ts | 6 ++- .../processPastedContentFromExcelTest.ts | 9 +++-- .../processPastedContentFromPowerPointTest.ts | 10 ++--- ...processPastedContentFromWordDesktopTest.ts | 6 ++- .../lib/editor/EditorCore.ts | 17 ++++++-- .../lib/editor/IEditor.ts | 13 ++++++- .../lib/index.ts | 6 ++- .../lib/parameter/TrustedHTMLHandler.ts | 14 ++++++- .../lib/editor/EditorAdapter.ts | 6 +-- 24 files changed, 209 insertions(+), 94 deletions(-) create mode 100644 packages/roosterjs-content-model-core/lib/utils/domCreator.ts create mode 100644 packages/roosterjs-content-model-core/test/utils/domCreatorTest.ts diff --git a/packages/roosterjs-content-model-core/lib/command/createModelFromHtml/createModelFromHtml.ts b/packages/roosterjs-content-model-core/lib/command/createModelFromHtml/createModelFromHtml.ts index 68b34cbc3a9..1505427ae3c 100644 --- a/packages/roosterjs-content-model-core/lib/command/createModelFromHtml/createModelFromHtml.ts +++ b/packages/roosterjs-content-model-core/lib/command/createModelFromHtml/createModelFromHtml.ts @@ -1,6 +1,7 @@ import { convertInlineCss, retrieveCssRules } from './convertInlineCss'; import { createDomToModelContextForSanitizing } from './createDomToModelContextForSanitizing'; import { createEmptyModel, domToContentModel, parseFormat } from 'roosterjs-content-model-dom'; +import { domCreator } from '../../utils/domCreator'; import type { ContentModelDocument, ContentModelSegmentFormat, @@ -21,9 +22,7 @@ export function createModelFromHtml( trustedHTMLHandler?: TrustedHTMLHandler, defaultSegmentFormat?: ContentModelSegmentFormat ): ContentModelDocument { - const doc = html - ? new DOMParser().parseFromString(trustedHTMLHandler?.(html) ?? html, 'text/html') - : null; + const doc = html ? domCreator(trustedHTMLHandler).htmlToDOM(html) : null; if (doc?.body) { const context = createDomToModelContextForSanitizing( diff --git a/packages/roosterjs-content-model-core/lib/command/paste/paste.ts b/packages/roosterjs-content-model-core/lib/command/paste/paste.ts index 69cd08340d5..1028bc20765 100644 --- a/packages/roosterjs-content-model-core/lib/command/paste/paste.ts +++ b/packages/roosterjs-content-model-core/lib/command/paste/paste.ts @@ -6,8 +6,8 @@ import { retrieveHtmlInfo } from './retrieveHtmlInfo'; import type { PasteTypeOrGetter, ClipboardData, - TrustedHTMLHandler, IEditor, + DOMCreator, } from 'roosterjs-content-model-types'; /** @@ -22,9 +22,6 @@ export function paste( pasteTypeOrGetter: PasteTypeOrGetter = 'normal' ) { editor.focus(); - - const trustedHTMLHandler = editor.getTrustedHTMLHandler(); - if (!clipboardData.modelBeforePaste) { editor.formatContentModel(model => { clipboardData.modelBeforePaste = cloneModelForPaste(model); @@ -34,7 +31,7 @@ export function paste( } // 1. Prepare variables - const doc = createDOMFromHtml(clipboardData.rawHtml, trustedHTMLHandler); + const doc = createDOMFromHtml(clipboardData.rawHtml, editor.getDOMCreator()); const pasteType = typeof pasteTypeOrGetter == 'function' ? pasteTypeOrGetter(doc, clipboardData) @@ -50,7 +47,7 @@ export function paste( pasteType, (clipboardData.rawHtml == clipboardData.html ? doc - : createDOMFromHtml(clipboardData.html, trustedHTMLHandler) + : createDOMFromHtml(clipboardData.html, editor.getDOMCreator()) )?.body ); @@ -72,7 +69,7 @@ export function paste( function createDOMFromHtml( html: string | null | undefined, - trustedHTMLHandler: TrustedHTMLHandler + domCreator: DOMCreator ): Document | null { - return html ? new DOMParser().parseFromString(trustedHTMLHandler(html), 'text/html') : null; + return html ? domCreator.htmlToDOM(html) : null; } diff --git a/packages/roosterjs-content-model-core/lib/coreApi/restoreUndoSnapshot/restoreSnapshotHTML.ts b/packages/roosterjs-content-model-core/lib/coreApi/restoreUndoSnapshot/restoreSnapshotHTML.ts index 96556c0e296..b8f8a76b34d 100644 --- a/packages/roosterjs-content-model-core/lib/coreApi/restoreUndoSnapshot/restoreSnapshotHTML.ts +++ b/packages/roosterjs-content-model-core/lib/coreApi/restoreUndoSnapshot/restoreSnapshotHTML.ts @@ -18,10 +18,7 @@ export function restoreSnapshotHTML(core: EditorCore, snapshot: Snapshot) { } = core; let refNode: Node | null = physicalRoot.firstChild; - const body = new DOMParser().parseFromString( - core.trustedHTMLHandler?.(snapshot.html) ?? snapshot.html, - 'text/html' - ).body; + const body = core.domCreator.htmlToDOM(snapshot.html).body; for (let currentNode = body.firstChild; currentNode; ) { const next = currentNode.nextSibling; diff --git a/packages/roosterjs-content-model-core/lib/editor/Editor.ts b/packages/roosterjs-content-model-core/lib/editor/Editor.ts index 6977040e9fa..84d4d653e8e 100644 --- a/packages/roosterjs-content-model-core/lib/editor/Editor.ts +++ b/packages/roosterjs-content-model-core/lib/editor/Editor.ts @@ -25,13 +25,14 @@ import type { SnapshotsManager, EditorCore, EditorOptions, - TrustedHTMLHandler, Rect, EntityState, CachedElementHandler, DomToModelOptionForCreateModel, AnnounceData, ExperimentalFeature, + LegacyTrustedHTMLHandler, + DOMCreator, } from 'roosterjs-content-model-types'; /** @@ -359,15 +360,26 @@ export class Editor implements IEditor { } /** + * @deprecated * Get a function to convert HTML string to trusted HTML string. * By default it will just return the input HTML directly. To override this behavior, * pass your own trusted HTML handler to EditorOptions.trustedHTMLHandler * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types */ - getTrustedHTMLHandler(): TrustedHTMLHandler { + getTrustedHTMLHandler(): LegacyTrustedHTMLHandler { return this.getCore().trustedHTMLHandler; } + /** + * Get a function to convert HTML string to a trust Document. + * By default it will just convert the original HTML string into a Document object directly. + * To override, pass your own trusted HTML handler to EditorOptions.trustedHTMLHandler + * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types + */ + getDOMCreator(): DOMCreator { + return this.getCore().domCreator; + } + /** * Get the scroll container of the editor */ diff --git a/packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts b/packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts index 886b03dc7ee..daabda5152d 100644 --- a/packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts +++ b/packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts @@ -3,6 +3,7 @@ import { createDarkColorHandler } from './DarkColorHandlerImpl'; import { createDOMHelper } from './DOMHelperImpl'; import { createDomToModelSettings, createModelToDomSettings } from './createEditorDefaultSettings'; import { createEditorCorePlugins } from '../../corePlugin/createEditorCorePlugins'; +import { defaultTrustHtmlHandler, domCreator, isDOMCreator } from '../../utils/domCreator'; import type { EditorEnvironment, PluginState, @@ -18,7 +19,6 @@ import type { */ export function createEditorCore(contentDiv: HTMLDivElement, options: EditorOptions): EditorCore { const corePlugins = createEditorCorePlugins(options, contentDiv); - return { physicalRoot: contentDiv, logicalRoot: contentDiv, @@ -43,7 +43,11 @@ export function createEditorCore(contentDiv: HTMLDivElement, options: EditorOpti options.knownColors, options.generateColorKey ), - trustedHTMLHandler: options.trustedHTMLHandler || defaultTrustHtmlHandler, + trustedHTMLHandler: + options.trustedHTMLHandler && !isDOMCreator(options.trustedHTMLHandler) + ? options.trustedHTMLHandler + : defaultTrustHtmlHandler, + domCreator: domCreator(options.trustedHTMLHandler), domHelper: createDOMHelper(contentDiv), ...getPluginState(corePlugins), disposeErrorHandler: options.disposeErrorHandler, @@ -90,13 +94,6 @@ function getIsMobileOrTablet(userAgent: string) { return false; } -/** - * @internal export for test only - */ -export function defaultTrustHtmlHandler(html: string) { - return html; -} - function getPluginState(corePlugins: EditorCorePlugins): PluginState { return { domEvent: corePlugins.domEvent.getState(), diff --git a/packages/roosterjs-content-model-core/lib/utils/domCreator.ts b/packages/roosterjs-content-model-core/lib/utils/domCreator.ts new file mode 100644 index 00000000000..a45f509e027 --- /dev/null +++ b/packages/roosterjs-content-model-core/lib/utils/domCreator.ts @@ -0,0 +1,37 @@ +import type { + DOMCreator, + LegacyTrustedHTMLHandler, + TrustedHTMLHandler, +} from 'roosterjs-content-model-types'; + +/** + * @internal + */ +export function domCreator(trustedHTMLHandler?: TrustedHTMLHandler): DOMCreator { + return trustedHTMLHandler && isDOMCreator(trustedHTMLHandler) + ? trustedHTMLHandler + : trustedHTMLHandlerToDOMCreator(trustedHTMLHandler as LegacyTrustedHTMLHandler); +} + +/** + * @internal + */ +export function isDOMCreator( + trustedHTMLHandler: TrustedHTMLHandler +): trustedHTMLHandler is DOMCreator { + return typeof (trustedHTMLHandler as DOMCreator).htmlToDOM === 'function'; +} + +/** + * @internal + */ +export const defaultTrustHtmlHandler: LegacyTrustedHTMLHandler = (html: string) => { + return html; +}; + +function trustedHTMLHandlerToDOMCreator(trustedHTMLHandler?: LegacyTrustedHTMLHandler): DOMCreator { + const handler = trustedHTMLHandler || defaultTrustHtmlHandler; + return { + htmlToDOM: (html: string) => new DOMParser().parseFromString(handler(html), 'text/html'), + }; +} diff --git a/packages/roosterjs-content-model-core/test/coreApi/restoreUndoSnapshot/restoreSnapshotHTMLTest.ts b/packages/roosterjs-content-model-core/test/coreApi/restoreUndoSnapshot/restoreSnapshotHTMLTest.ts index a238d818a6b..6bccf1c0b66 100644 --- a/packages/roosterjs-content-model-core/test/coreApi/restoreUndoSnapshot/restoreSnapshotHTMLTest.ts +++ b/packages/roosterjs-content-model-core/test/coreApi/restoreUndoSnapshot/restoreSnapshotHTMLTest.ts @@ -1,7 +1,11 @@ -import { EditorCore, Snapshot } from 'roosterjs-content-model-types'; +import { DOMCreator, EditorCore, Snapshot } from 'roosterjs-content-model-types'; import { restoreSnapshotHTML } from '../../../lib/coreApi/restoreUndoSnapshot/restoreSnapshotHTML'; import { wrap } from 'roosterjs-content-model-dom'; +const domCreator: DOMCreator = { + htmlToDOM: (html: string) => new DOMParser().parseFromString(html, 'text/html'), +}; + describe('restoreSnapshotHTML', () => { let core: EditorCore; let div: HTMLDivElement; @@ -15,6 +19,7 @@ describe('restoreSnapshotHTML', () => { entity: { entityMap: {}, }, + domCreator: domCreator, } as any; }); @@ -39,18 +44,17 @@ describe('restoreSnapshotHTML', () => { }); it('Simple HTML, no entity, with trustHTMLHandler', () => { - const trustedHTMLHandler = jasmine - .createSpy('trustedHTMLHandler') - .and.callFake((html: string) => html + html); const snapshot: Snapshot = { html: '
test1
', } as any; - (core).trustedHTMLHandler = trustedHTMLHandler; + const htmlToDOMSpy = spyOn(core.domCreator, 'htmlToDOM').and.callFake((html: string) => + new DOMParser().parseFromString(html + html, 'text/html') + ); restoreSnapshotHTML(core, snapshot); - expect(trustedHTMLHandler).toHaveBeenCalledWith('
test1
'); + expect(htmlToDOMSpy).toHaveBeenCalledWith('
test1
'); expect(div.innerHTML).toBe('
test1
test1
'); }); diff --git a/packages/roosterjs-content-model-core/test/editor/core/createEditorCoreTest.ts b/packages/roosterjs-content-model-core/test/editor/core/createEditorCoreTest.ts index 197c55c1306..3731f579c9f 100644 --- a/packages/roosterjs-content-model-core/test/editor/core/createEditorCoreTest.ts +++ b/packages/roosterjs-content-model-core/test/editor/core/createEditorCoreTest.ts @@ -1,14 +1,11 @@ import * as createDefaultSettings from '../../../lib/editor/core/createEditorDefaultSettings'; import * as createEditorCorePlugins from '../../../lib/corePlugin/createEditorCorePlugins'; import * as DarkColorHandlerImpl from '../../../lib/editor/core/DarkColorHandlerImpl'; +import * as domCreator from '../../../lib/utils/domCreator'; import * as DOMHelperImpl from '../../../lib/editor/core/DOMHelperImpl'; import { coreApiMap } from '../../../lib/coreApi/coreApiMap'; import { EditorCore, EditorOptions } from 'roosterjs-content-model-types'; -import { - createEditorCore, - defaultTrustHtmlHandler, - getDarkColorFallback, -} from '../../../lib/editor/core/createEditorCore'; +import { createEditorCore, getDarkColorFallback } from '../../../lib/editor/core/createEditorCore'; describe('createEditorCore', () => { function createMockedPlugin(stateName: string): any { @@ -41,6 +38,7 @@ describe('createEditorCore', () => { const mockedDomToModelSettings = 'DOMTOMODEL' as any; const mockedModelToDomSettings = 'MODELTODOM' as any; const mockedDOMHelper = 'DOMHELPER' as any; + const mockedHtmlToDOM = 'HTMLTODOM' as any; beforeEach(() => { spyOn(createEditorCorePlugins, 'createEditorCorePlugins').and.returnValue(mockedPlugins); @@ -54,6 +52,7 @@ describe('createEditorCore', () => { mockedModelToDomSettings ); spyOn(DOMHelperImpl, 'createDOMHelper').and.returnValue(mockedDOMHelper); + spyOn(domCreator, 'domCreator').and.returnValue(mockedHtmlToDOM); }); function runTest( @@ -88,7 +87,8 @@ describe('createEditorCore', () => { modelToDomSettings: mockedModelToDomSettings, }, darkColorHandler: mockedDarkColorHandler, - trustedHTMLHandler: defaultTrustHtmlHandler, + trustedHTMLHandler: domCreator.defaultTrustHtmlHandler, + domCreator: mockedHtmlToDOM, cache: 'cache' as any, format: 'format' as any, copyPaste: 'copyPaste' as any, diff --git a/packages/roosterjs-content-model-core/test/utils/domCreatorTest.ts b/packages/roosterjs-content-model-core/test/utils/domCreatorTest.ts new file mode 100644 index 00000000000..943a47f5afb --- /dev/null +++ b/packages/roosterjs-content-model-core/test/utils/domCreatorTest.ts @@ -0,0 +1,39 @@ +import { domCreator, isDOMCreator } from '../../lib/utils/domCreator'; + +describe('domCreator', () => { + it('isDOMCreator - True', () => { + const trustedHTMLHandler = { + htmlToDOM: (html: string) => new DOMParser().parseFromString(html, 'text/html'), + }; + expect(isDOMCreator(trustedHTMLHandler)).toBe(true); + }); + + it('isDOMCreator - False', () => { + const trustedHTMLHandler = (html: string) => html; + expect(isDOMCreator(trustedHTMLHandler)).toBe(false); + }); + + it('domCreator - isDOMCreator', () => { + const trustedHTMLHandler = { + htmlToDOM: (html: string) => new DOMParser().parseFromString(html, 'text/html'), + }; + const result = domCreator(trustedHTMLHandler); + expect(result).toEqual(trustedHTMLHandler); + }); + + it('domCreator - undefined', () => { + const doc = document.implementation.createHTMLDocument(); + doc.body.appendChild(document.createTextNode('test')); + doc.ownerDocument; + const result = domCreator(undefined).htmlToDOM('test'); + expect(result.lastChild).toEqual(doc.lastChild); + }); + + it('domCreator - trustedHTML', () => { + const doc = document.implementation.createHTMLDocument(); + doc.body.appendChild(document.createTextNode('test trusted')); + const trustedHTMLHandler = (html: string) => html + ' trusted'; + const result = domCreator(trustedHTMLHandler).htmlToDOM('test'); + expect(result.lastChild).toEqual(doc.lastChild); + }); +}); diff --git a/packages/roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel.ts b/packages/roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel.ts index f13491b0556..5bbc167fe76 100644 --- a/packages/roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel.ts +++ b/packages/roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel.ts @@ -1,11 +1,7 @@ import { addParser } from '../utils/addParser'; import { isNodeOfType, moveChildNodes } from 'roosterjs-content-model-dom'; import { setProcessor } from '../utils/setProcessor'; -import type { - BeforePasteEvent, - ElementProcessor, - TrustedHTMLHandler, -} from 'roosterjs-content-model-types'; +import type { BeforePasteEvent, DOMCreator, ElementProcessor } from 'roosterjs-content-model-types'; const LAST_TD_END_REGEX = /<\/\s*td\s*>((?!<\/\s*tr\s*>)[\s\S])*$/i; const LAST_TR_END_REGEX = /<\/\s*tr\s*>((?!<\/\s*table\s*>)[\s\S])*$/i; @@ -21,14 +17,14 @@ const DEFAULT_BORDER_STYLE = 'solid 1px #d4d4d4'; export function processPastedContentFromExcel( event: BeforePasteEvent, - trustedHTMLHandler: TrustedHTMLHandler, + domCreator: DOMCreator, allowExcelNoBorderTable?: boolean ) { const { fragment, htmlBefore, clipboardData } = event; const html = clipboardData.html ? excelHandler(clipboardData.html, htmlBefore) : undefined; if (html && clipboardData.html != html) { - const doc = new DOMParser().parseFromString(trustedHTMLHandler(html), 'text/html'); + const doc = domCreator.htmlToDOM(html); moveChildNodes(fragment, doc?.body); } diff --git a/packages/roosterjs-content-model-plugins/lib/paste/PastePlugin.ts b/packages/roosterjs-content-model-plugins/lib/paste/PastePlugin.ts index d3a3d5fa7cb..5fea7005843 100644 --- a/packages/roosterjs-content-model-plugins/lib/paste/PastePlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/paste/PastePlugin.ts @@ -99,7 +99,7 @@ export class PastePlugin implements EditorPlugin { switch (pasteSource) { case 'wordDesktop': - processPastedContentFromWordDesktop(event, this.editor.getTrustedHTMLHandler()); + processPastedContentFromWordDesktop(event, this.editor.getDOMCreator()); break; case 'wacComponents': processPastedContentWacComponents(event); @@ -110,7 +110,7 @@ export class PastePlugin implements EditorPlugin { // Handle HTML copied from Excel processPastedContentFromExcel( event, - this.editor.getTrustedHTMLHandler(), + this.editor.getDOMCreator(), this.allowExcelNoBorderTable ); } @@ -121,7 +121,7 @@ export class PastePlugin implements EditorPlugin { ); break; case 'powerPointDesktop': - processPastedContentFromPowerPoint(event, this.editor.getTrustedHTMLHandler()); + processPastedContentFromPowerPoint(event, this.editor.getDOMCreator()); break; } diff --git a/packages/roosterjs-content-model-plugins/lib/paste/PowerPoint/processPastedContentFromPowerPoint.ts b/packages/roosterjs-content-model-plugins/lib/paste/PowerPoint/processPastedContentFromPowerPoint.ts index dad6ea6d2f5..2482c87758e 100644 --- a/packages/roosterjs-content-model-plugins/lib/paste/PowerPoint/processPastedContentFromPowerPoint.ts +++ b/packages/roosterjs-content-model-plugins/lib/paste/PowerPoint/processPastedContentFromPowerPoint.ts @@ -1,5 +1,5 @@ import { moveChildNodes } from 'roosterjs-content-model-dom'; -import type { BeforePasteEvent, TrustedHTMLHandler } from 'roosterjs-content-model-types'; +import type { BeforePasteEvent, DOMCreator } from 'roosterjs-content-model-types'; /** * @internal @@ -9,17 +9,14 @@ import type { BeforePasteEvent, TrustedHTMLHandler } from 'roosterjs-content-mod export function processPastedContentFromPowerPoint( event: BeforePasteEvent, - trustedHTMLHandler: TrustedHTMLHandler + domCreator: DOMCreator ) { const { fragment, clipboardData } = event; if (clipboardData.html && !clipboardData.text && clipboardData.image) { // It is possible that PowerPoint copied both image and HTML but not plain text. // We always prefer HTML if any. - const doc = new DOMParser().parseFromString( - trustedHTMLHandler(clipboardData.html), - 'text/html' - ); + const doc = domCreator.htmlToDOM(clipboardData.html); moveChildNodes(fragment, doc?.body); } diff --git a/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/getStyleMetadata.ts b/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/getStyleMetadata.ts index 72709b51b40..114914a902d 100644 --- a/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/getStyleMetadata.ts +++ b/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/getStyleMetadata.ts @@ -1,6 +1,6 @@ import { getObjectKeys } from 'roosterjs-content-model-dom'; import type { WordMetadata } from './WordMetadata'; -import type { BeforePasteEvent } from 'roosterjs-content-model-types'; +import type { BeforePasteEvent, DOMCreator } from 'roosterjs-content-model-types'; const FORMATING_REGEX = /[\n\t'{}"]+/g; @@ -24,12 +24,9 @@ const FORMATING_REGEX = /[\n\t'{}"]+/g; * 5. Save data in record and only use the required information. * */ -export function getStyleMetadata( - ev: BeforePasteEvent, - trustedHTMLHandler: (val: string) => string -) { +export function getStyleMetadata(ev: BeforePasteEvent, domCreator: DOMCreator) { const metadataMap: Map = new Map(); - const doc = new DOMParser().parseFromString(trustedHTMLHandler(ev.htmlBefore), 'text/html'); + const doc = domCreator.htmlToDOM(ev.htmlBefore); const styles = doc.querySelectorAll('style'); styles.forEach(style => { diff --git a/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts b/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts index 403127175cf..33f904b9935 100644 --- a/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts +++ b/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts @@ -11,6 +11,7 @@ import type { ContentModelBlockFormat, ContentModelListItemLevelFormat, ContentModelTableFormat, + DOMCreator, DomToModelContext, ElementProcessor, FormatParser, @@ -25,11 +26,8 @@ const DEFAULT_BROWSER_LINE_HEIGHT_PERCENTAGE = 1.2; * Handles Pasted content when source is Word Desktop * @param ev BeforePasteEvent */ -export function processPastedContentFromWordDesktop( - ev: BeforePasteEvent, - trustedHTMLHandler: (text: string) => string -) { - const metadataMap: Map = getStyleMetadata(ev, trustedHTMLHandler); +export function processPastedContentFromWordDesktop(ev: BeforePasteEvent, domCreator: DOMCreator) { + const metadataMap: Map = getStyleMetadata(ev, domCreator); setProcessor(ev.domToModelOption, 'element', wordDesktopElementProcessor(metadataMap)); addParser(ev.domToModelOption, 'block', adjustPercentileLineHeight); diff --git a/packages/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts b/packages/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts index e78f0beb1eb..56f955ebafb 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts @@ -4,11 +4,14 @@ import * as getPasteSource from '../../lib/paste/pasteSourceValidations/getPaste import * as PowerPointFile from '../../lib/paste/PowerPoint/processPastedContentFromPowerPoint'; import * as setProcessor from '../../lib/paste/utils/setProcessor'; import * as WacFile from '../../lib/paste/WacComponents/processPastedContentWacComponents'; -import { BeforePasteEvent, IEditor } from 'roosterjs-content-model-types'; +import { BeforePasteEvent, DOMCreator, IEditor } from 'roosterjs-content-model-types'; import { PastePlugin } from '../../lib/paste/PastePlugin'; import { PastePropertyNames } from '../../lib/paste/pasteSourceValidations/constants'; const trustedHTMLHandler = (val: string) => val; +const domCreator: DOMCreator = { + htmlToDOM: (html: string) => new DOMParser().parseFromString(html, 'text/html'), +}; const DEFAULT_TIMES_ADD_PARSER_CALLED = 4; describe('Content Model Paste Plugin Test', () => { @@ -17,6 +20,7 @@ describe('Content Model Paste Plugin Test', () => { beforeEach(() => { editor = ({ getTrustedHTMLHandler: () => trustedHTMLHandler, + getDOMCreator: () => domCreator, } as any) as IEditor; spyOn(addParser, 'addParser').and.callThrough(); spyOn(setProcessor, 'setProcessor').and.callThrough(); @@ -72,7 +76,7 @@ describe('Content Model Paste Plugin Test', () => { expect(ExcelFile.processPastedContentFromExcel).toHaveBeenCalledWith( event, - trustedHTMLHandler, + domCreator, undefined /*allowExcelNoBorderTable*/ ); expect(addParser.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 3); @@ -89,7 +93,7 @@ describe('Content Model Paste Plugin Test', () => { expect(ExcelFile.processPastedContentFromExcel).not.toHaveBeenCalledWith( event, - trustedHTMLHandler, + domCreator, undefined /*allowExcelNoBorderTable*/ ); expect(addParser.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED); @@ -105,7 +109,7 @@ describe('Content Model Paste Plugin Test', () => { expect(ExcelFile.processPastedContentFromExcel).toHaveBeenCalledWith( event, - trustedHTMLHandler, + domCreator, undefined /*allowExcelNoBorderTable*/ ); expect(addParser.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 1); @@ -121,7 +125,7 @@ describe('Content Model Paste Plugin Test', () => { expect(ExcelFile.processPastedContentFromExcel).toHaveBeenCalledWith( event, - trustedHTMLHandler, + domCreator, undefined /*allowExcelNoBorderTable*/ ); expect(addParser.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 1); @@ -137,7 +141,7 @@ describe('Content Model Paste Plugin Test', () => { expect(PowerPointFile.processPastedContentFromPowerPoint).toHaveBeenCalledWith( event, - trustedHTMLHandler + domCreator ); expect(addParser.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED); expect(setProcessor.setProcessor).toHaveBeenCalledTimes(0); diff --git a/packages/roosterjs-content-model-plugins/test/paste/getStyleMetadataTest.ts b/packages/roosterjs-content-model-plugins/test/paste/getStyleMetadataTest.ts index 21e7ac1e9ae..0aae54bd19c 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/getStyleMetadataTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/getStyleMetadataTest.ts @@ -1,13 +1,17 @@ import { BeforePasteEvent } from 'roosterjs-content-model-types'; import { getStyleMetadata } from '../../lib/paste/WordDesktop/getStyleMetadata'; +const domCreator = { + htmlToDOM: (html: string) => new DOMParser().parseFromString(html, 'text/html'), +}; + describe('getStyleMetadata', () => { it('Extract metadata from style element', () => { const event = ({ htmlBefore: '', }); - const result = getStyleMetadata(event, (val: string) => val); + const result = getStyleMetadata(event, domCreator); expect(result.get('l0:level1')).toEqual({ 'mso-level-number-format': 'roman-upper', diff --git a/packages/roosterjs-content-model-plugins/test/paste/processPastedContentFromExcelTest.ts b/packages/roosterjs-content-model-plugins/test/paste/processPastedContentFromExcelTest.ts index 449cb65591b..42cd593c605 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/processPastedContentFromExcelTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/processPastedContentFromExcelTest.ts @@ -1,5 +1,5 @@ import * as PastePluginFile from '../../lib/paste/Excel/processPastedContentFromExcel'; -import { ContentModelDocument } from 'roosterjs-content-model-types'; +import { ContentModelDocument, DOMCreator } from 'roosterjs-content-model-types'; import { createBeforePasteEventMock } from './processPastedContentFromWordDesktopTest'; import { processPastedContentFromExcel } from '../../lib/paste/Excel/processPastedContentFromExcel'; import { @@ -13,6 +13,9 @@ import { let div: HTMLElement; let fragment: DocumentFragment; +const domCreator: DOMCreator = { + htmlToDOM: (html: string) => new DOMParser().parseFromString(html, 'text/html'), +}; describe('processPastedContentFromExcelTest', () => { function runTest(source?: string, expected?: string, expectedModel?: ContentModelDocument) { @@ -26,7 +29,7 @@ describe('processPastedContentFromExcelTest', () => { const event = createBeforePasteEventMock(fragment); event.clipboardData.html = source; - processPastedContentFromExcel(event, (s: string) => s); + processPastedContentFromExcel(event, domCreator); const model = domToContentModel( fragment, @@ -349,7 +352,7 @@ describe('Do not run scenarios', () => { if (excelHandler) { spyOn(PastePluginFile, 'excelHandler').and.returnValue(excelHandler); } - processPastedContentFromExcel(event, (s: string) => s); + processPastedContentFromExcel(event, domCreator); // Assert while (div.firstChild) { diff --git a/packages/roosterjs-content-model-plugins/test/paste/processPastedContentFromPowerPointTest.ts b/packages/roosterjs-content-model-plugins/test/paste/processPastedContentFromPowerPointTest.ts index ed7bbef3432..73fc764fb6e 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/processPastedContentFromPowerPointTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/processPastedContentFromPowerPointTest.ts @@ -1,10 +1,6 @@ import * as moveChildNodes from 'roosterjs-content-model-dom/lib/domUtils/moveChildNodes'; import { processPastedContentFromPowerPoint } from '../../lib/paste/PowerPoint/processPastedContentFromPowerPoint'; -import type { - BeforePasteEvent, - ClipboardData, - TrustedHTMLHandler, -} from 'roosterjs-content-model-types'; +import type { BeforePasteEvent, ClipboardData, DOMCreator } from 'roosterjs-content-model-types'; const getPasteEvent = (): BeforePasteEvent => { return { @@ -29,7 +25,9 @@ const getPasteEvent = (): BeforePasteEvent => { describe('processPastedContentFromPowerPointTest |', () => { let ev: BeforePasteEvent; - let trustedHTMLHandlerMock: TrustedHTMLHandler = (html: string) => html; + let trustedHTMLHandlerMock: DOMCreator = { + htmlToDOM: (html: string) => new DOMParser().parseFromString(html, 'text/html'), + }; let image: HTMLImageElement; let doc: Document; diff --git a/packages/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts b/packages/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts index 07f17b4039d..7b301c86fe8 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts @@ -9,6 +9,10 @@ import { moveChildNodes, } from 'roosterjs-content-model-dom'; +const domCreator = { + htmlToDOM: (html: string) => new DOMParser().parseFromString(html, 'text/html'), +}; + describe('processPastedContentFromWordDesktopTest', () => { let div: HTMLElement; let fragment: DocumentFragment; @@ -27,7 +31,7 @@ describe('processPastedContentFromWordDesktopTest', () => { moveChildNodes(fragment, div); } const event = createBeforePasteEventMock(fragment, htmlBefore); - processPastedContentFromWordDesktop(event, (val: string) => val); + processPastedContentFromWordDesktop(event, domCreator); const model = domToContentModel( fragment, diff --git a/packages/roosterjs-content-model-types/lib/editor/EditorCore.ts b/packages/roosterjs-content-model-types/lib/editor/EditorCore.ts index 42144b97f61..965ebb00722 100644 --- a/packages/roosterjs-content-model-types/lib/editor/EditorCore.ts +++ b/packages/roosterjs-content-model-types/lib/editor/EditorCore.ts @@ -17,7 +17,7 @@ import type { EditorContext } from '../context/EditorContext'; import type { EditorEnvironment } from '../parameter/EditorEnvironment'; import type { ModelToDomOption } from '../context/ModelToDomOption'; import type { OnNodeCreated } from '../context/ModelToDomSettings'; -import type { TrustedHTMLHandler } from '../parameter/TrustedHTMLHandler'; +import type { DOMCreator, LegacyTrustedHTMLHandler } from '../parameter/TrustedHTMLHandler'; import type { Rect } from '../parameter/Rect'; import type { ContentModelFormatter, @@ -361,11 +361,20 @@ export interface EditorCore extends PluginState { readonly darkColorHandler: DarkColorHandler; /** - * A handler to convert HTML string to a trust HTML string. - * By default it will just return the original HTML string directly. + * @deprecated + * @see DOMCreator + * A handler to convert HTML string to a trust string. + * By default it will just convert the original HTML string into a string directly. * To override, pass your own trusted HTML handler to EditorOptions.trustedHTMLHandler */ - readonly trustedHTMLHandler: TrustedHTMLHandler; + readonly trustedHTMLHandler: LegacyTrustedHTMLHandler; + + /** + * A handler to convert HTML string to a trust Document. + * By default it will just convert the original HTML string into a Document object directly. + * To override, pass your own trusted HTML handler to EditorOptions.trustedHTMLHandler + */ + readonly domCreator: DOMCreator; /** * A helper class to provide DOM access APIs diff --git a/packages/roosterjs-content-model-types/lib/editor/IEditor.ts b/packages/roosterjs-content-model-types/lib/editor/IEditor.ts index 39c5e08e7ae..b34d2f24fec 100644 --- a/packages/roosterjs-content-model-types/lib/editor/IEditor.ts +++ b/packages/roosterjs-content-model-types/lib/editor/IEditor.ts @@ -15,7 +15,7 @@ import type { FormatContentModelOptions, } from '../parameter/FormatContentModelOptions'; import type { DarkColorHandler } from '../context/DarkColorHandler'; -import type { TrustedHTMLHandler } from '../parameter/TrustedHTMLHandler'; +import type { DOMCreator, LegacyTrustedHTMLHandler } from '../parameter/TrustedHTMLHandler'; import type { Rect } from '../parameter/Rect'; import type { EntityState } from '../parameter/FormatContentModelContext'; import type { ExperimentalFeature } from './ExperimentalFeature'; @@ -193,12 +193,21 @@ export interface IEditor { hasFocus(): boolean; /** + * @deprecated use getDOMCreator instead * Get a function to convert HTML string to trusted HTML string. * By default it will just return the input HTML directly. To override this behavior, * pass your own trusted HTML handler to EditorOptions.trustedHTMLHandler * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types */ - getTrustedHTMLHandler(): TrustedHTMLHandler; + getTrustedHTMLHandler(): LegacyTrustedHTMLHandler; + + /** + * Get a function to convert HTML string to a trust Document. + * By default it will just convert the original HTML string into a Document object directly. + * To override, pass your own trusted HTML handler to EditorOptions.trustedHTMLHandler + * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types + */ + getDOMCreator(): DOMCreator; /** * Get the scroll container of the editor diff --git a/packages/roosterjs-content-model-types/lib/index.ts b/packages/roosterjs-content-model-types/lib/index.ts index 215f43d77df..241660c5d01 100644 --- a/packages/roosterjs-content-model-types/lib/index.ts +++ b/packages/roosterjs-content-model-types/lib/index.ts @@ -424,7 +424,11 @@ export { DOMEventHandlerFunction, DOMEventRecord } from './parameter/DOMEventRec export { EdgeLinkPreview } from './parameter/EdgeLinkPreview'; export { ClipboardData } from './parameter/ClipboardData'; export { AnnounceData, KnownAnnounceStrings } from './parameter/AnnounceData'; -export { TrustedHTMLHandler } from './parameter/TrustedHTMLHandler'; +export { + TrustedHTMLHandler, + DOMCreator, + LegacyTrustedHTMLHandler, +} from './parameter/TrustedHTMLHandler'; export { Rect } from './parameter/Rect'; export { ValueSanitizer } from './parameter/ValueSanitizer'; export { DOMHelper } from './parameter/DOMHelper'; diff --git a/packages/roosterjs-content-model-types/lib/parameter/TrustedHTMLHandler.ts b/packages/roosterjs-content-model-types/lib/parameter/TrustedHTMLHandler.ts index 05785843669..80db39130a3 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/TrustedHTMLHandler.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/TrustedHTMLHandler.ts @@ -1,4 +1,16 @@ /** * A handler type to convert HTML string to a trust HTML string */ -export type TrustedHTMLHandler = (html: string) => string; +export type LegacyTrustedHTMLHandler = (html: string) => string; + +/** + * A handler type to convert HTML string to a DOM object + */ +export interface DOMCreator { + htmlToDOM: (html: string) => Document; +} + +/** + * A handler type to convert HTML string to a trust HTML string or a DOM object + */ +export type TrustedHTMLHandler = DOMCreator | LegacyTrustedHTMLHandler; diff --git a/packages/roosterjs-editor-adapter/lib/editor/EditorAdapter.ts b/packages/roosterjs-editor-adapter/lib/editor/EditorAdapter.ts index b80589167e9..d6ff22bcb70 100644 --- a/packages/roosterjs-editor-adapter/lib/editor/EditorAdapter.ts +++ b/packages/roosterjs-editor-adapter/lib/editor/EditorAdapter.ts @@ -421,10 +421,8 @@ export class EditorAdapter extends Editor implements ILegacyEditor { insertContent(content: string, option?: InsertOption) { if (content) { const doc = this.getDocument(); - const body = new DOMParser().parseFromString( - this.getCore().trustedHTMLHandler(content), - 'text/html' - )?.body; + const body = this.getCore().domCreator.htmlToDOM(content).body; + let allNodes = body?.childNodes ? toArray(body.childNodes) : []; // If it is to insert on new line, and there are more than one node in the collection, wrap all nodes with From 87a13e999f9744c5da4d7e7a0a9d55ed8b827cdc Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Tue, 3 Dec 2024 18:04:39 -0300 Subject: [PATCH 2/6] nit --- .../roosterjs-content-model-core/test/utils/domCreatorTest.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/roosterjs-content-model-core/test/utils/domCreatorTest.ts b/packages/roosterjs-content-model-core/test/utils/domCreatorTest.ts index 943a47f5afb..c3393bc4783 100644 --- a/packages/roosterjs-content-model-core/test/utils/domCreatorTest.ts +++ b/packages/roosterjs-content-model-core/test/utils/domCreatorTest.ts @@ -24,7 +24,6 @@ describe('domCreator', () => { it('domCreator - undefined', () => { const doc = document.implementation.createHTMLDocument(); doc.body.appendChild(document.createTextNode('test')); - doc.ownerDocument; const result = domCreator(undefined).htmlToDOM('test'); expect(result.lastChild).toEqual(doc.lastChild); }); From cb829f2b0726354b27d42a580aa95bc8e58b95e3 Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Tue, 3 Dec 2024 18:11:26 -0300 Subject: [PATCH 3/6] nit --- packages/roosterjs-editor-adapter/lib/editor/EditorAdapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/roosterjs-editor-adapter/lib/editor/EditorAdapter.ts b/packages/roosterjs-editor-adapter/lib/editor/EditorAdapter.ts index d6ff22bcb70..082368f5489 100644 --- a/packages/roosterjs-editor-adapter/lib/editor/EditorAdapter.ts +++ b/packages/roosterjs-editor-adapter/lib/editor/EditorAdapter.ts @@ -421,7 +421,7 @@ export class EditorAdapter extends Editor implements ILegacyEditor { insertContent(content: string, option?: InsertOption) { if (content) { const doc = this.getDocument(); - const body = this.getCore().domCreator.htmlToDOM(content).body; + const body = this.getCore().domCreator.htmlToDOM(content)?.body; let allNodes = body?.childNodes ? toArray(body.childNodes) : []; From de05dc5ad49c3da1788f582b4122769f51bd5e60 Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Thu, 5 Dec 2024 15:44:54 -0300 Subject: [PATCH 4/6] trusted HTML refactor --- .../createModelFromHtml/createModelFromHtml.ts | 4 ++-- .../lib/editor/core/createEditorCore.ts | 12 ++++++------ .../lib/utils/domCreator.ts | 9 ++++++++- .../test/editor/core/createEditorCoreTest.ts | 18 ++++++++++++------ .../test/utils/domCreatorTest.ts | 14 +++++++------- .../lib/parameter/TrustedHTMLHandler.ts | 1 + 6 files changed, 36 insertions(+), 22 deletions(-) diff --git a/packages/roosterjs-content-model-core/lib/command/createModelFromHtml/createModelFromHtml.ts b/packages/roosterjs-content-model-core/lib/command/createModelFromHtml/createModelFromHtml.ts index 1505427ae3c..714cad17fb6 100644 --- a/packages/roosterjs-content-model-core/lib/command/createModelFromHtml/createModelFromHtml.ts +++ b/packages/roosterjs-content-model-core/lib/command/createModelFromHtml/createModelFromHtml.ts @@ -1,7 +1,7 @@ import { convertInlineCss, retrieveCssRules } from './convertInlineCss'; +import { createDOMCreator } from '../../utils/domCreator'; import { createDomToModelContextForSanitizing } from './createDomToModelContextForSanitizing'; import { createEmptyModel, domToContentModel, parseFormat } from 'roosterjs-content-model-dom'; -import { domCreator } from '../../utils/domCreator'; import type { ContentModelDocument, ContentModelSegmentFormat, @@ -22,7 +22,7 @@ export function createModelFromHtml( trustedHTMLHandler?: TrustedHTMLHandler, defaultSegmentFormat?: ContentModelSegmentFormat ): ContentModelDocument { - const doc = html ? domCreator(trustedHTMLHandler).htmlToDOM(html) : null; + const doc = html ? createDOMCreator(trustedHTMLHandler).htmlToDOM(html) : null; if (doc?.body) { const context = createDomToModelContextForSanitizing( diff --git a/packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts b/packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts index daabda5152d..73e33595118 100644 --- a/packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts +++ b/packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts @@ -1,9 +1,9 @@ import { coreApiMap } from '../../coreApi/coreApiMap'; import { createDarkColorHandler } from './DarkColorHandlerImpl'; +import { createDOMCreator, createTrustedHTMLHandler } from '../../utils/domCreator'; import { createDOMHelper } from './DOMHelperImpl'; import { createDomToModelSettings, createModelToDomSettings } from './createEditorDefaultSettings'; import { createEditorCorePlugins } from '../../corePlugin/createEditorCorePlugins'; -import { defaultTrustHtmlHandler, domCreator, isDOMCreator } from '../../utils/domCreator'; import type { EditorEnvironment, PluginState, @@ -19,6 +19,9 @@ import type { */ export function createEditorCore(contentDiv: HTMLDivElement, options: EditorOptions): EditorCore { const corePlugins = createEditorCorePlugins(options, contentDiv); + const domCreator = createDOMCreator(options.trustedHTMLHandler); + const trustedHTMLHandler = createTrustedHTMLHandler(domCreator); + return { physicalRoot: contentDiv, logicalRoot: contentDiv, @@ -43,11 +46,8 @@ export function createEditorCore(contentDiv: HTMLDivElement, options: EditorOpti options.knownColors, options.generateColorKey ), - trustedHTMLHandler: - options.trustedHTMLHandler && !isDOMCreator(options.trustedHTMLHandler) - ? options.trustedHTMLHandler - : defaultTrustHtmlHandler, - domCreator: domCreator(options.trustedHTMLHandler), + trustedHTMLHandler: trustedHTMLHandler, + domCreator: domCreator, domHelper: createDOMHelper(contentDiv), ...getPluginState(corePlugins), disposeErrorHandler: options.disposeErrorHandler, diff --git a/packages/roosterjs-content-model-core/lib/utils/domCreator.ts b/packages/roosterjs-content-model-core/lib/utils/domCreator.ts index a45f509e027..d9432630d52 100644 --- a/packages/roosterjs-content-model-core/lib/utils/domCreator.ts +++ b/packages/roosterjs-content-model-core/lib/utils/domCreator.ts @@ -7,7 +7,14 @@ import type { /** * @internal */ -export function domCreator(trustedHTMLHandler?: TrustedHTMLHandler): DOMCreator { +export const createTrustedHTMLHandler = (domCreator: DOMCreator): LegacyTrustedHTMLHandler => { + return (html: string) => domCreator.htmlToDOM(html).body.innerHTML; +}; + +/** + * @internal + */ +export function createDOMCreator(trustedHTMLHandler?: TrustedHTMLHandler): DOMCreator { return trustedHTMLHandler && isDOMCreator(trustedHTMLHandler) ? trustedHTMLHandler : trustedHTMLHandlerToDOMCreator(trustedHTMLHandler as LegacyTrustedHTMLHandler); diff --git a/packages/roosterjs-content-model-core/test/editor/core/createEditorCoreTest.ts b/packages/roosterjs-content-model-core/test/editor/core/createEditorCoreTest.ts index 3731f579c9f..ce01f1e758c 100644 --- a/packages/roosterjs-content-model-core/test/editor/core/createEditorCoreTest.ts +++ b/packages/roosterjs-content-model-core/test/editor/core/createEditorCoreTest.ts @@ -4,8 +4,8 @@ import * as DarkColorHandlerImpl from '../../../lib/editor/core/DarkColorHandler import * as domCreator from '../../../lib/utils/domCreator'; import * as DOMHelperImpl from '../../../lib/editor/core/DOMHelperImpl'; import { coreApiMap } from '../../../lib/coreApi/coreApiMap'; -import { EditorCore, EditorOptions } from 'roosterjs-content-model-types'; import { createEditorCore, getDarkColorFallback } from '../../../lib/editor/core/createEditorCore'; +import { DOMCreator, EditorCore, EditorOptions } from 'roosterjs-content-model-types'; describe('createEditorCore', () => { function createMockedPlugin(stateName: string): any { @@ -38,7 +38,10 @@ describe('createEditorCore', () => { const mockedDomToModelSettings = 'DOMTOMODEL' as any; const mockedModelToDomSettings = 'MODELTODOM' as any; const mockedDOMHelper = 'DOMHELPER' as any; - const mockedHtmlToDOM = 'HTMLTODOM' as any; + const mockedDOMCreator: DOMCreator = { + htmlToDOM: mockedDOMHelper, + }; + const mockedTrustHtmlHandler = 'TRUSTED' as any; beforeEach(() => { spyOn(createEditorCorePlugins, 'createEditorCorePlugins').and.returnValue(mockedPlugins); @@ -52,7 +55,8 @@ describe('createEditorCore', () => { mockedModelToDomSettings ); spyOn(DOMHelperImpl, 'createDOMHelper').and.returnValue(mockedDOMHelper); - spyOn(domCreator, 'domCreator').and.returnValue(mockedHtmlToDOM); + spyOn(domCreator, 'createDOMCreator').and.returnValue(mockedDOMCreator); + spyOn(domCreator, 'createTrustedHTMLHandler').and.returnValue(mockedTrustHtmlHandler); }); function runTest( @@ -87,8 +91,8 @@ describe('createEditorCore', () => { modelToDomSettings: mockedModelToDomSettings, }, darkColorHandler: mockedDarkColorHandler, - trustedHTMLHandler: domCreator.defaultTrustHtmlHandler, - domCreator: mockedHtmlToDOM, + trustedHTMLHandler: mockedTrustHtmlHandler, + domCreator: mockedDOMCreator, cache: 'cache' as any, format: 'format' as any, copyPaste: 'copyPaste' as any, @@ -146,7 +150,7 @@ describe('createEditorCore', () => { const mockedPlugin1 = 'P1' as any; const mockedPlugin2 = 'P2' as any; const mockedGetDarkColor = 'DARK' as any; - const mockedTrustHtmlHandler = 'TRUST' as any; + const mockedTrustHtmlHandler = 'OPTIONAL TRUSTED' as any; const mockedDisposeErrorHandler = 'DISPOSE' as any; const mockedGenerateColorKey = 'KEY' as any; const mockedKnownColors = 'COLORS' as any; @@ -164,6 +168,8 @@ describe('createEditorCore', () => { onFixUpModel: mockedOnFixUpModel, } as any; + spyOn(domCreator, 'createTrustedHTMLHandler').and.returnValue(mockedTrustHtmlHandler); + runTest(mockedDiv, mockedOptions, { physicalRoot: mockedDiv, logicalRoot: mockedDiv, diff --git a/packages/roosterjs-content-model-core/test/utils/domCreatorTest.ts b/packages/roosterjs-content-model-core/test/utils/domCreatorTest.ts index c3393bc4783..48d35e60e84 100644 --- a/packages/roosterjs-content-model-core/test/utils/domCreatorTest.ts +++ b/packages/roosterjs-content-model-core/test/utils/domCreatorTest.ts @@ -1,4 +1,4 @@ -import { domCreator, isDOMCreator } from '../../lib/utils/domCreator'; +import { createDOMCreator, isDOMCreator } from '../../lib/utils/domCreator'; describe('domCreator', () => { it('isDOMCreator - True', () => { @@ -13,26 +13,26 @@ describe('domCreator', () => { expect(isDOMCreator(trustedHTMLHandler)).toBe(false); }); - it('domCreator - isDOMCreator', () => { + it('createDOMCreator - isDOMCreator', () => { const trustedHTMLHandler = { htmlToDOM: (html: string) => new DOMParser().parseFromString(html, 'text/html'), }; - const result = domCreator(trustedHTMLHandler); + const result = createDOMCreator(trustedHTMLHandler); expect(result).toEqual(trustedHTMLHandler); }); - it('domCreator - undefined', () => { + it('createDOMCreator - undefined', () => { const doc = document.implementation.createHTMLDocument(); doc.body.appendChild(document.createTextNode('test')); - const result = domCreator(undefined).htmlToDOM('test'); + const result = createDOMCreator(undefined).htmlToDOM('test'); expect(result.lastChild).toEqual(doc.lastChild); }); - it('domCreator - trustedHTML', () => { + it('createDOMCreator - trustedHTML', () => { const doc = document.implementation.createHTMLDocument(); doc.body.appendChild(document.createTextNode('test trusted')); const trustedHTMLHandler = (html: string) => html + ' trusted'; - const result = domCreator(trustedHTMLHandler).htmlToDOM('test'); + const result = createDOMCreator(trustedHTMLHandler).htmlToDOM('test'); expect(result.lastChild).toEqual(doc.lastChild); }); }); diff --git a/packages/roosterjs-content-model-types/lib/parameter/TrustedHTMLHandler.ts b/packages/roosterjs-content-model-types/lib/parameter/TrustedHTMLHandler.ts index 80db39130a3..16c678ee01e 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/TrustedHTMLHandler.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/TrustedHTMLHandler.ts @@ -1,4 +1,5 @@ /** + * @deprecated Use DOMCreator instead * A handler type to convert HTML string to a trust HTML string */ export type LegacyTrustedHTMLHandler = (html: string) => string; From 304a15bb736c825b54c31ba94053f3701a54fe8a Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Thu, 5 Dec 2024 15:53:11 -0300 Subject: [PATCH 5/6] fix test --- .../test/editor/core/createEditorCoreTest.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/roosterjs-content-model-core/test/editor/core/createEditorCoreTest.ts b/packages/roosterjs-content-model-core/test/editor/core/createEditorCoreTest.ts index ce01f1e758c..4bc20b76df3 100644 --- a/packages/roosterjs-content-model-core/test/editor/core/createEditorCoreTest.ts +++ b/packages/roosterjs-content-model-core/test/editor/core/createEditorCoreTest.ts @@ -150,7 +150,7 @@ describe('createEditorCore', () => { const mockedPlugin1 = 'P1' as any; const mockedPlugin2 = 'P2' as any; const mockedGetDarkColor = 'DARK' as any; - const mockedTrustHtmlHandler = 'OPTIONAL TRUSTED' as any; + const mockedTrustHtmlHandler = 'TRUSTED' as any; const mockedDisposeErrorHandler = 'DISPOSE' as any; const mockedGenerateColorKey = 'KEY' as any; const mockedKnownColors = 'COLORS' as any; @@ -168,8 +168,6 @@ describe('createEditorCore', () => { onFixUpModel: mockedOnFixUpModel, } as any; - spyOn(domCreator, 'createTrustedHTMLHandler').and.returnValue(mockedTrustHtmlHandler); - runTest(mockedDiv, mockedOptions, { physicalRoot: mockedDiv, logicalRoot: mockedDiv, From 72dfb20bfd98d7f0883d777a20dca33d9c0066af Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Fri, 6 Dec 2024 19:50:23 -0300 Subject: [PATCH 6/6] fixes --- .../lib/editor/core/createEditorCore.ts | 8 +++++--- .../test/editor/core/createEditorCoreTest.ts | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts b/packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts index 73e33595118..1a6351a4513 100644 --- a/packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts +++ b/packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts @@ -1,6 +1,6 @@ import { coreApiMap } from '../../coreApi/coreApiMap'; import { createDarkColorHandler } from './DarkColorHandlerImpl'; -import { createDOMCreator, createTrustedHTMLHandler } from '../../utils/domCreator'; +import { createDOMCreator, createTrustedHTMLHandler, isDOMCreator } from '../../utils/domCreator'; import { createDOMHelper } from './DOMHelperImpl'; import { createDomToModelSettings, createModelToDomSettings } from './createEditorDefaultSettings'; import { createEditorCorePlugins } from '../../corePlugin/createEditorCorePlugins'; @@ -20,7 +20,6 @@ import type { export function createEditorCore(contentDiv: HTMLDivElement, options: EditorOptions): EditorCore { const corePlugins = createEditorCorePlugins(options, contentDiv); const domCreator = createDOMCreator(options.trustedHTMLHandler); - const trustedHTMLHandler = createTrustedHTMLHandler(domCreator); return { physicalRoot: contentDiv, @@ -46,7 +45,10 @@ export function createEditorCore(contentDiv: HTMLDivElement, options: EditorOpti options.knownColors, options.generateColorKey ), - trustedHTMLHandler: trustedHTMLHandler, + trustedHTMLHandler: + options.trustedHTMLHandler && !isDOMCreator(options.trustedHTMLHandler) + ? options.trustedHTMLHandler + : createTrustedHTMLHandler(domCreator), domCreator: domCreator, domHelper: createDOMHelper(contentDiv), ...getPluginState(corePlugins), diff --git a/packages/roosterjs-content-model-core/test/editor/core/createEditorCoreTest.ts b/packages/roosterjs-content-model-core/test/editor/core/createEditorCoreTest.ts index 4bc20b76df3..810cfc4ba1b 100644 --- a/packages/roosterjs-content-model-core/test/editor/core/createEditorCoreTest.ts +++ b/packages/roosterjs-content-model-core/test/editor/core/createEditorCoreTest.ts @@ -150,7 +150,7 @@ describe('createEditorCore', () => { const mockedPlugin1 = 'P1' as any; const mockedPlugin2 = 'P2' as any; const mockedGetDarkColor = 'DARK' as any; - const mockedTrustHtmlHandler = 'TRUSTED' as any; + const mockedTrustHtmlHandler = 'OPTIONS TRUSTED' as any; const mockedDisposeErrorHandler = 'DISPOSE' as any; const mockedGenerateColorKey = 'KEY' as any; const mockedKnownColors = 'COLORS' as any;