mirror of
https://github.com/groldsf/obsidian_check_plugin.git
synced 2026-07-22 05:37:48 +00:00
add settings tab.
This commit is contained in:
parent
0c43e9f5e7
commit
1234817949
5 changed files with 223 additions and 132 deletions
|
|
@ -1,92 +1,110 @@
|
|||
import { findCheckboxesLine, syncCheckboxes } from "../src/checkboxUtils";
|
||||
import { CheckboxUtils } from "../src/checkboxUtils";
|
||||
|
||||
describe("findCheckboxesLine", () => {
|
||||
test("распознаёт корректные строки с чекбоксами", () => {
|
||||
expect(findCheckboxesLine("- [ ] Task")).not.toBeNull();
|
||||
expect(findCheckboxesLine(" - [ ] Task")).not.toBeNull();
|
||||
expect(findCheckboxesLine("- [x] Task")).not.toBeNull();
|
||||
expect(findCheckboxesLine("* [ ] Another task")).not.toBeNull();
|
||||
expect(findCheckboxesLine("+ [x] Task")).not.toBeNull();
|
||||
expect(findCheckboxesLine("1. [ ] Numbered task")).not.toBeNull();
|
||||
expect(findCheckboxesLine("1. [x] Numbered task")).not.toBeNull();
|
||||
describe("CheckboxUtils", () => {
|
||||
let checkboxUtilsXOnly: CheckboxUtils;
|
||||
let checkboxUtilsSpaceOnly: CheckboxUtils;
|
||||
|
||||
beforeEach(() => {
|
||||
checkboxUtilsXOnly = new CheckboxUtils({ xOnlyMode: true });
|
||||
checkboxUtilsSpaceOnly = new CheckboxUtils({ xOnlyMode: false });
|
||||
});
|
||||
|
||||
test("корректно обрабатывает разные состояния чекбокса", () => {
|
||||
const states = [" ", "x", "-", "?", "!", "*", "#", "@", "[", "]", "y", "X", "Y"];
|
||||
|
||||
for (const state of states) {
|
||||
const match = findCheckboxesLine(`- [${state}] Task`);
|
||||
expect(match).not.toBeNull();
|
||||
expect(match![3]).toBe(state);
|
||||
}
|
||||
describe("findCheckboxesLine", () => {
|
||||
test("распознаёт корректные строки с чекбоксами", () => {
|
||||
expect(checkboxUtilsXOnly.findCheckboxesLine("- [ ] Task")).not.toBeNull();
|
||||
expect(checkboxUtilsXOnly.findCheckboxesLine(" - [ ] Task")).not.toBeNull();
|
||||
expect(checkboxUtilsXOnly.findCheckboxesLine("- [x] Task")).not.toBeNull();
|
||||
expect(checkboxUtilsXOnly.findCheckboxesLine("* [ ] Another task")).not.toBeNull();
|
||||
expect(checkboxUtilsXOnly.findCheckboxesLine("+ [x] Task")).not.toBeNull();
|
||||
expect(checkboxUtilsXOnly.findCheckboxesLine("1. [ ] Numbered task")).not.toBeNull();
|
||||
expect(checkboxUtilsXOnly.findCheckboxesLine("1. [x] Numbered task")).not.toBeNull();
|
||||
});
|
||||
|
||||
test("корректно обрабатывает разные состояния чекбокса", () => {
|
||||
const states = [" ", "x", "-", "?", "!", "*", "#", "@", "[", "]", "y", "X", "Y"];
|
||||
|
||||
for (const state of states) {
|
||||
const match = checkboxUtilsXOnly.findCheckboxesLine(`- [${state}] Task`);
|
||||
expect(match).not.toBeNull();
|
||||
expect(match![3]).toBe(state);
|
||||
}
|
||||
});
|
||||
|
||||
test("корректно обрабатывает отступы", () => {
|
||||
const noIndentMatch = checkboxUtilsXOnly.findCheckboxesLine("- [ ] Task");
|
||||
expect(noIndentMatch).not.toBeNull();
|
||||
expect(noIndentMatch![1]).toBe("");
|
||||
|
||||
const singleSpaceMatch = checkboxUtilsXOnly.findCheckboxesLine(" - [ ] Task");
|
||||
expect(singleSpaceMatch).not.toBeNull();
|
||||
expect(singleSpaceMatch![1]).toBe(" ");
|
||||
|
||||
const multiSpaceMatch = checkboxUtilsXOnly.findCheckboxesLine(" - [ ] Task");
|
||||
expect(multiSpaceMatch).not.toBeNull();
|
||||
expect(multiSpaceMatch![1]).toBe(" ");
|
||||
|
||||
const tabMatch = checkboxUtilsXOnly.findCheckboxesLine("\t- [ ] Task");
|
||||
expect(tabMatch).not.toBeNull();
|
||||
expect(tabMatch![1]).toBe("\t");
|
||||
});
|
||||
|
||||
test("игнорирует некорректные строки", () => {
|
||||
expect(checkboxUtilsXOnly.findCheckboxesLine("-[ ] Task")).toBeNull();
|
||||
expect(checkboxUtilsXOnly.findCheckboxesLine("*[x] Task")).toBeNull();
|
||||
expect(checkboxUtilsXOnly.findCheckboxesLine("1.[ ] Numbered task")).toBeNull();
|
||||
expect(checkboxUtilsXOnly.findCheckboxesLine("- [] Task")).toBeNull();
|
||||
expect(checkboxUtilsXOnly.findCheckboxesLine(" - Task")).toBeNull();
|
||||
expect(checkboxUtilsXOnly.findCheckboxesLine("Just some text")).toBeNull();
|
||||
expect(checkboxUtilsXOnly.findCheckboxesLine("1. Some numbered task")).toBeNull();
|
||||
expect(checkboxUtilsXOnly.findCheckboxesLine("")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test("корректно обрабатывает отступы", () => {
|
||||
const noIndentMatch = findCheckboxesLine("- [ ] Task");
|
||||
expect(noIndentMatch).not.toBeNull();
|
||||
expect(noIndentMatch![1]).toBe("");
|
||||
describe("isCheckedSymbol", () => {
|
||||
test("в режиме xOnlyMode только 'x' считается отмеченным", () => {
|
||||
expect(checkboxUtilsXOnly.isCheckedSymbol("x")).toBe(true);
|
||||
expect(checkboxUtilsXOnly.isCheckedSymbol(" ")).toBe(false);
|
||||
expect(checkboxUtilsXOnly.isCheckedSymbol("-")).toBe(false);
|
||||
expect(checkboxUtilsXOnly.isCheckedSymbol("?")).toBe(false);
|
||||
});
|
||||
|
||||
const singleSpaceMatch = findCheckboxesLine(" - [ ] Task");
|
||||
expect(singleSpaceMatch).not.toBeNull();
|
||||
expect(singleSpaceMatch![1]).toBe(" ");
|
||||
|
||||
const multiSpaceMatch = findCheckboxesLine(" - [ ] Task");
|
||||
expect(multiSpaceMatch).not.toBeNull();
|
||||
expect(multiSpaceMatch![1]).toBe(" ");
|
||||
|
||||
const tabMatch = findCheckboxesLine("\t- [ ] Task");
|
||||
expect(tabMatch).not.toBeNull();
|
||||
expect(tabMatch![1]).toBe("\t");
|
||||
test("в обычном режиме все символы кроме пробела считаются отмеченными", () => {
|
||||
expect(checkboxUtilsSpaceOnly.isCheckedSymbol("x")).toBe(true);
|
||||
expect(checkboxUtilsSpaceOnly.isCheckedSymbol(" ")).toBe(false);
|
||||
expect(checkboxUtilsSpaceOnly.isCheckedSymbol("-")).toBe(true);
|
||||
expect(checkboxUtilsSpaceOnly.isCheckedSymbol("?")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("игнорирует некорректные строки", () => {
|
||||
// Отсутствует пробел после маркера списка
|
||||
expect(findCheckboxesLine("-[ ] Task")).toBeNull();
|
||||
expect(findCheckboxesLine("*[x] Task")).toBeNull();
|
||||
expect(findCheckboxesLine("1.[ ] Numbered task")).toBeNull();
|
||||
// Пустые скобки недопустимы
|
||||
expect(findCheckboxesLine("- [] Task")).toBeNull();
|
||||
// Не соответствует шаблону чекбокса
|
||||
expect(findCheckboxesLine(" - Task")).toBeNull();
|
||||
expect(findCheckboxesLine("Just some text")).toBeNull();
|
||||
expect(findCheckboxesLine("1. Some numbered task")).toBeNull();
|
||||
expect(findCheckboxesLine("")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("syncCheckboxes", () => {
|
||||
test("обновляет родительский чекбокс, если все дочерние отмечены", () => {
|
||||
const text = "- [ ] Parent\n - [x] Child 1\n - [x] Child 2";
|
||||
// Для строки "- [ ] Parent":
|
||||
// match[1] = "" (нулевой отступ), match[2] = "-" (1 символ)
|
||||
// => позиция чекбокса = 0 + 1 + 2 = 3
|
||||
expect(syncCheckboxes(text)).toEqual([{ line: 0, ch: 3, value: "x" }]);
|
||||
});
|
||||
|
||||
test("снимает отметку у родителя, если не все дочерние отмечены", () => {
|
||||
const text = "- [x] Parent\n - [x] Child 1\n - [ ] Child 2";
|
||||
// Родительский чекбокс должен стать незамеченным, то есть "x" -> " "
|
||||
expect(syncCheckboxes(text)).toEqual([{ line: 0, ch: 3, value: " " }]);
|
||||
});
|
||||
|
||||
test("не изменяет родителя, если у него нет дочерних элементов", () => {
|
||||
const text = "- [x] Single";
|
||||
expect(syncCheckboxes(text)).toEqual([]);
|
||||
});
|
||||
|
||||
test("тройная вложенность: обновляет только промежуточный уровень", () => {
|
||||
const text = "- [ ] Parent\n - [ ] Child\n - [x] Grandchild";
|
||||
// Для строки " - [ ] Child":
|
||||
// match[1] = " " (2 пробела), match[2] = "-" (1 символ)
|
||||
// => позиция чекбокса = 2 + 1 + 2 = 5
|
||||
expect(syncCheckboxes(text)).toEqual([{ line: 1, ch: 5, value: "x" }]);
|
||||
});
|
||||
|
||||
test("тройная вложенность: обновляет родительский чекбокс, если все вложенные отмечены", () => {
|
||||
const text = "- [ ] Parent\n - [x] Child\n - [x] Grandchild";
|
||||
// Здесь для строки " - [x] Child" изменений не требуется (она уже отмечена).
|
||||
// Родительский элемент имеет единственного дочернего, отмеченного как [x],
|
||||
// поэтому ожидается обновление строки Parent: позиция = 0 + 1 + 2 = 3.
|
||||
expect(syncCheckboxes(text)).toEqual([{ line: 0, ch: 3, value: "x" }]);
|
||||
describe("syncCheckboxes", () => {
|
||||
test("обновляет родительский чекбокс, если все дочерние отмечены (xOnlyMode)", () => {
|
||||
const text = "- [ ] Parent\n - [x] Child 1\n - [x] Child 2";
|
||||
expect(checkboxUtilsXOnly.syncCheckboxes(text)).toEqual([{ line: 0, ch: 3, value: "x" }]);
|
||||
});
|
||||
|
||||
test("обновляет родительский чекбокс, если все дочерние отмечены (spaceOnly)", () => {
|
||||
const text = "- [ ] Parent\n - [?] Child 1\n - [-] Child 2";
|
||||
expect(checkboxUtilsSpaceOnly.syncCheckboxes(text)).toEqual([{ line: 0, ch: 3, value: "x" }]);
|
||||
});
|
||||
|
||||
test("снимает отметку у родителя, если не все дочерние отмечены", () => {
|
||||
const text = "- [x] Parent\n - [x] Child 1\n - [ ] Child 2";
|
||||
expect(checkboxUtilsXOnly.syncCheckboxes(text)).toEqual([{ line: 0, ch: 3, value: " " }]);
|
||||
});
|
||||
|
||||
test("не изменяет родителя, если у него нет дочерних элементов", () => {
|
||||
const text = "- [x] Single";
|
||||
expect(checkboxUtilsXOnly.syncCheckboxes(text)).toEqual([]);
|
||||
});
|
||||
|
||||
test("тройная вложенность: обновляет только промежуточный уровень", () => {
|
||||
const text = "- [ ] Parent\n - [ ] Child\n - [x] Grandchild";
|
||||
expect(checkboxUtilsXOnly.syncCheckboxes(text)).toEqual([{ line: 1, ch: 5, value: "x" }]);
|
||||
});
|
||||
|
||||
test("тройная вложенность: обновляет родительский чекбокс, если все вложенные отмечены", () => {
|
||||
const text = "- [ ] Parent\n - [x] Child\n - [x] Grandchild";
|
||||
expect(checkboxUtilsXOnly.syncCheckboxes(text)).toEqual([{ line: 0, ch: 3, value: "x" }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
28
src/CheckboxSyncPluginSettingTab.ts
Normal file
28
src/CheckboxSyncPluginSettingTab.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { App, PluginSettingTab, Setting } from "obsidian";
|
||||
import CheckboxSyncPlugin from "./main";
|
||||
|
||||
export class CheckboxSyncPluginSettingTab extends PluginSettingTab {
|
||||
plugin: CheckboxSyncPlugin;
|
||||
|
||||
constructor(app: App, plugin: CheckboxSyncPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("X-Only Mode")
|
||||
.setDesc("When enabled, only 'x' marks a task as complete. When disabled, any character except space marks a task as complete")
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.xOnlyMode)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.xOnlyMode = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,51 +1,56 @@
|
|||
/**
|
||||
* Функция для поиска строк с чекбоксами в формате Markdown.
|
||||
*/
|
||||
export function findCheckboxesLine(line: string): RegExpMatchArray | null {
|
||||
return line.match(/^(\s*)([*+-]|\d+\.) \[(.)\] /);
|
||||
}
|
||||
import { CheckboxSyncPluginSettings } from "./types";
|
||||
|
||||
export function isCheckedSymbol(text: string): boolean {
|
||||
return text === "x";
|
||||
}
|
||||
/**
|
||||
* Возвращает список изменений (позиции и новые значения), но не модифицирует текст напрямую.
|
||||
*/
|
||||
export function syncCheckboxes(text: string): { line: number; ch: number; value: string }[] {
|
||||
const lines = text.split("\n");
|
||||
let updates: { line: number; ch: number; value: string }[] = [];
|
||||
export class CheckboxUtils {
|
||||
constructor(private settings: CheckboxSyncPluginSettings) {}
|
||||
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const match = findCheckboxesLine(lines[i]);
|
||||
if (!match) continue;
|
||||
updateSettings(settings: CheckboxSyncPluginSettings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
const indent = match[1].length;
|
||||
const isChecked = isCheckedSymbol(match[3]);
|
||||
let allChildrenChecked = true;
|
||||
let hasChildren = false;
|
||||
let j = i + 1;
|
||||
findCheckboxesLine(line: string): RegExpMatchArray | null {
|
||||
return line.match(/^(\s*)([*+-]|\d+\.) \[(.)\]\s/);
|
||||
}
|
||||
|
||||
while (j < lines.length) {
|
||||
const childMatch = findCheckboxesLine(lines[j]);
|
||||
if (!childMatch || childMatch[1].length <= indent) break;
|
||||
hasChildren = true;
|
||||
const childrenIsChecked = isCheckedSymbol(childMatch[3]);
|
||||
if (!childrenIsChecked) {
|
||||
allChildrenChecked = false;
|
||||
break;
|
||||
}
|
||||
j++;
|
||||
}
|
||||
isCheckedSymbol(text: string): boolean {
|
||||
return this.settings.xOnlyMode ? text === "x" : text !== " ";
|
||||
}
|
||||
|
||||
if (hasChildren) {
|
||||
const checkboxPos = match[1].length + match[2].length + 2;
|
||||
if (allChildrenChecked && !isChecked) {
|
||||
updates.push({ line: i, ch: checkboxPos, value: "x" });
|
||||
} else if (!allChildrenChecked && isChecked) {
|
||||
updates.push({ line: i, ch: checkboxPos, value: " " });
|
||||
}
|
||||
}
|
||||
}
|
||||
syncCheckboxes(text: string): { line: number; ch: number; value: string }[] {
|
||||
const lines = text.split("\n");
|
||||
let updates: { line: number; ch: number; value: string }[] = [];
|
||||
|
||||
return updates;
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const match = this.findCheckboxesLine(lines[i]);
|
||||
if (!match) continue;
|
||||
|
||||
const indent = match[1].length;
|
||||
const isChecked = this.isCheckedSymbol(match[3]);
|
||||
let allChildrenChecked = true;
|
||||
let hasChildren = false;
|
||||
let j = i + 1;
|
||||
|
||||
while (j < lines.length) {
|
||||
const childMatch = this.findCheckboxesLine(lines[j]);
|
||||
if (!childMatch || childMatch[1].length <= indent) break;
|
||||
hasChildren = true;
|
||||
const childrenIsChecked = this.isCheckedSymbol(childMatch[3]);
|
||||
if (!childrenIsChecked) {
|
||||
allChildrenChecked = false;
|
||||
break;
|
||||
}
|
||||
j++;
|
||||
}
|
||||
|
||||
if (hasChildren) {
|
||||
const checkboxPos = match[1].length + match[2].length + 2;
|
||||
if (allChildrenChecked && !isChecked) {
|
||||
updates.push({ line: i, ch: checkboxPos, value: "x" });
|
||||
} else if (!allChildrenChecked && isChecked) {
|
||||
updates.push({ line: i, ch: checkboxPos, value: " " });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updates;
|
||||
}
|
||||
}
|
||||
49
src/main.ts
49
src/main.ts
|
|
@ -1,22 +1,45 @@
|
|||
import { Plugin, TFile } from "obsidian";
|
||||
import { syncCheckboxes } from "./checkboxUtils";
|
||||
import { CheckboxUtils } from "./checkboxUtils";
|
||||
import { CheckboxSyncPluginSettingTab } from "./CheckboxSyncPluginSettingTab";
|
||||
import { CheckboxSyncPluginSettings } from "./types";
|
||||
|
||||
const DEFAULT_SETTINGS: CheckboxSyncPluginSettings = {
|
||||
xOnlyMode: true,
|
||||
};
|
||||
|
||||
export default class CheckboxSyncPlugin extends Plugin {
|
||||
settings: CheckboxSyncPluginSettings;
|
||||
|
||||
private isProcessing = false;
|
||||
checkboxUtils: CheckboxUtils;
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
this.checkboxUtils = new CheckboxUtils(this.settings);
|
||||
this.addSettingTab(new CheckboxSyncPluginSettingTab(this.app, this));
|
||||
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("file-open", async (file) => {
|
||||
if (this.isProcessing || !(file instanceof TFile) || file.extension !== "md") return;
|
||||
|
||||
this.isProcessing = true;
|
||||
try {
|
||||
await this.syncFileCheckboxes(file);
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("editor-change", (editor) => {
|
||||
const updates = syncCheckboxes(editor.getValue());
|
||||
const updates = this.checkboxUtils.syncCheckboxes(editor.getValue());
|
||||
if (updates.length === 0) return;
|
||||
|
||||
const firstUpdateLine = updates[0].line;
|
||||
// Перемещаем курсор на строку первого изменения (символ 0)
|
||||
editor.setCursor({ line: firstUpdateLine, ch: editor.getLine(firstUpdateLine).length });
|
||||
// Делаем курсор невидимым
|
||||
editor.blur();
|
||||
|
||||
// Выполняем обновления текста
|
||||
updates.forEach(({ line, ch, value }) => {
|
||||
editor.replaceRange(value, { line, ch }, { line, ch: ch + 1 });
|
||||
});
|
||||
|
|
@ -38,7 +61,7 @@ export default class CheckboxSyncPlugin extends Plugin {
|
|||
|
||||
async syncFileCheckboxes(file: TFile) {
|
||||
const text = await this.app.vault.read(file);
|
||||
const updates = syncCheckboxes(text);
|
||||
const updates = this.checkboxUtils.syncCheckboxes(text);
|
||||
if (updates.length === 0) return;
|
||||
|
||||
const lines = text.split("\n");
|
||||
|
|
@ -49,4 +72,18 @@ export default class CheckboxSyncPlugin extends Plugin {
|
|||
const updatedText = lines.join("\n");
|
||||
await this.app.vault.modify(file, updatedText);
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
this.checkboxUtils.updateSettings(this.settings);
|
||||
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
if (activeFile instanceof TFile && activeFile.extension === "md") {
|
||||
await this.syncFileCheckboxes(activeFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
src/types.ts
Normal file
3
src/types.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export interface CheckboxSyncPluginSettings {
|
||||
xOnlyMode: boolean;
|
||||
}
|
||||
Loading…
Reference in a new issue