Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use DOMCreator instead of TrustedHTMLHandler #2898

Merged
merged 6 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { convertInlineCss, retrieveCssRules } from './convertInlineCss';
import { createDOMCreator } from '../../utils/domCreator';
import { createDomToModelContextForSanitizing } from './createDomToModelContextForSanitizing';
import { createEmptyModel, domToContentModel, parseFormat } from 'roosterjs-content-model-dom';
import type {
Expand All @@ -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 ? createDOMCreator(trustedHTMLHandler).htmlToDOM(html) : null;

if (doc?.body) {
const context = createDomToModelContextForSanitizing(
Expand Down
13 changes: 5 additions & 8 deletions packages/roosterjs-content-model-core/lib/command/paste/paste.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { retrieveHtmlInfo } from './retrieveHtmlInfo';
import type {
PasteTypeOrGetter,
ClipboardData,
TrustedHTMLHandler,
IEditor,
DOMCreator,
} from 'roosterjs-content-model-types';

/**
Expand All @@ -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);
Expand All @@ -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)
Expand All @@ -50,7 +47,7 @@ export function paste(
pasteType,
(clipboardData.rawHtml == clipboardData.html
? doc
: createDOMFromHtml(clipboardData.html, trustedHTMLHandler)
: createDOMFromHtml(clipboardData.html, editor.getDOMCreator())
)?.body
);

Expand All @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 14 additions & 2 deletions packages/roosterjs-content-model-core/lib/editor/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@ import type {
SnapshotsManager,
EditorCore,
EditorOptions,
TrustedHTMLHandler,
Rect,
EntityState,
CachedElementHandler,
DomToModelOptionForCreateModel,
AnnounceData,
ExperimentalFeature,
LegacyTrustedHTMLHandler,
DOMCreator,
} from 'roosterjs-content-model-types';

/**
Expand Down Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { coreApiMap } from '../../coreApi/coreApiMap';
import { createDarkColorHandler } from './DarkColorHandlerImpl';
import { createDOMCreator, createTrustedHTMLHandler, isDOMCreator } from '../../utils/domCreator';
import { createDOMHelper } from './DOMHelperImpl';
import { createDomToModelSettings, createModelToDomSettings } from './createEditorDefaultSettings';
import { createEditorCorePlugins } from '../../corePlugin/createEditorCorePlugins';
Expand All @@ -18,6 +19,7 @@ import type {
*/
export function createEditorCore(contentDiv: HTMLDivElement, options: EditorOptions): EditorCore {
const corePlugins = createEditorCorePlugins(options, contentDiv);
const domCreator = createDOMCreator(options.trustedHTMLHandler);

return {
physicalRoot: contentDiv,
Expand All @@ -43,7 +45,11 @@ export function createEditorCore(contentDiv: HTMLDivElement, options: EditorOpti
options.knownColors,
options.generateColorKey
),
trustedHTMLHandler: options.trustedHTMLHandler || defaultTrustHtmlHandler,
trustedHTMLHandler:
options.trustedHTMLHandler && !isDOMCreator(options.trustedHTMLHandler)
? options.trustedHTMLHandler
: createTrustedHTMLHandler(domCreator),
domCreator: domCreator,
domHelper: createDOMHelper(contentDiv),
...getPluginState(corePlugins),
disposeErrorHandler: options.disposeErrorHandler,
Expand Down Expand Up @@ -90,13 +96,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(),
Expand Down
44 changes: 44 additions & 0 deletions packages/roosterjs-content-model-core/lib/utils/domCreator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type {
DOMCreator,
LegacyTrustedHTMLHandler,
TrustedHTMLHandler,
} from 'roosterjs-content-model-types';

/**
* @internal
*/
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);
}

/**
* @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'),
};
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,6 +19,7 @@ describe('restoreSnapshotHTML', () => {
entity: {
entityMap: {},
},
domCreator: domCreator,
} as any;
});

Expand All @@ -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: '<div>test1</div>',
} as any;

(<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('<div>test1</div>');
expect(htmlToDOMSpy).toHaveBeenCalledWith('<div>test1</div>');
expect(div.innerHTML).toBe('<div>test1</div><div>test1</div>');
});

Expand Down
Original file line number Diff line number Diff line change
@@ -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';
import { DOMCreator, EditorCore, EditorOptions } from 'roosterjs-content-model-types';

describe('createEditorCore', () => {
function createMockedPlugin(stateName: string): any {
Expand Down Expand Up @@ -41,6 +38,10 @@ describe('createEditorCore', () => {
const mockedDomToModelSettings = 'DOMTOMODEL' as any;
const mockedModelToDomSettings = 'MODELTODOM' as any;
const mockedDOMHelper = 'DOMHELPER' as any;
const mockedDOMCreator: DOMCreator = {
htmlToDOM: mockedDOMHelper,
};
const mockedTrustHtmlHandler = 'TRUSTED' as any;

beforeEach(() => {
spyOn(createEditorCorePlugins, 'createEditorCorePlugins').and.returnValue(mockedPlugins);
Expand All @@ -54,6 +55,8 @@ describe('createEditorCore', () => {
mockedModelToDomSettings
);
spyOn(DOMHelperImpl, 'createDOMHelper').and.returnValue(mockedDOMHelper);
spyOn(domCreator, 'createDOMCreator').and.returnValue(mockedDOMCreator);
spyOn(domCreator, 'createTrustedHTMLHandler').and.returnValue(mockedTrustHtmlHandler);
});

function runTest(
Expand Down Expand Up @@ -88,7 +91,8 @@ describe('createEditorCore', () => {
modelToDomSettings: mockedModelToDomSettings,
},
darkColorHandler: mockedDarkColorHandler,
trustedHTMLHandler: defaultTrustHtmlHandler,
trustedHTMLHandler: mockedTrustHtmlHandler,
domCreator: mockedDOMCreator,
cache: 'cache' as any,
format: 'format' as any,
copyPaste: 'copyPaste' as any,
Expand Down Expand Up @@ -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 = 'OPTIONS TRUSTED' as any;
const mockedDisposeErrorHandler = 'DISPOSE' as any;
const mockedGenerateColorKey = 'KEY' as any;
const mockedKnownColors = 'COLORS' as any;
Expand Down
38 changes: 38 additions & 0 deletions packages/roosterjs-content-model-core/test/utils/domCreatorTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { createDOMCreator, 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('createDOMCreator - isDOMCreator', () => {
const trustedHTMLHandler = {
htmlToDOM: (html: string) => new DOMParser().parseFromString(html, 'text/html'),
};
const result = createDOMCreator(trustedHTMLHandler);
expect(result).toEqual(trustedHTMLHandler);
});

it('createDOMCreator - undefined', () => {
const doc = document.implementation.createHTMLDocument();
doc.body.appendChild(document.createTextNode('test'));
const result = createDOMCreator(undefined).htmlToDOM('test');
expect(result.lastChild).toEqual(doc.lastChild);
});

it('createDOMCreator - trustedHTML', () => {
const doc = document.implementation.createHTMLDocument();
doc.body.appendChild(document.createTextNode('test trusted'));
const trustedHTMLHandler = (html: string) => html + ' trusted';
const result = createDOMCreator(trustedHTMLHandler).htmlToDOM('test');
expect(result.lastChild).toEqual(doc.lastChild);
});
});
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
}

Expand Down
Loading
Loading