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..2f983dee8ce 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts @@ -1,6 +1,8 @@ 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'; import type { ContentChangedEvent, @@ -34,6 +36,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 +51,7 @@ const DefaultOptions: Required = { autoNumbering: false, autoUnlink: false, autoLink: false, + autoHyphen: false, }; /** @@ -52,13 +60,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 +120,52 @@ 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; - keyboardListTrigger(editor, autoBullet, autoNumbering); - if (autoLink) { - createLinkAfterSpace(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 new file mode 100644 index 00000000000..95a865d734e --- /dev/null +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/hyphen/transformHyphen.ts @@ -0,0 +1,46 @@ +import { splitTextSegment } from '../../pluginUtils/splitTextSegment'; +import type { + ContentModelParagraph, + ContentModelText, + FormatContentModelContext, +} from 'roosterjs-content-model-types'; + +/** + * @internal + */ +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; + } + } + } + return false; +} 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/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 deleted file mode 100644 index caa1e54845e..00000000000 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkSegment.ts +++ /dev/null @@ -1,29 +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 = 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; - } - } - 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/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 new file mode 100644 index 00000000000..62814ba6dff --- /dev/null +++ b/packages/roosterjs-content-model-plugins/lib/pluginUtils/formatTextSegmentBeforeSelectionMarker.ts @@ -0,0 +1,41 @@ +import { getSelectedSegmentsAndParagraphs } from 'roosterjs-content-model-dom'; +import type { + ContentModelDocument, + ContentModelParagraph, + ContentModelText, + FormatContentModelContext, + IEditor, +} from 'roosterjs-content-model-types'; + +/** + * @internal + */ +export function formatTextSegmentBeforeSelectionMarker( + editor: IEditor, + callback: ( + model: ContentModelDocument, + previousSegment: ContentModelText, + paragraph: ContentModelParagraph, + 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(model, previousSegment, paragraph, context); + } + } + } + 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..d491984dbe0 100644 --- a/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts @@ -1,39 +1,46 @@ 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 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: () => ({ - type: -1, + type: 'range', + range: { + collapsed: true, + }, } as any), // Force return invalid range to go through content model code formatContentModel: () => {}, } as any) as IEditor; }); describe('onPluginEvent - keyboardListTrigger', () => { - let keyboardListTriggerSpy: jasmine.Spy; - - beforeEach(() => { - keyboardListTriggerSpy = spyOn(keyboardTrigger, 'keyboardListTrigger'); - }); - function runTest( event: EditorInputEvent, - shouldCallTrigger: boolean, options?: { autoBullet: boolean; autoNumbering: boolean; @@ -44,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', () => { @@ -60,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, }); @@ -71,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, }); @@ -82,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', () => { @@ -90,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', () => { @@ -98,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', () => { @@ -106,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); }); }); @@ -221,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; } @@ -238,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', () => { @@ -251,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, }); }); @@ -261,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, }); }); @@ -275,9 +296,74 @@ describe('Content Model Auto Format Plugin Test', () => { inputType: 'insertText', } as any, }; - runTest(event, false, { + runTest(event, { autoLink: true, }); }); }); + + describe('onPluginEvent - transformHyphen', () => { + function runTest( + event: EditorInputEvent, + options?: { + autoHyphen: boolean; + } + ) { + const plugin = new AutoFormatPlugin(options as AutoFormatOptions); + plugin.initialize(editor); + + plugin.onPluginEvent(event); + 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', () => { + const event: EditorInputEvent = { + eventType: 'input', + rawEvent: { data: ' ', preventDefault: () => {}, inputType: 'insertText' } as any, + }; + runTest(event, { + autoHyphen: true, + }); + }); + + it('should not call transformHyphen - disable options', () => { + const event: EditorInputEvent = { + eventType: 'input', + rawEvent: { data: ' ', preventDefault: () => {}, inputType: 'insertText' } as any, + }; + runTest(event, { + autoHyphen: false, + }); + }); + + it('should not call transformHyphen - not space', () => { + const event: EditorInputEvent = { + eventType: 'input', + rawEvent: { + data: 'Backspace', + preventDefault: () => {}, + inputType: 'insertText', + } as any, + }; + 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 new file mode 100644 index 00000000000..5e4c09c23ab --- /dev/null +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/hyphen/transformHyphenTest.ts @@ -0,0 +1,143 @@ +import { transformHyphen } from '../../../lib/autoFormat/hyphen/transformHyphen'; +import { + ContentModelParagraph, + ContentModelText, + FormatContentModelContext, +} from 'roosterjs-content-model-types'; + +describe('transformHyphen', () => { + function runTest( + previousSegment: ContentModelText, + paragraph: ContentModelParagraph, + context: FormatContentModelContext, + expectedResult: boolean + ) { + const result = transformHyphen(previousSegment, paragraph, context); + expect(result).toBe(expectedResult); + } + + it('with hyphen', () => { + const segment: ContentModelText = { + segmentType: 'Text', + text: 'test--test', + format: {}, + }; + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [segment], + format: {}, + }; + runTest(segment, paragraph, { canUndoByBackspace: true } as any, true); + }); + + it('No hyphen', () => { + const segment: ContentModelText = { + segmentType: 'Text', + text: 'test', + format: {}, + }; + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [segment], + format: {}, + }; + runTest(segment, paragraph, { canUndoByBackspace: true } as any, false); + }); + + it('with hyphen between spaces', () => { + const segment: ContentModelText = { + segmentType: 'Text', + text: 'test -- test', + format: {}, + }; + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [segment], + format: {}, + }; + runTest(segment, paragraph, { canUndoByBackspace: true } as any, true); + }); + + it('with hyphen at the end', () => { + const segment: ContentModelText = { + segmentType: 'Text', + text: 'test--', + format: {}, + }; + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [segment], + format: {}, + }; + runTest(segment, paragraph, { canUndoByBackspace: true } as any, false); + }); + it('with hyphen at the start', () => { + const segment: ContentModelText = { + segmentType: 'Text', + text: '--test', + format: {}, + }; + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [segment], + format: {}, + }; + runTest(segment, paragraph, { canUndoByBackspace: true } as any, false); + }); + + it('with hyphen and space right', () => { + 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); + }); + + 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); + }); + + 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); + }); + + 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(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/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: {}, - }, - }); - }); -}); 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 new file mode 100644 index 00000000000..056199897f7 --- /dev/null +++ b/packages/roosterjs-content-model-plugins/test/pluginUtils/formatTextSegmentBeforeSelectionMarkerTest.ts @@ -0,0 +1,1865 @@ +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, + ContentModelText, + FormatContentModelContext, +} from 'roosterjs-content-model-types'; + +describe('formatTextSegmentBeforeSelectionMarker', () => { + function runTest( + input: ContentModelDocument, + callback: ( + model: ContentModelDocument, + previousSegment: ContentModelText, + paragraph: ContentModelParagraph, + 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, + (_model, previousSegment) => { + previousSegment.format = { textColor: 'red' }; + return true; + }, + expectedModel, + true + ); + }); +}); + +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); + }); +});