@@ -172,6 +172,159 @@ function taskListRule(listType: NodeType, itemType: NodeType): InputRule {
172172 } ) ;
173173}
174174
175+ /**
176+ * 无序列表项 → 有序列表
177+ * 在 bullet_list > list_item > paragraph 开头输入 `数字. ` 时转换
178+ */
179+ function bulletToOrderedRule (
180+ orderedListType : NodeType ,
181+ bulletListType : NodeType ,
182+ itemType : NodeType
183+ ) : InputRule {
184+ return new InputRule ( / ^ ( \d + ) \. \s $ / , ( state , match , start , end ) => {
185+ const startNum = parseInt ( match [ 1 ] , 10 ) ;
186+ const $start = state . doc . resolve ( start ) ;
187+
188+ if ( $start . depth < 2 ) return null ;
189+
190+ const listItemDepth = $start . depth - 1 ;
191+ const listItem = $start . node ( listItemDepth ) ;
192+ const listDepth = listItemDepth - 1 ;
193+ const list = $start . node ( listDepth ) ;
194+
195+ if ( listItem . type . name !== "list_item" || list . type !== bulletListType ) return null ;
196+
197+ const paraStart = $start . start ( $start . depth ) ;
198+ if ( start !== paraStart ) return null ;
199+
200+ const listPos = $start . before ( listDepth ) ;
201+ const matchLen = end - start ;
202+
203+ const para = $start . node ( $start . depth ) ;
204+ const newParaContent = para . content . cut ( matchLen ) ;
205+ const newPara = para . type . create (
206+ para . attrs ,
207+ newParaContent . size > 0 ? newParaContent : undefined
208+ ) ;
209+
210+ const itemChildren : any [ ] = [ newPara ] ;
211+ for ( let i = 1 ; i < listItem . childCount ; i ++ ) {
212+ itemChildren . push ( listItem . child ( i ) ) ;
213+ }
214+
215+ const newItem = itemType . create ( null , itemChildren ) ;
216+ const newList = orderedListType . create ( { start : startNum } , newItem ) ;
217+
218+ if ( list . childCount === 1 ) {
219+ let tr = state . tr . replaceWith ( listPos , listPos + list . nodeSize , newList ) ;
220+ tr = tr . setSelection ( TextSelection . near ( tr . doc . resolve ( listPos + 2 ) ) ) ;
221+ return tr ;
222+ }
223+
224+ const itemIndex = $start . index ( listDepth ) ;
225+ const beforeItems : any [ ] = [ ] ;
226+ const afterItems : any [ ] = [ ] ;
227+ list . forEach ( ( child , _offset , index ) => {
228+ if ( index < itemIndex ) beforeItems . push ( child ) ;
229+ else if ( index > itemIndex ) afterItems . push ( child ) ;
230+ } ) ;
231+
232+ const fragments : any [ ] = [ ] ;
233+ if ( beforeItems . length > 0 ) {
234+ fragments . push ( bulletListType . create ( list . attrs , Fragment . from ( beforeItems ) ) ) ;
235+ }
236+ fragments . push ( newList ) ;
237+ if ( afterItems . length > 0 ) {
238+ fragments . push ( bulletListType . create ( list . attrs , Fragment . from ( afterItems ) ) ) ;
239+ }
240+
241+ let tr = state . tr . replaceWith ( listPos , listPos + list . nodeSize , fragments ) ;
242+ let cursorPos = listPos ;
243+ if ( beforeItems . length > 0 ) {
244+ cursorPos += beforeItems . reduce ( ( s : number , n : any ) => s + n . nodeSize , 0 ) + 2 ;
245+ }
246+ cursorPos += 2 ;
247+ tr = tr . setSelection ( TextSelection . near ( tr . doc . resolve ( cursorPos ) ) ) ;
248+ return tr ;
249+ } ) ;
250+ }
251+
252+ /**
253+ * 有序列表项 → 无序列表
254+ * 在 ordered_list > list_item > paragraph 开头输入 `- ` 或 `* ` 或 `+ ` 时转换
255+ */
256+ function orderedToBulletRule (
257+ bulletListType : NodeType ,
258+ orderedListType : NodeType ,
259+ itemType : NodeType
260+ ) : InputRule {
261+ return new InputRule ( / ^ [ - * + ] \s $ / , ( state , match , start , end ) => {
262+ const $start = state . doc . resolve ( start ) ;
263+
264+ if ( $start . depth < 2 ) return null ;
265+
266+ const listItemDepth = $start . depth - 1 ;
267+ const listItem = $start . node ( listItemDepth ) ;
268+ const listDepth = listItemDepth - 1 ;
269+ const list = $start . node ( listDepth ) ;
270+
271+ if ( listItem . type . name !== "list_item" || list . type !== orderedListType ) return null ;
272+
273+ const paraStart = $start . start ( $start . depth ) ;
274+ if ( start !== paraStart ) return null ;
275+
276+ const listPos = $start . before ( listDepth ) ;
277+ const matchLen = end - start ;
278+
279+ const para = $start . node ( $start . depth ) ;
280+ const newParaContent = para . content . cut ( matchLen ) ;
281+ const newPara = para . type . create (
282+ para . attrs ,
283+ newParaContent . size > 0 ? newParaContent : undefined
284+ ) ;
285+
286+ const itemChildren : any [ ] = [ newPara ] ;
287+ for ( let i = 1 ; i < listItem . childCount ; i ++ ) {
288+ itemChildren . push ( listItem . child ( i ) ) ;
289+ }
290+
291+ const newItem = itemType . create ( null , itemChildren ) ;
292+ const newList = bulletListType . create ( null , newItem ) ;
293+
294+ if ( list . childCount === 1 ) {
295+ let tr = state . tr . replaceWith ( listPos , listPos + list . nodeSize , newList ) ;
296+ tr = tr . setSelection ( TextSelection . near ( tr . doc . resolve ( listPos + 2 ) ) ) ;
297+ return tr ;
298+ }
299+
300+ const itemIndex = $start . index ( listDepth ) ;
301+ const beforeItems : any [ ] = [ ] ;
302+ const afterItems : any [ ] = [ ] ;
303+ list . forEach ( ( child , _offset , index ) => {
304+ if ( index < itemIndex ) beforeItems . push ( child ) ;
305+ else if ( index > itemIndex ) afterItems . push ( child ) ;
306+ } ) ;
307+
308+ const fragments : any [ ] = [ ] ;
309+ if ( beforeItems . length > 0 ) {
310+ fragments . push ( orderedListType . create ( list . attrs , Fragment . from ( beforeItems ) ) ) ;
311+ }
312+ fragments . push ( newList ) ;
313+ if ( afterItems . length > 0 ) {
314+ fragments . push ( orderedListType . create ( list . attrs , Fragment . from ( afterItems ) ) ) ;
315+ }
316+
317+ let tr = state . tr . replaceWith ( listPos , listPos + list . nodeSize , fragments ) ;
318+ let cursorPos = listPos ;
319+ if ( beforeItems . length > 0 ) {
320+ cursorPos += beforeItems . reduce ( ( s : number , n : any ) => s + n . nodeSize , 0 ) + 2 ;
321+ }
322+ cursorPos += 2 ;
323+ tr = tr . setSelection ( TextSelection . near ( tr . doc . resolve ( cursorPos ) ) ) ;
324+ return tr ;
325+ } ) ;
326+ }
327+
175328/**
176329 * 创建带 syntax_marker 的行内规则
177330 * 保持与解析器一致的文档结构
@@ -435,15 +588,33 @@ export function createInputRulesPlugin(schema: Schema = milkupSchema): Plugin {
435588 if ( schema . nodes . horizontal_rule ) {
436589 rules . push ( horizontalRuleRule ( schema . nodes . horizontal_rule ) ) ;
437590 }
591+ // 列表类型转换规则(必须在基础列表规则之前,否则 wrappingInputRule 会先匹配)
592+ if ( schema . nodes . bullet_list && schema . nodes . ordered_list && schema . nodes . list_item ) {
593+ rules . push (
594+ bulletToOrderedRule (
595+ schema . nodes . ordered_list ,
596+ schema . nodes . bullet_list ,
597+ schema . nodes . list_item
598+ )
599+ ) ;
600+ rules . push (
601+ orderedToBulletRule (
602+ schema . nodes . bullet_list ,
603+ schema . nodes . ordered_list ,
604+ schema . nodes . list_item
605+ )
606+ ) ;
607+ }
608+ if ( schema . nodes . task_list && schema . nodes . task_item ) {
609+ rules . push ( taskListRule ( schema . nodes . task_list , schema . nodes . task_item ) ) ;
610+ }
611+ // 基础列表创建规则
438612 if ( schema . nodes . bullet_list && schema . nodes . list_item ) {
439613 rules . push ( bulletListRule ( schema . nodes . bullet_list , schema . nodes . list_item ) ) ;
440614 }
441615 if ( schema . nodes . ordered_list && schema . nodes . list_item ) {
442616 rules . push ( orderedListRule ( schema . nodes . ordered_list , schema . nodes . list_item ) ) ;
443617 }
444- if ( schema . nodes . task_list && schema . nodes . task_item ) {
445- rules . push ( taskListRule ( schema . nodes . task_list , schema . nodes . task_item ) ) ;
446- }
447618 if ( schema . nodes . math_block ) {
448619 rules . push ( mathBlockRule ( schema . nodes . math_block ) ) ;
449620 rules . push ( mathBlockInlineRule ( schema . nodes . math_block ) ) ;
0 commit comments