import type { EditorView } from "@codemirror/view"; export type PlainleafEditorAction = | "undo" | "redo" | "heading-1" | "heading-2" | "bold" | "italic" | "bullet-list" | "numbered-list" | "task-list" | "quote" | "code" | "link" | "image"; const blockPrefix = /^\s*(?:#{1,6}\s+|[-*+]\s+(?:\[[ xX]\]\s+)?|\d+[.)]\s+|>\s+)?/; function replaceSelectedLines( view: EditorView, makePrefix: (index: number) => string, activePrefix: RegExp, ): void { const selection = view.state.selection.main; const firstLine = view.state.doc.lineAt(selection.from); const lastPosition = Math.max(selection.from, selection.to - 1); const lastLine = view.state.doc.lineAt(lastPosition); const source = view.state.doc.sliceString(firstLine.from, lastLine.to); const lines = source.split("\n"); const remove = lines.every((line) => !line.trim() || activePrefix.test(line)); const replacement = lines .map((line, index) => { if (!line.trim()) return line; const content = line.replace(blockPrefix, ""); return remove ? content : `${makePrefix(index)}${content}`; }) .join("\n"); view.dispatch({ changes: { from: firstLine.from, to: lastLine.to, insert: replacement }, selection: { anchor: firstLine.from, head: firstLine.from + replacement.length, }, }); } function wrapSelection( view: EditorView, before: string, after = before, placeholder = "文字", ): void { const selection = view.state.selection.main; const selected = view.state.doc.sliceString(selection.from, selection.to); const text = selected || placeholder; const insert = `${before}${text}${after}`; view.dispatch({ changes: { from: selection.from, to: selection.to, insert }, selection: selected ? { anchor: selection.from + before.length, head: selection.to + before.length, } : { anchor: selection.from + before.length, head: selection.from + before.length + placeholder.length, }, }); } export function applyPlainleafEditorAction( view: EditorView, action: PlainleafEditorAction, ): boolean { switch (action) { case "heading-1": replaceSelectedLines(view, () => "# ", /^\s*#\s+/); break; case "heading-2": replaceSelectedLines(view, () => "## ", /^\s*##\s+/); break; case "bullet-list": replaceSelectedLines(view, () => "- ", /^\s*[-*+]\s+/); break; case "numbered-list": replaceSelectedLines(view, (index) => `${index + 1}. `, /^\s*\d+[.)]\s+/); break; case "task-list": replaceSelectedLines(view, () => "- [ ] ", /^\s*[-*+]\s+\[[ xX]\]\s+/); break; case "quote": replaceSelectedLines(view, () => "> ", /^\s*>\s+/); break; case "bold": wrapSelection(view, "**"); break; case "italic": wrapSelection(view, "_"); break; case "code": { const selection = view.state.selection.main; const selected = view.state.doc.sliceString(selection.from, selection.to); if (selected.includes("\n")) wrapSelection(view, "```\n", "\n```", "代码"); else wrapSelection(view, "`", "`", "代码"); break; } case "link": { const selection = view.state.selection.main; const selected = view.state.doc.sliceString(selection.from, selection.to); const label = selected || "链接文字"; const insert = `[${label}](https://)`; const urlFrom = selection.from + label.length + 3; view.dispatch({ changes: { from: selection.from, to: selection.to, insert }, selection: { anchor: urlFrom, head: urlFrom + 8 }, }); break; } case "undo": case "redo": case "image": return false; } view.focus(); return true; }