From 847729a6362ad1aab1deb9150a8c197e8f73c2c6 Mon Sep 17 00:00:00 2001 From: marcinkiewicz Date: Tue, 10 May 2022 09:40:05 -0700 Subject: [PATCH 01/27] Add target to createLink function. (#966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add anchor target attribute when creating link. * Remove attribute if update with no target provided. * Use string as target type to allow passing frame name. Co-authored-by: Przemyslaw Marcinkiewicz 🦒 --- .../roosterjs-editor-api/lib/format/createLink.ts | 15 ++++++++++++++- .../test/format/createLinkTest.ts | 9 +++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/roosterjs-editor-api/lib/format/createLink.ts b/packages/roosterjs-editor-api/lib/format/createLink.ts index 062c603a1f9..c165976a0c9 100644 --- a/packages/roosterjs-editor-api/lib/format/createLink.ts +++ b/packages/roosterjs-editor-api/lib/format/createLink.ts @@ -42,6 +42,7 @@ function applyLinkPrefix(url: string): string { * When protocol is not specified, a best matched protocol will be predicted. * @param altText Optional alt text of the link, will be shown when hover on the link * @param displayText Optional display text for the link. + * @param target Optional display target for the link ("_blank"|"_self"|"_parent"|"_top"|"{framename}") * If specified, the display text of link will be replaced with this text. * If not specified and there wasn't a link, the link url will be used as display text. */ @@ -49,7 +50,8 @@ export default function createLink( editor: IEditor, link: string, altText?: string, - displayText?: string + displayText?: string, + target?: string ) { editor.focus(); let url = (checkXss(link) || '').trim(); @@ -96,6 +98,9 @@ export default function createLink( if (altText && anchor) { anchor.title = altText; } + if (anchor) { + updateAnchorTarget(anchor, target); + } return anchor; }, ChangeSource.CreateLink); } @@ -111,6 +116,14 @@ function updateAnchorDisplayText(anchor: HTMLAnchorElement, displayText: string) } } +function updateAnchorTarget(anchor: HTMLAnchorElement, target?: string) { + if (target) { + anchor.target = target; + } else if (!target && anchor.getAttribute('target')) { + anchor.removeAttribute('target'); + } +} + function checkXss(link: string): string { const sanitizer = new HtmlSanitizer(); const a = document.createElement('a'); diff --git a/packages/roosterjs-editor-api/test/format/createLinkTest.ts b/packages/roosterjs-editor-api/test/format/createLinkTest.ts index 2c0300907b1..78de23298ea 100644 --- a/packages/roosterjs-editor-api/test/format/createLinkTest.ts +++ b/packages/roosterjs-editor-api/test/format/createLinkTest.ts @@ -84,6 +84,15 @@ describe('createLink()', () => { expect(link.outerHTML).toBe('this is my link'); }); + it('sets target attribute in the link', () => { + // Act + createLink(editor, 'www.example.com', undefined, undefined, '_self'); + + // Assert + let link = document.getElementsByTagName('a')[0]; + expect(link.target).toBe('_self'); + }); + it('Issue when selection is under another tag', () => { editor.setContent( '
Hello world ðŸ™‚ this is a test
' From ed9ef41eadb5daedf098ebbc3316a395aaf9ce59 Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Wed, 11 May 2022 13:46:31 -0600 Subject: [PATCH 02/27] Prevent Table Selection to be cleared on some keys (#970) * init * increase version --- package.json | 2 +- .../TableCellSelection/TableCellSelection.ts | 18 +++++++++++++++--- .../roosterjs-editor-types/lib/enum/Keys.ts | 4 ++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index d13055df295..5846d7ef470 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roosterjs", - "version": "8.22.0", + "version": "8.22.1", "description": "Framework-independent javascript editor", "repository": { "type": "git", diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/TableCellSelection.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/TableCellSelection.ts index 56cfc5b58be..030a8e25aad 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/TableCellSelection.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/TableCellSelection.ts @@ -31,7 +31,13 @@ import { const TABLE_CELL_SELECTOR = 'td,th'; const LEFT_CLICK = 1; const RIGHT_CLICK = 3; - +const IGNORE_KEY_UP_KEYS = [ + Keys.SHIFT, + Keys.ALT, + Keys.META_LEFT, + Keys.CTRL_LEFT, + Keys.PRINT_SCREEN, +]; /** * TableCellSelectionPlugin help highlight table cells */ @@ -242,8 +248,14 @@ export default class TableCellSelection implements EditorPlugin { } private handleKeyUpEvent(event: PluginKeyUpEvent) { - const { shiftKey, which } = event.rawEvent; - if (!shiftKey && which != Keys.SHIFT && this.firstTarget && !this.preventKeyUp) { + const { shiftKey, which, ctrlKey } = event.rawEvent; + if ( + !shiftKey && + !ctrlKey && + this.firstTarget && + !this.preventKeyUp && + IGNORE_KEY_UP_KEYS.indexOf(which) == -1 + ) { this.clearState(); } this.preventKeyUp = false; diff --git a/packages/roosterjs-editor-types/lib/enum/Keys.ts b/packages/roosterjs-editor-types/lib/enum/Keys.ts index 2d1a6fa138c..17a0d4268d0 100644 --- a/packages/roosterjs-editor-types/lib/enum/Keys.ts +++ b/packages/roosterjs-editor-types/lib/enum/Keys.ts @@ -7,6 +7,8 @@ export const enum Keys { TAB = 9, ENTER = 13, SHIFT = 16, + CTRL_LEFT = 17, + ALT = 18, ESCAPE = 27, SPACE = 32, PAGEUP = 33, @@ -14,6 +16,7 @@ export const enum Keys { UP = 38, RIGHT = 39, DOWN = 40, + PRINT_SCREEN = 44, DELETE = 46, /** * @deprecated Just for backward compatibility @@ -25,6 +28,7 @@ export const enum Keys { U = 85, Y = 89, Z = 90, + META_LEFT = 91, COMMA = 188, DASH_UNDERSCORE = 189, PERIOD = 190, From 161f4e7fafec4824438dd8433160f3ce84fcfa0e Mon Sep 17 00:00:00 2001 From: marcinkiewicz Date: Wed, 11 May 2022 14:46:45 -0700 Subject: [PATCH 03/27] Add optional image element attributes. (#968) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Przemyslaw Marcinkiewicz 🦒 Co-authored-by: Jiuqing Song --- .../lib/format/insertImage.ts | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/roosterjs-editor-api/lib/format/insertImage.ts b/packages/roosterjs-editor-api/lib/format/insertImage.ts index 688638b9d3f..8f17725b503 100644 --- a/packages/roosterjs-editor-api/lib/format/insertImage.ts +++ b/packages/roosterjs-editor-api/lib/format/insertImage.ts @@ -6,32 +6,53 @@ import { readFile } from 'roosterjs-editor-dom'; * @param editor The editor instance * @param imageFile The image file. There are at least 3 ways to obtain the file object: * From local file, from clipboard data, from drag-and-drop + * @param attributes Optional image element attributes */ -export default function insertImage(editor: IEditor, imageFile: File): void; +export default function insertImage( + editor: IEditor, + imageFile: File, + attributes?: Record +): void; /** * Insert an image to editor at current selection * @param editor The editor instance - * @param imageFile The image link. + * @param imageFile The image link + * @param attributes Optional image element attributes */ -export default function insertImage(editor: IEditor, url: string): void; +export default function insertImage( + editor: IEditor, + url: string, + attributes?: Record +): void; -export default function insertImage(editor: IEditor, imageFile: File | string): void { +export default function insertImage( + editor: IEditor, + imageFile: File | string, + attributes?: Record +): void { if (typeof imageFile == 'string') { - insertImageWithSrc(editor, imageFile); + insertImageWithSrc(editor, imageFile, attributes); } else { readFile(imageFile, dataUrl => { if (dataUrl && !editor.isDisposed()) { - insertImageWithSrc(editor, dataUrl); + insertImageWithSrc(editor, dataUrl, attributes); } }); } } -function insertImageWithSrc(editor: IEditor, src: string) { +function insertImageWithSrc(editor: IEditor, src: string, attributes?: Record) { editor.addUndoSnapshot(() => { const image = editor.getDocument().createElement('img'); image.src = src; + + if (attributes) { + for (const attribute in attributes) { + image.setAttribute(attribute, attributes[attribute]); + } + } + image.style.maxWidth = '100%'; editor.insertNode(image); }, ChangeSource.Format); From 60519db6af22a587aabfe666c5f155aaa59de9c9 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Wed, 11 May 2022 14:49:51 -0700 Subject: [PATCH 04/27] Revert "Add optional image element attributes. (#968)" (#972) This reverts commit 161f4e7fafec4824438dd8433160f3ce84fcfa0e. --- .../lib/format/insertImage.ts | 35 ++++--------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/packages/roosterjs-editor-api/lib/format/insertImage.ts b/packages/roosterjs-editor-api/lib/format/insertImage.ts index 8f17725b503..688638b9d3f 100644 --- a/packages/roosterjs-editor-api/lib/format/insertImage.ts +++ b/packages/roosterjs-editor-api/lib/format/insertImage.ts @@ -6,53 +6,32 @@ import { readFile } from 'roosterjs-editor-dom'; * @param editor The editor instance * @param imageFile The image file. There are at least 3 ways to obtain the file object: * From local file, from clipboard data, from drag-and-drop - * @param attributes Optional image element attributes */ -export default function insertImage( - editor: IEditor, - imageFile: File, - attributes?: Record -): void; +export default function insertImage(editor: IEditor, imageFile: File): void; /** * Insert an image to editor at current selection * @param editor The editor instance - * @param imageFile The image link - * @param attributes Optional image element attributes + * @param imageFile The image link. */ -export default function insertImage( - editor: IEditor, - url: string, - attributes?: Record -): void; +export default function insertImage(editor: IEditor, url: string): void; -export default function insertImage( - editor: IEditor, - imageFile: File | string, - attributes?: Record -): void { +export default function insertImage(editor: IEditor, imageFile: File | string): void { if (typeof imageFile == 'string') { - insertImageWithSrc(editor, imageFile, attributes); + insertImageWithSrc(editor, imageFile); } else { readFile(imageFile, dataUrl => { if (dataUrl && !editor.isDisposed()) { - insertImageWithSrc(editor, dataUrl, attributes); + insertImageWithSrc(editor, dataUrl); } }); } } -function insertImageWithSrc(editor: IEditor, src: string, attributes?: Record) { +function insertImageWithSrc(editor: IEditor, src: string) { editor.addUndoSnapshot(() => { const image = editor.getDocument().createElement('img'); image.src = src; - - if (attributes) { - for (const attribute in attributes) { - image.setAttribute(attribute, attributes[attribute]); - } - } - image.style.maxWidth = '100%'; editor.insertNode(image); }, ChangeSource.Format); From 7d5636fdf29f0febf3ce0990402a262676c1b711 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Wed, 11 May 2022 14:58:46 -0700 Subject: [PATCH 05/27] Add metadata API for roosterjs (#967) * Metadata API * add test * improve --- packages/roosterjs-editor-dom/lib/index.ts | 10 + .../lib/metadata/definitionCreators.ts | 99 ++++++ .../lib/metadata/metadata.ts | 65 ++++ .../lib/metadata/validate.ts | 60 ++++ .../lib/selection/setHtmlWithSelectionPath.ts | 86 +++-- .../test/metadata/definitionCreatorsTest.ts | 130 ++++++++ .../test/metadata/metadataTest.ts | 124 ++++++++ .../test/metadata/validateTest.ts | 295 ++++++++++++++++++ .../selections/deleteSelectedContentTest.ts | 4 +- .../lib/enum/DefinitionType.ts | 34 ++ .../roosterjs-editor-types/lib/enum/index.ts | 1 + .../lib/type/Definition.ts | 135 ++++++++ .../roosterjs-editor-types/lib/type/index.ts | 12 + tools/buildTools/dts.js | 17 +- 14 files changed, 1019 insertions(+), 53 deletions(-) create mode 100644 packages/roosterjs-editor-dom/lib/metadata/definitionCreators.ts create mode 100644 packages/roosterjs-editor-dom/lib/metadata/metadata.ts create mode 100644 packages/roosterjs-editor-dom/lib/metadata/validate.ts create mode 100644 packages/roosterjs-editor-dom/test/metadata/definitionCreatorsTest.ts create mode 100644 packages/roosterjs-editor-dom/test/metadata/metadataTest.ts create mode 100644 packages/roosterjs-editor-dom/test/metadata/validateTest.ts create mode 100644 packages/roosterjs-editor-types/lib/enum/DefinitionType.ts create mode 100644 packages/roosterjs-editor-types/lib/type/Definition.ts diff --git a/packages/roosterjs-editor-dom/lib/index.ts b/packages/roosterjs-editor-dom/lib/index.ts index 4b69ab20eba..8ad6466b313 100644 --- a/packages/roosterjs-editor-dom/lib/index.ts +++ b/packages/roosterjs-editor-dom/lib/index.ts @@ -112,3 +112,13 @@ export { default as setStyles } from './style/setStyles'; export { default as adjustInsertPosition } from './edit/adjustInsertPosition'; export { default as deleteSelectedContent } from './edit/deleteSelectedContent'; export { default as getTextContent } from './edit/getTextContent'; + +export { default as validate } from './metadata/validate'; +export { + createNumberDefinition, + createBooleanDefinition, + createStringDefinition, + createArrayDefinition, + createObjectDefinition, +} from './metadata/definitionCreators'; +export { getMetadata, setMetadata, removeMetadata } from './metadata/metadata'; diff --git a/packages/roosterjs-editor-dom/lib/metadata/definitionCreators.ts b/packages/roosterjs-editor-dom/lib/metadata/definitionCreators.ts new file mode 100644 index 00000000000..1c946b2c545 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/metadata/definitionCreators.ts @@ -0,0 +1,99 @@ +import { + Definition, + DefinitionType, + NumberDefinition, + ArrayDefinition, + BooleanDefinition, + StringDefinition, + ObjectDefinition, + ObjectPropertyDefinition, +} from 'roosterjs-editor-types'; + +/** + * Create a number definition + * @param isOptional Whether this property is optional + * @param value Optional value of the number + * @param minValue Optional minimum value + * @param maxValue Optional maximum value + * @returns The number definition object + */ +export function createNumberDefinition( + isOptional?: boolean, + value?: number, + minValue?: number, + maxValue?: number +): NumberDefinition { + return { + type: DefinitionType.Number, + isOptional, + value, + maxValue, + minValue, + }; +} + +/** + * Create a boolean definition + * @param isOptional Whether this property is optional + * @param value Optional expected boolean value + * @returns The boolean definition object + */ +export function createBooleanDefinition(isOptional?: boolean, value?: boolean): BooleanDefinition { + return { + type: DefinitionType.Boolean, + isOptional, + value, + }; +} + +/** + * Create a string definition + * @param isOptional Whether this property is optional + * @param value Optional expected string value + * @returns The string definition object + */ +export function createStringDefinition(isOptional?: boolean, value?: string): StringDefinition { + return { + type: DefinitionType.String, + isOptional, + value, + }; +} + +/** + * Create an array definition + * @param itemDef Definition of each item of the related array + * @param isOptional Whether this property is optional + * @returns The array definition object + */ +export function createArrayDefinition( + itemDef: Definition, + isOptional?: boolean, + minLength?: number, + maxLength?: number +): ArrayDefinition { + return { + type: DefinitionType.Array, + isOptional, + itemDef, + minLength, + maxLength, + }; +} + +/** + * Create an object definition + * @param propertyDef Definition of each property of the related object + * @param isOptional Whether this property is optional + * @returns The object definition object + */ +export function createObjectDefinition( + propertyDef: ObjectPropertyDefinition, + isOptional?: boolean +): ObjectDefinition { + return { + type: DefinitionType.Object, + isOptional, + propertyDef, + }; +} diff --git a/packages/roosterjs-editor-dom/lib/metadata/metadata.ts b/packages/roosterjs-editor-dom/lib/metadata/metadata.ts new file mode 100644 index 00000000000..fcb097fba15 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/metadata/metadata.ts @@ -0,0 +1,65 @@ +import validate from './validate'; +import { Definition } from 'roosterjs-editor-types'; + +const MetadataDataSetName = 'editingInfo'; + +/** + * Get metadata object from an HTML element + * @param element The HTML element to get metadata object from + * @param definition The type definition of this metadata used for validate this metadata object. + * If not specified, no validation will be performed and always return whatever we get from the element + * @param defaultValue The default value to return if the retrieved object cannot pass the validation, + * or there is no metadata object at all + * @returns The strong-type metadata object if it can be validated, or null + */ +export function getMetadata( + element: HTMLElement, + definition?: Definition, + defaultValue?: T +): T | null { + const str = element.dataset[MetadataDataSetName]; + let obj: any; + + try { + obj = str ? JSON.parse(str) : null; + } catch {} + + if (typeof obj !== 'undefined') { + if (!definition) { + return obj as T; + } else if (validate(obj, definition)) { + return obj; + } + } + + if (defaultValue) { + return defaultValue; + } else { + return null; + } +} + +/** + * Set metadata object into an HTML element + * @param element The HTML element to set metadata object to + * @param metadata The metadata object to set + * @param def An optional type definition object used for validate this metadata object. + * If not specified, metadata will be set without validation + * @returns True if metadata is set, otherwise false + */ +export function setMetadata(element: HTMLElement, metadata: T, def?: Definition): boolean { + if (!def || validate(metadata, def)) { + element.dataset[MetadataDataSetName] = JSON.stringify(metadata); + return true; + } else { + return false; + } +} + +/** + * Remove metadata from the given element if any + * @param element The element to remove metadata from + */ +export function removeMetadata(element: HTMLElement) { + delete element.dataset[MetadataDataSetName]; +} diff --git a/packages/roosterjs-editor-dom/lib/metadata/validate.ts b/packages/roosterjs-editor-dom/lib/metadata/validate.ts new file mode 100644 index 00000000000..f6c6de26680 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/metadata/validate.ts @@ -0,0 +1,60 @@ +import { Definition, DefinitionType } from 'roosterjs-editor-types'; + +/** + * Validate the given object with a type definition object + * @param input The object to validate + * @param def The type definition object used for validation + * @returns True if the object passed the validation, otherwise false + */ +export default function validate(input: any, def: Definition): input is T { + let result = false; + if (def.isOptional && typeof input === 'undefined') { + result = true; + } else { + switch (def.type) { + case DefinitionType.String: + result = + typeof input === 'string' && + (typeof def.value === 'undefined' || input === def.value); + break; + + case DefinitionType.Number: + result = + typeof input === 'number' && + (typeof def.value === 'undefined' || areSameNumbers(def.value, input)) && + (typeof def.minValue === 'undefined' || input >= def.minValue) && + (typeof def.maxValue === 'undefined' || input <= def.maxValue); + break; + + case DefinitionType.Boolean: + result = + typeof input === 'boolean' && + (typeof def.value === 'undefined' || input === def.value); + break; + + case DefinitionType.Array: + result = + Array.isArray(input) && + (typeof def.minLength === 'undefined' || input.length >= def.minLength) && + (typeof def.maxLength === 'undefined' || input.length <= def.maxLength) && + input.every(x => validate(x, def.itemDef)); + break; + + case DefinitionType.Object: + result = + typeof input === 'object' && + Object.keys(def.propertyDef).every(x => validate(input[x], def.propertyDef[x])); + break; + + case DefinitionType.Customize: + result = def.validator(input); + break; + } + } + + return result; +} + +function areSameNumbers(n1: number, n2: number) { + return Math.abs(n1 - n2) < 1e-3; +} diff --git a/packages/roosterjs-editor-dom/lib/selection/setHtmlWithSelectionPath.ts b/packages/roosterjs-editor-dom/lib/selection/setHtmlWithSelectionPath.ts index 42ee1239057..3049fa5ccc9 100644 --- a/packages/roosterjs-editor-dom/lib/selection/setHtmlWithSelectionPath.ts +++ b/packages/roosterjs-editor-dom/lib/selection/setHtmlWithSelectionPath.ts @@ -1,5 +1,13 @@ import createRange from './createRange'; import safeInstanceOf from '../utils/safeInstanceOf'; +import validate from '../metadata/validate'; +import { + createArrayDefinition, + createBooleanDefinition, + createNumberDefinition, + createObjectDefinition, + createStringDefinition, +} from '../metadata/definitionCreators'; import { ContentMetadata, SelectionRangeTypes, @@ -9,6 +17,30 @@ import { Coordinates, } from 'roosterjs-editor-types'; +const NumberArrayDefinition = createArrayDefinition(createNumberDefinition()); + +const CoordinatesDefinition = createObjectDefinition({ + x: createNumberDefinition(), + y: createNumberDefinition(), +}); + +const IsDarkModeDefinition = createBooleanDefinition(true /*isOptional*/); + +const NormalContentMetadataDefinition = createObjectDefinition({ + type: createNumberDefinition(true /*isOptional*/, SelectionRangeTypes.Normal), + isDarkMode: IsDarkModeDefinition, + start: NumberArrayDefinition, + end: NumberArrayDefinition, +}); + +const TableContentMetadataDefinition = createObjectDefinition({ + type: createNumberDefinition(false /*isOptional*/, SelectionRangeTypes.TableSelection), + isDarkMode: IsDarkModeDefinition, + tableId: createStringDefinition(), + firstCell: CoordinatesDefinition, + lastCell: CoordinatesDefinition, +}); + /** * @deprecated Use setHtmlWithMetadata instead * Restore inner HTML of a root element from given html string. If the string contains selection path, @@ -55,8 +87,14 @@ export function setHtmlWithMetadata( try { const obj = JSON.parse(potentialMetadataComment.nodeValue || ''); - if (isContentMetadata(obj)) { + if ( + validate(obj, NormalContentMetadataDefinition) || + validate(obj, TableContentMetadataDefinition) + ) { rootNode.removeChild(potentialMetadataComment); + obj.type = typeof obj.type === 'undefined' ? SelectionRangeTypes.Normal : obj.type; + obj.isDarkMode = obj.isDarkMode || false; + return obj; } } catch {} @@ -64,49 +102,3 @@ export function setHtmlWithMetadata( return undefined; } - -function isContentMetadata(obj: any): obj is ContentMetadata { - if (!obj || typeof obj != 'object') { - return false; - } - - switch (obj.type || SelectionRangeTypes.Normal) { - case SelectionRangeTypes.Normal: - const regularMetadata = obj as NormalContentMetadata; - if (isNumberArray(regularMetadata.start) && isNumberArray(regularMetadata.end)) { - obj.type = SelectionRangeTypes.Normal; - obj.isDarkMode = !!obj.isDarkMode; - return true; - } - break; - - case SelectionRangeTypes.TableSelection: - const tableMetadata = obj as TableContentMetadata; - if ( - typeof tableMetadata.tableId == 'string' && - !!tableMetadata.tableId && - isCoordinates(tableMetadata.firstCell) && - isCoordinates(tableMetadata.lastCell) - ) { - obj.isDarkMode = !!obj.isDarkMode; - return true; - } - break; - } - - return false; -} - -function isNumberArray(obj: any): obj is number[] { - return obj && Array.isArray(obj) && obj.every(o => typeof o == 'number'); -} - -function isCoordinates(obj: any): obj is Coordinates { - const coordinates = obj as Coordinates; - return ( - coordinates && - typeof coordinates == 'object' && - typeof coordinates.x == 'number' && - typeof coordinates.y == 'number' - ); -} diff --git a/packages/roosterjs-editor-dom/test/metadata/definitionCreatorsTest.ts b/packages/roosterjs-editor-dom/test/metadata/definitionCreatorsTest.ts new file mode 100644 index 00000000000..dc149977440 --- /dev/null +++ b/packages/roosterjs-editor-dom/test/metadata/definitionCreatorsTest.ts @@ -0,0 +1,130 @@ +import { Definition, DefinitionType, ObjectPropertyDefinition } from 'roosterjs-editor-types'; +import { + createNumberDefinition, + createBooleanDefinition, + createStringDefinition, + createArrayDefinition, + createObjectDefinition, +} from '../../lib/metadata/definitionCreators'; + +describe('createNumberDefinition', () => { + it('normal case', () => { + const def = createNumberDefinition(); + expect(def).toEqual({ + type: DefinitionType.Number, + isOptional: undefined, + value: undefined, + maxValue: undefined, + minValue: undefined, + }); + }); + + it('full case', () => { + const def = createNumberDefinition(true, 2, 1, 3); + expect(def).toEqual({ + type: DefinitionType.Number, + isOptional: true, + value: 2, + minValue: 1, + maxValue: 3, + }); + }); +}); + +describe('createBooleanDefinition', () => { + it('normal case', () => { + const def = createBooleanDefinition(); + expect(def).toEqual({ + type: DefinitionType.Boolean, + isOptional: undefined, + value: undefined, + }); + }); + + it('full case', () => { + const def = createBooleanDefinition(true, false); + expect(def).toEqual({ + type: DefinitionType.Boolean, + isOptional: true, + value: false, + }); + }); +}); + +describe('createStringDefinition', () => { + it('normal case', () => { + const def = createStringDefinition(); + expect(def).toEqual({ + type: DefinitionType.String, + isOptional: undefined, + value: undefined, + }); + }); + + it('full case', () => { + const def = createStringDefinition(true, 'test'); + expect(def).toEqual({ + type: DefinitionType.String, + isOptional: true, + value: 'test', + }); + }); +}); + +describe('createArrayDefinition', () => { + const itemDef: Definition = { + type: DefinitionType.Number, + }; + + it('normal case', () => { + const def = createArrayDefinition(itemDef); + expect(def).toEqual({ + type: DefinitionType.Array, + itemDef, + isOptional: undefined, + minLength: undefined, + maxLength: undefined, + }); + }); + + it('full case', () => { + const def = createArrayDefinition(itemDef, true, 1, 3); + expect(def).toEqual({ + type: DefinitionType.Array, + isOptional: true, + itemDef, + minLength: 1, + maxLength: 3, + }); + }); +}); + +interface TestType { + x: number; + y: string; +} + +describe('createObjectDefinition', () => { + const propertyDef: ObjectPropertyDefinition = { + x: { type: DefinitionType.Number }, + y: { type: DefinitionType.String }, + }; + + it('normal case', () => { + const def = createObjectDefinition(propertyDef); + expect(def).toEqual({ + type: DefinitionType.Object, + propertyDef, + isOptional: undefined, + }); + }); + + it('full case', () => { + const def = createObjectDefinition(propertyDef, true); + expect(def).toEqual({ + type: DefinitionType.Object, + isOptional: true, + propertyDef, + }); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/metadata/metadataTest.ts b/packages/roosterjs-editor-dom/test/metadata/metadataTest.ts new file mode 100644 index 00000000000..3dc99b78b8a --- /dev/null +++ b/packages/roosterjs-editor-dom/test/metadata/metadataTest.ts @@ -0,0 +1,124 @@ +import { CustomizeDefinition, DefinitionType } from 'roosterjs-editor-types'; +import { getMetadata, removeMetadata, setMetadata } from '../../lib/metadata/metadata'; + +describe('metadata', () => { + it('getMetadata gets a valid metadata', () => { + const validators = { + trueValidator: (input: any) => true, + falseValidator: (input: any) => false, + }; + const obj = { x: 1, y: 'test' }; + const validatorSpy = spyOn(validators, 'trueValidator').and.callThrough(); + const div = document.createElement('div'); + div.innerHTML = 'test'; + const node = div.firstChild as HTMLElement; + node.setAttribute('data-editing-info', JSON.stringify(obj)); + const def: CustomizeDefinition = { + type: DefinitionType.Customize, + validator: validators.trueValidator, + }; + + const metadata = getMetadata(node, def); + + expect(validatorSpy).toHaveBeenCalled(); + expect(metadata).toEqual(obj); + }); + + it('getMetadata gets an invalid metadata', () => { + const validators = { + trueValidator: (input: any) => true, + falseValidator: (input: any) => false, + }; + const obj = { x: 1, y: 'test' }; + const validatorSpy = spyOn(validators, 'falseValidator').and.callThrough(); + const div = document.createElement('div'); + div.innerHTML = 'test'; + const node = div.firstChild as HTMLElement; + + node.setAttribute('data-editing-info', JSON.stringify(obj)); + + const def: CustomizeDefinition = { + type: DefinitionType.Customize, + validator: validators.falseValidator, + }; + + const metadata = getMetadata(node, def); + + expect(validatorSpy).toHaveBeenCalled(); + expect(metadata).toBeNull(); + }); + + it('getMetadata gets an invalid metadata and return default value', () => { + const validators = { + trueValidator: (input: any) => true, + falseValidator: (input: any) => false, + }; + const obj = { x: 1, y: 'test' }; + const validatorSpy = spyOn(validators, 'falseValidator').and.callThrough(); + const div = document.createElement('div'); + div.innerHTML = 'test'; + const node = div.firstChild as HTMLElement; + + node.setAttribute('data-editing-info', JSON.stringify(obj)); + + const def: CustomizeDefinition = { + type: DefinitionType.Customize, + validator: validators.falseValidator, + }; + + const metadata = getMetadata(node, def, obj); + + expect(validatorSpy).toHaveBeenCalled(); + expect(metadata).toBe(obj); + }); + + it('setMetadata sets a valid metadata', () => { + const validators = { + trueValidator: (input: any) => true, + falseValidator: (input: any) => false, + }; + const obj = { x: 1, y: 'test' }; + const validatorSpy = spyOn(validators, 'trueValidator').and.callThrough(); + const node = document.createElement('div'); + const def: CustomizeDefinition = { + type: DefinitionType.Customize, + validator: validators.trueValidator, + }; + const result = setMetadata(node, obj, def); + + expect(validatorSpy).toHaveBeenCalled(); + expect(result).toBeTrue(); + expect(node.outerHTML).toBe( + '
' + ); + }); + + it('setMetadata sets an invalid metadata', () => { + const validators = { + trueValidator: (input: any) => true, + falseValidator: (input: any) => false, + }; + const validatorSpy = spyOn(validators, 'falseValidator').and.callThrough(); + const node = document.createElement('div'); + const def: CustomizeDefinition = { + type: DefinitionType.Customize, + validator: validators.falseValidator, + }; + const obj = { x: 1, y: 'test' }; + const result = setMetadata(node, obj, def); + + expect(validatorSpy).toHaveBeenCalled(); + expect(result).toBeFalse(); + expect(node.outerHTML).toBe('
'); + }); +}); + +describe('removeMetadata', () => { + it('removeElement', () => { + const obj = { x: 1, y: 'test' }; + const div = document.createElement('div'); + div.setAttribute('data-editing-info', JSON.stringify(obj)); + removeMetadata(div); + expect(div.outerHTML).toBe('
'); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/metadata/validateTest.ts b/packages/roosterjs-editor-dom/test/metadata/validateTest.ts new file mode 100644 index 00000000000..32689ec700b --- /dev/null +++ b/packages/roosterjs-editor-dom/test/metadata/validateTest.ts @@ -0,0 +1,295 @@ +import validate from '../../lib/metadata/validate'; +import { + Definition, + ObjectPropertyDefinition, + PluginEventType, + DefinitionType, +} from 'roosterjs-editor-types'; +import { + createArrayDefinition, + createBooleanDefinition, + createNumberDefinition, + createObjectDefinition, + createStringDefinition, +} from '../../lib/metadata/definitionCreators'; + +describe('validate', () => { + function runTestInternal(input: any, def: Definition, result: boolean) { + expect(validate(input, def)).toBe(result); + } + + function runNumberTest( + input: any, + resultForRequired: boolean, + resultForOptional: boolean, + value?: number + ) { + const requiredDef = createNumberDefinition(false, value); + const optionalDef = createNumberDefinition(true, value); + runTestInternal(input, requiredDef, resultForRequired); + runTestInternal(input, optionalDef, resultForOptional); + } + + function runStringTest( + input: any, + resultForRequired: boolean, + resultForOptional: boolean, + value?: string + ) { + const requiredDef = createStringDefinition(false, value); + const optionalDef = createStringDefinition(true, value); + runTestInternal(input, requiredDef, resultForRequired); + runTestInternal(input, optionalDef, resultForOptional); + } + + function runBooleanTest( + input: any, + resultForRequired: boolean, + resultForOptional: boolean, + value?: boolean + ) { + const requiredDef = createBooleanDefinition(false, value); + const optionalDef = createBooleanDefinition(true, value); + runTestInternal(input, requiredDef, resultForRequired); + runTestInternal(input, optionalDef, resultForOptional); + } + + function runArrayTest( + input: any, + resultForRequired: boolean, + resultForOptional: boolean, + minLength?: number, + maxLength?: number + ) { + const itemDef = createNumberDefinition(); + const requiredDef = createArrayDefinition(itemDef, false, minLength, maxLength); + const optionalDef = createArrayDefinition(itemDef, true, minLength, maxLength); + runTestInternal(input, requiredDef, resultForRequired); + runTestInternal(input, optionalDef, resultForOptional); + } + + interface TestObj { + x: number; + y: string; + } + + function runObjectTest(input: any, resultForRequired: boolean, resultForOptional: boolean) { + const propertyDef: ObjectPropertyDefinition = { + x: createNumberDefinition(), + y: createStringDefinition(), + }; + const requiredDef = createObjectDefinition(propertyDef, false); + const optionalDef = createObjectDefinition(propertyDef, true); + runTestInternal(input, requiredDef, resultForRequired); + runTestInternal(input, optionalDef, resultForOptional); + } + + it('Validate number', () => { + runNumberTest(0, true, true); + runNumberTest(0, true, true, 0); + runNumberTest(0, true, true, 0.000001); + runNumberTest(0, false, false, 1); + runNumberTest(undefined, false, true); + runNumberTest(null, false, false); + runNumberTest('test', false, false); + runNumberTest(PluginEventType.EditorReady, true, true); + runNumberTest(true, false, false); + runNumberTest({}, false, false); + runNumberTest([], false, false); + runNumberTest({ x: 1 }, false, false); + runNumberTest([1], false, false); + }); + + it('Validate string', () => { + runStringTest('test', true, true); + runStringTest('test', true, true, 'test'); + runStringTest('test', false, false, 'test1'); + runStringTest(undefined, false, true); + runStringTest(null, false, false); + runStringTest(1, false, false); + runStringTest(PluginEventType.EditorReady, false, false); + runStringTest(true, false, false); + runStringTest({}, false, false); + runStringTest([], false, false); + runStringTest({ x: 1 }, false, false); + runStringTest([1], false, false); + }); + + it('Validate boolean', () => { + runBooleanTest(true, true, true); + runBooleanTest(true, true, true, true); + runBooleanTest(true, false, false, false); + runBooleanTest(undefined, false, true); + runBooleanTest(null, false, false); + runBooleanTest(1, false, false); + runBooleanTest(PluginEventType.EditorReady, false, false); + runBooleanTest('test', false, false); + runBooleanTest({}, false, false); + runBooleanTest([], false, false); + runBooleanTest({ x: 1 }, false, false); + runBooleanTest([1], false, false); + }); + + it('Validate array', () => { + runArrayTest([], true, true); + runArrayTest(undefined, false, true); + runArrayTest([1, 2, 3], true, true); + runArrayTest([1, 2, 'test'], false, false); + runArrayTest([null], false, false); + runArrayTest([1, 2], true, true, 0, 3); + runArrayTest([1, 2], false, false, 3); + runArrayTest([1, 2], false, false, undefined, 1); + runArrayTest(true, false, false); + runArrayTest(null, false, false); + runArrayTest(1, false, false); + runArrayTest(PluginEventType.EditorReady, false, false); + runArrayTest('test', false, false); + runArrayTest({}, false, false); + runArrayTest({ x: 1 }, false, false); + }); + + it('Validate object', () => { + runObjectTest({ x: 1, y: 'test' }, true, true); + runObjectTest(undefined, false, true); + runObjectTest({ x: 1, y: 2 }, false, false); + runObjectTest({ x: 1 }, false, false); + runObjectTest([], false, false); + runObjectTest(true, false, false); + runObjectTest(1, false, false); + runObjectTest('test', false, false); + }); + + interface TestObj2 { + a: number[]; + b?: TestObj; + } + + it('Validate object 2', () => { + const def: Definition = createObjectDefinition({ + a: createArrayDefinition(createNumberDefinition()), + b: createObjectDefinition( + { + x: createNumberDefinition(), + y: createStringDefinition(), + }, + true + ), + }); + + expect( + validate( + { + a: [1, 2, 3], + b: { + x: 1, + y: 'test', + }, + }, + def + ) + ).toBeTrue(); + expect( + validate( + { + a: [1, 2, 3], + }, + def + ) + ).toBeTrue(); + expect( + validate( + { + a: [1, 2, 3, 'test'], + b: { + x: 1, + y: 'test', + }, + }, + def + ) + ).toBeFalse(); + expect( + validate( + { + a: null, + b: { + x: 1, + y: 'test', + }, + }, + def + ) + ).toBeFalse(); + expect( + validate( + { + a: [1, 2, 3], + b: { + x: 1, + y: 'test', + }, + c: 0, + }, + def + ) + ).toBeTrue(); + }); +}); + +describe('Validate customize', () => { + it('Validate true', () => { + const validators = { + trueValidator: (input: any) => true, + falseValidator: (input: any) => false, + }; + const trueSpy = spyOn(validators, 'trueValidator').and.callThrough(); + const input = {}; + const result = validate( + {}, + { type: DefinitionType.Customize, validator: validators.trueValidator } + ); + + expect(result).toBe(true); + expect(trueSpy).toHaveBeenCalledWith(input); + }); + + it('Validate false', () => { + const validators = { + trueValidator: (input: any) => true, + falseValidator: (input: any) => false, + }; + const falseSpy = spyOn(validators, 'falseValidator').and.callThrough(); + const input = {}; + const result = validate( + {}, + { type: DefinitionType.Customize, validator: validators.falseValidator } + ); + + expect(result).toBe(false); + expect(falseSpy).toHaveBeenCalledWith(input); + }); + + it('Validate object', () => { + interface TestObj { + name: string; + value: number; + } + const validators = { + trueValidator: (input: any) => true, + falseValidator: (input: any) => false, + }; + + const trueSpy = spyOn(validators, 'trueValidator').and.callThrough(); + const def = createObjectDefinition({ + name: createStringDefinition(), + value: { + type: DefinitionType.Customize, + validator: validators.trueValidator, + }, + }); + const result = validate({ name: 'test', value: 1 }, def); + + expect(result).toBeTrue(); + expect(trueSpy).toHaveBeenCalledWith(1); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/selections/deleteSelectedContentTest.ts b/packages/roosterjs-editor-dom/test/selections/deleteSelectedContentTest.ts index 6a394b48c23..56061c287c1 100644 --- a/packages/roosterjs-editor-dom/test/selections/deleteSelectedContentTest.ts +++ b/packages/roosterjs-editor-dom/test/selections/deleteSelectedContentTest.ts @@ -81,14 +81,14 @@ describe('deleteSelectedContent', () => { ); }); - it('Whole talbe 1', () => { + it('Whole table 1', () => { runTest( 'aa
line1line2
line3line4
bb', 'aabb' ); }); - it('Whole talbe 2', () => { + it('Whole table 2', () => { // TODO: the result contains separated continuous text object at root // Selection path gives wrong result. Need to revisit here runTest( diff --git a/packages/roosterjs-editor-types/lib/enum/DefinitionType.ts b/packages/roosterjs-editor-types/lib/enum/DefinitionType.ts new file mode 100644 index 00000000000..0a808a52732 --- /dev/null +++ b/packages/roosterjs-editor-types/lib/enum/DefinitionType.ts @@ -0,0 +1,34 @@ +/** + * Types of definitions, used by Definition type + */ +export const enum DefinitionType { + /** + * Boolean type definition, represents a boolean type value + */ + Boolean, + + /** + * Number type definition, represents a number type value + */ + Number, + + /** + * String type definition, represents a string type value + */ + String, + + /** + * Array type definition, represents an array with a given item type + */ + Array, + + /** + * Object type definition, represents an object with the given property types + */ + Object, + + /** + * Customize type definition, represents a customized type with a validator function + */ + Customize, +} diff --git a/packages/roosterjs-editor-types/lib/enum/index.ts b/packages/roosterjs-editor-types/lib/enum/index.ts index fcd44de00e5..fd751963e1d 100644 --- a/packages/roosterjs-editor-types/lib/enum/index.ts +++ b/packages/roosterjs-editor-types/lib/enum/index.ts @@ -27,3 +27,4 @@ export { KnownCreateElementDataIndex } from './KnownCreateElementDataIndex'; export { TableBorderFormat } from './TableBorderFormat'; export { PluginEventType } from './PluginEventType'; export { SelectionRangeTypes } from './SelectionRangeTypes'; +export { DefinitionType } from './DefinitionType'; diff --git a/packages/roosterjs-editor-types/lib/type/Definition.ts b/packages/roosterjs-editor-types/lib/type/Definition.ts new file mode 100644 index 00000000000..678e0950fc0 --- /dev/null +++ b/packages/roosterjs-editor-types/lib/type/Definition.ts @@ -0,0 +1,135 @@ +import { DefinitionType } from '../enum/DefinitionType'; +import type { CompatibleDefinitionType } from '../compatibleEnum/DefinitionType'; + +/** + * A type template to get item type of an array + */ +export type ArrayItemType = T extends (infer U)[] ? U : never; + +/** + * Base interface of property definition + */ +export interface DefinitionBase { + /** + * Type of this property + */ + type: T; + + /** + * Whether this property is optional + */ + isOptional?: boolean; +} + +/** + * String property definition. This definition can also be used for string based enum property + */ +export interface StringDefinition + extends DefinitionBase { + /** + * An optional value of this property. When specified, the given property must have exactly same value of this value + */ + value?: string; +} + +/** + * Number property definition. This definition can also be used for number based enum property + */ +export interface NumberDefinition + extends DefinitionBase { + /** + * An optional value of this property. When specified, the given property must have same value of this value + */ + value?: number; + + /** + * An optional minimum value of this property. When specified, the given property must be greater or equal to this value + */ + minValue?: number; + + /** + * An optional maximum value of this property. When specified, the given property must be less or equal to this value + */ + maxValue?: number; +} + +/** + * Boolean property definition + */ +export interface BooleanDefinition + extends DefinitionBase { + /** + * An optional value of this property. When specified, the given property must have same value of this value + */ + value?: boolean; +} + +/** + * Array property definition. + */ +export interface ArrayDefinition + extends DefinitionBase { + /** + * Definition of each item of this array. All items of the given array must have the same type. Otherwise, use CustomizeDefinition instead. + */ + itemDef: Definition>; + + /** + * An optional minimum length of this array. When specified, the given array must have at least this value of items + */ + minLength?: number; + + /** + * An optional maximum length of this array. When specified, the given array must have at most this value of items + */ + maxLength?: number; +} + +/** + * Object property definition type used by Object Definition + */ +export type ObjectPropertyDefinition = { + [Key in keyof T]: Definition; +}; + +/** + * Object property definition. + */ +export interface ObjectDefinition + extends DefinitionBase { + /** + * A key-value map to specify the definition of each possible property of this object + */ + propertyDef: ObjectPropertyDefinition; +} + +/** + * Customize property definition. When all other property definition type cannot satisfy your requirement, + * use this definition with a customized validator function to do property validation. + */ +export interface CustomizeDefinition + extends DefinitionBase { + /** + * The customized validator function to do customized validation + * @param input The value to validate + * @returns True means the given value is of the specified type, otherwise false + */ + validator: (input: any) => boolean; +} + +/** + * A combination of all definition types + */ +export type Definition = + | CustomizeDefinition + | (T extends any[] + ? ArrayDefinition + : T extends Record + ? ObjectDefinition + : T extends String + ? StringDefinition + : T extends Number + ? NumberDefinition + : T extends Boolean + ? BooleanDefinition + : never); diff --git a/packages/roosterjs-editor-types/lib/type/index.ts b/packages/roosterjs-editor-types/lib/type/index.ts index b501c9c8fed..9a6b8815f5f 100644 --- a/packages/roosterjs-editor-types/lib/type/index.ts +++ b/packages/roosterjs-editor-types/lib/type/index.ts @@ -11,3 +11,15 @@ export { 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'; diff --git a/tools/buildTools/dts.js b/tools/buildTools/dts.js index 4495b9098d6..94d85c751c5 100644 --- a/tools/buildTools/dts.js +++ b/tools/buildTools/dts.js @@ -273,6 +273,10 @@ function parseImportFrom(content, currentFileName, queue, baseDir, projDir, exte return newContent.replace(regImportFrom, ''); } +function parseEmptyExport(content) { + return content.replace(/export \{\};/g, ''); +} + function process(baseDir, queue, index, projDir, externalHandler) { var item = queue[index]; var currentFileName = item.filename; @@ -286,10 +290,15 @@ function process(baseDir, queue, index, projDir, externalHandler) { content = parseImportFrom(content, currentFileName, queue, baseDir, projDir, externalHandler); // 3. Parse all the public elements - content = [parseClasses, parseFunctions, parseEnum, parseType, parseConst, parseExport].reduce( - (c, func) => func(c, item.elements), - content - ); + content = [ + parseClasses, + parseFunctions, + parseEnum, + parseType, + parseConst, + parseExport, + parseEmptyExport, + ].reduce((c, func) => func(c, item.elements), content); // 4. Remove single line comments content = content.replace(singleLineComment, ''); From 3463085d7e5da135269a8583b92edce7b2f91b33 Mon Sep 17 00:00:00 2001 From: marcinkiewicz Date: Wed, 11 May 2022 16:36:43 -0700 Subject: [PATCH 06/27] Add image attributes. (#973) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Przemyslaw Marcinkiewicz 🦒 --- .../lib/format/insertImage.ts | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/roosterjs-editor-api/lib/format/insertImage.ts b/packages/roosterjs-editor-api/lib/format/insertImage.ts index 688638b9d3f..0e87aa65501 100644 --- a/packages/roosterjs-editor-api/lib/format/insertImage.ts +++ b/packages/roosterjs-editor-api/lib/format/insertImage.ts @@ -6,32 +6,53 @@ import { readFile } from 'roosterjs-editor-dom'; * @param editor The editor instance * @param imageFile The image file. There are at least 3 ways to obtain the file object: * From local file, from clipboard data, from drag-and-drop + * @param attributes Optional image element attributes */ -export default function insertImage(editor: IEditor, imageFile: File): void; +export default function insertImage( + editor: IEditor, + imageFile: File, + attributes?: Record +): void; /** * Insert an image to editor at current selection * @param editor The editor instance - * @param imageFile The image link. + * @param url The image link + * @param attributes Optional image element attributes */ -export default function insertImage(editor: IEditor, url: string): void; +export default function insertImage( + editor: IEditor, + url: string, + attributes?: Record +): void; -export default function insertImage(editor: IEditor, imageFile: File | string): void { +export default function insertImage( + editor: IEditor, + imageFile: File | string, + attributes?: Record +): void { if (typeof imageFile == 'string') { - insertImageWithSrc(editor, imageFile); + insertImageWithSrc(editor, imageFile, attributes); } else { readFile(imageFile, dataUrl => { if (dataUrl && !editor.isDisposed()) { - insertImageWithSrc(editor, dataUrl); + insertImageWithSrc(editor, dataUrl, attributes); } }); } } -function insertImageWithSrc(editor: IEditor, src: string) { +function insertImageWithSrc(editor: IEditor, src: string, attributes?: Record) { editor.addUndoSnapshot(() => { const image = editor.getDocument().createElement('img'); image.src = src; + + if (attributes) { + Object.keys(attributes).forEach(attribute => + image.setAttribute(attribute, attributes[attribute]) + ); + } + image.style.maxWidth = '100%'; editor.insertNode(image); }, ChangeSource.Format); From a644b5377c899f7aa21302690e4fb05335c6e2d2 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Wed, 11 May 2022 16:42:02 -0700 Subject: [PATCH 07/27] Log more info for exception from SelectionBlockScoper (#971) --- .../contentTraverser/SelectionBlockScoper.ts | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/roosterjs-editor-dom/lib/contentTraverser/SelectionBlockScoper.ts b/packages/roosterjs-editor-dom/lib/contentTraverser/SelectionBlockScoper.ts index 357dc0bbaca..1ccff11c4b5 100644 --- a/packages/roosterjs-editor-dom/lib/contentTraverser/SelectionBlockScoper.ts +++ b/packages/roosterjs-editor-dom/lib/contentTraverser/SelectionBlockScoper.ts @@ -35,9 +35,27 @@ export default class SelectionBlockScoper implements TraversingScoper { position: NodePosition | Range, private startFrom: ContentPosition | CompatibleContentPosition ) { - position = safeInstanceOf(position, 'Range') ? Position.getStart(position) : position; - this.position = position.normalize(); - this.block = getBlockElementAtNode(this.rootNode, this.position.node); + // Debugging info, will be removed later + let isPosition = false; + + if (safeInstanceOf(position, 'Range')) { + position = Position.getStart(position); + } else { + isPosition = true; + } + + try { + this.position = position.normalize(); + this.block = getBlockElementAtNode(this.rootNode, this.position.node); + } catch (e) { + throw new Error( + `${ + (e as any)?.message + }; isPosition: ${isPosition}; actual type: ${typeof position}; String name: ${ + typeof position?.toString === 'function' ? position.toString() : 'No toString()' + }` + ); + } } /** From 9be597bfebcde12084633a746fee952aed2f6214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Thu, 12 May 2022 17:07:30 -0300 Subject: [PATCH 08/27] add metadata to table format info --- .../lib/metadata/definitionCreators.ts | 11 +- .../lib/metadata/validate.ts | 2 +- .../lib/table/tableFormatInfo.ts | 128 ++++++++---------- .../test/metadata/definitionCreatorsTest.ts | 28 +++- .../test/table/tableFormatInfoTest.ts | 2 +- .../lib/type/Definition.ts | 5 + 6 files changed, 98 insertions(+), 78 deletions(-) diff --git a/packages/roosterjs-editor-dom/lib/metadata/definitionCreators.ts b/packages/roosterjs-editor-dom/lib/metadata/definitionCreators.ts index 1c946b2c545..95ad0f2fe5f 100644 --- a/packages/roosterjs-editor-dom/lib/metadata/definitionCreators.ts +++ b/packages/roosterjs-editor-dom/lib/metadata/definitionCreators.ts @@ -52,11 +52,16 @@ export function createBooleanDefinition(isOptional?: boolean, value?: boolean): * @param value Optional expected string value * @returns The string definition object */ -export function createStringDefinition(isOptional?: boolean, value?: string): StringDefinition { +export function createStringDefinition( + isOptional?: boolean, + value?: string, + allowNull?: boolean +): StringDefinition { return { type: DefinitionType.String, isOptional, value, + allowNull, }; } @@ -89,11 +94,13 @@ export function createArrayDefinition( */ export function createObjectDefinition( propertyDef: ObjectPropertyDefinition, - isOptional?: boolean + isOptional?: boolean, + allowNull?: boolean ): ObjectDefinition { return { type: DefinitionType.Object, isOptional, propertyDef, + allowNull, }; } diff --git a/packages/roosterjs-editor-dom/lib/metadata/validate.ts b/packages/roosterjs-editor-dom/lib/metadata/validate.ts index f6c6de26680..d5ca6266610 100644 --- a/packages/roosterjs-editor-dom/lib/metadata/validate.ts +++ b/packages/roosterjs-editor-dom/lib/metadata/validate.ts @@ -8,7 +8,7 @@ import { Definition, DefinitionType } from 'roosterjs-editor-types'; */ export default function validate(input: any, def: Definition): input is T { let result = false; - if (def.isOptional && typeof input === 'undefined') { + if ((def.isOptional && typeof input === 'undefined') || (def.allowNull && !input)) { result = true; } else { switch (def.type) { diff --git a/packages/roosterjs-editor-dom/lib/table/tableFormatInfo.ts b/packages/roosterjs-editor-dom/lib/table/tableFormatInfo.ts index 2fcf8482a4a..053fa47c34f 100644 --- a/packages/roosterjs-editor-dom/lib/table/tableFormatInfo.ts +++ b/packages/roosterjs-editor-dom/lib/table/tableFormatInfo.ts @@ -1,6 +1,59 @@ +import { getMetadata, setMetadata } from '../metadata/metadata'; import { TableFormat } from 'roosterjs-editor-types'; +import { + createBooleanDefinition, + createNumberDefinition, + createObjectDefinition, + createStringDefinition, +} from '../metadata/definitionCreators'; -const TABLE_STYLE_INFO = 'roosterTableInfo'; +const tableFormatDefinition = createObjectDefinition>( + { + topBorderColor: createStringDefinition( + false /** isOptional */, + undefined /** value */, + true /** allowNull */ + ), + bottomBorderColor: createStringDefinition( + false /** isOptional */, + undefined /** value */, + true /** allowNull */ + ), + verticalBorderColor: createStringDefinition( + false /** isOptional */, + undefined /** value */, + true /** allowNull */ + ), + hasHeaderRow: createBooleanDefinition(false /** isOptional */), + headerRowColor: createStringDefinition( + false /** isOptional */, + undefined /** value */, + true /** allowNull */ + ), + hasFirstColumn: createBooleanDefinition(false /** isOptional */), + hasBandedColumns: createBooleanDefinition(false /** isOptional */), + hasBandedRows: createBooleanDefinition(false /** isOptional */), + bgColorEven: createStringDefinition( + false /** isOptional */, + undefined /** value */, + true /** allowNull */ + ), + bgColorOdd: createStringDefinition( + false /** isOptional */, + undefined /** value */, + true /** allowNull */ + ), + tableBorderFormat: createNumberDefinition( + false /** isOptional */, + undefined /* value */, + 0, + 7 + ), + keepCellShade: createBooleanDefinition(false /** isOptional */), + }, + false /* isOptional */, + true /** allowNull */ +); /** * @internal @@ -9,8 +62,7 @@ const TABLE_STYLE_INFO = 'roosterTableInfo'; * @param table The table that has the info */ export function getTableFormatInfo(table: HTMLTableElement) { - const obj = safeParseJSON(table?.dataset[TABLE_STYLE_INFO]); - return checkIfTableFormatIsValid(obj) ? obj : null; + return getMetadata(table, tableFormatDefinition); } /** @@ -21,74 +73,6 @@ export function getTableFormatInfo(table: HTMLTableElement) { */ export function saveTableInfo(table: HTMLTableElement, format: TableFormat) { if (table && format) { - table.dataset[TABLE_STYLE_INFO] = JSON.stringify(format); - } -} - -function checkIfTableFormatIsValid(format: any): format is Required { - if (!format) { - return false; - } - const { - topBorderColor, - verticalBorderColor, - bottomBorderColor, - bgColorOdd, - bgColorEven, - hasBandedColumns, - hasBandedRows, - hasFirstColumn, - hasHeaderRow, - tableBorderFormat, - } = format; - const colorsValues = [ - topBorderColor, - verticalBorderColor, - bottomBorderColor, - bgColorOdd, - bgColorEven, - ]; - const stateValues = [hasBandedColumns, hasBandedRows, hasFirstColumn, hasHeaderRow]; - - if ( - colorsValues.some(key => !isAValidColor(key)) || - stateValues.some(key => !isBoolean(key)) || - !isAValidTableBorderType(tableBorderFormat) - ) { - return false; - } - - return true; -} - -function isAValidColor(color: any) { - if (color === null || color === undefined || typeof color === 'string') { - return true; - } - return false; -} - -function isBoolean(a: any) { - if (typeof a === 'boolean') { - return true; - } - return false; -} - -function isAValidTableBorderType(border: any) { - if (-1 < border && border < 8) { - return true; - } - return false; -} - -function safeParseJSON(json: string | undefined): any { - if (!json) { - return null; - } - try { - return JSON.parse(json); - } catch { - return null; + setMetadata(table, format); } } diff --git a/packages/roosterjs-editor-dom/test/metadata/definitionCreatorsTest.ts b/packages/roosterjs-editor-dom/test/metadata/definitionCreatorsTest.ts index dc149977440..b1f91934a15 100644 --- a/packages/roosterjs-editor-dom/test/metadata/definitionCreatorsTest.ts +++ b/packages/roosterjs-editor-dom/test/metadata/definitionCreatorsTest.ts @@ -58,15 +58,27 @@ describe('createStringDefinition', () => { type: DefinitionType.String, isOptional: undefined, value: undefined, + allowNull: undefined, }); }); - it('full case', () => { + it('optional case', () => { const def = createStringDefinition(true, 'test'); expect(def).toEqual({ type: DefinitionType.String, isOptional: true, value: 'test', + allowNull: undefined, + }); + }); + + it('full case', () => { + const def = createStringDefinition(true, 'test', true); + expect(def).toEqual({ + type: DefinitionType.String, + isOptional: true, + value: 'test', + allowNull: true, }); }); }); @@ -116,15 +128,27 @@ describe('createObjectDefinition', () => { type: DefinitionType.Object, propertyDef, isOptional: undefined, + allowNull: undefined, }); }); - it('full case', () => { + it('isOptional case', () => { const def = createObjectDefinition(propertyDef, true); expect(def).toEqual({ type: DefinitionType.Object, isOptional: true, propertyDef, + allowNull: undefined, + }); + }); + + it('full case', () => { + const def = createObjectDefinition(propertyDef, true, true); + expect(def).toEqual({ + type: DefinitionType.Object, + isOptional: true, + propertyDef, + allowNull: true, }); }); }); diff --git a/packages/roosterjs-editor-dom/test/table/tableFormatInfoTest.ts b/packages/roosterjs-editor-dom/test/table/tableFormatInfoTest.ts index 4c8de881ceb..a17314cb93b 100644 --- a/packages/roosterjs-editor-dom/test/table/tableFormatInfoTest.ts +++ b/packages/roosterjs-editor-dom/test/table/tableFormatInfoTest.ts @@ -2,7 +2,7 @@ import VTable from '../../lib/table/VTable'; import { getTableFormatInfo, saveTableInfo } from '../../lib/table/tableFormatInfo'; import { TableFormat } from 'roosterjs-editor-types'; -const TABLE_STYLE_INFO = 'roosterTableInfo'; +const TABLE_STYLE_INFO = 'editingInfo'; const format: TableFormat = { topBorderColor: '#0C64C0', bottomBorderColor: '#0C64C0', diff --git a/packages/roosterjs-editor-types/lib/type/Definition.ts b/packages/roosterjs-editor-types/lib/type/Definition.ts index 678e0950fc0..20756f7e6ad 100644 --- a/packages/roosterjs-editor-types/lib/type/Definition.ts +++ b/packages/roosterjs-editor-types/lib/type/Definition.ts @@ -19,6 +19,11 @@ export interface DefinitionBase Date: Thu, 12 May 2022 17:17:26 -0300 Subject: [PATCH 09/27] remane table metadata --- packages/roosterjs-editor-dom/lib/table/tableFormatInfo.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/roosterjs-editor-dom/lib/table/tableFormatInfo.ts b/packages/roosterjs-editor-dom/lib/table/tableFormatInfo.ts index 053fa47c34f..cd29c0b9f5a 100644 --- a/packages/roosterjs-editor-dom/lib/table/tableFormatInfo.ts +++ b/packages/roosterjs-editor-dom/lib/table/tableFormatInfo.ts @@ -7,7 +7,7 @@ import { createStringDefinition, } from '../metadata/definitionCreators'; -const tableFormatDefinition = createObjectDefinition>( +const TableFormatMetadata = createObjectDefinition>( { topBorderColor: createStringDefinition( false /** isOptional */, @@ -62,7 +62,7 @@ const tableFormatDefinition = createObjectDefinition>( * @param table The table that has the info */ export function getTableFormatInfo(table: HTMLTableElement) { - return getMetadata(table, tableFormatDefinition); + return getMetadata(table, TableFormatMetadata); } /** @@ -73,6 +73,6 @@ export function getTableFormatInfo(table: HTMLTableElement) { */ export function saveTableInfo(table: HTMLTableElement, format: TableFormat) { if (table && format) { - setMetadata(table, format); + setMetadata(table, format, TableFormatMetadata); } } From d3ff5a5a445c072e420d20039fefc872b19a7285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Thu, 12 May 2022 17:52:06 -0300 Subject: [PATCH 10/27] fix unit --- .../roosterjs-editor-dom/test/table/applyTableFormatTest.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/roosterjs-editor-dom/test/table/applyTableFormatTest.ts b/packages/roosterjs-editor-dom/test/table/applyTableFormatTest.ts index 0083b34454e..bb4feb4c67d 100644 --- a/packages/roosterjs-editor-dom/test/table/applyTableFormatTest.ts +++ b/packages/roosterjs-editor-dom/test/table/applyTableFormatTest.ts @@ -20,9 +20,10 @@ const format: Required = { describe('applyTableFormat', () => { let table = - '









'; + '









'; let expectedTableChrome = - '









'; + '









'; + let div = document.createElement('div'); document.body.appendChild(div); const id = 'id1'; From 573f33e3d8cb6ad9fb957ceb016f5bc02bfa927b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Thu, 12 May 2022 18:04:01 -0300 Subject: [PATCH 11/27] fix unit --- .../roosterjs-editor-dom/test/table/applyTableFormatTest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/roosterjs-editor-dom/test/table/applyTableFormatTest.ts b/packages/roosterjs-editor-dom/test/table/applyTableFormatTest.ts index bb4feb4c67d..b47e227c3f5 100644 --- a/packages/roosterjs-editor-dom/test/table/applyTableFormatTest.ts +++ b/packages/roosterjs-editor-dom/test/table/applyTableFormatTest.ts @@ -20,9 +20,9 @@ const format: Required = { describe('applyTableFormat', () => { let table = - '









'; + '









'; let expectedTableChrome = - '









'; + '









'; let div = document.createElement('div'); document.body.appendChild(div); From e9446891b4c21d6d6bc7461a37674b951d9e1375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Thu, 12 May 2022 21:11:30 -0300 Subject: [PATCH 12/27] add sanitize to remove deprecated colors --- .../lib/plugins/Paste/Paste.ts | 2 + .../deprecatedColorList.ts | 30 +++++++ .../sanitizeHtmlTextFromPastedContent.ts | 27 ++++++ .../test/paste/sanitizeHtmlTextTest.ts | 90 +++++++++++++++++++ .../word/convertPastedContentFromWordTest.ts | 8 +- 5 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlTextFromPastedContent/deprecatedColorList.ts create mode 100644 packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlTextFromPastedContent/sanitizeHtmlTextFromPastedContent.ts create mode 100644 packages/roosterjs-editor-plugins/test/paste/sanitizeHtmlTextTest.ts diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/Paste.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/Paste.ts index ae3b4c4d2d6..a87640e6117 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Paste/Paste.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/Paste.ts @@ -4,6 +4,7 @@ import convertPastedContentFromExcel from './excelConverter/convertPastedContent import convertPastedContentFromPowerPoint from './pptConverter/convertPastedContentFromPowerPoint'; import convertPastedContentFromWord from './wordConverter/convertPastedContentFromWord'; import handleLineMerge from './lineMerge/handleLineMerge'; +import sanitizeHtmlTextFromPastedContent from './sanitizeHtmlTextFromPastedContent/sanitizeHtmlTextFromPastedContent'; import { toArray } from 'roosterjs-editor-dom'; import { EditorPlugin, @@ -75,6 +76,7 @@ export default class Paste implements EditorPlugin { const trustedHTMLHandler = this.editor.getTrustedHTMLHandler(); let wacListElements: Node[]; + sanitizeHtmlTextFromPastedContent(fragment, sanitizingOption); if (isWordDocument(htmlAttributes)) { // Handle HTML copied from Word convertPastedContentFromWord(event); diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlTextFromPastedContent/deprecatedColorList.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlTextFromPastedContent/deprecatedColorList.ts new file mode 100644 index 00000000000..05588a27f19 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlTextFromPastedContent/deprecatedColorList.ts @@ -0,0 +1,30 @@ +/** + * @internal + * List of deprecated colors that should be removed + */ + +export const DeprecatedColorList: string[] = [ + 'activeborder', + 'activecaption', + 'appworkspace', + 'background', + 'buttonhighlight', + 'buttonshadow', + 'captiontext', + 'inactiveborder', + 'inactivecaption', + 'inactivecaptiontext', + 'infobackground', + 'infotext', + 'menu', + 'menutext', + 'scrollbar', + 'threeddarkshadow', + 'threedface', + 'threedhighlight', + 'threedlightshadow', + 'threedfhadow', + 'window', + 'windowframe', + 'windowtext', +]; diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlTextFromPastedContent/sanitizeHtmlTextFromPastedContent.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlTextFromPastedContent/sanitizeHtmlTextFromPastedContent.ts new file mode 100644 index 00000000000..a073d83a316 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlTextFromPastedContent/sanitizeHtmlTextFromPastedContent.ts @@ -0,0 +1,27 @@ +import { chainSanitizerCallback, getTagOfNode } from 'roosterjs-editor-dom'; +import { DeprecatedColorList } from './deprecatedColorList'; +import { HtmlSanitizerOptions } from 'roosterjs-editor-types'; + +/** + * @internal + * Remove the deprecated colors from pasted content + * @sanitizingOption + * */ +export default function sanitizeHtmlTextFromPastedContent( + fragment: DocumentFragment, + sanitizingOption: Required +) { + const htmlElements = fragment.querySelectorAll('*') as NodeListOf; + htmlElements.forEach(tag => + chainSanitizerCallback(sanitizingOption.elementCallbacks, getTagOfNode(tag), element => { + if (DeprecatedColorList.indexOf(element.style.color) > -1) { + element.style.removeProperty('color'); + } + + if (DeprecatedColorList.indexOf(element.style.backgroundColor) > -1) { + element.style.removeProperty('background-color'); + } + return true; + }) + ); +} diff --git a/packages/roosterjs-editor-plugins/test/paste/sanitizeHtmlTextTest.ts b/packages/roosterjs-editor-plugins/test/paste/sanitizeHtmlTextTest.ts new file mode 100644 index 00000000000..23c06d2c5f9 --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/paste/sanitizeHtmlTextTest.ts @@ -0,0 +1,90 @@ +import sanitizeHtmlTextFromPastedContent from '../../lib/plugins/Paste/sanitizeHtmlTextFromPastedContent/sanitizeHtmlTextFromPastedContent'; +import { HtmlSanitizer } from 'roosterjs-editor-dom'; +import { + BeforePasteEvent, + SanitizeHtmlOptions, + PluginEventType, + ClipboardData, +} from 'roosterjs-editor-types'; + +describe('sanitizeHtmlTextFromPastedContent', () => { + function callSanitizer(fragment: DocumentFragment, sanitizingOption: SanitizeHtmlOptions) { + const sanitizer = new HtmlSanitizer(sanitizingOption); + sanitizer.convertGlobalCssToInlineCss(fragment); + sanitizer.sanitize(fragment); + } + + function runTest(source: string, expected: string) { + const doc = new DOMParser().parseFromString(source, 'text/html'); + const fragment = doc.createDocumentFragment(); + while (doc.body.firstChild) { + fragment.appendChild(doc.body.firstChild); + } + + const event = createBeforePasteEventMock(fragment); + sanitizeHtmlTextFromPastedContent(fragment, event.sanitizingOption); + callSanitizer(fragment, event.sanitizingOption); + + while (fragment.firstChild) { + doc.body.appendChild(fragment.firstChild); + } + + expect(doc.body.innerHTML).toBe(expected); + } + + it('sanitize on a div', () => { + runTest('
', '
'); + }); + + it('sanitize on a div', () => { + runTest( + '
', + '
' + ); + }); + + it('sanitize on a p', () => { + runTest( + '

', + '

' + ); + }); + + it('sanitize on nested elements', () => { + runTest( + '

', + '

' + ); + }); + + it('sanitize on nested elements with background color', () => { + runTest( + '

', + '

' + ); + }); +}); + +function createBeforePasteEventMock(fragment: DocumentFragment) { + return ({ + eventType: PluginEventType.BeforePaste, + clipboardData: {}, + fragment: fragment, + sanitizingOption: { + elementCallbacks: {}, + attributeCallbacks: {}, + cssStyleCallbacks: {}, + additionalTagReplacements: {}, + additionalAllowedAttributes: [], + additionalAllowedCssClasses: [], + additionalDefaultStyleValues: {}, + additionalGlobalStyleNodes: [], + additionalPredefinedCssForElement: {}, + preserveHtmlComments: false, + unknownTagReplacement: null, + }, + htmlBefore: '', + htmlAfter: '', + htmlAttributes: {}, + } as unknown) as BeforePasteEvent; +} diff --git a/packages/roosterjs-editor-plugins/test/paste/word/convertPastedContentFromWordTest.ts b/packages/roosterjs-editor-plugins/test/paste/word/convertPastedContentFromWordTest.ts index 4dba0a56f33..9581e982c01 100644 --- a/packages/roosterjs-editor-plugins/test/paste/word/convertPastedContentFromWordTest.ts +++ b/packages/roosterjs-editor-plugins/test/paste/word/convertPastedContentFromWordTest.ts @@ -1,7 +1,11 @@ import convertPastedContentFromWord from '../../../lib/plugins/Paste/wordConverter/convertPastedContentFromWord'; -import { BeforePasteEvent, SanitizeHtmlOptions } from 'roosterjs-editor-types'; -import { ClipboardData, PluginEventType } from '../../../../roosterjs/lib'; import { HtmlSanitizer, moveChildNodes } from 'roosterjs-editor-dom'; +import { + BeforePasteEvent, + SanitizeHtmlOptions, + PluginEventType, + ClipboardData, +} from 'roosterjs-editor-types'; describe('convertPastedContentFromWord', () => { function callSanitizer(fragment: DocumentFragment, sanitizingOption: SanitizeHtmlOptions) { From b225008aa1c069c6fcba7a6b77582235b08d66b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Fri, 13 May 2022 11:41:04 -0300 Subject: [PATCH 13/27] add comments and rename function --- .../roosterjs-editor-plugins/lib/plugins/Paste/Paste.ts | 5 +++-- .../deprecatedColorList.ts | 0 .../sanitizeHtmlColorsFromPastedContent.ts} | 5 +++-- ...xtTest.ts => sanitizeHtmlColorsFromPastedContentTest.ts} | 6 +++--- 4 files changed, 9 insertions(+), 7 deletions(-) rename packages/roosterjs-editor-plugins/lib/plugins/Paste/{sanitizeHtmlTextFromPastedContent => sanitizeHtmlColorsFromPastedContent}/deprecatedColorList.ts (100%) rename packages/roosterjs-editor-plugins/lib/plugins/Paste/{sanitizeHtmlTextFromPastedContent/sanitizeHtmlTextFromPastedContent.ts => sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent.ts} (85%) rename packages/roosterjs-editor-plugins/test/paste/{sanitizeHtmlTextTest.ts => sanitizeHtmlColorsFromPastedContentTest.ts} (91%) diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/Paste.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/Paste.ts index a87640e6117..b50761e53ad 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Paste/Paste.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/Paste.ts @@ -4,7 +4,7 @@ import convertPastedContentFromExcel from './excelConverter/convertPastedContent import convertPastedContentFromPowerPoint from './pptConverter/convertPastedContentFromPowerPoint'; import convertPastedContentFromWord from './wordConverter/convertPastedContentFromWord'; import handleLineMerge from './lineMerge/handleLineMerge'; -import sanitizeHtmlTextFromPastedContent from './sanitizeHtmlTextFromPastedContent/sanitizeHtmlTextFromPastedContent'; +import sanitizeHtmlColorsFromPastedContent from './sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent'; import { toArray } from 'roosterjs-editor-dom'; import { EditorPlugin, @@ -76,7 +76,8 @@ export default class Paste implements EditorPlugin { const trustedHTMLHandler = this.editor.getTrustedHTMLHandler(); let wacListElements: Node[]; - sanitizeHtmlTextFromPastedContent(fragment, sanitizingOption); + sanitizeHtmlColorsFromPastedContent(fragment, sanitizingOption); + if (isWordDocument(htmlAttributes)) { // Handle HTML copied from Word convertPastedContentFromWord(event); diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlTextFromPastedContent/deprecatedColorList.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/deprecatedColorList.ts similarity index 100% rename from packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlTextFromPastedContent/deprecatedColorList.ts rename to packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/deprecatedColorList.ts diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlTextFromPastedContent/sanitizeHtmlTextFromPastedContent.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent.ts similarity index 85% rename from packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlTextFromPastedContent/sanitizeHtmlTextFromPastedContent.ts rename to packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent.ts index a073d83a316..601cb7e5103 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlTextFromPastedContent/sanitizeHtmlTextFromPastedContent.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent.ts @@ -5,9 +5,10 @@ import { HtmlSanitizerOptions } from 'roosterjs-editor-types'; /** * @internal * Remove the deprecated colors from pasted content - * @sanitizingOption + * @fragment the pasted fragment + * @sanitizingOption the sanitizingOption of BeforePasteEvent * */ -export default function sanitizeHtmlTextFromPastedContent( +export default function sanitizeHtmlColorsFromPastedContent( fragment: DocumentFragment, sanitizingOption: Required ) { diff --git a/packages/roosterjs-editor-plugins/test/paste/sanitizeHtmlTextTest.ts b/packages/roosterjs-editor-plugins/test/paste/sanitizeHtmlColorsFromPastedContentTest.ts similarity index 91% rename from packages/roosterjs-editor-plugins/test/paste/sanitizeHtmlTextTest.ts rename to packages/roosterjs-editor-plugins/test/paste/sanitizeHtmlColorsFromPastedContentTest.ts index 23c06d2c5f9..ccfc8136dce 100644 --- a/packages/roosterjs-editor-plugins/test/paste/sanitizeHtmlTextTest.ts +++ b/packages/roosterjs-editor-plugins/test/paste/sanitizeHtmlColorsFromPastedContentTest.ts @@ -1,4 +1,4 @@ -import sanitizeHtmlTextFromPastedContent from '../../lib/plugins/Paste/sanitizeHtmlTextFromPastedContent/sanitizeHtmlTextFromPastedContent'; +import sanitizeHtmlColorsFromPastedContent from '../../lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent'; import { HtmlSanitizer } from 'roosterjs-editor-dom'; import { BeforePasteEvent, @@ -7,7 +7,7 @@ import { ClipboardData, } from 'roosterjs-editor-types'; -describe('sanitizeHtmlTextFromPastedContent', () => { +describe('sanitizeHtmlColorsFromPastedContent', () => { function callSanitizer(fragment: DocumentFragment, sanitizingOption: SanitizeHtmlOptions) { const sanitizer = new HtmlSanitizer(sanitizingOption); sanitizer.convertGlobalCssToInlineCss(fragment); @@ -22,7 +22,7 @@ describe('sanitizeHtmlTextFromPastedContent', () => { } const event = createBeforePasteEventMock(fragment); - sanitizeHtmlTextFromPastedContent(fragment, event.sanitizingOption); + sanitizeHtmlColorsFromPastedContent(fragment, event.sanitizingOption); callSanitizer(fragment, event.sanitizingOption); while (fragment.firstChild) { From 124b35a5dc2975d0b2bcc952ecc6855fb6c360b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Fri, 13 May 2022 11:46:19 -0300 Subject: [PATCH 14/27] add comments --- .../roosterjs-editor-dom/lib/metadata/definitionCreators.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/roosterjs-editor-dom/lib/metadata/definitionCreators.ts b/packages/roosterjs-editor-dom/lib/metadata/definitionCreators.ts index 95ad0f2fe5f..6874f21c4ca 100644 --- a/packages/roosterjs-editor-dom/lib/metadata/definitionCreators.ts +++ b/packages/roosterjs-editor-dom/lib/metadata/definitionCreators.ts @@ -50,6 +50,7 @@ export function createBooleanDefinition(isOptional?: boolean, value?: boolean): * Create a string definition * @param isOptional Whether this property is optional * @param value Optional expected string value + * @param allowNull Allow the property to be null * @returns The string definition object */ export function createStringDefinition( @@ -90,6 +91,7 @@ export function createArrayDefinition( * Create an object definition * @param propertyDef Definition of each property of the related object * @param isOptional Whether this property is optional + * @param allowNull Allow the property to be null * @returns The object definition object */ export function createObjectDefinition( From 6571886c89436969381a5be68c58371e50f758b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Fri, 13 May 2022 11:47:04 -0300 Subject: [PATCH 15/27] add param --- .../sanitizeHtmlColorsFromPastedContent.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent.ts index 601cb7e5103..5492fb2557e 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent.ts @@ -5,8 +5,8 @@ import { HtmlSanitizerOptions } from 'roosterjs-editor-types'; /** * @internal * Remove the deprecated colors from pasted content - * @fragment the pasted fragment - * @sanitizingOption the sanitizingOption of BeforePasteEvent + * @param fragment the pasted fragment + * @param sanitizingOption the sanitizingOption of BeforePasteEvent * */ export default function sanitizeHtmlColorsFromPastedContent( fragment: DocumentFragment, From f5c95a2020e30faf20bbbad9a0886dc6d36da6e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Fri, 13 May 2022 12:01:12 -0300 Subject: [PATCH 16/27] remove duplicated tags --- .../sanitizeHtmlColorsFromPastedContent.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent.ts index 5492fb2557e..681ce8a05c9 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent.ts @@ -13,8 +13,11 @@ export default function sanitizeHtmlColorsFromPastedContent( sanitizingOption: Required ) { const htmlElements = fragment.querySelectorAll('*') as NodeListOf; - htmlElements.forEach(tag => - chainSanitizerCallback(sanitizingOption.elementCallbacks, getTagOfNode(tag), element => { + const allTags = Array.from(htmlElements).map(el => getTagOfNode(el)); + const uniqueTags = allTags.filter((tag, index) => allTags.indexOf(tag) == index); + + uniqueTags.forEach(tag => + chainSanitizerCallback(sanitizingOption.elementCallbacks, tag, element => { if (DeprecatedColorList.indexOf(element.style.color) > -1) { element.style.removeProperty('color'); } From dc4bb71f69ecf917c8f9a63654dcb66b3c7ce352 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Fri, 13 May 2022 09:49:05 -0700 Subject: [PATCH 17/27] Fix #974 (#975) --- .../roosterjs-editor-dom/lib/utils/shouldSkipNode.ts | 2 +- .../test/utils/shouldSkipNodeTest.ts | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/roosterjs-editor-dom/lib/utils/shouldSkipNode.ts b/packages/roosterjs-editor-dom/lib/utils/shouldSkipNode.ts index d4421048a24..4e3dfd4ecd4 100644 --- a/packages/roosterjs-editor-dom/lib/utils/shouldSkipNode.ts +++ b/packages/roosterjs-editor-dom/lib/utils/shouldSkipNode.ts @@ -2,7 +2,7 @@ import getTagOfNode from './getTagOfNode'; import { getComputedStyle } from './getComputedStyles'; import { NodeType } from 'roosterjs-editor-types'; -const CRLF = /^[\r\n]+$/gm; +const CRLF = /^[\r\n]+$/g; const CRLF_SPACE = /[\t\r\n\u0020\u200B]/gm; // We should only find new line, real space or ZeroWidthSpace (TAB, %20, but not  ) /** diff --git a/packages/roosterjs-editor-dom/test/utils/shouldSkipNodeTest.ts b/packages/roosterjs-editor-dom/test/utils/shouldSkipNodeTest.ts index 1f0f534f8f3..63d7169a661 100644 --- a/packages/roosterjs-editor-dom/test/utils/shouldSkipNodeTest.ts +++ b/packages/roosterjs-editor-dom/test/utils/shouldSkipNodeTest.ts @@ -30,6 +30,17 @@ describe('shouldSkipNode, shouldSkipNode()', () => { expect(shouldSkip).toBe(true); }); + it('CRLF+text textNode', () => { + // Arrange + let node = document.createTextNode('\r\ntest'); + + // Act + let shouldSkip = shouldSkipNode(node); + + // Assert + expect(shouldSkip).toBe(false); + }); + it('DisplayNone', () => { // Arrange let node = DomTestHelper.createElementFromContent( From cb73e93b0065e2db12b032af233a893fa7fa7609 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Fri, 13 May 2022 09:53:51 -0700 Subject: [PATCH 18/27] test (#976) --- .github/workflows/build-and-test.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 34967d99707..f78446075cf 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -1,11 +1,5 @@ name: Build and Test -on: - push: - branches-ignore: - - master - pull_request: - branches-ignore: - - master +on: [push, pull_request] jobs: build: From d15fb8a993935a74fd3c176d344e9cd1858ce498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Fri, 13 May 2022 14:43:21 -0300 Subject: [PATCH 19/27] add sup and sub tags to apply style outside them --- .../lib/inlineElements/applyTextStyle.ts | 2 +- .../test/inlineElements/applyTextStyleTest.ts | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/roosterjs-editor-dom/lib/inlineElements/applyTextStyle.ts b/packages/roosterjs-editor-dom/lib/inlineElements/applyTextStyle.ts index 632b5e209cb..6e4dcb67249 100644 --- a/packages/roosterjs-editor-dom/lib/inlineElements/applyTextStyle.ts +++ b/packages/roosterjs-editor-dom/lib/inlineElements/applyTextStyle.ts @@ -6,7 +6,7 @@ import { getNextLeafSibling } from '../utils/getLeafSibling'; import { NodePosition, NodeType, PositionType } from 'roosterjs-editor-types'; import { splitBalancedNodeRange } from '../utils/splitParentNode'; -const STYLET_AGS = 'SPAN,B,I,U,EM,STRONG,STRIKE,S,SMALL'.split(','); +const STYLET_AGS = 'SPAN,B,I,U,EM,STRONG,STRIKE,S,SMALL,SUP,SUB'.split(','); /** * Apply style using a styler function to the given container node in the given range diff --git a/packages/roosterjs-editor-dom/test/inlineElements/applyTextStyleTest.ts b/packages/roosterjs-editor-dom/test/inlineElements/applyTextStyleTest.ts index b39e0efec96..4aea114d618 100644 --- a/packages/roosterjs-editor-dom/test/inlineElements/applyTextStyleTest.ts +++ b/packages/roosterjs-editor-dom/test/inlineElements/applyTextStyleTest.ts @@ -186,6 +186,22 @@ describe('applyTextStyle()', () => { ); }); + it('applyTextStyle() text node with SUP/SUB', () => { + let div = document.createElement('DIV'); + div.innerHTML = 'test1test2test3test4test5'; + let start = new Position(div, PositionType.Begin).normalize().move(2); + let end = new Position(div, PositionType.End).normalize().move(-2); + applyTextStyle( + div, + (node, isInnerNode) => (node.style.color = isInnerNode ? '' : 'red'), + start, + end + ); + expect(div.innerHTML).toBe( + 'test1test2test3test4test5' + ); + }); + it('applyTextStyle() text node with double span', () => { let div = document.createElement('DIV'); div.innerHTML = 'text'; From e4b79dcb5a04ffffbc96cd78bc351c1ce88e39ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Fri, 13 May 2022 16:52:03 -0300 Subject: [PATCH 20/27] refactor --- .../lib/plugins/Paste/Paste.ts | 4 +-- .../sanitizeHtmlColorsFromPastedContent.ts | 25 ++++++------------- ...sanitizeHtmlColorsFromPastedContentTest.ts | 2 +- 3 files changed, 10 insertions(+), 21 deletions(-) diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/Paste.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/Paste.ts index b50761e53ad..1201f7c1f03 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Paste/Paste.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/Paste.ts @@ -76,8 +76,6 @@ export default class Paste implements EditorPlugin { const trustedHTMLHandler = this.editor.getTrustedHTMLHandler(); let wacListElements: Node[]; - sanitizeHtmlColorsFromPastedContent(fragment, sanitizingOption); - if (isWordDocument(htmlAttributes)) { // Handle HTML copied from Word convertPastedContentFromWord(event); @@ -117,6 +115,8 @@ export default class Paste implements EditorPlugin { handleLineMerge(fragment); } + sanitizeHtmlColorsFromPastedContent(sanitizingOption); + // Replace unknown tags with SPAN sanitizingOption.unknownTagReplacement = this.unknownTagReplacement; } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent.ts index 681ce8a05c9..717bc3c80bb 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent.ts @@ -1,31 +1,20 @@ -import { chainSanitizerCallback, getTagOfNode } from 'roosterjs-editor-dom'; +import { chainSanitizerCallback } from 'roosterjs-editor-dom'; import { DeprecatedColorList } from './deprecatedColorList'; import { HtmlSanitizerOptions } from 'roosterjs-editor-types'; /** * @internal * Remove the deprecated colors from pasted content - * @param fragment the pasted fragment * @param sanitizingOption the sanitizingOption of BeforePasteEvent * */ export default function sanitizeHtmlColorsFromPastedContent( - fragment: DocumentFragment, sanitizingOption: Required ) { - const htmlElements = fragment.querySelectorAll('*') as NodeListOf; - const allTags = Array.from(htmlElements).map(el => getTagOfNode(el)); - const uniqueTags = allTags.filter((tag, index) => allTags.indexOf(tag) == index); - - uniqueTags.forEach(tag => - chainSanitizerCallback(sanitizingOption.elementCallbacks, tag, element => { - if (DeprecatedColorList.indexOf(element.style.color) > -1) { - element.style.removeProperty('color'); - } - - if (DeprecatedColorList.indexOf(element.style.backgroundColor) > -1) { - element.style.removeProperty('background-color'); + ['color', 'background-color'].forEach(property => { + chainSanitizerCallback(sanitizingOption.cssStyleCallbacks, property, (value: string) => { + if (DeprecatedColorList.indexOf(value) < 0) { + return true; } - return true; - }) - ); + }); + }); } diff --git a/packages/roosterjs-editor-plugins/test/paste/sanitizeHtmlColorsFromPastedContentTest.ts b/packages/roosterjs-editor-plugins/test/paste/sanitizeHtmlColorsFromPastedContentTest.ts index ccfc8136dce..b123c36566b 100644 --- a/packages/roosterjs-editor-plugins/test/paste/sanitizeHtmlColorsFromPastedContentTest.ts +++ b/packages/roosterjs-editor-plugins/test/paste/sanitizeHtmlColorsFromPastedContentTest.ts @@ -22,7 +22,7 @@ describe('sanitizeHtmlColorsFromPastedContent', () => { } const event = createBeforePasteEventMock(fragment); - sanitizeHtmlColorsFromPastedContent(fragment, event.sanitizingOption); + sanitizeHtmlColorsFromPastedContent(event.sanitizingOption); callSanitizer(fragment, event.sanitizingOption); while (fragment.firstChild) { From bdfefd4e500c46ea8562ddd14011a812a429ddf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Fri, 13 May 2022 17:05:33 -0300 Subject: [PATCH 21/27] refactor --- .../lib/metadata/definitionCreators.ts | 18 +++++-- .../lib/metadata/validate.ts | 2 +- .../lib/table/tableFormatInfo.ts | 54 +++++++------------ 3 files changed, 35 insertions(+), 39 deletions(-) diff --git a/packages/roosterjs-editor-dom/lib/metadata/definitionCreators.ts b/packages/roosterjs-editor-dom/lib/metadata/definitionCreators.ts index 6874f21c4ca..670ddc8586f 100644 --- a/packages/roosterjs-editor-dom/lib/metadata/definitionCreators.ts +++ b/packages/roosterjs-editor-dom/lib/metadata/definitionCreators.ts @@ -15,13 +15,15 @@ import { * @param value Optional value of the number * @param minValue Optional minimum value * @param maxValue Optional maximum value + * @param allowNull Allow the property to be null * @returns The number definition object */ export function createNumberDefinition( isOptional?: boolean, value?: number, minValue?: number, - maxValue?: number + maxValue?: number, + allowNull?: boolean ): NumberDefinition { return { type: DefinitionType.Number, @@ -29,6 +31,7 @@ export function createNumberDefinition( value, maxValue, minValue, + allowNull, }; } @@ -36,13 +39,19 @@ export function createNumberDefinition( * Create a boolean definition * @param isOptional Whether this property is optional * @param value Optional expected boolean value + * @param allowNull Allow the property to be null * @returns The boolean definition object */ -export function createBooleanDefinition(isOptional?: boolean, value?: boolean): BooleanDefinition { +export function createBooleanDefinition( + isOptional?: boolean, + value?: boolean, + allowNull?: boolean +): BooleanDefinition { return { type: DefinitionType.Boolean, isOptional, value, + allowNull, }; } @@ -70,13 +79,15 @@ export function createStringDefinition( * Create an array definition * @param itemDef Definition of each item of the related array * @param isOptional Whether this property is optional + * @param allowNull Allow the property to be null * @returns The array definition object */ export function createArrayDefinition( itemDef: Definition, isOptional?: boolean, minLength?: number, - maxLength?: number + maxLength?: number, + allowNull?: boolean ): ArrayDefinition { return { type: DefinitionType.Array, @@ -84,6 +95,7 @@ export function createArrayDefinition( itemDef, minLength, maxLength, + allowNull, }; } diff --git a/packages/roosterjs-editor-dom/lib/metadata/validate.ts b/packages/roosterjs-editor-dom/lib/metadata/validate.ts index d5ca6266610..4d816b19b63 100644 --- a/packages/roosterjs-editor-dom/lib/metadata/validate.ts +++ b/packages/roosterjs-editor-dom/lib/metadata/validate.ts @@ -8,7 +8,7 @@ import { Definition, DefinitionType } from 'roosterjs-editor-types'; */ export default function validate(input: any, def: Definition): input is T { let result = false; - if ((def.isOptional && typeof input === 'undefined') || (def.allowNull && !input)) { + if ((def.isOptional && typeof input === 'undefined') || (def.allowNull && input === null)) { result = true; } else { switch (def.type) { diff --git a/packages/roosterjs-editor-dom/lib/table/tableFormatInfo.ts b/packages/roosterjs-editor-dom/lib/table/tableFormatInfo.ts index cd29c0b9f5a..c2448a22422 100644 --- a/packages/roosterjs-editor-dom/lib/table/tableFormatInfo.ts +++ b/packages/roosterjs-editor-dom/lib/table/tableFormatInfo.ts @@ -7,49 +7,33 @@ import { createStringDefinition, } from '../metadata/definitionCreators'; +const NullStringDefinition = createStringDefinition( + false /** isOptional */, + undefined /** value */, + true /** allowNull */ +); + +const BooleanDefinition = createBooleanDefinition(false /** isOptional */); + const TableFormatMetadata = createObjectDefinition>( { - topBorderColor: createStringDefinition( - false /** isOptional */, - undefined /** value */, - true /** allowNull */ - ), - bottomBorderColor: createStringDefinition( - false /** isOptional */, - undefined /** value */, - true /** allowNull */ - ), - verticalBorderColor: createStringDefinition( - false /** isOptional */, - undefined /** value */, - true /** allowNull */ - ), - hasHeaderRow: createBooleanDefinition(false /** isOptional */), - headerRowColor: createStringDefinition( - false /** isOptional */, - undefined /** value */, - true /** allowNull */ - ), - hasFirstColumn: createBooleanDefinition(false /** isOptional */), - hasBandedColumns: createBooleanDefinition(false /** isOptional */), - hasBandedRows: createBooleanDefinition(false /** isOptional */), - bgColorEven: createStringDefinition( - false /** isOptional */, - undefined /** value */, - true /** allowNull */ - ), - bgColorOdd: createStringDefinition( - false /** isOptional */, - undefined /** value */, - true /** allowNull */ - ), + topBorderColor: NullStringDefinition, + bottomBorderColor: NullStringDefinition, + verticalBorderColor: NullStringDefinition, + hasHeaderRow: BooleanDefinition, + headerRowColor: NullStringDefinition, + hasFirstColumn: BooleanDefinition, + hasBandedColumns: BooleanDefinition, + hasBandedRows: BooleanDefinition, + bgColorEven: NullStringDefinition, + bgColorOdd: NullStringDefinition, tableBorderFormat: createNumberDefinition( false /** isOptional */, undefined /* value */, 0, 7 ), - keepCellShade: createBooleanDefinition(false /** isOptional */), + keepCellShade: BooleanDefinition, }, false /* isOptional */, true /** allowNull */ From e966fe00702e5f4ee256fd06121626c8f49e9243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Fri, 13 May 2022 17:19:42 -0300 Subject: [PATCH 22/27] fix unit test --- .../test/metadata/definitionCreatorsTest.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/roosterjs-editor-dom/test/metadata/definitionCreatorsTest.ts b/packages/roosterjs-editor-dom/test/metadata/definitionCreatorsTest.ts index b1f91934a15..3a3b6fb451f 100644 --- a/packages/roosterjs-editor-dom/test/metadata/definitionCreatorsTest.ts +++ b/packages/roosterjs-editor-dom/test/metadata/definitionCreatorsTest.ts @@ -16,6 +16,7 @@ describe('createNumberDefinition', () => { value: undefined, maxValue: undefined, minValue: undefined, + allowNull: undefined, }); }); @@ -27,6 +28,7 @@ describe('createNumberDefinition', () => { value: 2, minValue: 1, maxValue: 3, + allowNull: undefined, }); }); }); @@ -38,6 +40,7 @@ describe('createBooleanDefinition', () => { type: DefinitionType.Boolean, isOptional: undefined, value: undefined, + allowNull: undefined, }); }); @@ -47,6 +50,7 @@ describe('createBooleanDefinition', () => { type: DefinitionType.Boolean, isOptional: true, value: false, + allowNull: undefined, }); }); }); @@ -96,6 +100,7 @@ describe('createArrayDefinition', () => { isOptional: undefined, minLength: undefined, maxLength: undefined, + allowNull: undefined, }); }); @@ -107,6 +112,7 @@ describe('createArrayDefinition', () => { itemDef, minLength: 1, maxLength: 3, + allowNull: undefined, }); }); }); From 6a2317050e42ac55bffda631944c3a3cfe4dd595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Fri, 13 May 2022 17:48:57 -0300 Subject: [PATCH 23/27] add comments --- packages/roosterjs-editor-dom/lib/table/tableFormatInfo.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/roosterjs-editor-dom/lib/table/tableFormatInfo.ts b/packages/roosterjs-editor-dom/lib/table/tableFormatInfo.ts index c2448a22422..44aef0d545d 100644 --- a/packages/roosterjs-editor-dom/lib/table/tableFormatInfo.ts +++ b/packages/roosterjs-editor-dom/lib/table/tableFormatInfo.ts @@ -30,8 +30,8 @@ const TableFormatMetadata = createObjectDefinition>( tableBorderFormat: createNumberDefinition( false /** isOptional */, undefined /* value */, - 0, - 7 + 0 /* first table border format */, + 7 /* last table border format */ ), keepCellShade: BooleanDefinition, }, From 85e7f102e55774903c266f6b09015c037e3d8840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Fri, 13 May 2022 17:59:02 -0300 Subject: [PATCH 24/27] refactor --- .../sanitizeHtmlColorsFromPastedContent.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent.ts index 717bc3c80bb..e3da37455a6 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent.ts @@ -11,10 +11,10 @@ export default function sanitizeHtmlColorsFromPastedContent( sanitizingOption: Required ) { ['color', 'background-color'].forEach(property => { - chainSanitizerCallback(sanitizingOption.cssStyleCallbacks, property, (value: string) => { - if (DeprecatedColorList.indexOf(value) < 0) { - return true; - } - }); + chainSanitizerCallback( + sanitizingOption.cssStyleCallbacks, + property, + (value: string) => DeprecatedColorList.indexOf(value) < 0 + ); }); } From 16e29dc082102c95dc8318fdf44a911305f4c8f2 Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Mon, 16 May 2022 14:57:08 -0600 Subject: [PATCH 25/27] Uncaught TypeError: Cannot read properties of null (reading 'equals') (#982) --- .../lib/plugins/ContentEdit/features/textFeatures.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/textFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/textFeatures.ts index 444dc2dcb40..b1a86f667d1 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/textFeatures.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/textFeatures.ts @@ -129,6 +129,10 @@ function shouldSetIndentation(editor: IEditor, range: Range): boolean { const firstBlock = editor.getBlockElementAtNode(startPosition.node); const lastBlock = editor.getBlockElementAtNode(endPosition.node); + if (!firstBlock || !lastBlock) { + return false; + } + if (!firstBlock.equals(lastBlock)) { //If the selections has more than one block, we indent all the blocks in the selection return true; From bf93f947a719430197749d6b29b0ddaf566ec50b Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Tue, 17 May 2022 08:24:50 -0600 Subject: [PATCH 26/27] TypeError: Cannot read properties of null (reading 'node') (#984) * fix * Fix build --- .../lib/plugins/TableCellSelection/TableCellSelection.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/TableCellSelection.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/TableCellSelection.ts index 030a8e25aad..2068cd8325e 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/TableCellSelection.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/TableCellSelection.ts @@ -217,8 +217,7 @@ export default class TableCellSelection implements EditorPlugin { if (shiftKey) { if (!this.firstTarget) { const pos = this.editor.getFocusedPosition(); - - const cell = getCellAtCursor(this.editor, pos.node); + const cell = pos && getCellAtCursor(this.editor, pos.node); this.firstTarget = this.firstTarget || cell; } @@ -229,7 +228,10 @@ export default class TableCellSelection implements EditorPlugin { } this.editor.runAsync(editor => { const pos = editor.getFocusedPosition(); - this.setData(this.tableSelection ? this.lastTarget : pos.node); + const newTarget = this.tableSelection ? this.lastTarget : pos?.node; + if (newTarget) { + this.setData(newTarget); + } if (this.firstTable! == this.targetTable!) { if (!this.shouldConvertToTableSelection() && !this.tableSelection) { From 18b361eed744f711c5526cba2a963b25302fa7ec Mon Sep 17 00:00:00 2001 From: "microsoft-github-policy-service[bot]" <77245923+microsoft-github-policy-service[bot]@users.noreply.github.com> Date: Tue, 17 May 2022 09:37:52 -0700 Subject: [PATCH 27/27] Microsoft mandatory file (#985) Co-authored-by: microsoft-github-policy-service[bot] <77245923+microsoft-github-policy-service[bot]@users.noreply.github.com> --- SECURITY.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000000..766e6f88789 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/msrc/pgp-key-msrc). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/msrc/cvd). + +