From 69f1ff65b8d6f8c5c293918368a19e21ded47dda Mon Sep 17 00:00:00 2001 From: Haowen Chen Date: Wed, 1 Jul 2026 16:11:10 +0800 Subject: [PATCH 1/2] Add auto markdown conversion callback --- .../lib/plugins/MarkdownPastePlugin.ts | 6 ++- .../test/plugins/MarkdownPastePluginTest.ts | 51 +++++++++++++++++-- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/packages/roosterjs-content-model-markdown/lib/plugins/MarkdownPastePlugin.ts b/packages/roosterjs-content-model-markdown/lib/plugins/MarkdownPastePlugin.ts index a7968332bfda..17690d5879ee 100644 --- a/packages/roosterjs-content-model-markdown/lib/plugins/MarkdownPastePlugin.ts +++ b/packages/roosterjs-content-model-markdown/lib/plugins/MarkdownPastePlugin.ts @@ -28,13 +28,16 @@ const DefaultOptions: MarkdownPasteOptions = { export class MarkdownPastePlugin implements EditorPlugin { private editor: IEditor | null = null; private options: MarkdownPasteOptions; + private onAutoConvert?: () => void; /** * Construct a new instance of MarkdownPastePlugin * @param options Options to control the markdown paste behavior + * @param onAutoConvert Optional callback that runs after auto conversion is applied */ - constructor(options?: MarkdownPasteOptions) { + constructor(options?: MarkdownPasteOptions, onAutoConvert?: () => void) { this.options = options ?? DefaultOptions; + this.onAutoConvert = onAutoConvert; } /** @@ -105,6 +108,7 @@ export class MarkdownPastePlugin implements EditorPlugin { scrollCaretIntoView: true, } ); + this.onAutoConvert?.(); } } else if (event.eventType === 'beforePaste' && !event.clipboardData.pasteNativeEvent) { const shouldConvert = event.pasteType === 'asMarkdown'; diff --git a/packages/roosterjs-content-model-markdown/test/plugins/MarkdownPastePluginTest.ts b/packages/roosterjs-content-model-markdown/test/plugins/MarkdownPastePluginTest.ts index adb7388033db..eb5911c57b26 100644 --- a/packages/roosterjs-content-model-markdown/test/plugins/MarkdownPastePluginTest.ts +++ b/packages/roosterjs-content-model-markdown/test/plugins/MarkdownPastePluginTest.ts @@ -33,14 +33,21 @@ describe('MarkdownPastePlugin', () => { } as unknown) as IEditor; }); - function createPlugin(autoConversion?: boolean, undoConversion?: boolean): MarkdownPastePlugin { + function createPlugin( + autoConversion?: boolean, + undoConversion?: boolean, + onAutoConvert?: () => void + ): MarkdownPastePlugin { const plugin = autoConversion === undefined && undoConversion === undefined ? new MarkdownPastePlugin() - : new MarkdownPastePlugin({ - autoConversion: !!autoConversion, - undoConversion: !!undoConversion, - }); + : new MarkdownPastePlugin( + { + autoConversion: !!autoConversion, + undoConversion: !!undoConversion, + }, + onAutoConvert + ); plugin.initialize(editor); return plugin; } @@ -344,6 +351,40 @@ describe('MarkdownPastePlugin', () => { plugin.dispose(); }); + it('calls the optional callback after auto conversion', () => { + const callback = jasmine.createSpy('onAutoConvert'); + const plugin = createPlugin(true /*autoConversion*/, false /*undoConversion*/, callback); + const event = createContentChangedPasteEvent({ + text: '# Heading', + rawHtml: '', + customValues: {}, + pasteNativeEvent: true, + modelBeforePaste: createEmptyModel(), + }); + + plugin.onPluginEvent(event); + + expect(callback).toHaveBeenCalledTimes(1); + plugin.dispose(); + }); + + it('does not call the optional callback when auto conversion does not run', () => { + const callback = jasmine.createSpy('onAutoConvert'); + const plugin = createPlugin(true /*autoConversion*/, false /*undoConversion*/, callback); + const event = createContentChangedPasteEvent({ + text: 'just plain text', + rawHtml: '', + customValues: {}, + pasteNativeEvent: true, + modelBeforePaste: createEmptyModel(), + }); + + plugin.onPluginEvent(event); + + expect(callback).not.toHaveBeenCalled(); + plugin.dispose(); + }); + it('does nothing after dispose', () => { const plugin = createPlugin(true /*autoConversion*/); plugin.dispose(); From 342eb862289b8df1a3753838ef8ac4975b16691d Mon Sep 17 00:00:00 2001 From: Haowen Chen Date: Thu, 2 Jul 2026 11:17:06 +0800 Subject: [PATCH 2/2] Use auto markdown conversion source --- .../lib/constants/ChangeSource.ts | 4 ++ .../lib/plugins/MarkdownPastePlugin.ts | 7 +-- .../test/plugins/MarkdownPastePluginTest.ts | 60 ++++--------------- 3 files changed, 18 insertions(+), 53 deletions(-) diff --git a/packages/roosterjs-content-model-dom/lib/constants/ChangeSource.ts b/packages/roosterjs-content-model-dom/lib/constants/ChangeSource.ts index e77d03952922..cbaa372038f1 100644 --- a/packages/roosterjs-content-model-dom/lib/constants/ChangeSource.ts +++ b/packages/roosterjs-content-model-dom/lib/constants/ChangeSource.ts @@ -23,6 +23,10 @@ export const ChangeSource = { * Content changed by paste */ Paste: 'Paste', + /** + * Content changed by auto markdown conversion + */ + AutoMarkdownConversion: 'AutoMarkdownConversion', /** * Content changed by setContent API */ diff --git a/packages/roosterjs-content-model-markdown/lib/plugins/MarkdownPastePlugin.ts b/packages/roosterjs-content-model-markdown/lib/plugins/MarkdownPastePlugin.ts index 17690d5879ee..b75e2c576e5c 100644 --- a/packages/roosterjs-content-model-markdown/lib/plugins/MarkdownPastePlugin.ts +++ b/packages/roosterjs-content-model-markdown/lib/plugins/MarkdownPastePlugin.ts @@ -28,16 +28,13 @@ const DefaultOptions: MarkdownPasteOptions = { export class MarkdownPastePlugin implements EditorPlugin { private editor: IEditor | null = null; private options: MarkdownPasteOptions; - private onAutoConvert?: () => void; /** * Construct a new instance of MarkdownPastePlugin * @param options Options to control the markdown paste behavior - * @param onAutoConvert Optional callback that runs after auto conversion is applied */ - constructor(options?: MarkdownPasteOptions, onAutoConvert?: () => void) { + constructor(options?: MarkdownPasteOptions) { this.options = options ?? DefaultOptions; - this.onAutoConvert = onAutoConvert; } /** @@ -105,10 +102,10 @@ export class MarkdownPastePlugin implements EditorPlugin { }, { apiName: 'MarkdownConversion', + changeSource: ChangeSource.AutoMarkdownConversion, scrollCaretIntoView: true, } ); - this.onAutoConvert?.(); } } else if (event.eventType === 'beforePaste' && !event.clipboardData.pasteNativeEvent) { const shouldConvert = event.pasteType === 'asMarkdown'; diff --git a/packages/roosterjs-content-model-markdown/test/plugins/MarkdownPastePluginTest.ts b/packages/roosterjs-content-model-markdown/test/plugins/MarkdownPastePluginTest.ts index eb5911c57b26..c63417184699 100644 --- a/packages/roosterjs-content-model-markdown/test/plugins/MarkdownPastePluginTest.ts +++ b/packages/roosterjs-content-model-markdown/test/plugins/MarkdownPastePluginTest.ts @@ -1,4 +1,5 @@ import { MarkdownPastePlugin } from '../../lib/plugins/MarkdownPastePlugin'; +import { ChangeSource } from 'roosterjs-content-model-dom'; import type { BeforePasteEvent, ClipboardData, @@ -33,21 +34,14 @@ describe('MarkdownPastePlugin', () => { } as unknown) as IEditor; }); - function createPlugin( - autoConversion?: boolean, - undoConversion?: boolean, - onAutoConvert?: () => void - ): MarkdownPastePlugin { + function createPlugin(autoConversion?: boolean, undoConversion?: boolean): MarkdownPastePlugin { const plugin = autoConversion === undefined && undoConversion === undefined ? new MarkdownPastePlugin() - : new MarkdownPastePlugin( - { - autoConversion: !!autoConversion, - undoConversion: !!undoConversion, - }, - onAutoConvert - ); + : new MarkdownPastePlugin({ + autoConversion: !!autoConversion, + undoConversion: !!undoConversion, + }); plugin.initialize(editor); return plugin; } @@ -97,7 +91,7 @@ describe('MarkdownPastePlugin', () => { ): ContentChangedEvent { return ({ eventType: 'contentChanged', - source: 'Paste', + source: ChangeSource.Paste, data: clipboardData as ClipboardData, } as unknown) as ContentChangedEvent; } @@ -178,7 +172,11 @@ describe('MarkdownPastePlugin', () => { (model: ContentModelDocument) => boolean, FormatContentModelOptions ]; - expect(options).toEqual({ apiName: 'MarkdownConversion', scrollCaretIntoView: true }); + expect(options).toEqual({ + apiName: 'MarkdownConversion', + changeSource: ChangeSource.AutoMarkdownConversion, + scrollCaretIntoView: true, + }); const target = createEmptyModel(); expect(callback(target)).toBe(true); @@ -351,40 +349,6 @@ describe('MarkdownPastePlugin', () => { plugin.dispose(); }); - it('calls the optional callback after auto conversion', () => { - const callback = jasmine.createSpy('onAutoConvert'); - const plugin = createPlugin(true /*autoConversion*/, false /*undoConversion*/, callback); - const event = createContentChangedPasteEvent({ - text: '# Heading', - rawHtml: '', - customValues: {}, - pasteNativeEvent: true, - modelBeforePaste: createEmptyModel(), - }); - - plugin.onPluginEvent(event); - - expect(callback).toHaveBeenCalledTimes(1); - plugin.dispose(); - }); - - it('does not call the optional callback when auto conversion does not run', () => { - const callback = jasmine.createSpy('onAutoConvert'); - const plugin = createPlugin(true /*autoConversion*/, false /*undoConversion*/, callback); - const event = createContentChangedPasteEvent({ - text: 'just plain text', - rawHtml: '', - customValues: {}, - pasteNativeEvent: true, - modelBeforePaste: createEmptyModel(), - }); - - plugin.onPluginEvent(event); - - expect(callback).not.toHaveBeenCalled(); - plugin.dispose(); - }); - it('does nothing after dispose', () => { const plugin = createPlugin(true /*autoConversion*/); plugin.dispose();