文件
mynode/packages/editor/CodeMirror/utils/markdown/toggleCheckboxAt.ts
chen 301194963b
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

203 行
5.6 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

import { Command, EditorView } from '@codemirror/view';
export interface ToggleCheckboxOptions {
autoSort?: boolean;
}
export interface ChecklistItemBlock {
lines: string[];
isChecked: boolean;
isTarget: boolean;
}
const CHECKLIST_REGEX = /^(\s*(?:[-*+]|\d+\.)\s+)\[([ xX])\]/;
/**
* 判断单行文本是否符合 Markdown 任务列表语法(如 `- [ ] 待办`、`1. [x] 已完成`)
*/
export const isChecklistLine = (lineText: string): boolean => {
return CHECKLIST_REGEX.test(lineText);
};
/**
* 获取单行前置缩进空格数
*/
export const getLineIndent = (lineText: string): number => {
const match = lineText.match(/^\s*/);
return match ? match[0].length : 0;
};
/**
* 对连续待办列表项执行重排(对标 Apple 备忘录自动沉底体验)
* - 未勾选项保留在顶部
* - 新勾选的项沉底至已完成列表末尾
* - 取消勾选的项浮动至未完成列表末尾
*/
export const reorderChecklistItems = (
items: ChecklistItemBlock[],
targetChecked: boolean,
): ChecklistItemBlock[] => {
const unchecked: ChecklistItemBlock[] = [];
const checked: ChecklistItemBlock[] = [];
let targetItem: ChecklistItemBlock | null = null;
for (const item of items) {
const isItemChecked = item.isTarget ? targetChecked : item.isChecked;
const updatedItem: ChecklistItemBlock = {
...item,
isChecked: isItemChecked,
lines: item.lines.map((l, idx) => {
if (item.isTarget && idx === 0) {
return l.replace(CHECKLIST_REGEX, (_match, prefix) => `${prefix}[${targetChecked ? 'x' : ' '}]`);
}
return l;
}),
};
if (item.isTarget) {
targetItem = updatedItem;
} else if (isItemChecked) {
checked.push(updatedItem);
} else {
unchecked.push(updatedItem);
}
}
if (targetItem) {
if (targetChecked) {
// 勾选完成:沉底至已完成项的最末端
checked.push(targetItem);
} else {
// 取消勾选:浮动至未完成项的最末端
unchecked.push(targetItem);
}
}
return [...unchecked, ...checked];
};
/**
* 切换指定位置的复选框状态,并支持连续待办清单自动沉底
*/
const toggleCheckbox = (linePos: number, options: ToggleCheckboxOptions = { autoSort: true }): Command => (target: EditorView) => {
const state = target.state;
if (linePos >= state.doc.length) {
return false;
}
const line = state.doc.lineAt(linePos);
const checklistMatch = line.text.match(CHECKLIST_REGEX);
// 非行首标准列表项,回退到就地匹配替换
if (!checklistMatch) {
const genericMatch = line.text.match(/\[([ xX])\]/);
if (!genericMatch) {
return false;
}
const isChecked = genericMatch[1].toLowerCase() === 'x';
const checkboxPos = genericMatch.index! + line.from;
target.dispatch({
changes: [{ from: checkboxPos, to: checkboxPos + 3, insert: isChecked ? '[ ]' : '[x]' }],
});
return true;
}
const isCurrentlyChecked = checklistMatch[2].toLowerCase() === 'x';
const newChecked = !isCurrentlyChecked;
const shouldAutoSort = options.autoSort !== false;
if (!shouldAutoSort) {
const checkboxPos = checklistMatch[1].length + line.from;
target.dispatch({
changes: [{ from: checkboxPos, to: checkboxPos + 3, insert: newChecked ? '[x]' : '[ ]' }],
});
return true;
}
// 自动排序模式:查找同缩进层级的连续清单块
const baseIndent = getLineIndent(line.text);
let startLineNo = line.number;
let endLineNo = line.number;
// 向上探测列表起始行
for (let i = line.number - 1; i >= 1; i--) {
const prevLine = state.doc.line(i);
if (!prevLine.text.trim()) break; // 空行阻断清单块
const indent = getLineIndent(prevLine.text);
if (isChecklistLine(prevLine.text) && indent === baseIndent) {
startLineNo = i;
} else if (indent > baseIndent) {
continue;
} else {
break;
}
}
// 向下探测列表结束行
for (let i = line.number + 1; i <= state.doc.lines; i++) {
const nextLine = state.doc.line(i);
if (!nextLine.text.trim()) break; // 空行阻断清单块
const indent = getLineIndent(nextLine.text);
if (isChecklistLine(nextLine.text) && indent === baseIndent) {
endLineNo = i;
} else if (indent > baseIndent) {
endLineNo = i;
} else {
break;
}
}
// 将行聚合成项(包含可能的子缩进行)
const items: ChecklistItemBlock[] = [];
let currentItem: ChecklistItemBlock | null = null;
for (let i = startLineNo; i <= endLineNo; i++) {
const currentDocLine = state.doc.line(i);
const indent = getLineIndent(currentDocLine.text);
const isItemHeader = isChecklistLine(currentDocLine.text) && indent === baseIndent;
if (isItemHeader) {
if (currentItem) {
items.push(currentItem);
}
const match = currentDocLine.text.match(CHECKLIST_REGEX);
currentItem = {
lines: [currentDocLine.text],
isChecked: match ? match[2].toLowerCase() === 'x' : false,
isTarget: i === line.number,
};
} else if (currentItem) {
currentItem.lines.push(currentDocLine.text);
}
}
if (currentItem) {
items.push(currentItem);
}
// 单项列表直接就地切换,不触发重排
if (items.length <= 1) {
const checkboxPos = checklistMatch[1].length + line.from;
target.dispatch({
changes: [{ from: checkboxPos, to: checkboxPos + 3, insert: newChecked ? '[x]' : '[ ]' }],
});
return true;
}
const reordered = reorderChecklistItems(items, newChecked);
const newContentLines: string[] = [];
for (const item of reordered) {
newContentLines.push(...item.lines);
}
const replaceFrom = state.doc.line(startLineNo).from;
const replaceTo = state.doc.line(endLineNo).to;
const newText = newContentLines.join('\n');
target.dispatch({
changes: [{ from: replaceFrom, to: replaceTo, insert: newText }],
});
return true;
};
export default toggleCheckbox;