Skip to content

Commit 6777a96

Browse files
committed
perf: 提供更符合直觉的列表输入,可直接输入不同的标记改变当前列表属性
1 parent 1bb661e commit 6777a96

2 files changed

Lines changed: 182 additions & 3 deletions

File tree

lang/index.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3638,5 +3638,13 @@
36383638
"ru": "",
36393639
"en": "",
36403640
"fr": ""
3641+
},
3642+
"gl01kl7e": {
3643+
"zh-cn": "\n<br />\n\n# MilkUp\n\n<br />\n\n> 这是一段测试文字\n\n<br />\n\n***\n\n<br />\n\n* item1\n* item2\n* item3\n\n代码:\n\n```JavaScript\nconst text = \"Hello Word!\"\nconsole.log(text)\n```\n\n<br />\n\n| 1 | 2 | 3 |\n| :- | :- | :- |\n| 1 | 2 | 3 |\n| 1 | 2 | 3 |\n\n\n",
3644+
"ja": "",
3645+
"ko": "",
3646+
"ru": "",
3647+
"en": "",
3648+
"fr": ""
36413649
}
36423650
}

src/core/plugins/input-rules.ts

Lines changed: 174 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)