diff --git a/packages/core/src/extensions/list.test.ts b/packages/core/src/extensions/list.test.ts index 0076abf2..c6f405d6 100644 --- a/packages/core/src/extensions/list.test.ts +++ b/packages/core/src/extensions/list.test.ts @@ -120,6 +120,55 @@ describe('commands', () => { fixture.editor.commands.cycleCheckableList() expect(docToMarkdown(fixture.doc)).toBe('+ [ ] outer\n - [ ] inner\n') }) + + it('cycleOrderedList cycles plain content through bullet, ordered, and text', () => { + using fixture = setupFixture() + const { n } = fixture + fixture.set(n.doc(n.paragraph('todo'))) + + expect(docToMarkdown(fixture.doc).trim()).toMatchInlineSnapshot(`"todo"`) + fixture.editor.commands.cycleOrderedList() + expect(docToMarkdown(fixture.doc).trim()).toMatchInlineSnapshot(`"- todo"`) + fixture.editor.commands.cycleOrderedList() + expect(docToMarkdown(fixture.doc).trim()).toMatchInlineSnapshot(`"1. todo"`) + fixture.editor.commands.cycleOrderedList() + expect(docToMarkdown(fixture.doc).trim()).toMatchInlineSnapshot(`"todo"`) + fixture.editor.commands.cycleOrderedList() + expect(docToMarkdown(fixture.doc).trim()).toMatchInlineSnapshot(`"- todo"`) + }) + + it('cycleOrderedList converts a task to a plain bullet first', () => { + using fixture = setupFixture() + const { n } = fixture + fixture.set(n.doc(n.list({ kind: 'task', marker: '+', checked: true }, n.paragraph('done')))) + + fixture.editor.commands.cycleOrderedList() + + expect(fixture.doc.child(0).attrs).toMatchObject({ + kind: 'bullet', + checked: false, + }) + expect(docToMarkdown(fixture.doc)).toBe('- done\n') + }) + + it('cycleOrderedList changes only the closest nested list', () => { + using fixture = setupFixture() + const { n } = fixture + fixture.set( + n.doc( + n.list( + { kind: 'bullet' }, + n.paragraph('outer'), + n.list({ kind: 'bullet' }, n.paragraph('inner')), + ), + ), + ) + + fixture.editor.commands.cycleOrderedList() + expect(docToMarkdown(fixture.doc)).toBe('- outer\n 1. inner\n') + fixture.editor.commands.cycleOrderedList() + expect(docToMarkdown(fixture.doc)).toBe('- outer\n\n inner\n') + }) }) describe('keymap', () => { diff --git a/packages/core/src/extensions/list.ts b/packages/core/src/extensions/list.ts index 14ae3f2d..75698079 100644 --- a/packages/core/src/extensions/list.ts +++ b/packages/core/src/extensions/list.ts @@ -20,8 +20,8 @@ import { type ListAttrs, } from '@prosekit/extensions/list' import type { ProseMirrorNode } from '@prosekit/pm/model' -import { Plugin } from '@prosekit/pm/state' import type { Command, EditorState } from '@prosekit/pm/state' +import { Plugin } from '@prosekit/pm/state' import { createListRenderingPlugin, createSafariInputMethodWorkaroundPlugin, @@ -300,8 +300,11 @@ function getListAttrsAtSelection(state: EditorState): MeowdownListAttrs | null { } /** - * Cycle the selected block between square and circle checkbox tasks. Non-task - * content becomes an unchecked square task; shape changes preserve checked state. + * Cycle the selected block between square and circle checkbox tasks. + * + * - A square task becomes a circle task, keeping its checked state; + * - A circle task becomes a square task, keeping its checked state; + * - Other content becomes an unchecked square task. */ function cycleCheckableList(): Command { return (state, dispatch, view) => { @@ -316,11 +319,35 @@ function cycleCheckableList(): Command { } } -function defineTaskCommands() { +/** + * Cycle the selected block between a bullet list, an ordered list, and no list. + * + * - A bullet list becomes an ordered list; + * - An ordered list unwraps; + * - Other content, including a task, becomes a bullet list. + */ +function cycleOrderedList(): Command { + return (state, dispatch, view) => { + const attrs = getListAttrsAtSelection(state) + const next: MeowdownListAttrs = + attrs?.kind === 'bullet' || attrs?.kind === 'ordered' + ? { kind: 'ordered', marker: null, checked: false, collapsed: false } + : { kind: 'bullet', marker: null, checked: false, collapsed: false } + return toggleList(next)(state, dispatch, view) + } +} + +function toggleListCollapsed(): Command { + return createToggleCollapsedCommand({ isToggleable: isCollapsibleBullet }) +} + +function defineMeowdownListCommands() { return defineCommands({ cycleCheckableList, + cycleOrderedList, wrapInCircleTask, wrapInSquareTask, + toggleListCollapsed, }) } @@ -413,12 +440,6 @@ function defineMeowdownListPlugins(): PlainExtension { ]) } -function defineCollapseCommands() { - return defineCommands({ - toggleListCollapsed: () => createToggleCollapsedCommand({ isToggleable: isCollapsibleBullet }), - }) -} - function defineMeowdownListKeymap(): PlainExtension { return defineKeymap({ 'Mod-Enter': rotateSquareTask(), @@ -450,7 +471,6 @@ export function defineMeowdownList() { defineListMarkerAttr(), defineListTaskMarkerAttr(), defineListMarkerGapAttr(), - defineTaskCommands(), - defineCollapseCommands(), + defineMeowdownListCommands(), ) }