diff --git a/packages/roosterjs-content-model-api/lib/publicApi/utils/checkXss.ts b/packages/roosterjs-content-model-api/lib/publicApi/utils/checkXss.ts index 49408eff7d21..5f192bb4a174 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/utils/checkXss.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/utils/checkXss.ts @@ -1,15 +1,10 @@ -import { stripInvisibleUnicode } from 'roosterjs-content-model-dom'; - /** * @internal Check if there is XSS attack in the link * @param link The link to be checked - * @returns The safe link with invisible Unicode characters stripped, or empty string if there is XSS attack - * @remarks This function strips invisible Unicode characters (zero-width chars, Unicode Tags, etc.) - * and checks for patterns like s\nc\nr\ni\np\nt: to prevent XSS attacks. This may block some valid links, + * @returns The safe link, or empty string if there is XSS attack + * @remarks This function checks for patterns like s\nc\nr\ni\np\nt: to prevent XSS attacks. This may block some valid links, * but it is necessary for security reasons. We treat the word "script" as safe if there are "/" before it. */ export function checkXss(link: string): string { - // Defense-in-depth: strip invisible Unicode even if already handled elsewhere - const sanitized = stripInvisibleUnicode(link); - return sanitized.match(/^[^\/]*s\n*c\n*r\n*i\n*p\n*t\n*:/i) ? '' : sanitized; + return link.match(/^[^\/]*s\n*c\n*r\n*i\n*p\n*t\n*:/i) ? '' : link; } diff --git a/packages/roosterjs-content-model-api/test/publicApi/utils/checkXssTest.ts b/packages/roosterjs-content-model-api/test/publicApi/utils/checkXssTest.ts index 13afbf7309d4..9069906435da 100644 --- a/packages/roosterjs-content-model-api/test/publicApi/utils/checkXssTest.ts +++ b/packages/roosterjs-content-model-api/test/publicApi/utils/checkXssTest.ts @@ -30,36 +30,4 @@ describe('checkXss', () => { const link = 'https://example.com/script:.js'; expect(checkXss(link)).toBe(link); }); - - it('should strip invisible Unicode from link', () => { - const link = 'https://www\u200B.example\u200C.com'; - expect(checkXss(link)).toBe('https://www.example.com'); - }); - - it('should strip invisible Unicode from mailto link', () => { - const link = 'mailto:\u200Buser@example.com'; - expect(checkXss(link)).toBe('mailto:user@example.com'); - }); - - it('should detect XSS hidden behind invisible Unicode in script:', () => { - // script: with zero-width spaces between characters should still be caught - const link = 's\u200Bc\u200Cr\u200Di\u200Ep\u200Ft:alert(1)'; - expect(checkXss(link)).toBe(''); - }); - - it('should strip Unicode Tags (supplementary plane) from link', () => { - // U+E0061 = \uDB40\uDC61 (Tag Latin Small Letter A) - const link = 'mailto:\uDB40\uDC61user@example.com'; - expect(checkXss(link)).toBe('mailto:user@example.com'); - }); - - it('should strip bidirectional marks from link', () => { - const link = 'mailto:\u202Auser\u202E@example.com'; - expect(checkXss(link)).toBe('mailto:user@example.com'); - }); - - it('should strip invisible Unicode from mailto subject and body', () => { - const link = 'mailto:user@example.com?subject=Hello\u200BWorld&body=Test\u200CContent'; - expect(checkXss(link)).toBe('mailto:user@example.com?subject=HelloWorld&body=TestContent'); - }); }); diff --git a/packages/roosterjs-content-model-core/lib/editor/Editor.ts b/packages/roosterjs-content-model-core/lib/editor/Editor.ts index 6ac364a1b68e..a0fe1140632b 100644 --- a/packages/roosterjs-content-model-core/lib/editor/Editor.ts +++ b/packages/roosterjs-content-model-core/lib/editor/Editor.ts @@ -6,7 +6,6 @@ import { transformColor, createDomToModelContextWithConfig, domToContentModel, - sanitizeInvisibleUnicode, } from 'roosterjs-content-model-dom'; import type { ContentModelDocument, @@ -53,8 +52,6 @@ export class Editor implements IEditor { const initialModel = options.initialModel ?? createEmptyModel(this.core.format.defaultFormat); - sanitizeInvisibleUnicode(initialModel); - this.core.api.setContentModel( this.core, initialModel, diff --git a/packages/roosterjs-content-model-core/test/editor/EditorTest.ts b/packages/roosterjs-content-model-core/test/editor/EditorTest.ts index d3329027a544..c6ff9050e646 100644 --- a/packages/roosterjs-content-model-core/test/editor/EditorTest.ts +++ b/packages/roosterjs-content-model-core/test/editor/EditorTest.ts @@ -26,12 +26,14 @@ describe('Editor', () => { updateKnownColorSpy = jasmine.createSpy('updateKnownColor'); createEditorCoreSpy = spyOn(createEditorCore, 'createEditorCore').and.callThrough(); setContentModelSpy = jasmine.createSpy('setContentModel'); - createEmptyModelSpy = spyOn(createEmptyModel, 'createEmptyModel').and.callThrough(); + createEmptyModelSpy = spyOn(createEmptyModel, 'createEmptyModel'); }); it('ctor and dispose, no options', () => { const div = document.createElement('div'); + createEmptyModelSpy.and.callThrough(); + const editor = new Editor(div); expect(createEditorCoreSpy).toHaveBeenCalledWith(div, {}); @@ -65,7 +67,7 @@ describe('Editor', () => { } as any; const setContentModelSpy = jasmine.createSpy('setContentModel'); const disposeErrorHandlerSpy = jasmine.createSpy('disposeErrorHandler'); - const mockedInitialModel = { blocks: [] } as any; + const mockedInitialModel = 'INITMODEL' as any; const options: EditorOptions = { plugins: [mockedPlugin1, mockedPlugin2], disposeErrorHandler: disposeErrorHandlerSpy, @@ -76,6 +78,8 @@ describe('Editor', () => { }, }; + createEmptyModelSpy.and.callThrough(); + const editor = new Editor(div, options); expect(createEditorCoreSpy).toHaveBeenCalledWith(div, options); diff --git a/packages/roosterjs-content-model-dom/lib/domUtils/stripInvisibleUnicode.ts b/packages/roosterjs-content-model-dom/lib/domUtils/stripInvisibleUnicode.ts deleted file mode 100644 index 20bfdb56697e..000000000000 --- a/packages/roosterjs-content-model-dom/lib/domUtils/stripInvisibleUnicode.ts +++ /dev/null @@ -1,18 +0,0 @@ -const INVISIBLE_UNICODE_REGEX = - // eslint-disable-next-line no-misleading-character-class - /[\u00AD\u034F\u061C\u115F\u1160\u17B4\u17B5\u180B-\u180E\u200B-\u200F\u202A-\u202E\u2028\u2029\u2060-\u2064\u2066-\u2069\u3164\uFEFF\uFFA0\uFFF9-\uFFFB]|\uDB40[\uDC01-\uDCFF]/g; - -/** - * Strip invisible Unicode characters from a string. - * This removes zero-width characters, bidirectional marks, Unicode Tags (U+E0001-U+E00FF), - * interlinear annotation anchors, Mongolian free variation selectors, - * and other invisible formatting characters that can be used to hide content in links. - * - * @remarks This function strips ZWJ (U+200D) which may affect emoji sequences. - * It should only be applied to href attributes, not to visible text content. - * @param value The string to strip invisible characters from - * @returns The string with invisible characters removed - */ -export function stripInvisibleUnicode(value: string): string { - return value.replace(INVISIBLE_UNICODE_REGEX, ''); -} diff --git a/packages/roosterjs-content-model-dom/lib/index.ts b/packages/roosterjs-content-model-dom/lib/index.ts index 477ddc1f5b7d..df09746614b0 100644 --- a/packages/roosterjs-content-model-dom/lib/index.ts +++ b/packages/roosterjs-content-model-dom/lib/index.ts @@ -70,7 +70,6 @@ export { addTextSegment } from './modelApi/common/addTextSegment'; export { normalizeParagraph } from './modelApi/common/normalizeParagraph'; export { normalizeContentModel } from './modelApi/common/normalizeContentModel'; -export { sanitizeInvisibleUnicode } from './modelApi/common/sanitizeInvisibleUnicode'; export { isGeneralSegment } from './modelApi/typeCheck/isGeneralSegment'; export { unwrapBlock } from './modelApi/common/unwrapBlock'; export { addSegment } from './modelApi/common/addSegment'; @@ -119,7 +118,6 @@ export { isCharacterValue, isModifierKey, isCursorMovingKey } from './domUtils/e export { getNodePositionFromEvent } from './domUtils/event/getNodePositionFromEvent'; export { combineBorderValue, extractBorderValues } from './domUtils/style/borderValues'; export { isPunctuation, isSpace, normalizeText } from './domUtils/stringUtil'; -export { stripInvisibleUnicode } from './domUtils/stripInvisibleUnicode'; export { parseTableCells } from './domUtils/table/parseTableCells'; export { readFile } from './domUtils/readFile'; export { retrieveDocumentMetadata } from './domUtils/retrieveDocumentMetadata'; diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/common/sanitizeInvisibleUnicode.ts b/packages/roosterjs-content-model-dom/lib/modelApi/common/sanitizeInvisibleUnicode.ts deleted file mode 100644 index 51853bd7f49d..000000000000 --- a/packages/roosterjs-content-model-dom/lib/modelApi/common/sanitizeInvisibleUnicode.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { stripInvisibleUnicode } from '../../domUtils/stripInvisibleUnicode'; -import type { - ContentModelBlock, - ContentModelBlockGroup, - ContentModelDocument, - ContentModelSegment, -} from 'roosterjs-content-model-types'; - -/** - * Strip invisible Unicode characters from all text and link hrefs in a content model. - * This sanitizes the model at initialization time to prevent hidden content in links - * or text (e.g. zero-width chars, bidirectional marks, Unicode Tags). - * For General segments, all Text nodes under the element are also sanitized. - * @param model The content model document to sanitize in-place - */ -export function sanitizeInvisibleUnicode(model: ContentModelDocument): void { - sanitizeBlockGroup(model); -} - -function sanitizeBlockGroup(group: ContentModelBlockGroup): void { - for (const block of group.blocks) { - sanitizeBlock(block); - } -} - -function sanitizeBlock(block: ContentModelBlock): void { - switch (block.blockType) { - case 'Paragraph': - for (const segment of block.segments) { - sanitizeSegment(segment); - } - break; - - case 'Table': - for (const row of block.rows) { - for (const cell of row.cells) { - sanitizeBlockGroup(cell); - } - } - break; - - case 'BlockGroup': - sanitizeBlockGroup(block); - - if (block.blockGroupType === 'General' && block.element) { - sanitizeTextNodes(block.element); - } - break; - - case 'Entity': - case 'Divider': - break; - } -} - -function sanitizeSegment(segment: ContentModelSegment): void { - if (segment.link?.format.href) { - segment.link.format.href = stripInvisibleUnicode(segment.link.format.href); - } - - switch (segment.segmentType) { - case 'Text': - segment.text = stripInvisibleUnicode(segment.text); - break; - - case 'General': - sanitizeTextNodes(segment.element); - sanitizeBlockGroup(segment); - break; - - case 'Image': - case 'Entity': - case 'Br': - case 'SelectionMarker': - break; - } -} - -function sanitizeTextNodes(element: HTMLElement): void { - const walker = element.ownerDocument.createTreeWalker(element, NodeFilter.SHOW_TEXT); - - let node: Text | null; - - while ((node = walker.nextNode() as Text | null)) { - if (node.nodeValue) { - node.nodeValue = stripInvisibleUnicode(node.nodeValue); - } - } -} diff --git a/packages/roosterjs-content-model-dom/test/domUtils/stripInvisibleUnicodeTest.ts b/packages/roosterjs-content-model-dom/test/domUtils/stripInvisibleUnicodeTest.ts deleted file mode 100644 index a598bccf784c..000000000000 --- a/packages/roosterjs-content-model-dom/test/domUtils/stripInvisibleUnicodeTest.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { stripInvisibleUnicode } from '../../lib/domUtils/stripInvisibleUnicode'; - -describe('stripInvisibleUnicode', () => { - it('should return empty string for empty input', () => { - expect(stripInvisibleUnicode('')).toBe(''); - }); - - it('should return the same string when no invisible characters are present', () => { - expect(stripInvisibleUnicode('mailto:user@example.com')).toBe('mailto:user@example.com'); - }); - - it('should return the same string for regular text with no invisible chars', () => { - expect(stripInvisibleUnicode('Hello World! 123')).toBe('Hello World! 123'); - }); - - it('should strip zero-width space (U+200B)', () => { - expect(stripInvisibleUnicode('mailto:\u200Buser@example.com')).toBe( - 'mailto:user@example.com' - ); - }); - - it('should strip zero-width non-joiner (U+200C)', () => { - expect(stripInvisibleUnicode('mailto:\u200Cuser@example.com')).toBe( - 'mailto:user@example.com' - ); - }); - - it('should strip zero-width joiner (U+200D)', () => { - expect(stripInvisibleUnicode('mailto:\u200Duser@example.com')).toBe( - 'mailto:user@example.com' - ); - }); - - it('should strip left-to-right mark (U+200E)', () => { - expect(stripInvisibleUnicode('mailto:\u200Euser@example.com')).toBe( - 'mailto:user@example.com' - ); - }); - - it('should strip right-to-left mark (U+200F)', () => { - expect(stripInvisibleUnicode('mailto:\u200Fuser@example.com')).toBe( - 'mailto:user@example.com' - ); - }); - - it('should strip BOM / zero-width no-break space (U+FEFF)', () => { - expect(stripInvisibleUnicode('\uFEFFmailto:user@example.com')).toBe( - 'mailto:user@example.com' - ); - }); - - it('should strip soft hyphen (U+00AD)', () => { - expect(stripInvisibleUnicode('mailto:us\u00ADer@example.com')).toBe( - 'mailto:user@example.com' - ); - }); - - it('should strip bidirectional override characters (U+202A-U+202E)', () => { - expect(stripInvisibleUnicode('mailto:\u202Auser\u202E@example.com')).toBe( - 'mailto:user@example.com' - ); - }); - - it('should strip bidirectional isolate characters (U+2066-U+2069)', () => { - expect(stripInvisibleUnicode('mailto:\u2066user\u2069@example.com')).toBe( - 'mailto:user@example.com' - ); - }); - - it('should strip word joiner (U+2060)', () => { - expect(stripInvisibleUnicode('mailto:\u2060user@example.com')).toBe( - 'mailto:user@example.com' - ); - }); - - it('should strip Arabic letter mark (U+061C)', () => { - expect(stripInvisibleUnicode('mailto:\u061Cuser@example.com')).toBe( - 'mailto:user@example.com' - ); - }); - - it('should strip combining grapheme joiner (U+034F)', () => { - expect(stripInvisibleUnicode('mailto:\u034Fuser@example.com')).toBe( - 'mailto:user@example.com' - ); - }); - - it('should strip Mongolian vowel separator (U+180E)', () => { - expect(stripInvisibleUnicode('mailto:\u180Euser@example.com')).toBe( - 'mailto:user@example.com' - ); - }); - - it('should strip line separator (U+2028) and paragraph separator (U+2029)', () => { - expect(stripInvisibleUnicode('mailto:\u2028user\u2029@example.com')).toBe( - 'mailto:user@example.com' - ); - }); - - it('should strip Unicode Tags (U+E0001-U+E007F) - supplementary plane', () => { - // U+E0061 = surrogate pair \uDB40\uDC61 (Tag Latin Small Letter A) - // U+E0062 = surrogate pair \uDB40\uDC62 (Tag Latin Small Letter B) - expect(stripInvisibleUnicode('mailto:\uDB40\uDC61\uDB40\uDC62user@example.com')).toBe( - 'mailto:user@example.com' - ); - }); - - it('should strip Unicode Tag Begin (U+E0001)', () => { - // U+E0001 = surrogate pair \uDB40\uDC01 - expect(stripInvisibleUnicode('mailto:\uDB40\uDC01user@example.com')).toBe( - 'mailto:user@example.com' - ); - }); - - it('should strip Unicode Tag Cancel (U+E007F)', () => { - // U+E007F = surrogate pair \uDB40\uDC7F - expect(stripInvisibleUnicode('mailto:\uDB40\uDC7Fuser@example.com')).toBe( - 'mailto:user@example.com' - ); - }); - - it('should strip multiple different invisible characters in one string', () => { - expect( - stripInvisibleUnicode( - 'mailto:\u200B\u200C\u200D\u200E\u200F\u202A\u202E\uFEFFuser@example.com' - ) - ).toBe('mailto:user@example.com'); - }); - - it('should return empty string when input contains only invisible characters', () => { - expect(stripInvisibleUnicode('\u200B\u200C\u200D\uFEFF')).toBe(''); - }); - - it('should handle invisible characters scattered throughout a mailto link', () => { - expect( - stripInvisibleUnicode('m\u200Ba\u200Ci\u200Dl\u200Et\u200Fo\u202A:user@example.com') - ).toBe('mailto:user@example.com'); - }); - - it('should strip invisible characters from mailto subject parameter', () => { - expect( - stripInvisibleUnicode( - 'mailto:user@example.com?subject=Hello\u200B\u200CWorld&body=Test\u200DContent' - ) - ).toBe('mailto:user@example.com?subject=HelloWorld&body=TestContent'); - }); - - it('should handle http links with invisible characters', () => { - expect(stripInvisibleUnicode('https://www\u200B.example\u200C.com')).toBe( - 'https://www.example.com' - ); - }); - - it('should not strip regular visible Unicode characters', () => { - // Common non-ASCII visible chars should be preserved - expect(stripInvisibleUnicode('mailto:über@example.com')).toBe('mailto:über@example.com'); - expect(stripInvisibleUnicode('mailto:用户@example.com')).toBe('mailto:用户@example.com'); - expect(stripInvisibleUnicode('mailto:пользователь@example.com')).toBe( - 'mailto:пользователь@example.com' - ); - }); - - it('should preserve emoji characters', () => { - expect(stripInvisibleUnicode('mailto:user@example.com?subject=Hello 👋')).toBe( - 'mailto:user@example.com?subject=Hello 👋' - ); - }); - - it('should strip Hangul fillers', () => { - expect(stripInvisibleUnicode('mailto:\u115F\u1160\u3164\uFFA0user@example.com')).toBe( - 'mailto:user@example.com' - ); - }); - - it('should strip Khmer vowel inherent characters', () => { - expect(stripInvisibleUnicode('mailto:\u17B4\u17B5user@example.com')).toBe( - 'mailto:user@example.com' - ); - }); - - it('should strip Mongolian free variation selectors (U+180B-U+180D)', () => { - expect(stripInvisibleUnicode('mailto:\u180B\u180C\u180Duser@example.com')).toBe( - 'mailto:user@example.com' - ); - }); - - it('should strip interlinear annotation anchors (U+FFF9-U+FFFB)', () => { - expect(stripInvisibleUnicode('mailto:\uFFF9\uFFFA\uFFFBuser@example.com')).toBe( - 'mailto:user@example.com' - ); - }); - - it('should strip extended Unicode Tags beyond U+E007F (U+E0080-U+E00FF)', () => { - // U+E0080 = surrogate pair \uDB40\uDC80 - // U+E00FF = surrogate pair \uDB40\uDCFF - expect(stripInvisibleUnicode('mailto:\uDB40\uDC80\uDB40\uDCFFuser@example.com')).toBe( - 'mailto:user@example.com' - ); - }); - - it('should not modify URL-encoded sequences (percent-encoded content is preserved as-is)', () => { - // %E2%80%8B is URL-encoded U+200B - we do not decode, as the content may be intentional - const link = 'mailto:%E2%80%8Buser@example.com'; - expect(stripInvisibleUnicode(link)).toBe(link); - }); -}); diff --git a/packages/roosterjs-content-model-dom/test/modelApi/common/sanitizeInvisibleUnicodeTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/common/sanitizeInvisibleUnicodeTest.ts deleted file mode 100644 index 57d05c49ca27..000000000000 --- a/packages/roosterjs-content-model-dom/test/modelApi/common/sanitizeInvisibleUnicodeTest.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { sanitizeInvisibleUnicode } from '../../../lib/modelApi/common/sanitizeInvisibleUnicode'; -import { ContentModelDocument } from 'roosterjs-content-model-types'; - -describe('sanitizeInvisibleUnicode', () => { - it('should not modify model with no invisible characters', () => { - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'Text', - format: {}, - text: 'Hello World', - }, - ], - }, - ], - }; - - sanitizeInvisibleUnicode(model); - - expect(model.blocks[0]).toEqual({ - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'Text', - format: {}, - text: 'Hello World', - }, - ], - }); - }); - - it('should strip invisible Unicode from text segments', () => { - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'Text', - format: {}, - text: 'Hello\u200B\u200CWorld', - }, - ], - }, - ], - }; - - sanitizeInvisibleUnicode(model); - - expect((model.blocks[0] as any).segments[0].text).toBe('HelloWorld'); - }); - - it('should strip invisible Unicode from link href', () => { - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'Text', - format: {}, - text: 'Click here', - link: { - format: { - href: 'mailto:\u200Buser@example.com', - underline: true, - }, - dataset: {}, - }, - }, - ], - }, - ], - }; - - sanitizeInvisibleUnicode(model); - - expect((model.blocks[0] as any).segments[0].link.format.href).toBe( - 'mailto:user@example.com' - ); - }); - - it('should strip invisible Unicode from text inside table cells', () => { - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Table', - format: {}, - widths: [100], - rows: [ - { - format: {}, - height: 0, - cells: [ - { - blockGroupType: 'TableCell', - format: {}, - spanAbove: false, - spanLeft: false, - isHeader: false, - dataset: {}, - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'Text', - format: {}, - text: 'Cell\u200BText', - }, - ], - }, - ], - }, - ], - }, - ], - dataset: {}, - }, - ], - }; - - sanitizeInvisibleUnicode(model); - - const cell = (model.blocks[0] as any).rows[0].cells[0]; - expect(cell.blocks[0].segments[0].text).toBe('CellText'); - }); - - it('should sanitize text nodes in General segment element', () => { - const element = document.createElement('div'); - element.innerHTML = 'Hello\u200B World\u200C'; - - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'General', - blockGroupType: 'General', - blockType: 'BlockGroup', - format: {}, - element: element, - blocks: [], - }, - ], - }, - ], - }; - - sanitizeInvisibleUnicode(model); - - expect(element.textContent).toBe('Hello World'); - }); - - it('should handle empty model', () => { - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [], - }; - - sanitizeInvisibleUnicode(model); - - expect(model.blocks.length).toBe(0); - }); - - it('should not modify URL-encoded sequences in link href', () => { - const href = 'mailto:%E2%80%8Buser@example.com'; - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'Text', - format: {}, - text: 'Click', - link: { - format: { - href: href, - underline: true, - }, - dataset: {}, - }, - }, - ], - }, - ], - }; - - sanitizeInvisibleUnicode(model); - - expect((model.blocks[0] as any).segments[0].link.format.href).toBe(href); - }); - - it('should handle nested block groups (e.g. list items)', () => { - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - format: {}, - formatHolder: { segmentType: 'SelectionMarker', format: {}, isSelected: false }, - levels: [{ listType: 'UL', format: {}, dataset: {} }], - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'Text', - format: {}, - text: 'Item\u200BOne', - }, - ], - }, - ], - }, - ], - }; - - sanitizeInvisibleUnicode(model); - - expect((model.blocks[0] as any).blocks[0].segments[0].text).toBe('ItemOne'); - }); - - it('should not modify Br or SelectionMarker segments', () => { - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'Br', - format: {}, - }, - { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - ], - }, - ], - }; - - sanitizeInvisibleUnicode(model); - - expect(model.blocks[0]).toEqual({ - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'Br', - format: {}, - }, - { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - ], - }); - }); -}); diff --git a/versions.json b/versions.json index 5069611a0ddf..e170021a53bd 100644 --- a/versions.json +++ b/versions.json @@ -1,6 +1,6 @@ { "react": "9.0.4", - "main": "9.50.0", + "main": "9.50.1", "legacyAdapter": "8.65.3", "overrides": {} }