From 498a9f40d1e67ba4f402a9e49e6a4f432ff1f246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Tue, 2 Apr 2024 19:08:10 -0300 Subject: [PATCH 1/5] port hyphen --- demo/scripts/controlsV2/mainPane/MainPane.tsx | 9 +- .../editorOptions/EditorOptionsPlugin.ts | 1 + .../lib/autoFormat/AutoFormatPlugin.ts | 22 +- .../lib/autoFormat/hyphen/transformHyphen.ts | 69 +++++ .../test/autoFormat/AutoFormatPluginTest.ts | 67 ++++- .../autoFormat/hyphen/transformHyphenTest.ts | 246 ++++++++++++++++++ 6 files changed, 403 insertions(+), 11 deletions(-) create mode 100644 packages/roosterjs-content-model-plugins/lib/autoFormat/hyphen/transformHyphen.ts create mode 100644 packages/roosterjs-content-model-plugins/test/autoFormat/hyphen/transformHyphenTest.ts diff --git a/demo/scripts/controlsV2/mainPane/MainPane.tsx b/demo/scripts/controlsV2/mainPane/MainPane.tsx index 39e5889141b..a4e8715b74e 100644 --- a/demo/scripts/controlsV2/mainPane/MainPane.tsx +++ b/demo/scripts/controlsV2/mainPane/MainPane.tsx @@ -475,15 +475,10 @@ export class MainPane extends React.Component<{}, MainPaneState> { imageMenu, watermarkText, markdownOptions, + autoFormatOptions, } = this.state.initState; return [ - pluginList.autoFormat && - new AutoFormatPlugin({ - autoBullet: true, - autoNumbering: true, - autoUnlink: true, - autoLink: true, - }), + pluginList.autoFormat && new AutoFormatPlugin(autoFormatOptions), pluginList.edit && new EditPlugin(), pluginList.paste && new PastePlugin(allowExcelNoBorderTable), pluginList.shortcut && new ShortcutPlugin(), diff --git a/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts b/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts index f79a807df64..45d5348c455 100644 --- a/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts +++ b/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts @@ -43,6 +43,7 @@ const initialState: OptionState = { autoLink: true, autoNumbering: true, autoUnlink: false, + autoHyphen: true, }, markdownOptions: { bold: true, diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts index b10e68a94ce..e00da3d3f7d 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts @@ -1,6 +1,7 @@ import { createLink } from './link/createLink'; import { createLinkAfterSpace } from './link/createLinkAfterSpace'; import { keyboardListTrigger } from './list/keyboardListTrigger'; +import { transformHyphen } from './hyphen/transformHyphen'; import { unlink } from './link/unlink'; import type { ContentChangedEvent, @@ -34,6 +35,11 @@ export type AutoFormatOptions = { * When paste content, create hyperlink for the pasted link */ autoLink: boolean; + + /** + * Transform -- into hyphen, if typed between two words + */ + autoHyphen: boolean; }; /** @@ -44,6 +50,7 @@ const DefaultOptions: Required = { autoNumbering: false, autoUnlink: false, autoLink: false, + autoHyphen: false, }; /** @@ -52,13 +59,13 @@ const DefaultOptions: Required = { */ export class AutoFormatPlugin implements EditorPlugin { private editor: IEditor | null = null; - /** * @param options An optional parameter that takes in an object of type AutoFormatOptions, which includes the following properties: * - autoBullet: A boolean that enables or disables automatic bullet list formatting. Defaults to false. * - autoNumbering: A boolean that enables or disables automatic numbering formatting. Defaults to false. * - autoLink: A boolean that enables or disables automatic hyperlink creation when pasting or typing content. Defaults to false. * - autoUnlink: A boolean that enables or disables automatic hyperlink removal when pressing backspace. Defaults to false. + * - autoHyphen: A boolean that enables or disables automatic hyphen transformation. Defaults to false. */ constructor(private options: AutoFormatOptions = DefaultOptions) {} @@ -112,14 +119,23 @@ export class AutoFormatPlugin implements EditorPlugin { private handleEditorInputEvent(editor: IEditor, event: EditorInputEvent) { const rawEvent = event.rawEvent; - if (rawEvent.inputType === 'insertText') { + const selection = editor.getDOMSelection(); + if ( + rawEvent.inputType === 'insertText' && + selection && + selection.type === 'range' && + selection.range.collapsed + ) { switch (rawEvent.data) { case ' ': - const { autoBullet, autoNumbering, autoLink } = this.options; + const { autoBullet, autoNumbering, autoLink, autoHyphen } = this.options; keyboardListTrigger(editor, autoBullet, autoNumbering); if (autoLink) { createLinkAfterSpace(editor); } + if (autoHyphen) { + transformHyphen(editor); + } break; } } diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/hyphen/transformHyphen.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/hyphen/transformHyphen.ts new file mode 100644 index 00000000000..4820b966dda --- /dev/null +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/hyphen/transformHyphen.ts @@ -0,0 +1,69 @@ +import { getSelectedSegmentsAndParagraphs } from 'roosterjs-content-model-dom'; +import { splitTextSegment } from '../../pluginUtils/splitTextSegment'; +import type { IEditor } from 'roosterjs-content-model-types'; + +/** + * @internal + */ +export function transformHyphen(editor: IEditor) { + editor.formatContentModel((model, context) => { + const selectedAndParagraphs = getSelectedSegmentsAndParagraphs( + model, + false /*includingFormatHolder*/ + ); + if (selectedAndParagraphs.length > 0 && selectedAndParagraphs[0][1]) { + const marker = selectedAndParagraphs[0][0]; + if (marker.segmentType == 'SelectionMarker') { + const paragraph = selectedAndParagraphs[0][1]; + const markerIndex = paragraph.segments.indexOf(marker); + if (markerIndex > 0) { + const previousSegment = paragraph.segments[markerIndex - 1]; + if ( + previousSegment.segmentType === 'Text' && + previousSegment.text.indexOf('--') > -1 + ) { + const segments = previousSegment.text.split(' '); + const dashes = segments[segments.length - 2]; + if (dashes === '--') { + const textIndex = previousSegment.text.lastIndexOf('--'); + const textSegment = splitTextSegment( + previousSegment, + paragraph, + textIndex, + textIndex + 2 + ); + + textSegment.text = textSegment.text.replace('--', '—'); + context.canUndoByBackspace = true; + return true; + } else { + const text = previousSegment.text.split(' ').pop(); + const hasDashes = text && text?.indexOf('--') > -1; + if (hasDashes && text.trim() !== '--') { + const textIndex = previousSegment.text.indexOf(text); + const textSegment = splitTextSegment( + previousSegment, + paragraph, + textIndex, + textIndex + text.length - 1 + ); + + const textLength = textSegment.text.length; + if ( + textSegment.text[0] !== '-' && + textSegment.text[textLength - 1] !== '-' + ) { + textSegment.text = textSegment.text.replace('--', '—'); + context.canUndoByBackspace = true; + return true; + } + } + } + } + } + } + } + + return false; + }); +} diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts index 23afee5ee30..50f418d5d0c 100644 --- a/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts @@ -1,6 +1,7 @@ import * as createLink from '../../lib/autoFormat/link/createLink'; import * as createLinkAfterSpace from '../../lib/autoFormat/link/createLinkAfterSpace'; import * as keyboardTrigger from '../../lib/autoFormat/list/keyboardListTrigger'; +import * as transformHyphen from '../../lib/autoFormat/hyphen/transformHyphen'; import * as unlink from '../../lib/autoFormat/link/unlink'; import { AutoFormatOptions, AutoFormatPlugin } from '../../lib/autoFormat/AutoFormatPlugin'; import { @@ -18,7 +19,10 @@ describe('Content Model Auto Format Plugin Test', () => { focus: () => {}, getDOMSelection: () => ({ - type: -1, + type: 'range', + range: { + collapsed: true, + }, } as any), // Force return invalid range to go through content model code formatContentModel: () => {}, } as any) as IEditor; @@ -280,4 +284,65 @@ describe('Content Model Auto Format Plugin Test', () => { }); }); }); + + describe('onPluginEvent - transformHyphen', () => { + let transformHyphenSpy: jasmine.Spy; + + beforeEach(() => { + transformHyphenSpy = spyOn(transformHyphen, 'transformHyphen'); + }); + + function runTest( + event: EditorInputEvent, + shouldCallTrigger: boolean, + options?: { + autoHyphen: boolean; + } + ) { + const plugin = new AutoFormatPlugin(options as AutoFormatOptions); + plugin.initialize(editor); + + plugin.onPluginEvent(event); + + if (shouldCallTrigger) { + expect(transformHyphenSpy).toHaveBeenCalledWith(editor); + } else { + expect(transformHyphenSpy).not.toHaveBeenCalled(); + } + } + + it('should call transformHyphen', () => { + const event: EditorInputEvent = { + eventType: 'input', + rawEvent: { data: ' ', preventDefault: () => {}, inputType: 'insertText' } as any, + }; + runTest(event, true, { + autoHyphen: true, + }); + }); + + it('should not call transformHyphen - disable options', () => { + const event: EditorInputEvent = { + eventType: 'input', + rawEvent: { data: ' ', preventDefault: () => {}, inputType: 'insertText' } as any, + }; + runTest(event, false, { + autoHyphen: false, + }); + }); + + it('should not call transformHyphen - not space', () => { + const event: EditorInputEvent = { + eventType: 'input', + rawEvent: { + data: 'Backspace', + preventDefault: () => {}, + inputType: 'insertText', + } as any, + }; + runTest(event, false, { + autoHyphen: true, + }); + }); + }); }); diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/hyphen/transformHyphenTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/hyphen/transformHyphenTest.ts new file mode 100644 index 00000000000..b4cacc73a59 --- /dev/null +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/hyphen/transformHyphenTest.ts @@ -0,0 +1,246 @@ +import { ContentModelDocument } from 'roosterjs-content-model-types'; +import { transformHyphen } from '../../../lib/autoFormat/hyphen/transformHyphen'; + +describe('transformHyphen', () => { + function runTest( + input: ContentModelDocument, + expectedModel: ContentModelDocument, + expectedResult: boolean + ) { + const formatWithContentModelSpy = jasmine + .createSpy('formatWithContentModel') + .and.callFake((callback, options) => { + const result = callback(input, { + newEntities: [], + deletedEntities: [], + newImages: [], + canUndoByBackspace: true, + }); + expect(result).toBe(expectedResult); + }); + + transformHyphen({ + focus: () => {}, + formatContentModel: formatWithContentModelSpy, + } as any); + + expect(formatWithContentModelSpy).toHaveBeenCalled(); + expect(input).toEqual(expectedModel); + } + + it('no selected segments', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test--test', + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, input, false); + }); + + it('No hyphen', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, input, false); + }); + + it('with hyphen', () => { + const text = 'test--test'; + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: text, + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test—test', + format: {}, + }, + { + segmentType: 'Text', + text: ' ', + format: {}, + }, + + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); + + it('with hyphen and left space', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test-- test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + + runTest(input, input, false); + }); + + it('with hyphen and left space', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test --test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + + runTest(input, input, false); + }); + + it('with hyphen between spaces', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test -- test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test ', + format: {}, + }, + { + segmentType: 'Text', + text: '—', + format: {}, + }, + { + segmentType: 'Text', + text: ' test ', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); +}); From 28ecdae92eaadd7c7938ac9f2357f98dc6d6b918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Wed, 3 Apr 2024 13:43:06 -0300 Subject: [PATCH 2/5] fix tests --- .../lib/autoFormat/hyphen/transformHyphen.ts | 2 +- .../autoFormat/hyphen/transformHyphenTest.ts | 79 ++++++++++++++++++- 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/hyphen/transformHyphen.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/hyphen/transformHyphen.ts index 4820b966dda..3bba617b0f4 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/hyphen/transformHyphen.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/hyphen/transformHyphen.ts @@ -37,7 +37,7 @@ export function transformHyphen(editor: IEditor) { context.canUndoByBackspace = true; return true; } else { - const text = previousSegment.text.split(' ').pop(); + const text = segments.pop(); const hasDashes = text && text?.indexOf('--') > -1; if (hasDashes && text.trim() !== '--') { const textIndex = previousSegment.text.indexOf(text); diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/hyphen/transformHyphenTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/hyphen/transformHyphenTest.ts index b4cacc73a59..fdc6e7585a2 100644 --- a/packages/roosterjs-content-model-plugins/test/autoFormat/hyphen/transformHyphenTest.ts +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/hyphen/transformHyphenTest.ts @@ -77,6 +77,7 @@ describe('transformHyphen', () => { it('with hyphen', () => { const text = 'test--test'; + spyOn(text, 'split').and.returnValue(['test--test ']); const input: ContentModelDocument = { blockGroupType: 'Document', blocks: [ @@ -108,13 +109,15 @@ describe('transformHyphen', () => { segments: [ { segmentType: 'Text', - text: 'test—test', + text: 'test—tes', format: {}, + isSelected: undefined, }, { segmentType: 'Text', - text: ' ', + text: 't', format: {}, + isSelected: undefined, }, { @@ -186,6 +189,8 @@ describe('transformHyphen', () => { }); it('with hyphen between spaces', () => { + const text = 'test -- test'; + spyOn(text, 'split').and.returnValue(['test', '--', 'test']); const input: ContentModelDocument = { blockGroupType: 'Document', blocks: [ @@ -219,16 +224,19 @@ describe('transformHyphen', () => { segmentType: 'Text', text: 'test ', format: {}, + isSelected: undefined, }, { segmentType: 'Text', text: '—', format: {}, + isSelected: undefined, }, { segmentType: 'Text', - text: ' test ', + text: ' test', format: {}, + isSelected: undefined, }, { segmentType: 'SelectionMarker', @@ -243,4 +251,69 @@ describe('transformHyphen', () => { }; runTest(input, expected, true); }); + + it('with hyphen and multiple words', () => { + const text = 'testing test--test'; + spyOn(text, 'split').and.returnValue(['testing', 'test--test ']); + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: text, + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'testing ', + format: {}, + isSelected: undefined, + }, + { + segmentType: 'Text', + text: 'test—tes', + format: {}, + isSelected: undefined, + }, + { + segmentType: 'Text', + text: 't', + format: {}, + isSelected: undefined, + }, + + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); }); From 80f1ecade9aa42344764ab37267e8a9f31097908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Wed, 3 Apr 2024 16:04:39 -0300 Subject: [PATCH 3/5] add formatTextSegmentBeforeSelectionMarker --- .../lib/autoFormat/hyphen/transformHyphen.ts | 90 ++++----- .../formatTextSegmentBeforeSelectionMarker.ts | 42 +++++ ...matTextSegmentBeforeSelectionMarkerTest.ts | 178 ++++++++++++++++++ 3 files changed, 255 insertions(+), 55 deletions(-) create mode 100644 packages/roosterjs-content-model-plugins/lib/pluginUtils/formatTextSegmentBeforeSelectionMarker.ts create mode 100644 packages/roosterjs-content-model-plugins/test/pluginUtils/formatTextSegmentBeforeSelectionMarkerTest.ts diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/hyphen/transformHyphen.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/hyphen/transformHyphen.ts index 3bba617b0f4..e1527ec6fb2 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/hyphen/transformHyphen.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/hyphen/transformHyphen.ts @@ -1,4 +1,4 @@ -import { getSelectedSegmentsAndParagraphs } from 'roosterjs-content-model-dom'; +import { formatTextSegmentBeforeSelectionMarker } from '../../pluginUtils/formatTextSegmentBeforeSelectionMarker'; import { splitTextSegment } from '../../pluginUtils/splitTextSegment'; import type { IEditor } from 'roosterjs-content-model-types'; @@ -6,64 +6,44 @@ import type { IEditor } from 'roosterjs-content-model-types'; * @internal */ export function transformHyphen(editor: IEditor) { - editor.formatContentModel((model, context) => { - const selectedAndParagraphs = getSelectedSegmentsAndParagraphs( - model, - false /*includingFormatHolder*/ - ); - if (selectedAndParagraphs.length > 0 && selectedAndParagraphs[0][1]) { - const marker = selectedAndParagraphs[0][0]; - if (marker.segmentType == 'SelectionMarker') { - const paragraph = selectedAndParagraphs[0][1]; - const markerIndex = paragraph.segments.indexOf(marker); - if (markerIndex > 0) { - const previousSegment = paragraph.segments[markerIndex - 1]; - if ( - previousSegment.segmentType === 'Text' && - previousSegment.text.indexOf('--') > -1 - ) { - const segments = previousSegment.text.split(' '); - const dashes = segments[segments.length - 2]; - if (dashes === '--') { - const textIndex = previousSegment.text.lastIndexOf('--'); - const textSegment = splitTextSegment( - previousSegment, - paragraph, - textIndex, - textIndex + 2 - ); + formatTextSegmentBeforeSelectionMarker( + editor, + (previousSegment, paragraph, _marker, _markerIndex, context) => { + const segments = previousSegment.text.split(' '); + const dashes = segments[segments.length - 2]; + if (dashes === '--') { + const textIndex = previousSegment.text.lastIndexOf('--'); + const textSegment = splitTextSegment( + previousSegment, + paragraph, + textIndex, + textIndex + 2 + ); - textSegment.text = textSegment.text.replace('--', '—'); - context.canUndoByBackspace = true; - return true; - } else { - const text = segments.pop(); - const hasDashes = text && text?.indexOf('--') > -1; - if (hasDashes && text.trim() !== '--') { - const textIndex = previousSegment.text.indexOf(text); - const textSegment = splitTextSegment( - previousSegment, - paragraph, - textIndex, - textIndex + text.length - 1 - ); + textSegment.text = textSegment.text.replace('--', '—'); + context.canUndoByBackspace = true; + return true; + } else { + const text = segments.pop(); + const hasDashes = text && text?.indexOf('--') > -1; + if (hasDashes && text.trim() !== '--') { + const textIndex = previousSegment.text.indexOf(text); + const textSegment = splitTextSegment( + previousSegment, + paragraph, + textIndex, + textIndex + text.length - 1 + ); - const textLength = textSegment.text.length; - if ( - textSegment.text[0] !== '-' && - textSegment.text[textLength - 1] !== '-' - ) { - textSegment.text = textSegment.text.replace('--', '—'); - context.canUndoByBackspace = true; - return true; - } - } - } + const textLength = textSegment.text.length; + if (textSegment.text[0] !== '-' && textSegment.text[textLength - 1] !== '-') { + textSegment.text = textSegment.text.replace('--', '—'); + context.canUndoByBackspace = true; + return true; } } } + return false; } - - return false; - }); + ); } diff --git a/packages/roosterjs-content-model-plugins/lib/pluginUtils/formatTextSegmentBeforeSelectionMarker.ts b/packages/roosterjs-content-model-plugins/lib/pluginUtils/formatTextSegmentBeforeSelectionMarker.ts new file mode 100644 index 00000000000..6bd0f8fbed4 --- /dev/null +++ b/packages/roosterjs-content-model-plugins/lib/pluginUtils/formatTextSegmentBeforeSelectionMarker.ts @@ -0,0 +1,42 @@ +import { getSelectedSegmentsAndParagraphs } from 'roosterjs-content-model-dom'; +import type { + ContentModelParagraph, + ContentModelSelectionMarker, + ContentModelText, + FormatContentModelContext, + IEditor, +} from 'roosterjs-content-model-types'; + +/** + * @internal + */ +export function formatTextSegmentBeforeSelectionMarker( + editor: IEditor, + callback: ( + previousSegment: ContentModelText, + paragraph: ContentModelParagraph, + marker: ContentModelSelectionMarker, + markerIndex: number, + context: FormatContentModelContext + ) => boolean +) { + editor.formatContentModel((model, context) => { + const selectedSegmentsAndParagraphs = getSelectedSegmentsAndParagraphs( + model, + false /*includeFormatHolder*/ + ); + + if (selectedSegmentsAndParagraphs.length > 0 && selectedSegmentsAndParagraphs[0][1]) { + const marker = selectedSegmentsAndParagraphs[0][0]; + const paragraph = selectedSegmentsAndParagraphs[0][1]; + const markerIndex = paragraph.segments.indexOf(marker); + if (marker.segmentType === 'SelectionMarker' && markerIndex > 0) { + const previousSegment = paragraph.segments[markerIndex - 1]; + if (previousSegment && previousSegment.segmentType === 'Text') { + return callback(previousSegment, paragraph, marker, markerIndex, context); + } + } + } + return false; + }); +} diff --git a/packages/roosterjs-content-model-plugins/test/pluginUtils/formatTextSegmentBeforeSelectionMarkerTest.ts b/packages/roosterjs-content-model-plugins/test/pluginUtils/formatTextSegmentBeforeSelectionMarkerTest.ts new file mode 100644 index 00000000000..484fd3dcb53 --- /dev/null +++ b/packages/roosterjs-content-model-plugins/test/pluginUtils/formatTextSegmentBeforeSelectionMarkerTest.ts @@ -0,0 +1,178 @@ +import { formatTextSegmentBeforeSelectionMarker } from '../../lib/pluginUtils/formatTextSegmentBeforeSelectionMarker'; +import { + ContentModelDocument, + ContentModelParagraph, + ContentModelSelectionMarker, + ContentModelText, + FormatContentModelContext, +} from 'roosterjs-content-model-types'; + +describe('formatTextSegmentBeforeSelectionMarker', () => { + function runTest( + input: ContentModelDocument, + callback: ( + previousSegment: ContentModelText, + paragraph: ContentModelParagraph, + marker: ContentModelSelectionMarker, + markerIndex: number, + context: FormatContentModelContext + ) => boolean, + expectedModel: ContentModelDocument, + expectedResult: boolean + ) { + const formatWithContentModelSpy = jasmine + .createSpy('formatWithContentModel') + .and.callFake((callback, options) => { + const result = callback(input, { + newEntities: [], + deletedEntities: [], + newImages: [], + canUndoByBackspace: true, + }); + expect(result).toBe(expectedResult); + }); + + formatTextSegmentBeforeSelectionMarker( + { + focus: () => {}, + formatContentModel: formatWithContentModelSpy, + } as any, + callback + ); + + expect(formatWithContentModelSpy).toHaveBeenCalled(); + expect(input).toEqual(expectedModel); + } + + it('no selection marker', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + ], + format: {}, + }, + ], + }; + runTest(input, () => true, input, false); + }); + + it('no previous segment', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + }; + runTest(input, () => true, input, false); + }); + + it('previous segment is not text', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Image', + src: 'test', + format: {}, + dataset: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + }; + runTest(input, () => true, input, false); + }); + + it('format segment', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'first', + format: {}, + }, + { + segmentType: 'Text', + text: 'second', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + }; + const expectedModel: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'first', + format: {}, + }, + { + segmentType: 'Text', + text: 'second', + format: { + textColor: 'red', + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + }; + runTest( + input, + previousSegment => { + previousSegment.format = { textColor: 'red' }; + return true; + }, + expectedModel, + true + ); + }); +}); From b1b398ed6a3757d55f3f476bc92280d939d07941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Thu, 4 Apr 2024 14:29:58 -0300 Subject: [PATCH 4/5] refactor code using formatTextSegmentBeforeSelectionMarkerTest --- .../lib/autoFormat/AutoFormatPlugin.ts | 46 +- .../lib/autoFormat/hyphen/transformHyphen.ts | 71 +- .../autoFormat/link/createLinkAfterSpace.ts | 78 +- .../lib/autoFormat/link/getLinkSegment.ts | 25 +- .../autoFormat/list/keyboardListTrigger.ts | 48 +- .../lib/markdown/utils/setFormat.ts | 91 +- .../formatTextSegmentBeforeSelectionMarker.ts | 7 +- .../test/autoFormat/AutoFormatPluginTest.ts | 131 +- .../autoFormat/hyphen/transformHyphenTest.ts | 374 +--- .../link/createLinkAfterSpaceTest.ts | 352 +--- .../list/keyboardListTriggerTest.ts | 979 ++-------- ...matTextSegmentBeforeSelectionMarkerTest.ts | 1695 ++++++++++++++++- 12 files changed, 2282 insertions(+), 1615 deletions(-) diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts index e00da3d3f7d..2f983dee8ce 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts @@ -1,5 +1,6 @@ import { createLink } from './link/createLink'; import { createLinkAfterSpace } from './link/createLinkAfterSpace'; +import { formatTextSegmentBeforeSelectionMarker } from '../pluginUtils/formatTextSegmentBeforeSelectionMarker'; import { keyboardListTrigger } from './list/keyboardListTrigger'; import { transformHyphen } from './hyphen/transformHyphen'; import { unlink } from './link/unlink'; @@ -128,14 +129,43 @@ export class AutoFormatPlugin implements EditorPlugin { ) { switch (rawEvent.data) { case ' ': - const { autoBullet, autoNumbering, autoLink, autoHyphen } = this.options; - keyboardListTrigger(editor, autoBullet, autoNumbering); - if (autoLink) { - createLinkAfterSpace(editor); - } - if (autoHyphen) { - transformHyphen(editor); - } + formatTextSegmentBeforeSelectionMarker( + editor, + (model, previousSegment, paragraph, context) => { + const { + autoBullet, + autoNumbering, + autoLink, + autoHyphen, + } = this.options; + let shouldHyphen = false; + let shouldLink = false; + + if (autoLink) { + shouldLink = createLinkAfterSpace( + previousSegment, + paragraph, + context + ); + } + + if (autoHyphen) { + shouldHyphen = transformHyphen(previousSegment, paragraph, context); + } + + return ( + keyboardListTrigger( + model, + paragraph, + context, + autoBullet, + autoNumbering + ) || + shouldHyphen || + shouldLink + ); + } + ); break; } } diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/hyphen/transformHyphen.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/hyphen/transformHyphen.ts index e1527ec6fb2..95a865d734e 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/hyphen/transformHyphen.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/hyphen/transformHyphen.ts @@ -1,49 +1,46 @@ -import { formatTextSegmentBeforeSelectionMarker } from '../../pluginUtils/formatTextSegmentBeforeSelectionMarker'; import { splitTextSegment } from '../../pluginUtils/splitTextSegment'; -import type { IEditor } from 'roosterjs-content-model-types'; +import type { + ContentModelParagraph, + ContentModelText, + FormatContentModelContext, +} from 'roosterjs-content-model-types'; /** * @internal */ -export function transformHyphen(editor: IEditor) { - formatTextSegmentBeforeSelectionMarker( - editor, - (previousSegment, paragraph, _marker, _markerIndex, context) => { - const segments = previousSegment.text.split(' '); - const dashes = segments[segments.length - 2]; - if (dashes === '--') { - const textIndex = previousSegment.text.lastIndexOf('--'); - const textSegment = splitTextSegment( - previousSegment, - paragraph, - textIndex, - textIndex + 2 - ); +export function transformHyphen( + previousSegment: ContentModelText, + paragraph: ContentModelParagraph, + context: FormatContentModelContext +): boolean { + const segments = previousSegment.text.split(' '); + const dashes = segments[segments.length - 2]; + if (dashes === '--') { + const textIndex = previousSegment.text.lastIndexOf('--'); + const textSegment = splitTextSegment(previousSegment, paragraph, textIndex, textIndex + 2); + textSegment.text = textSegment.text.replace('--', '—'); + context.canUndoByBackspace = true; + return true; + } else { + const text = segments.pop(); + const hasDashes = text && text?.indexOf('--') > -1; + if (hasDashes && text.trim() !== '--') { + const textIndex = previousSegment.text.indexOf(text); + const textSegment = splitTextSegment( + previousSegment, + paragraph, + textIndex, + textIndex + text.length - 1 + ); + + const textLength = textSegment.text.length; + if (textSegment.text[0] !== '-' && textSegment.text[textLength - 1] !== '-') { textSegment.text = textSegment.text.replace('--', '—'); context.canUndoByBackspace = true; return true; - } else { - const text = segments.pop(); - const hasDashes = text && text?.indexOf('--') > -1; - if (hasDashes && text.trim() !== '--') { - const textIndex = previousSegment.text.indexOf(text); - const textSegment = splitTextSegment( - previousSegment, - paragraph, - textIndex, - textIndex + text.length - 1 - ); - - const textLength = textSegment.text.length; - if (textSegment.text[0] !== '-' && textSegment.text[textLength - 1] !== '-') { - textSegment.text = textSegment.text.replace('--', '—'); - context.canUndoByBackspace = true; - return true; - } - } } - return false; } - ); + } + return false; } diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts index d529474d025..ca39668f0b5 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts @@ -1,57 +1,41 @@ -import { getSelectedSegmentsAndParagraphs } from 'roosterjs-content-model-dom'; import { matchLink } from 'roosterjs-content-model-api'; import { splitTextSegment } from '../../pluginUtils/splitTextSegment'; -import type { IEditor, LinkData } from 'roosterjs-content-model-types'; +import type { + ContentModelParagraph, + ContentModelText, + FormatContentModelContext, + LinkData, +} from 'roosterjs-content-model-types'; /** * @internal */ -export function createLinkAfterSpace(editor: IEditor) { - editor.formatContentModel((model, context) => { - const selectedSegmentsAndParagraphs = getSelectedSegmentsAndParagraphs( - model, - false /* includingFormatHolder */ +export function createLinkAfterSpace( + previousSegment: ContentModelText, + paragraph: ContentModelParagraph, + context: FormatContentModelContext +) { + const link = previousSegment.text.split(' ').pop(); + const url = link?.trim(); + let linkData: LinkData | null = null; + if (url && link && (linkData = matchLink(url))) { + const linkSegment = splitTextSegment( + previousSegment, + paragraph, + previousSegment.text.length - link.trimLeft().length, + previousSegment.text.trimRight().length ); - if (selectedSegmentsAndParagraphs.length > 0 && selectedSegmentsAndParagraphs[0][1]) { - const markerIndex = selectedSegmentsAndParagraphs[0][1].segments.findIndex( - segment => segment.segmentType == 'SelectionMarker' - ); - const paragraph = selectedSegmentsAndParagraphs[0][1]; - if (markerIndex > 0) { - const textSegment = paragraph.segments[markerIndex - 1]; - const marker = paragraph.segments[markerIndex]; - if ( - marker.segmentType == 'SelectionMarker' && - textSegment && - textSegment.segmentType == 'Text' && - !textSegment.link - ) { - const link = textSegment.text.split(' ').pop(); - const url = link?.trim(); - let linkData: LinkData | null = null; - if (url && link && (linkData = matchLink(url))) { - const linkSegment = splitTextSegment( - textSegment, - paragraph, - textSegment.text.length - link.trimLeft().length, - textSegment.text.trimRight().length - ); - linkSegment.link = { - format: { - href: linkData.normalizedUrl, - underline: true, - }, - dataset: {}, - }; + linkSegment.link = { + format: { + href: linkData.normalizedUrl, + underline: true, + }, + dataset: {}, + }; - context.canUndoByBackspace = true; + context.canUndoByBackspace = true; - return true; - } - } - } - } - - return false; - }); + return true; + } + return false; } diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkSegment.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkSegment.ts index caa1e54845e..9c34b7370e6 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkSegment.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkSegment.ts @@ -12,17 +12,20 @@ export function getLinkSegment(model: ContentModelDocument) { ); if (selectedSegmentsAndParagraphs.length == 1 && selectedSegmentsAndParagraphs[0][1]) { const selectedParagraph = selectedSegmentsAndParagraphs[0][1]; - const marker = selectedParagraph.segments[selectedParagraph.segments.length - 1]; - const link = selectedParagraph.segments[selectedParagraph.segments.length - 2]; - if ( - marker && - link && - marker.segmentType === 'SelectionMarker' && - marker.isSelected && - link.segmentType === 'Text' && - (matchLink(link.text) || link.link) - ) { - return link; + const marker = selectedSegmentsAndParagraphs[0][0]; + if (marker && marker.segmentType === 'SelectionMarker') { + const markerIndex = selectedParagraph.segments.indexOf(marker); + const link = selectedParagraph.segments[markerIndex - 1]; + if ( + marker && + link && + marker.segmentType === 'SelectionMarker' && + marker.isSelected && + link.segmentType === 'Text' && + (matchLink(link.text) || link.link) + ) { + return link; + } } } return undefined; diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/list/keyboardListTrigger.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/list/keyboardListTrigger.ts index 91357ddd9b1..12d8d632375 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/list/keyboardListTrigger.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/list/keyboardListTrigger.ts @@ -1,47 +1,41 @@ import { getListTypeStyle } from './getListTypeStyle'; -import { getSelectedSegmentsAndParagraphs } from 'roosterjs-content-model-dom'; import { setListType, setModelListStartNumber, setModelListStyle, } from 'roosterjs-content-model-api'; -import type { ContentModelDocument, IEditor } from 'roosterjs-content-model-types'; +import type { + ContentModelDocument, + ContentModelParagraph, + FormatContentModelContext, +} from 'roosterjs-content-model-types'; /** * @internal */ export function keyboardListTrigger( - editor: IEditor, + model: ContentModelDocument, + paragraph: ContentModelParagraph, + context: FormatContentModelContext, shouldSearchForBullet: boolean = true, shouldSearchForNumbering: boolean = true ) { if (shouldSearchForBullet || shouldSearchForNumbering) { - editor.formatContentModel( - (model, context) => { - const listStyleType = getListTypeStyle( - model, - shouldSearchForBullet, - shouldSearchForNumbering - ); - if (listStyleType) { - const segmentsAndParagraphs = getSelectedSegmentsAndParagraphs(model, false); - if (segmentsAndParagraphs[0] && segmentsAndParagraphs[0][1]) { - segmentsAndParagraphs[0][1].segments.splice(0, 1); - } - const { listType, styleType, index } = listStyleType; - triggerList(model, listType, styleType, index); - context.canUndoByBackspace = true; - - return true; - } - - return false; - }, - { - apiName: 'autoToggleList', - } + const listStyleType = getListTypeStyle( + model, + shouldSearchForBullet, + shouldSearchForNumbering ); + if (listStyleType) { + paragraph.segments.splice(0, 1); + const { listType, styleType, index } = listStyleType; + triggerList(model, listType, styleType, index); + context.canUndoByBackspace = true; + + return true; + } } + return false; } const triggerList = ( diff --git a/packages/roosterjs-content-model-plugins/lib/markdown/utils/setFormat.ts b/packages/roosterjs-content-model-plugins/lib/markdown/utils/setFormat.ts index b9ba6af91cd..c314ca66601 100644 --- a/packages/roosterjs-content-model-plugins/lib/markdown/utils/setFormat.ts +++ b/packages/roosterjs-content-model-plugins/lib/markdown/utils/setFormat.ts @@ -1,4 +1,4 @@ -import { getSelectedSegmentsAndParagraphs } from 'roosterjs-content-model-dom'; +import { formatTextSegmentBeforeSelectionMarker } from '../../pluginUtils/formatTextSegmentBeforeSelectionMarker'; import { splitTextSegment } from '../../pluginUtils/splitTextSegment'; import type { @@ -16,65 +16,40 @@ export function setFormat( format: ContentModelSegmentFormat, codeFormat?: ContentModelCodeFormat ) { - editor.formatContentModel((model, context) => { - const selectedSegmentsAndParagraphs = getSelectedSegmentsAndParagraphs( - model, - false /*includeFormatHolder*/ - ); - - if (selectedSegmentsAndParagraphs.length > 0 && selectedSegmentsAndParagraphs[0][1]) { - const marker = selectedSegmentsAndParagraphs[0][0]; - context.newPendingFormat = { - ...marker.format, - strikethrough: !!marker.format.strikethrough, - italic: !!marker.format.italic, - fontWeight: marker.format?.fontWeight ? 'bold' : undefined, - }; - - const paragraph = selectedSegmentsAndParagraphs[0][1]; - if (marker.segmentType == 'SelectionMarker') { - const markerIndex = paragraph.segments.indexOf(marker); - if (markerIndex > 0 && paragraph.segments[markerIndex - 1]) { - const segmentBeforeMarker = paragraph.segments[markerIndex - 1]; - - if ( - segmentBeforeMarker.segmentType == 'Text' && - segmentBeforeMarker.text[segmentBeforeMarker.text.length - 1] == character - ) { - const textBeforeMarker = segmentBeforeMarker.text.slice(0, -1); - if (textBeforeMarker.indexOf(character) > -1) { - const lastCharIndex = segmentBeforeMarker.text.length; - const firstCharIndex = segmentBeforeMarker.text - .substring(0, lastCharIndex - 1) - .lastIndexOf(character); - - const formattedText = splitTextSegment( - segmentBeforeMarker, - paragraph, - firstCharIndex, - lastCharIndex - ); - - formattedText.text = formattedText.text - .replace(character, '') - .slice(0, -1); - formattedText.format = { - ...formattedText.format, - ...format, - }; - if (codeFormat) { - formattedText.code = { - format: codeFormat, - }; - } - - context.canUndoByBackspace = true; - return true; - } + formatTextSegmentBeforeSelectionMarker( + editor, + (_model, previousSegment, paragraph, context) => { + if (previousSegment.text[previousSegment.text.length - 1] == character) { + const textBeforeMarker = previousSegment.text.slice(0, -1); + if (textBeforeMarker.indexOf(character) > -1) { + const lastCharIndex = previousSegment.text.length; + const firstCharIndex = previousSegment.text + .substring(0, lastCharIndex - 1) + .lastIndexOf(character); + + const formattedText = splitTextSegment( + previousSegment, + paragraph, + firstCharIndex, + lastCharIndex + ); + + formattedText.text = formattedText.text.replace(character, '').slice(0, -1); + formattedText.format = { + ...formattedText.format, + ...format, + }; + if (codeFormat) { + formattedText.code = { + format: codeFormat, + }; } + + context.canUndoByBackspace = true; + return true; } } + return false; } - return false; - }); + ); } diff --git a/packages/roosterjs-content-model-plugins/lib/pluginUtils/formatTextSegmentBeforeSelectionMarker.ts b/packages/roosterjs-content-model-plugins/lib/pluginUtils/formatTextSegmentBeforeSelectionMarker.ts index 6bd0f8fbed4..62814ba6dff 100644 --- a/packages/roosterjs-content-model-plugins/lib/pluginUtils/formatTextSegmentBeforeSelectionMarker.ts +++ b/packages/roosterjs-content-model-plugins/lib/pluginUtils/formatTextSegmentBeforeSelectionMarker.ts @@ -1,7 +1,7 @@ import { getSelectedSegmentsAndParagraphs } from 'roosterjs-content-model-dom'; import type { + ContentModelDocument, ContentModelParagraph, - ContentModelSelectionMarker, ContentModelText, FormatContentModelContext, IEditor, @@ -13,10 +13,9 @@ import type { export function formatTextSegmentBeforeSelectionMarker( editor: IEditor, callback: ( + model: ContentModelDocument, previousSegment: ContentModelText, paragraph: ContentModelParagraph, - marker: ContentModelSelectionMarker, - markerIndex: number, context: FormatContentModelContext ) => boolean ) { @@ -33,7 +32,7 @@ export function formatTextSegmentBeforeSelectionMarker( if (marker.segmentType === 'SelectionMarker' && markerIndex > 0) { const previousSegment = paragraph.segments[markerIndex - 1]; if (previousSegment && previousSegment.segmentType === 'Text') { - return callback(previousSegment, paragraph, marker, markerIndex, context); + return callback(model, previousSegment, paragraph, context); } } } diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts index 50f418d5d0c..d491984dbe0 100644 --- a/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts @@ -1,20 +1,30 @@ import * as createLink from '../../lib/autoFormat/link/createLink'; -import * as createLinkAfterSpace from '../../lib/autoFormat/link/createLinkAfterSpace'; -import * as keyboardTrigger from '../../lib/autoFormat/list/keyboardListTrigger'; -import * as transformHyphen from '../../lib/autoFormat/hyphen/transformHyphen'; +import * as formatTextSegmentBeforeSelectionMarker from '../../lib/pluginUtils/formatTextSegmentBeforeSelectionMarker'; import * as unlink from '../../lib/autoFormat/link/unlink'; import { AutoFormatOptions, AutoFormatPlugin } from '../../lib/autoFormat/AutoFormatPlugin'; +import { createLinkAfterSpace } from '../../lib/autoFormat/link/createLinkAfterSpace'; +import { keyboardListTrigger } from '../../lib/autoFormat/list/keyboardListTrigger'; +import { transformHyphen } from '../../lib/autoFormat/hyphen/transformHyphen'; import { ContentChangedEvent, + ContentModelDocument, + ContentModelParagraph, + ContentModelText, EditorInputEvent, + FormatContentModelContext, IEditor, KeyDownEvent, } from 'roosterjs-content-model-types'; describe('Content Model Auto Format Plugin Test', () => { let editor: IEditor; + let formatTextSegmentBeforeSelectionMarkerSpy: jasmine.Spy; beforeEach(() => { + formatTextSegmentBeforeSelectionMarkerSpy = spyOn( + formatTextSegmentBeforeSelectionMarker, + 'formatTextSegmentBeforeSelectionMarker' + ); editor = ({ focus: () => {}, getDOMSelection: () => @@ -29,15 +39,8 @@ describe('Content Model Auto Format Plugin Test', () => { }); describe('onPluginEvent - keyboardListTrigger', () => { - let keyboardListTriggerSpy: jasmine.Spy; - - beforeEach(() => { - keyboardListTriggerSpy = spyOn(keyboardTrigger, 'keyboardListTrigger'); - }); - function runTest( event: EditorInputEvent, - shouldCallTrigger: boolean, options?: { autoBullet: boolean; autoNumbering: boolean; @@ -48,15 +51,25 @@ describe('Content Model Auto Format Plugin Test', () => { plugin.onPluginEvent(event); - if (shouldCallTrigger) { - expect(keyboardListTriggerSpy).toHaveBeenCalledWith( + formatTextSegmentBeforeSelectionMarkerSpy.and.callFake((editor, callback) => { + expect(callback).toBe( editor, - options ? options.autoBullet : true, - options ? options.autoNumbering : true + ( + model: ContentModelDocument, + _previousSegment: ContentModelText, + paragraph: ContentModelParagraph, + context: FormatContentModelContext + ) => { + return keyboardListTrigger( + model, + paragraph, + context, + options!.autoBullet, + options!.autoNumbering + ); + } ); - } else { - expect(keyboardListTriggerSpy).not.toHaveBeenCalled(); - } + }); } it('should trigger keyboardListTrigger', () => { @@ -64,7 +77,7 @@ describe('Content Model Auto Format Plugin Test', () => { eventType: 'input', rawEvent: { data: ' ', defaultPrevented: false, inputType: 'insertText' } as any, }; - runTest(event, true, { + runTest(event, { autoBullet: true, autoNumbering: true, }); @@ -75,7 +88,7 @@ describe('Content Model Auto Format Plugin Test', () => { eventType: 'input', rawEvent: { data: '*', defaultPrevented: false, inputType: 'insertText' } as any, }; - runTest(event, false, { + runTest(event, { autoBullet: true, autoNumbering: true, }); @@ -86,7 +99,7 @@ describe('Content Model Auto Format Plugin Test', () => { eventType: 'input', rawEvent: { data: ' ', defaultPrevented: false, inputType: 'insertText' } as any, }; - runTest(event, true, { autoBullet: false, autoNumbering: false } as AutoFormatOptions); + runTest(event, { autoBullet: false, autoNumbering: false } as AutoFormatOptions); }); it('should trigger keyboardListTrigger with auto bullet only', () => { @@ -94,7 +107,7 @@ describe('Content Model Auto Format Plugin Test', () => { eventType: 'input', rawEvent: { data: ' ', defaultPrevented: false, inputType: 'insertText' } as any, }; - runTest(event, true, { autoBullet: true, autoNumbering: false } as AutoFormatOptions); + runTest(event, { autoBullet: true, autoNumbering: false } as AutoFormatOptions); }); it('should trigger keyboardListTrigger with auto numbering only', () => { @@ -102,7 +115,7 @@ describe('Content Model Auto Format Plugin Test', () => { eventType: 'input', rawEvent: { data: ' ', defaultPrevented: false, inputType: 'insertText' } as any, }; - runTest(event, true, { autoBullet: false, autoNumbering: true } as AutoFormatOptions); + runTest(event, { autoBullet: false, autoNumbering: true } as AutoFormatOptions); }); it('should not trigger keyboardListTrigger if the input type is different from insertText', () => { @@ -110,7 +123,7 @@ describe('Content Model Auto Format Plugin Test', () => { eventType: 'input', rawEvent: { key: ' ', defaultPrevented: false, inputType: 'test' } as any, }; - runTest(event, false, { autoBullet: true, autoNumbering: true } as AutoFormatOptions); + runTest(event, { autoBullet: true, autoNumbering: true } as AutoFormatOptions); }); }); @@ -225,15 +238,8 @@ describe('Content Model Auto Format Plugin Test', () => { }); describe('onPluginEvent - createLinkAfterSpace', () => { - let createLinkAfterSpaceSpy: jasmine.Spy; - - beforeEach(() => { - createLinkAfterSpaceSpy = spyOn(createLinkAfterSpace, 'createLinkAfterSpace'); - }); - function runTest( event: EditorInputEvent, - shouldCallTrigger: boolean, options?: { autoLink: boolean; } @@ -242,12 +248,23 @@ describe('Content Model Auto Format Plugin Test', () => { plugin.initialize(editor); plugin.onPluginEvent(event); - - if (shouldCallTrigger) { - expect(createLinkAfterSpaceSpy).toHaveBeenCalledWith(editor); - } else { - expect(createLinkAfterSpaceSpy).not.toHaveBeenCalled(); - } + formatTextSegmentBeforeSelectionMarkerSpy.and.callFake((editor, callback) => { + expect(callback).toBe( + editor, + ( + _model: ContentModelDocument, + previousSegment: ContentModelText, + paragraph: ContentModelParagraph, + context: FormatContentModelContext + ) => { + return ( + options && + options.autoLink && + createLinkAfterSpace(previousSegment, paragraph, context) + ); + } + ); + }); } it('should call createLinkAfterSpace', () => { @@ -255,7 +272,7 @@ describe('Content Model Auto Format Plugin Test', () => { eventType: 'input', rawEvent: { data: ' ', preventDefault: () => {}, inputType: 'insertText' } as any, }; - runTest(event, true, { + runTest(event, { autoLink: true, }); }); @@ -265,7 +282,7 @@ describe('Content Model Auto Format Plugin Test', () => { eventType: 'input', rawEvent: { data: ' ', preventDefault: () => {}, inputType: 'insertText' } as any, }; - runTest(event, false, { + runTest(event, { autoLink: false, }); }); @@ -279,22 +296,15 @@ describe('Content Model Auto Format Plugin Test', () => { inputType: 'insertText', } as any, }; - runTest(event, false, { + runTest(event, { autoLink: true, }); }); }); describe('onPluginEvent - transformHyphen', () => { - let transformHyphenSpy: jasmine.Spy; - - beforeEach(() => { - transformHyphenSpy = spyOn(transformHyphen, 'transformHyphen'); - }); - function runTest( event: EditorInputEvent, - shouldCallTrigger: boolean, options?: { autoHyphen: boolean; } @@ -303,12 +313,23 @@ describe('Content Model Auto Format Plugin Test', () => { plugin.initialize(editor); plugin.onPluginEvent(event); - - if (shouldCallTrigger) { - expect(transformHyphenSpy).toHaveBeenCalledWith(editor); - } else { - expect(transformHyphenSpy).not.toHaveBeenCalled(); - } + formatTextSegmentBeforeSelectionMarkerSpy.and.callFake((editor, callback) => { + expect(callback).toBe( + editor, + ( + _model: ContentModelDocument, + previousSegment: ContentModelText, + paragraph: ContentModelParagraph, + context: FormatContentModelContext + ) => { + return ( + options && + options.autoHyphen && + transformHyphen(previousSegment, paragraph, context) + ); + } + ); + }); } it('should call transformHyphen', () => { @@ -316,7 +337,7 @@ describe('Content Model Auto Format Plugin Test', () => { eventType: 'input', rawEvent: { data: ' ', preventDefault: () => {}, inputType: 'insertText' } as any, }; - runTest(event, true, { + runTest(event, { autoHyphen: true, }); }); @@ -326,7 +347,7 @@ describe('Content Model Auto Format Plugin Test', () => { eventType: 'input', rawEvent: { data: ' ', preventDefault: () => {}, inputType: 'insertText' } as any, }; - runTest(event, false, { + runTest(event, { autoHyphen: false, }); }); @@ -340,7 +361,7 @@ describe('Content Model Auto Format Plugin Test', () => { inputType: 'insertText', } as any, }; - runTest(event, false, { + runTest(event, { autoHyphen: true, }); }); diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/hyphen/transformHyphenTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/hyphen/transformHyphenTest.ts index fdc6e7585a2..5e4c09c23ab 100644 --- a/packages/roosterjs-content-model-plugins/test/autoFormat/hyphen/transformHyphenTest.ts +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/hyphen/transformHyphenTest.ts @@ -1,319 +1,143 @@ -import { ContentModelDocument } from 'roosterjs-content-model-types'; import { transformHyphen } from '../../../lib/autoFormat/hyphen/transformHyphen'; +import { + ContentModelParagraph, + ContentModelText, + FormatContentModelContext, +} from 'roosterjs-content-model-types'; describe('transformHyphen', () => { function runTest( - input: ContentModelDocument, - expectedModel: ContentModelDocument, + previousSegment: ContentModelText, + paragraph: ContentModelParagraph, + context: FormatContentModelContext, expectedResult: boolean ) { - const formatWithContentModelSpy = jasmine - .createSpy('formatWithContentModel') - .and.callFake((callback, options) => { - const result = callback(input, { - newEntities: [], - deletedEntities: [], - newImages: [], - canUndoByBackspace: true, - }); - expect(result).toBe(expectedResult); - }); - - transformHyphen({ - focus: () => {}, - formatContentModel: formatWithContentModelSpy, - } as any); - - expect(formatWithContentModelSpy).toHaveBeenCalled(); - expect(input).toEqual(expectedModel); + const result = transformHyphen(previousSegment, paragraph, context); + expect(result).toBe(expectedResult); } - it('no selected segments', () => { - const input: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test--test', - format: {}, - }, - ], - format: {}, - }, - ], + it('with hyphen', () => { + const segment: ContentModelText = { + segmentType: 'Text', + text: 'test--test', + format: {}, + }; + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [segment], format: {}, }; - runTest(input, input, false); + runTest(segment, paragraph, { canUndoByBackspace: true } as any, true); }); it('No hyphen', () => { - const input: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test', - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], + const segment: ContentModelText = { + segmentType: 'Text', + text: 'test', + format: {}, + }; + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [segment], format: {}, }; - runTest(input, input, false); + runTest(segment, paragraph, { canUndoByBackspace: true } as any, false); }); - it('with hyphen', () => { - const text = 'test--test'; - spyOn(text, 'split').and.returnValue(['test--test ']); - const input: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: text, - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], + it('with hyphen between spaces', () => { + const segment: ContentModelText = { + segmentType: 'Text', + text: 'test -- test', format: {}, }; - - const expected: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test—tes', - format: {}, - isSelected: undefined, - }, - { - segmentType: 'Text', - text: 't', - format: {}, - isSelected: undefined, - }, - - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [segment], format: {}, }; - runTest(input, expected, true); + runTest(segment, paragraph, { canUndoByBackspace: true } as any, true); }); - it('with hyphen and left space', () => { - const input: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test-- test', - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], + it('with hyphen at the end', () => { + const segment: ContentModelText = { + segmentType: 'Text', + text: 'test--', format: {}, }; - - runTest(input, input, false); + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [segment], + format: {}, + }; + runTest(segment, paragraph, { canUndoByBackspace: true } as any, false); }); - - it('with hyphen and left space', () => { - const input: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test --test', - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], + it('with hyphen at the start', () => { + const segment: ContentModelText = { + segmentType: 'Text', + text: '--test', format: {}, }; - - runTest(input, input, false); + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [segment], + format: {}, + }; + runTest(segment, paragraph, { canUndoByBackspace: true } as any, false); }); - it('with hyphen between spaces', () => { - const text = 'test -- test'; - spyOn(text, 'split').and.returnValue(['test', '--', 'test']); - const input: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test -- test', - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], + it('with hyphen and space right', () => { + const segment: ContentModelText = { + segmentType: 'Text', + text: 'test-- test', format: {}, }; - - const expected: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test ', - format: {}, - isSelected: undefined, - }, - { - segmentType: 'Text', - text: '—', - format: {}, - isSelected: undefined, - }, - { - segmentType: 'Text', - text: ' test', - format: {}, - isSelected: undefined, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [segment], format: {}, }; - runTest(input, expected, true); + runTest(segment, paragraph, { canUndoByBackspace: true } as any, false); }); - it('with hyphen and multiple words', () => { - const text = 'testing test--test'; - spyOn(text, 'split').and.returnValue(['testing', 'test--test ']); - const input: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: text, - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], + it('with hyphen and space left', () => { + const segment: ContentModelText = { + segmentType: 'Text', + text: 'test --test', + format: {}, + }; + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [segment], format: {}, }; + runTest(segment, paragraph, { canUndoByBackspace: true } as any, false); + }); - const expected: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'testing ', - format: {}, - isSelected: undefined, - }, - { - segmentType: 'Text', - text: 'test—tes', - format: {}, - isSelected: undefined, - }, - { - segmentType: 'Text', - text: 't', - format: {}, - isSelected: undefined, - }, + it('with hyphen and more text', () => { + const segment: ContentModelText = { + segmentType: 'Text', + text: 'testing hyphen test test--test', + format: {}, + }; + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [segment], + format: {}, + }; + runTest(segment, paragraph, { canUndoByBackspace: true } as any, true); + }); - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], + it('text after dashes', () => { + const segment: ContentModelText = { + segmentType: 'Text', + text: 'test--test testing hyphen test', + format: {}, + }; + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [segment], format: {}, }; - runTest(input, expected, true); + runTest(segment, paragraph, { canUndoByBackspace: true } as any, false); }); }); diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkAfterSpaceTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkAfterSpaceTest.ts index a93df5e9573..def1ba834f2 100644 --- a/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkAfterSpaceTest.ts +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkAfterSpaceTest.ts @@ -1,344 +1,60 @@ -import { ContentModelDocument } from 'roosterjs-content-model-types'; import { createLinkAfterSpace } from '../../../lib/autoFormat/link/createLinkAfterSpace'; +import { + ContentModelParagraph, + ContentModelText, + FormatContentModelContext, +} from 'roosterjs-content-model-types'; describe('createLinkAfterSpace', () => { function runTest( - input: ContentModelDocument, - expectedModel: ContentModelDocument, + previousSegment: ContentModelText, + paragraph: ContentModelParagraph, + context: FormatContentModelContext, expectedResult: boolean ) { - const formatWithContentModelSpy = jasmine - .createSpy('formatWithContentModel') - .and.callFake((callback, options) => { - const result = callback(input, { - newEntities: [], - deletedEntities: [], - newImages: [], - canUndoByBackspace: true, - }); - expect(result).toBe(expectedResult); - }); - - createLinkAfterSpace({ - focus: () => {}, - formatContentModel: formatWithContentModelSpy, - } as any); - - expect(formatWithContentModelSpy).toHaveBeenCalled(); - expect(input).toEqual(expectedModel); + const result = createLinkAfterSpace(previousSegment, paragraph, context); + expect(result).toBe(expectedResult); } - it('no selected segments', () => { - const input: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test', - format: {}, - }, - ], - format: {}, - }, - ], - format: {}, - }; - runTest(input, input, false); - }); - - it('no link segment', () => { - const input: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test', - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], - format: {}, - }; - - runTest(input, input, false); - }); - - it('link segment with WWW', () => { - const input: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'www.bing.com', - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], + it('with link', () => { + const segment: ContentModelText = { + segmentType: 'Text', + text: 'test http://bing.com', format: {}, }; - - const expected: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'www.bing.com', - format: {}, - isSelected: undefined, - link: { - format: { - href: 'http://www.bing.com', - underline: true, - }, - dataset: {}, - }, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], - format: {}, - }; - runTest(input, expected, true); - }); - - it('link segment with hyperlink', () => { - const input: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'www.bing.com', - format: {}, - link: { - format: { - href: 'www.bing.com', - underline: true, - }, - dataset: {}, - }, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [segment], format: {}, }; - - runTest(input, input, false); + runTest(segment, paragraph, { canUndoByBackspace: true } as any, true); }); - it('link with text', () => { - const input: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'this is the link www.bing.com', - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], + it('No link', () => { + const segment: ContentModelText = { + segmentType: 'Text', + text: 'test', format: {}, }; - const expected: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'this is the link ', - format: {}, - isSelected: undefined, - }, - { - segmentType: 'Text', - text: 'www.bing.com', - format: {}, - isSelected: undefined, - link: { - format: { - underline: true, - href: 'http://www.bing.com', - }, - dataset: {}, - }, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], - format: {}, - }; - - runTest(input, expected, true); - }); - - it('link before text', () => { - const input: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'www.bing.com this is the link', - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [segment], format: {}, }; - runTest(input, input, false); + runTest(segment, paragraph, { canUndoByBackspace: true } as any, false); }); - it('link after link', () => { - const input: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'www.bing.com', - format: {}, - link: { - format: { - href: 'www.bing.com', - underline: true, - }, - dataset: {}, - }, - }, - { - segmentType: 'Text', - text: ' www.bing.com', - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], + it('with text after link ', () => { + const segment: ContentModelText = { + segmentType: 'Text', + text: 'http://bing.com test', format: {}, }; - const expected: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'www.bing.com', - format: {}, - link: { - format: { - href: 'www.bing.com', - underline: true, - }, - dataset: {}, - }, - }, - { - segmentType: 'Text', - text: ' ', - format: {}, - isSelected: undefined, - }, - { - segmentType: 'Text', - text: 'www.bing.com', - format: {}, - isSelected: undefined, - link: { - format: { - href: 'http://www.bing.com', - underline: true, - }, - dataset: {}, - }, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [segment], format: {}, }; - runTest(input, expected, true); + runTest(segment, paragraph, { canUndoByBackspace: true } as any, false); }); }); diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/list/keyboardListTriggerTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/list/keyboardListTriggerTest.ts index 991c67f1e11..a2cb74f5401 100644 --- a/packages/roosterjs-content-model-plugins/test/autoFormat/list/keyboardListTriggerTest.ts +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/list/keyboardListTriggerTest.ts @@ -1,587 +1,83 @@ -import { ContentModelDocument } from 'roosterjs-content-model-types'; import { keyboardListTrigger } from '../../../lib/autoFormat/list/keyboardListTrigger'; +import { + ContentModelDocument, + ContentModelParagraph, + FormatContentModelContext, +} from 'roosterjs-content-model-types'; describe('keyboardListTrigger', () => { function runTest( - input: ContentModelDocument, - expectedModel: ContentModelDocument, + model: ContentModelDocument, + paragraph: ContentModelParagraph, + context: FormatContentModelContext, expectedResult: boolean, shouldSearchForBullet: boolean = true, shouldSearchForNumbering: boolean = true ) { - const formatWithContentModelSpy = jasmine - .createSpy('formatWithContentModel') - .and.callFake((callback, options) => { - const result = callback(input, { - newEntities: [], - deletedEntities: [], - newImages: [], - canUndoByBackspace: true, - }); - expect(result).toBe(expectedResult); - expect(options.apiName).toBe('autoToggleList'); - }); - - keyboardListTrigger( - { - focus: () => {}, - formatContentModel: formatWithContentModelSpy, - } as any, - shouldSearchForBullet, - shouldSearchForNumbering - ); - - expect(formatWithContentModelSpy).toHaveBeenCalled(); - expect(input).toEqual(expectedModel); - } - - it('trigger numbering list', () => { - runTest( - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: '1)', - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], - format: {}, - }, - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - { segmentType: 'Br', format: {} }, - ], - format: {}, - }, - ], - levels: [ - { - listType: 'OL', - format: { - startNumberOverride: 1, - direction: undefined, - textAlign: undefined, - marginBottom: undefined, - marginTop: undefined, - }, - dataset: { - editingInfo: '{"orderedStyleType":3}', - }, - }, - ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, - format: { - fontFamily: undefined, - fontSize: undefined, - textColor: undefined, - }, - }, - format: {}, - }, - ], - format: {}, - }, - true - ); - }); - - it('trigger continued numbering list', () => { - runTest( - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: ' test', - format: {}, - }, - ], - format: {}, - }, - ], - levels: [ - { - listType: 'OL', - format: {}, - dataset: { - editingInfo: '{"orderedStyleType":3,"unorderedStyleType":1}', - }, - }, - ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, - format: {}, - }, - format: { - listStyleType: '"1) "', - }, - }, - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: '2)', - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - { - segmentType: 'Br', - format: {}, - }, - ], - format: {}, - }, - ], - format: {}, - }, - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test', - format: {}, - }, - ], - format: {}, - }, - ], - levels: [ - { - listType: 'OL', - format: {}, - dataset: { - editingInfo: '{"orderedStyleType":3,"unorderedStyleType":1}', - }, - }, - ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, - format: {}, - }, - format: { - listStyleType: '"1) "', - }, - }, - { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - { - segmentType: 'Br', - format: {}, - }, - ], - format: {}, - }, - ], - levels: [ - { - listType: 'OL', - format: { - startNumberOverride: 2, - direction: undefined, - textAlign: undefined, - marginBottom: undefined, - marginTop: undefined, - }, - dataset: { - editingInfo: '{"orderedStyleType":3}', - }, - }, - ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, - format: { - fontFamily: undefined, - fontSize: undefined, - textColor: undefined, - }, - }, - format: {}, - }, - ], - format: {}, - }, - true - ); - }); - - it('should not trigger numbering list', () => { - runTest( - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: '1)', - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], - format: {}, - }, - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: '1)', - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], - format: {}, - }, - false, - undefined, - false - ); - }); - - it('should trigger bullet list', () => { - runTest( - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: '*', - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], - format: {}, - }, - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - { - segmentType: 'Br', - format: {}, - }, - ], - format: {}, - }, - ], - levels: [ - { - listType: 'UL', - format: { - startNumberOverride: 1, - direction: undefined, - textAlign: undefined, - marginBottom: undefined, - marginTop: undefined, - }, - dataset: { - editingInfo: '{"unorderedStyleType":1}', - }, - }, - ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, - format: { - fontFamily: undefined, - fontSize: undefined, - textColor: undefined, - }, - }, - format: {}, - }, - ], - format: {}, - }, - true - ); - }); - - it('should not trigger bullet list', () => { - runTest( - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: '*', - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], - format: {}, - }, - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: '*', - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], - format: {}, - }, - false, - false - ); - }); - - it('trigger continued numbering list between lists', () => { - runTest( - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test', - format: {}, - }, - ], - format: {}, - }, - ], - levels: [ - { - listType: 'OL', - format: {}, - dataset: { - editingInfo: '{"orderedStyleType":3}', - }, - }, - ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, - format: {}, - }, - format: { - listStyleType: '"1) "', - }, - }, - { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test', - format: {}, - }, - ], - format: {}, - }, - ], - levels: [ - { - listType: 'OL', - format: {}, - dataset: { - editingInfo: '{"orderedStyleType":3}', - }, - }, - ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, - format: {}, - }, - format: { - listStyleType: '"2) "', - }, - }, - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: '3)', - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Br', - format: {}, - }, - ], - format: {}, - }, - { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test', - format: {}, - }, - ], - format: {}, - }, - ], - levels: [ - { - listType: 'OL', - format: { - startNumberOverride: 1, - }, - dataset: { - editingInfo: '{"orderedStyleType":10}', - }, - }, - ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, - format: {}, - }, - format: { - listStyleType: '"A) "', - }, - }, + const result = keyboardListTrigger( + model, + paragraph, + context, + shouldSearchForBullet, + shouldSearchForNumbering + ); + expect(result).toBe(expectedResult); + } + + it('trigger numbering list', () => { + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: '1)', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }; + runTest( + { + blockGroupType: 'Document', + blocks: [paragraph], + format: {}, + }, + paragraph, + { canUndoByBackspace: true } as any, + true + ); + }); + + it('trigger continued numbering list', () => { + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: '2)', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + }; + runTest( + { + blockGroupType: 'Document', + blocks: [ { blockType: 'BlockGroup', blockGroupType: 'ListItem', @@ -591,7 +87,7 @@ describe('keyboardListTrigger', () => { segments: [ { segmentType: 'Text', - text: 'test', + text: ' test', format: {}, }, ], @@ -603,7 +99,7 @@ describe('keyboardListTrigger', () => { listType: 'OL', format: {}, dataset: { - editingInfo: '{"orderedStyleType":10}', + editingInfo: '{"orderedStyleType":3,"unorderedStyleType":1}', }, }, ], @@ -613,13 +109,124 @@ describe('keyboardListTrigger', () => { format: {}, }, format: { - listStyleType: '"B) "', + listStyleType: '"1) "', }, }, + paragraph, ], format: {}, }, + paragraph, + { canUndoByBackspace: true } as any, + true + ); + }); + + it('should not trigger numbering list', () => { + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'a1)', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }; + runTest( + { + blockGroupType: 'Document', + blocks: [paragraph], + format: {}, + }, + paragraph, + { canUndoByBackspace: true } as any, + false + ); + }); + + it('should trigger bullet list', () => { + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: '*', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }; + runTest( + { + blockGroupType: 'Document', + blocks: [paragraph], + format: {}, + }, + paragraph, + { canUndoByBackspace: true } as any, + true + ); + }); + + it('should not trigger bullet list', () => { + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'a*', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }; + runTest( + { + blockGroupType: 'Document', + blocks: [paragraph], + format: {}, + }, + paragraph, + {} as any, + false + ); + }); + it('trigger continued numbering list between lists', () => { + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: '3)', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }; + runTest( { blockGroupType: 'Document', blocks: [ @@ -691,52 +298,7 @@ describe('keyboardListTrigger', () => { listStyleType: '"2) "', }, }, - { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - { - segmentType: 'Br', - format: {}, - }, - ], - format: {}, - }, - ], - levels: [ - { - listType: 'OL', - format: { - startNumberOverride: 3, - direction: undefined, - textAlign: undefined, - marginBottom: undefined, - marginTop: undefined, - }, - dataset: { - editingInfo: '{"orderedStyleType":3}', - }, - }, - ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, - format: { - fontFamily: undefined, - fontSize: undefined, - textColor: undefined, - }, - }, - format: {}, - }, + paragraph, { blockType: 'Paragraph', segments: [ @@ -820,11 +382,29 @@ describe('keyboardListTrigger', () => { ], format: {}, }, + paragraph, + { canUndoByBackspace: true } as any, true ); }); it('trigger a new numbering list after a numbering list', () => { + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'A)', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }; runTest( { blockGroupType: 'Document', @@ -907,155 +487,12 @@ describe('keyboardListTrigger', () => { ], format: {}, }, - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'A)', - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], - format: {}, - }, - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test', - format: {}, - }, - ], - format: {}, - }, - ], - levels: [ - { - listType: 'OL', - format: {}, - dataset: { - editingInfo: '{"orderedStyleType":3}', - }, - }, - ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, - format: {}, - }, - format: { - listStyleType: '"1) "', - }, - }, - { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test', - format: {}, - }, - ], - format: {}, - }, - ], - levels: [ - { - listType: 'OL', - format: {}, - dataset: { - editingInfo: '{"orderedStyleType":3}', - }, - }, - ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, - format: {}, - }, - format: { - listStyleType: '"2) "', - }, - }, - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Br', - format: {}, - }, - ], - format: {}, - }, - { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - { - segmentType: 'Br', - format: {}, - }, - ], - format: {}, - }, - ], - levels: [ - { - listType: 'OL', - format: { - startNumberOverride: 1, - direction: undefined, - textAlign: undefined, - marginBottom: undefined, - marginTop: undefined, - }, - dataset: { - editingInfo: '{"orderedStyleType":10}', - }, - }, - ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, - format: { - fontFamily: undefined, - fontSize: undefined, - textColor: undefined, - }, - }, - format: {}, - }, + paragraph, ], format: {}, }, + paragraph, + { canUndoByBackspace: true } as any, true ); }); diff --git a/packages/roosterjs-content-model-plugins/test/pluginUtils/formatTextSegmentBeforeSelectionMarkerTest.ts b/packages/roosterjs-content-model-plugins/test/pluginUtils/formatTextSegmentBeforeSelectionMarkerTest.ts index 484fd3dcb53..056199897f7 100644 --- a/packages/roosterjs-content-model-plugins/test/pluginUtils/formatTextSegmentBeforeSelectionMarkerTest.ts +++ b/packages/roosterjs-content-model-plugins/test/pluginUtils/formatTextSegmentBeforeSelectionMarkerTest.ts @@ -1,8 +1,10 @@ +import { createLinkAfterSpace } from '../../lib/autoFormat/link/createLinkAfterSpace'; import { formatTextSegmentBeforeSelectionMarker } from '../../lib/pluginUtils/formatTextSegmentBeforeSelectionMarker'; +import { keyboardListTrigger } from '../../lib/autoFormat/list/keyboardListTrigger'; +import { transformHyphen } from '../../lib/autoFormat/hyphen/transformHyphen'; import { ContentModelDocument, ContentModelParagraph, - ContentModelSelectionMarker, ContentModelText, FormatContentModelContext, } from 'roosterjs-content-model-types'; @@ -11,10 +13,9 @@ describe('formatTextSegmentBeforeSelectionMarker', () => { function runTest( input: ContentModelDocument, callback: ( + model: ContentModelDocument, previousSegment: ContentModelText, paragraph: ContentModelParagraph, - marker: ContentModelSelectionMarker, - markerIndex: number, context: FormatContentModelContext ) => boolean, expectedModel: ContentModelDocument, @@ -167,7 +168,7 @@ describe('formatTextSegmentBeforeSelectionMarker', () => { }; runTest( input, - previousSegment => { + (_model, previousSegment) => { previousSegment.format = { textColor: 'red' }; return true; }, @@ -176,3 +177,1689 @@ describe('formatTextSegmentBeforeSelectionMarker', () => { ); }); }); + +describe('formatTextSegmentBeforeSelectionMarker - keyboardListTrigger', () => { + function runTest( + input: ContentModelDocument, + expectedModel: ContentModelDocument, + expectedResult: boolean, + autoBullet: boolean = true, + autoNumbering: boolean = true + ) { + const formatWithContentModelSpy = jasmine + .createSpy('formatWithContentModel') + .and.callFake((callback, options) => { + const result = callback(input, { + newEntities: [], + deletedEntities: [], + newImages: [], + canUndoByBackspace: true, + }); + expect(result).toBe(expectedResult); + }); + + formatTextSegmentBeforeSelectionMarker( + { + focus: () => {}, + formatContentModel: formatWithContentModelSpy, + } as any, + (model, _previousSegment, paragraph, context) => { + return keyboardListTrigger(model, paragraph, context, autoBullet, autoNumbering); + } + ); + + expect(formatWithContentModelSpy).toHaveBeenCalled(); + expect(input).toEqual(expectedModel); + } + + it('trigger numbering list', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: '1)', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expectedModel: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + { segmentType: 'Br', format: {} }, + ], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: { + startNumberOverride: 1, + direction: undefined, + textAlign: undefined, + marginBottom: undefined, + marginTop: undefined, + }, + dataset: { + editingInfo: '{"orderedStyleType":3}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: undefined, + fontSize: undefined, + textColor: undefined, + }, + }, + format: {}, + }, + ], + format: {}, + }; + runTest(input, expectedModel, true); + }); + + it('trigger continued numbering list', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: ' test', + format: {}, + }, + ], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: {}, + dataset: { + editingInfo: '{"orderedStyleType":3,"unorderedStyleType":1}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { + listStyleType: '"1) "', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: '2)', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + ], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: {}, + dataset: { + editingInfo: '{"orderedStyleType":3,"unorderedStyleType":1}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { + listStyleType: '"1) "', + }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: { + startNumberOverride: 2, + direction: undefined, + textAlign: undefined, + marginBottom: undefined, + marginTop: undefined, + }, + dataset: { + editingInfo: '{"orderedStyleType":3}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: undefined, + fontSize: undefined, + textColor: undefined, + }, + }, + format: {}, + }, + ], + format: {}, + }, + true + ); + }); + + it('should not trigger numbering list', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: '1)', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: '1)', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }, + false, + true, + false + ); + }); + + it('should trigger bullet list', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: '*', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + }, + ], + levels: [ + { + listType: 'UL', + format: { + startNumberOverride: 1, + direction: undefined, + textAlign: undefined, + marginBottom: undefined, + marginTop: undefined, + }, + dataset: { + editingInfo: '{"unorderedStyleType":1}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: undefined, + fontSize: undefined, + textColor: undefined, + }, + }, + format: {}, + }, + ], + format: {}, + }, + true + ); + }); + + it('should not trigger bullet list', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: '*', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: '*', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }, + false, + false, + true + ); + }); + + it('trigger continued numbering list between lists', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + ], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: {}, + dataset: { + editingInfo: '{"orderedStyleType":3}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { + listStyleType: '"1) "', + }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + ], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: {}, + dataset: { + editingInfo: '{"orderedStyleType":3}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { + listStyleType: '"2) "', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: '3)', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + ], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: { + startNumberOverride: 1, + }, + dataset: { + editingInfo: '{"orderedStyleType":10}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { + listStyleType: '"A) "', + }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + ], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: {}, + dataset: { + editingInfo: '{"orderedStyleType":10}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { + listStyleType: '"B) "', + }, + }, + ], + format: {}, + }, + + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + ], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: {}, + dataset: { + editingInfo: '{"orderedStyleType":3}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { + listStyleType: '"1) "', + }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + ], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: {}, + dataset: { + editingInfo: '{"orderedStyleType":3}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { + listStyleType: '"2) "', + }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: { + startNumberOverride: 3, + direction: undefined, + textAlign: undefined, + marginBottom: undefined, + marginTop: undefined, + }, + dataset: { + editingInfo: '{"orderedStyleType":3}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: undefined, + fontSize: undefined, + textColor: undefined, + }, + }, + format: {}, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + ], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: { + startNumberOverride: 1, + }, + dataset: { + editingInfo: '{"orderedStyleType":10}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { + listStyleType: '"A) "', + }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + ], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: {}, + dataset: { + editingInfo: '{"orderedStyleType":10}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { + listStyleType: '"B) "', + }, + }, + ], + format: {}, + }, + true + ); + }); + + it('trigger a new numbering list after a numbering list', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + ], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: {}, + dataset: { + editingInfo: '{"orderedStyleType":3}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { + listStyleType: '"1) "', + }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + ], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: {}, + dataset: { + editingInfo: '{"orderedStyleType":3}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { + listStyleType: '"2) "', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'A)', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + ], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: {}, + dataset: { + editingInfo: '{"orderedStyleType":3}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { + listStyleType: '"1) "', + }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + ], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: {}, + dataset: { + editingInfo: '{"orderedStyleType":3}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { + listStyleType: '"2) "', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: { + startNumberOverride: 1, + direction: undefined, + textAlign: undefined, + marginBottom: undefined, + marginTop: undefined, + }, + dataset: { + editingInfo: '{"orderedStyleType":10}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: undefined, + fontSize: undefined, + textColor: undefined, + }, + }, + format: {}, + }, + ], + format: {}, + }, + true + ); + }); +}); + +describe('formatTextSegmentBeforeSelectionMarker - createLinkAfterSpace', () => { + function runTest( + input: ContentModelDocument, + expectedModel: ContentModelDocument, + expectedResult: boolean + ) { + const formatWithContentModelSpy = jasmine + .createSpy('formatWithContentModel') + .and.callFake((callback, options) => { + const result = callback(input, { + newEntities: [], + deletedEntities: [], + newImages: [], + canUndoByBackspace: true, + }); + expect(result).toBe(expectedResult); + }); + + formatTextSegmentBeforeSelectionMarker( + { + focus: () => {}, + formatContentModel: formatWithContentModelSpy, + } as any, + (_model, previousSegment, paragraph, context) => { + return createLinkAfterSpace(previousSegment, paragraph, context); + } + ); + + expect(formatWithContentModelSpy).toHaveBeenCalled(); + expect(input).toEqual(expectedModel); + } + + it('no link segment', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + + runTest(input, input, false); + }); + + it('link segment with WWW', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'www.bing.com', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'www.bing.com', + format: {}, + isSelected: undefined, + link: { + format: { + href: 'http://www.bing.com', + underline: true, + }, + dataset: {}, + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); + + it('link segment with hyperlink', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'www.bing.com', + format: {}, + link: { + format: { + href: 'www.bing.com', + underline: true, + }, + dataset: {}, + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + + runTest(input, input, true); + }); + + it('link with text', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'this is the link www.bing.com', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'this is the link ', + format: {}, + isSelected: undefined, + }, + { + segmentType: 'Text', + text: 'www.bing.com', + format: {}, + isSelected: undefined, + link: { + format: { + underline: true, + href: 'http://www.bing.com', + }, + dataset: {}, + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + + runTest(input, expected, true); + }); + + it('link before text', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'www.bing.com this is the link', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, input, false); + }); + + it('link after link', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'www.bing.com', + format: {}, + link: { + format: { + href: 'www.bing.com', + underline: true, + }, + dataset: {}, + }, + }, + { + segmentType: 'Text', + text: ' www.bing.com', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'www.bing.com', + format: {}, + link: { + format: { + href: 'www.bing.com', + underline: true, + }, + dataset: {}, + }, + }, + { + segmentType: 'Text', + text: ' ', + format: {}, + isSelected: undefined, + }, + { + segmentType: 'Text', + text: 'www.bing.com', + format: {}, + isSelected: undefined, + link: { + format: { + href: 'http://www.bing.com', + underline: true, + }, + dataset: {}, + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); +}); + +describe('formatTextSegmentBeforeSelectionMarker - transformHyphen', () => { + function runTest( + input: ContentModelDocument, + expectedModel: ContentModelDocument, + expectedResult: boolean + ) { + const formatWithContentModelSpy = jasmine + .createSpy('formatWithContentModel') + .and.callFake((callback, options) => { + const result = callback(input, { + newEntities: [], + deletedEntities: [], + newImages: [], + canUndoByBackspace: true, + }); + expect(result).toBe(expectedResult); + }); + + formatTextSegmentBeforeSelectionMarker( + { + focus: () => {}, + formatContentModel: formatWithContentModelSpy, + } as any, + (_model, previousSegment, paragraph, context) => { + return transformHyphen(previousSegment, paragraph, context); + } + ); + + expect(formatWithContentModelSpy).toHaveBeenCalled(); + expect(input).toEqual(expectedModel); + } + + it('No hyphen', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, input, false); + }); + + it('with hyphen', () => { + const text = 'test--test'; + spyOn(text, 'split').and.returnValue(['test--test ']); + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: text, + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test—tes', + format: {}, + isSelected: undefined, + }, + { + segmentType: 'Text', + text: 't', + format: {}, + isSelected: undefined, + }, + + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); + + it('with hyphen and left space', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test-- test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + + runTest(input, input, false); + }); + + it('with hyphen and left space', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test --test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + + runTest(input, input, false); + }); + + it('with hyphen between spaces', () => { + const text = 'test -- test'; + spyOn(text, 'split').and.returnValue(['test', '--', 'test']); + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test -- test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test ', + format: {}, + isSelected: undefined, + }, + { + segmentType: 'Text', + text: '—', + format: {}, + isSelected: undefined, + }, + { + segmentType: 'Text', + text: ' test', + format: {}, + isSelected: undefined, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); + + it('with hyphen and multiple words', () => { + const text = 'testing test--test'; + spyOn(text, 'split').and.returnValue(['testing', 'test--test ']); + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: text, + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'testing ', + format: {}, + isSelected: undefined, + }, + { + segmentType: 'Text', + text: 'test—tes', + format: {}, + isSelected: undefined, + }, + { + segmentType: 'Text', + text: 't', + format: {}, + isSelected: undefined, + }, + + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); +}); From c1b2a9d0e542f65ddad9f30864b7415d61ff9411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Thu, 4 Apr 2024 15:03:32 -0300 Subject: [PATCH 5/5] remove getLinkSegment --- .../lib/autoFormat/link/createLink.ts | 15 +- .../lib/autoFormat/link/getLinkSegment.ts | 32 ---- .../lib/autoFormat/link/unlink.ts | 12 +- .../test/autoFormat/link/createLinkTest.ts | 2 +- .../autoFormat/link/getLinkSegmentTest.ts | 174 ------------------ 5 files changed, 15 insertions(+), 220 deletions(-) delete mode 100644 packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkSegment.ts delete mode 100644 packages/roosterjs-content-model-plugins/test/autoFormat/link/getLinkSegmentTest.ts diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLink.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLink.ts index baaa7a00108..bd26adf6993 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLink.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLink.ts @@ -1,17 +1,18 @@ import { addLink } from 'roosterjs-content-model-dom'; -import { getLinkSegment } from './getLinkSegment'; -import type { IEditor } from 'roosterjs-content-model-types'; +import { formatTextSegmentBeforeSelectionMarker } from '../../pluginUtils/formatTextSegmentBeforeSelectionMarker'; +import { matchLink } from 'roosterjs-content-model-api'; +import type { IEditor, LinkData } from 'roosterjs-content-model-types'; /** * @internal */ export function createLink(editor: IEditor) { - editor.formatContentModel(model => { - const link = getLinkSegment(model); - if (link && !link.link) { - addLink(link, { + formatTextSegmentBeforeSelectionMarker(editor, (_model, linkSegment, _paragraph) => { + let linkData: LinkData | null = null; + if (!linkSegment.link && (linkData = matchLink(linkSegment.text))) { + addLink(linkSegment, { format: { - href: link.text, + href: linkData.normalizedUrl, underline: true, }, dataset: {}, diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkSegment.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkSegment.ts deleted file mode 100644 index 9c34b7370e6..00000000000 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkSegment.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { getSelectedSegmentsAndParagraphs } from 'roosterjs-content-model-dom'; -import { matchLink } from 'roosterjs-content-model-api'; -import type { ContentModelDocument } from 'roosterjs-content-model-types'; - -/** - * @internal - */ -export function getLinkSegment(model: ContentModelDocument) { - const selectedSegmentsAndParagraphs = getSelectedSegmentsAndParagraphs( - model, - false /* includingFormatHolder */ - ); - if (selectedSegmentsAndParagraphs.length == 1 && selectedSegmentsAndParagraphs[0][1]) { - const selectedParagraph = selectedSegmentsAndParagraphs[0][1]; - const marker = selectedSegmentsAndParagraphs[0][0]; - if (marker && marker.segmentType === 'SelectionMarker') { - const markerIndex = selectedParagraph.segments.indexOf(marker); - const link = selectedParagraph.segments[markerIndex - 1]; - if ( - marker && - link && - marker.segmentType === 'SelectionMarker' && - marker.isSelected && - link.segmentType === 'Text' && - (matchLink(link.text) || link.link) - ) { - return link; - } - } - } - return undefined; -} diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/unlink.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/unlink.ts index 4648cc5b3e1..3acc5a3d81b 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/unlink.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/unlink.ts @@ -1,18 +1,18 @@ -import { getLinkSegment } from './getLinkSegment'; +import { formatTextSegmentBeforeSelectionMarker } from '../../pluginUtils/formatTextSegmentBeforeSelectionMarker'; + import type { IEditor } from 'roosterjs-content-model-types'; /** * @internal */ export function unlink(editor: IEditor, rawEvent: KeyboardEvent) { - editor.formatContentModel(model => { - const link = getLinkSegment(model); - if (link?.link) { - link.link = undefined; + formatTextSegmentBeforeSelectionMarker(editor, (_model, linkSegment, _paragraph) => { + if (linkSegment?.link) { + linkSegment.link = undefined; rawEvent.preventDefault(); + return true; } - return false; }); } diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkTest.ts index d87b4f9c562..038ce610a2f 100644 --- a/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkTest.ts +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkTest.ts @@ -110,7 +110,7 @@ describe('createLink', () => { format: {}, link: { format: { - href: 'www.bing.com', + href: 'http://www.bing.com', underline: true, }, dataset: {}, diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/link/getLinkSegmentTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/link/getLinkSegmentTest.ts deleted file mode 100644 index 5bb356c56f3..00000000000 --- a/packages/roosterjs-content-model-plugins/test/autoFormat/link/getLinkSegmentTest.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { ContentModelDocument, ContentModelText } from 'roosterjs-content-model-types'; -import { getLinkSegment } from '../../../lib/autoFormat/link/getLinkSegment'; - -describe('getLinkSegment', () => { - function runTest(model: ContentModelDocument, link: ContentModelText | undefined) { - const result = getLinkSegment(model); - expect(result).toEqual(link); - } - - it('no selected segments', () => { - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test', - format: {}, - }, - ], - format: {}, - }, - ], - format: {}, - }; - runTest(model, undefined); - }); - - it('no link segment', () => { - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test', - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], - format: {}, - }; - runTest(model, undefined); - }); - - it('link segment starting with WWW', () => { - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'www.bing.com', - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], - format: {}, - }; - runTest(model, { - segmentType: 'Text', - text: 'www.bing.com', - format: {}, - }); - }); - - it('link segment starting with hyperlink', () => { - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'www.bing.com', - format: {}, - link: { - format: { - href: 'www.bing.com', - underline: true, - }, - dataset: {}, - }, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], - format: {}, - }; - runTest(model, { - segmentType: 'Text', - text: 'www.bing.com', - format: {}, - link: { - format: { - href: 'www.bing.com', - underline: true, - }, - dataset: {}, - }, - }); - }); - - it('link segment starting with text and hyperlink', () => { - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'bing', - format: {}, - link: { - format: { - href: 'www.bing.com', - underline: true, - }, - dataset: {}, - }, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], - format: {}, - }; - runTest(model, { - segmentType: 'Text', - text: 'bing', - format: {}, - link: { - format: { - href: 'www.bing.com', - underline: true, - }, - dataset: {}, - }, - }); - }); -});