рабочая версия

This commit is contained in:
Grol Grol 2025-07-05 14:27:48 +03:00
parent a7f33e676d
commit 96727c61f5
25 changed files with 1496 additions and 307 deletions

1
.gitignore vendored
View file

@ -22,3 +22,4 @@ data.json
.DS_Store
coverage/
test-report.html

View file

@ -1,4 +1,4 @@
import { CheckboxUtils } from "src/checkboxUtils";
import { CheckboxUtils } from "src/core/checkboxUtils";
import { FileFilter } from "src/FileFilter";
import TextSyncPipeline from "src/TextSyncPipeline";
import { CheckboxSyncPluginSettings, DEFAULT_SETTINGS } from "src/types";

View file

@ -1,4 +1,4 @@
import { CheckboxLineInfo, CheckboxUtils } from '../src/checkboxUtils'; // Путь к вашему файлу
import { CheckboxLineInfo, CheckboxUtils } from '../src/core/checkboxUtils'; // Путь к вашему файлу
import { CheckboxState, CheckboxSyncPluginSettings, DEFAULT_SETTINGS } from '../src/types'; // Путь к вашему файлу типов
// Вспомогательная функция для создания настроек с переопределениями

View file

@ -1,6 +1,6 @@
// checkboxUtils.plainLists.test.ts
import { CheckboxUtils } from '../src/checkboxUtils';
import { CheckboxUtils } from '../src/core/checkboxUtils';
import { CheckboxState, CheckboxSyncPluginSettings, DEFAULT_SETTINGS } from '../src/types';
// Вспомогательная функция для создания настроек с переопределениями

View file

@ -0,0 +1,312 @@
// import { CheckboxLineInfo, CheckboxUtils } from '../src/checkboxUtils'; // Путь к вашему файлу
import { CheckboxUtils2 } from 'src/core/CheckboxUtils2';
import { CheckboxSyncPluginSettings, DEFAULT_SETTINGS } from 'src/types'; // Путь к вашему файлу типов
// Вспомогательная функция для создания настроек с переопределениями
const createSettings = (overrides: Partial<CheckboxSyncPluginSettings> = {}): Readonly<CheckboxSyncPluginSettings> => {
return { ...DEFAULT_SETTINGS, ...overrides } as Readonly<CheckboxSyncPluginSettings>;
};
describe('CheckboxUtils2', () => {
// --- Тесты для syncText ---
describe('syncText', () => {
it('should propagate down only if child sync enabled and diff matches', () => {
const utils = new CheckboxUtils2(createSettings({
enableAutomaticChildState: true,
enableAutomaticParentState: false, // Parent sync off
}));
const textBefore = [
'- [ ] Parent',
' - [ ] Child',
].join('\n');
const textAfterParentChecked = [ // Simulate user checking parent
'- [x] Parent',
' - [ ] Child',
].join('\n');
const expected = [
'- [x] Parent',
' - [x] Child', // Propagated down
].join('\n');
expect(utils.syncText(textAfterParentChecked, textBefore)).toBe(expected);
});
it('should propagate up only if parent sync enabled', () => {
const utils = new CheckboxUtils2(createSettings({
enableAutomaticChildState: false, // Child sync off
enableAutomaticParentState: true,
}));
const textBefore = [
'- [ ] Parent',
' - [ ] Child',
].join('\n');
const textAfterChildChecked = [ // Simulate user checking child
'- [ ] Parent',
' - [x] Child',
].join('\n');
const expected = [
'- [x] Parent', // Propagated up
' - [x] Child',
].join('\n');
// syncText calls propagateFromChildren unconditionally if enabled
expect(utils.syncText(textAfterChildChecked, textBefore)).toBe(expected);
});
it('should propagate down then up if both enabled', () => {
const utils = new CheckboxUtils2(createSettings({
enableAutomaticChildState: true,
enableAutomaticParentState: true,
}));
const textBefore = [
'- [ ] Parent',
' - [ ] Child',
].join('\n');
const textAfterParentChecked = [ // Simulate user checking parent
'- [x] Parent',
' - [ ] Child',
].join('\n');
// 1. User checks parent: '- [x] Parent', ' - [ ] Child'
// 2. Propagate down: '- [x] Parent', ' - [x] Child'
// 3. Propagate up: (no change needed as parent is already checked)
const expectedDown = [
'- [x] Parent',
' - [x] Child', // Propagated down
].join('\n');
expect(utils.syncText(textAfterParentChecked, textBefore)).toBe(expectedDown);
const textAfterChildChecked = [ // Simulate user checking child
'- [ ] Parent',
' - [x] Child',
].join('\n');
// 1. User checks child: '- [ ] Parent', ' - [x] Child'
// 2. Propagate down: (no change, as only one line changed, and it wasn't the parent)
// 3. Propagate up: '- [x] Parent', ' - [x] Child'
const expectedUp = [
'- [x] Parent', // Propagated up
' - [x] Child',
].join('\n');
expect(utils.syncText(textAfterChildChecked, textBefore)).toBe(expectedUp);
});
it('should do nothing if both syncs disabled', () => {
const utils = new CheckboxUtils2(createSettings({
enableAutomaticChildState: false,
enableAutomaticParentState: false,
}));
const textBefore = [
'- [ ] Parent',
' - [ ] Child',
].join('\n');
const textAfterParentChecked = [ // Simulate user checking parent
'- [x] Parent',
' - [ ] Child',
].join('\n');
const textAfterChildChecked = [ // Simulate user checking child
'- [ ] Parent',
' - [x] Child',
].join('\n');
expect(utils.syncText(textAfterParentChecked, textBefore)).toBe(textAfterParentChecked);
expect(utils.syncText(textAfterChildChecked, textBefore)).toBe(textAfterChildChecked);
});
it('should not propagate down if diff is not a single checkbox state change', () => {
const utils = new CheckboxUtils2(createSettings({
enableAutomaticChildState: true, // Down propagation enabled
enableAutomaticParentState: false,
}));
const textBefore = [
'- [ ] Parent',
' - [ ] Child',
].join('\n');
const textMultipleChanges = [
'- [x] Parent Changed', // Text changed too
' - [x] Child also changed',
].join('\n');
const textIndentChange = [
' - [x] Parent', // Indent changed
' - [ ] Child',
].join('\n');
// Multiple lines changed
expect(utils.syncText(textMultipleChanges, textBefore)).toBe(textMultipleChanges);
// Indent changed (diff > 1 line index)
expect(utils.syncText(textIndentChange, textBefore)).toBe(textIndentChange);
});
it('equals texts', () => {
const utils = new CheckboxUtils2(createSettings({
enableAutomaticChildState: true, // Down propagation enabled
enableAutomaticParentState: true,
}));
const textBefore = [
'- [ ] Parent',
' - [ ] Child',
].join('\n');
// Multiple lines changed
expect(utils.syncText(textBefore, textBefore)).toBe(textBefore);
});
it('checkbox, list', ()=> {
const utils = new CheckboxUtils2(createSettings({
enableAutomaticChildState: true,
enableAutomaticParentState: true,
}));
const textBefore = [
'- [ ] Parent',
' - Child1',
' - [ ] Child2',
].join('\n');
const actualText = [
'- [ ] Parent',
' - Child1',
' - [x] Child2',
].join('\n');
const expectedText = [
'- [x] Parent',
' - Child1',
' - [x] Child2',
].join('\n');
// Multiple lines changed
expect(utils.syncText(actualText, textBefore)).toBe(expectedText);
});
it('checkbox, plain text', ()=> {
const utils = new CheckboxUtils2(createSettings({
enableAutomaticChildState: true,
enableAutomaticParentState: true,
}));
const textBefore = [
'- [ ] Parent',
' Child1',
' - [ ] Child2',
].join('\n');
const actualText = [
'- [ ] Parent',
' Child1',
' - [x] Child2',
].join('\n');
const expectedText = [
'- [x] Parent',
' Child1',
' - [x] Child2',
].join('\n');
// Multiple lines changed
expect(utils.syncText(actualText, textBefore)).toBe(expectedText);
});
describe('without textBefore', () => {
it('done from child to parent', () => {
const utils = new CheckboxUtils2(createSettings({
enableAutomaticChildState: true,
enableAutomaticParentState: true,
}));
const actualText = [
'- [ ] Parent',
' - [x] Child',
].join('\n');
const expected = [
'- [x] Parent',
' - [x] Child',
].join('\n');
expect(utils.syncText(actualText, undefined)).toBe(expected);
});
it('todo from child to parent', () => {
const utils = new CheckboxUtils2(createSettings({
enableAutomaticChildState: true,
enableAutomaticParentState: true,
}));
const actualText = [
'- [x] Parent',
' - [ ] Child',
].join('\n');
const expected = [
'- [ ] Parent',
' - [ ] Child',
].join('\n');
expect(utils.syncText(actualText, undefined)).toBe(expected);
});
});
describe('many roots', () => {
it('two roots witout text before', () => {
const utils = new CheckboxUtils2(createSettings({
enableAutomaticChildState: true,
enableAutomaticParentState: true,
}));
const actualText = [
'- [x] Parent1',
' - [ ] Child1',
'- [ ] Parent2',
' - [x] Child2',
].join('\n');
const expected = [
'- [ ] Parent1',
' - [ ] Child1',
'- [x] Parent2',
' - [x] Child2',
].join('\n');
expect(utils.syncText(actualText, undefined)).toBe(expected);
});
it('two roots', () => {
const utils = new CheckboxUtils2(createSettings({
enableAutomaticChildState: true,
enableAutomaticParentState: true,
}));
const textBefore = [
'- [x] Parent1',
' - [x] Child1',
'- [ ] Parent2',
' - [ ] Child2',
].join('\n');
const actualText = [
'- [x] Parent1',
' - [ ] Child1',
'- [ ] Parent2',
' - [x] Child2',
].join('\n');
const expected = [
'- [ ] Parent1',
' - [ ] Child1',
'- [x] Parent2',
' - [x] Child2',
].join('\n');
expect(utils.syncText(actualText, textBefore)).toBe(expected);
});
it('all type roots', () => {
const utils = new CheckboxUtils2(createSettings({
enableAutomaticChildState: true,
enableAutomaticParentState: true,
}));
const textBefore = [
'- [x] Parent1',
' - [x] Child1',
'- Parent2',
' - Child2',
'Parent3',
' Child3',
].join('\n');
const actualText = [
'- [x] Parent1',
' - [ ] Child1',
'- Parent2',
' - Child2',
'Parent3',
' Child3',
].join('\n');
const expected = [
'- [ ] Parent1',
' - [ ] Child1',
'- Parent2',
' - Child2',
'Parent3',
' Child3',
].join('\n');
expect(utils.syncText(actualText, textBefore)).toBe(expected);
});
});
});
});

View file

@ -8,13 +8,13 @@ export class InMemoryFilePathStateHolder implements IFilePathStateHolder {
}
public setByPath(filePath: string, text: string): void {
console.log(`InMemoryFileStateHolder: Setting state for "${filePath}"`);
// console.log(`InMemoryFileStateHolder: Setting state for "${filePath}"`);
this.states.set(filePath, text);
}
public getByPath(filePath: string): string | undefined {
const state = this.states.get(filePath);
console.log(`InMemoryFileStateHolder: Getting state for "${filePath}". Found: ${!!state}`);
// console.log(`InMemoryFileStateHolder: Getting state for "${filePath}". Found: ${!!state}`);
return state;
}

View file

@ -7,6 +7,14 @@ import type {Config} from 'jest';
const config: Config = {
reporters: [
'default',
['jest-html-reporter', {
pageTitle: 'Test Report',
outputPath: './test-report.html',
}]
],
// Automatically clear mock calls, instances, contexts and results before every test
clearMocks: true,

1024
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -22,6 +22,7 @@
"builtin-modules": "3.3.0",
"esbuild": "^0.25.0",
"jest": "^29.7.0",
"jest-html-reporter": "^4.3.0",
"obsidian": "^1.7.2",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",

View file

@ -1,18 +1,15 @@
import { Mutex } from "async-mutex";
import { Editor, EditorChange, MarkdownFileInfo, MarkdownView, TFile, Vault } from "obsidian";
import { CheckboxUtils } from "./checkboxUtils";
import TextSyncPipeline from "./TextSyncPipeline";
export default class SyncController {
private vault: Vault;
private checkboxUtils: CheckboxUtils;
textSyncPipeline: TextSyncPipeline
private mutex: Mutex;
constructor(vault: Vault, checkboxUtils: CheckboxUtils, textSyncPipeline: TextSyncPipeline) {
constructor(vault: Vault, textSyncPipeline: TextSyncPipeline) {
this.vault = vault;
this.checkboxUtils = checkboxUtils;
this.textSyncPipeline = textSyncPipeline;
this.mutex = new Mutex();
}
@ -47,7 +44,7 @@ export default class SyncController {
const newLines = newText.split("\n");
const oldLines = oldText.split("\n");
const diffIndexes = this.checkboxUtils.findDifferentLineIndexes(oldLines, newLines);
const diffIndexes = this.findDifferentLineIndexes(oldLines, newLines);
const changes: EditorChange[] = [];
@ -73,4 +70,19 @@ export default class SyncController {
}
}
private findDifferentLineIndexes(lines1: string[], lines2: string[]): number[] {
if (lines1.length !== lines2.length) {
throw new Error("the length of the lines must be equal");
}
const length = lines1.length;
const result: number[] = [];
for (let i = 0; i < length; i++) {
if (lines1[i] !== lines2[i]) {
result.push(i);
}
}
return result;
}
}

View file

@ -1,14 +1,14 @@
import { CheckboxUtils } from "./checkboxUtils";
import { ICheckboxUtils } from "./core/interface/ICheckboxUtils";
import { FileFilter } from "./FileFilter";
import { IFilePathStateHolder } from "./IFilePathStateHolder";
export default class TextSyncPipeline {
private checkboxUtils: CheckboxUtils;
private checkboxUtils: ICheckboxUtils;
private fileStateHolder: IFilePathStateHolder;
private fileFilter: FileFilter;
constructor(checkboxUtils: CheckboxUtils, fileStateHolder: IFilePathStateHolder, fileFilter: FileFilter) {
constructor(checkboxUtils: ICheckboxUtils, fileStateHolder: IFilePathStateHolder, fileFilter: FileFilter) {
this.checkboxUtils = checkboxUtils;
this.fileStateHolder = fileStateHolder;
this.fileFilter = fileFilter;

View file

@ -1,26 +1,18 @@
import { CheckboxState, CheckboxSyncPluginSettings } from "src/types";
import { CheckboxSyncPluginSettings } from "src/types";
import { ICheckboxUtils } from "./interface/ICheckboxUtils";
import { ContextFactory } from "./model/ContextFactory";
import { PropagateStateToChildrenProcess } from "./process/PropagateStateToChildrenProcess";
import { PropagateStateToParentProcess } from "./process/PropagateStateToParentProcess";
export interface CheckboxLineInfo {
indent: number;
marker: string;
checkChar?: string;
checkboxCharPosition?: number;
checkboxState: CheckboxState;
isChecked?: boolean | undefined;
listItemText?: string;
}
export class CheckboxUtils2 implements ICheckboxUtils {
private settings: Readonly<CheckboxSyncPluginSettings>;
private propagateStateToChildrenProcces: PropagateStateToChildrenProcess;
private propagateStateToParentProcces: PropagateStateToParentProcess;
private propagateStateToChildrenProcess: PropagateStateToChildrenProcess;
private propagateStateToParentProcess: PropagateStateToParentProcess;
constructor(settings: Readonly<CheckboxSyncPluginSettings>) {
this.settings = settings;
this.propagateStateToChildrenProcess = new PropagateStateToChildrenProcess();
this.propagateStateToParentProcess = new PropagateStateToParentProcess();
}
syncText(text: string, textBefore: string | undefined): string {
@ -29,8 +21,8 @@ export class CheckboxUtils2 implements ICheckboxUtils {
}
const context = ContextFactory.createContext(text, textBefore, this.settings);
this.propagateStateToChildrenProcces.process(context);
this.propagateStateToParentProcces.process(context);
this.propagateStateToChildrenProcess.process(context);
this.propagateStateToParentProcess.process(context);
return context.getResultText();
}

View file

@ -1,4 +1,6 @@
import { CheckboxState, CheckboxSyncPluginSettings } from "./types";
import { CheckboxState, CheckboxSyncPluginSettings } from "src/types";
import { ICheckboxUtils } from "./interface/ICheckboxUtils";
export interface CheckboxLineInfo {
indent: number;
@ -10,7 +12,7 @@ export interface CheckboxLineInfo {
listItemText?: string;
}
export class CheckboxUtils {
export class CheckboxUtils implements ICheckboxUtils {
private settings: Readonly<CheckboxSyncPluginSettings>;
constructor(settings: Readonly<CheckboxSyncPluginSettings>) {
this.settings = settings;

View file

@ -3,66 +3,42 @@ import { Context } from "./Context";
import { TreeNode } from "./TreeNode";
import { Line } from "./Line";
import { View } from "./View";
import { CheckboxLine } from "./line/CheckboxLine";
import { ListLine } from "./line/ListLine";
import { TextLine } from "./line/TextLine";
export class ContextFactory {
static readonly CHECKBOX_REGEXP = /^(\s*)([*+-]|\d+\.) \[(.)\]\s(.*)$/;
static readonly LIST_ITEM_REGEXP = /^(\s*)([*+-]|\d+\.)\s+(?!\[.?\])(.*)$/;
static readonly TEXT_REGEXP = /^(\s*)(.*)$/;
private constructor() {
}
public static createContext(text: string, textBefore: string | undefined, settings: Readonly<CheckboxSyncPluginSettings>): Context {
static createContext(text: string, textBefore: string | undefined, settings: Readonly<CheckboxSyncPluginSettings>): Context {
return new Context(text, textBefore, settings);
}
static createLines(context: Context): Line[] {
const settings = context.getSettings();
const textLines = context.getTextLines();
const lines = this.createLinesFromTextLines(context.getTextLines(), settings);
if (context.textBeforeChangeIsPresent()) {
const textBeforeLines = context.getTextBeforeChangeLines()!;
if (textLines.length === textBeforeLines.length) {
const diffIndex = this.findSingleDiffLineIndex(textLines, textBeforeLines);
if (diffIndex) {
const oldLine = new Line(textBeforeLines[diffIndex], settings);
const actualLine = lines[diffIndex];
// если новая и старая строка чекбокс
// если текст остался старым
// если изменилось состояние чекбокса
if (oldLine.isCheckbox() &&
actualLine.isCheckbox() &&
oldLine.getText() === actualLine.getText() &&
oldLine.getState() !== actualLine.getState()
) {
actualLine.setChange(true);
}
}
}
}
return lines;
}
static createView(context: Context) {
const settings = context.getSettings();
const lines = context.getLines();
// построить дерево
const treeNodes = this.createNodesFromLines(lines);
// проверить ноды на modify
let isModify = false;
for (const treeNode of treeNodes) {
isModify = isModify || treeNode.isModify();
const treeNodes = this.createRootTreeNodesFromLines(lines);
return new View(treeNodes);
}
return new View(treeNodes, isModify);
// создаёт TreeNode из Line
// возвращает только корневые узлы
private static createRootTreeNodesFromLines(lines: Line[]): TreeNode[] {
const flatTreeNodes = this.createFlatNodesFromLines(lines);
return flatTreeNodes.filter(node => !node.getParent());
}
private static createNodesFromLines(textLines: Line[]): TreeNode[] {
// создаёт TreeNode из Line
// осторожно, возвращает ВСЕ ноды в порядке линий
private static createFlatNodesFromLines(lines: Line[]): TreeNode[] {
const nodes: TreeNode[] = [];
const stack: TreeNode[] = [];
for (let index = 0; index < textLines.length; index++) {
const line = textLines[index];
for (const line of lines) {
const node = new TreeNode(line);
// найти родителя
@ -86,15 +62,83 @@ export class ContextFactory {
return nodes;
}
static createLines(context: Context): Line[] {
const settings = context.getSettings();
const lines = this.createLinesFromTextLines(context.getTextLines(), settings);
this.markChangeIfNeeded(lines, context);
return lines;
}
// помечает line как изменённую при необходимости
static markChangeIfNeeded(lines: Line[], context: Context) {
if (!context.textBeforeChangeIsPresent()) {
return;
}
const textLines = context.getTextLines();
const textBeforeLines = context.getTextBeforeChangeLines()!;
if (textLines.length !== textBeforeLines.length) {
return;
}
const diffIndex = this.findSingleDiffLineIndex(textLines, textBeforeLines);
if (diffIndex === undefined) {
return;
}
const oldLine = this.createLineFromTextLine(textBeforeLines[diffIndex], context.getSettings());
const actualLine = lines[diffIndex];
// если новая и старая строка чекбокс
if (oldLine instanceof CheckboxLine && actualLine instanceof CheckboxLine) {
// если текст остался старым
// если изменилось состояние чекбокса
if (
oldLine.getText() === actualLine.getText() &&
oldLine.getState() !== actualLine.getState()
) {
actualLine.setChange(true);
}
}
}
// создаёт Line[] из текста
private static createLinesFromTextLines(textLines: string[], settings: Readonly<CheckboxSyncPluginSettings>): Line[] {
return textLines.map(line => {
return new Line(line, settings);
return this.createLineFromTextLine(line, settings);
});
}
// создаёт объект из одного из классов, кто реализует Line в зависимости от строки.
static createLineFromTextLine(textLine: string, settings: Readonly<CheckboxSyncPluginSettings>): Line {
const checkboxMatch = textLine.match(this.CHECKBOX_REGEXP);
if (checkboxMatch) {
const indentString = checkboxMatch[1];
const marker = checkboxMatch[2];
const checkChar = checkboxMatch[3];
const listItemText = checkboxMatch[4].trimStart();
return new CheckboxLine(indentString, marker, checkChar, listItemText, settings);
}
const listItemMatch = textLine.match(this.LIST_ITEM_REGEXP);
if (listItemMatch) {
const indentString = listItemMatch[1];
const marker = listItemMatch[2];
const listItemText = listItemMatch[3].trimStart();
return new ListLine(indentString, marker, listItemText, settings.tabSize);
}
const textLineMatch = textLine.match(this.TEXT_REGEXP)!;
const indentString = textLineMatch[1];
const itemText = textLineMatch[2];
return new TextLine(indentString, itemText, settings.tabSize);
}
private static findSingleDiffLineIndex(lines1: string[], lines2: string[]): number | undefined {
const diffLines = this.findDifferentLineIndexes(lines1, lines2);
if (diffLines.length == 1) {

View file

@ -1,3 +1,5 @@
import { CheckboxState } from "src/types";
export interface Line {
getIndent(): number;
@ -5,4 +7,6 @@ export interface Line {
toResultText(): string;
getState(): CheckboxState;
}

View file

@ -1,15 +1,17 @@
import { Line } from "./Line";
import { CheckboxLine } from "./line/CheckboxLine";
export class TreeNode {
private parent?: TreeNode;
private childrens: TreeNode[];
private childrens: TreeNode[] = [];
private line: Line;
// метка того, что Единичное изменение произошло в этой ноде или дочерних нодах
private modify = false;
constructor(line: Line) {
this.line = line;
if (line.isChange()) {
if (line instanceof CheckboxLine && line.isChange()) {
this.modify = true;
}
}

View file

@ -2,10 +2,16 @@ import { TreeNode } from "./TreeNode";
export class View {
private treeNodes: TreeNode[];
// метка того, что Единичное изменение произошло в этой View
private modify: boolean;
constructor(treeNodes: TreeNode[], isModify: boolean) {
constructor(treeNodes: TreeNode[]) {
this.treeNodes = treeNodes;
// проверить ноды на modify
let isModify = false;
for (const treeNode of treeNodes) {
isModify = isModify || treeNode.isModify();
}
this.modify = isModify;
}

View file

@ -1,16 +1,33 @@
import { CheckboxState } from "src/types";
import { Line } from "../Line";
export abstract class AbstractLine implements Line {
protected indentString: string;
protected tabSize: number;
// длина отступа
protected indent: number;
// текст после чекбокса
protected listText: string;
constructor(indent:number, lineText: string){
this.indent = indent;
constructor(indentString: string, lineText: string, tabSize: number) {
this.indentString = indentString;
this.listText = lineText;
this.tabSize = tabSize;
this.indent = this.getIndentFromString(indentString, tabSize);
}
private getIndentFromString(indentString: string, tabSize: number): number {
let indent = 0;
for (const char of indentString) {
if (char === '\t') {
indent += tabSize;
} else if (char === ' ') {
indent += 1;
}
}
return indent;
}
getIndent(): number {
@ -21,9 +38,7 @@ export abstract class AbstractLine implements Line {
return this.listText;
}
setText(lineText: string){
this.listText = lineText;
}
abstract toResultText(): string;
abstract getState(): CheckboxState;
}

View file

@ -8,26 +8,25 @@ export class CheckboxLine extends AbstractLine {
// символ в чекбоксе
private checkChar: string;
// позиция символа чекбокса в исходной строке
private checkboxCharPosition: number;
// private checkboxCharPosition: number;
// интерпретация символа чекбокса(или его отсутствия)
private checkboxState: CheckboxState;
// интерпретация состояния чекбокса(или его отсутствия)
// private isChecked?: boolean | undefined;
// метка того, что Единичное изменение произошло в этой строке
private hasChange = false;
protected settings: Readonly<CheckboxSyncPluginSettings>;
constructor(indent: number, marker: string, checkChar: string, lineText: string, settings: Readonly<CheckboxSyncPluginSettings>) {
super(indent, lineText);
constructor(indentString:string, marker: string, checkChar: string, listItemText: string, settings: Readonly<CheckboxSyncPluginSettings>) {
super(indentString, listItemText, settings.tabSize);
this.marker = marker;
this.checkChar = checkChar;
this.settings = settings;
this.checkboxCharPosition = ;
this.checkboxState = ;
throw new Error("Constructor not implemented.");
// this.checkboxCharPosition = indent + marker.length + 2;
this.checkboxState = this.getCheckboxState(checkChar);
}
isChange(): boolean {
@ -39,35 +38,11 @@ export class CheckboxLine extends AbstractLine {
}
isCheckbox(): boolean {
return this.checkboxState !== CheckboxState.NoCheckbox;
}
setState(state: CheckboxState): void {
// обновить checkboxState
this.checkboxState = state;
// обновить checkChar
switch (state) {
case CheckboxState.Checked:
this.checkChar = this.settings.checkedSymbols.length > 0 ? this.settings.checkedSymbols[0] : 'x'; // 'x' как дефолт
break;
case CheckboxState.Unchecked:
this.checkChar = this.settings.uncheckedSymbols.length > 0 ? this.settings.uncheckedSymbols[0] : ' '; // ' ' как дефолт
break;
case CheckboxState.Ignore:
if (this.settings.ignoreSymbols.length > 0) {
this.checkChar = this.settings.ignoreSymbols[0];
} else {
throw new Error("Not found ignore char.");
}
break;
case CheckboxState.NoCheckbox:
this.checkChar = undefined;
break;
default:
throw new Error(`Unexpected value for parameter [state] = ${state}.`);
}
this.checkChar = this.getCharFromState(state);
}
setStateIfNotEquals(newState: CheckboxState) {
@ -80,5 +55,42 @@ export class CheckboxLine extends AbstractLine {
return this.checkboxState;
}
toResultText(): string {
// const spaces = ' '.repeat(this.indent);
const resultText = this.indentString + this.marker + " [" + this.checkChar + "] " + this.listText;
return resultText;
}
private getCheckboxState(text: string): CheckboxState {
if (this.settings.checkedSymbols.includes(text)) {
return CheckboxState.Checked;
}
if (this.settings.uncheckedSymbols.includes(text)) {
return CheckboxState.Unchecked;
}
if (this.settings.ignoreSymbols.includes(text)) {
return CheckboxState.Ignore;
}
return this.settings.unknownSymbolPolicy;
}
private getCharFromState(state: CheckboxState): string {
switch (state) {
case CheckboxState.Checked:
return this.settings.checkedSymbols.length > 0 ? this.settings.checkedSymbols[0] : 'x'; // 'x' как дефолт
case CheckboxState.Unchecked:
return this.settings.uncheckedSymbols.length > 0 ? this.settings.uncheckedSymbols[0] : ' '; // ' ' как дефолт
case CheckboxState.Ignore:
if (this.settings.ignoreSymbols.length > 0) {
return this.settings.ignoreSymbols[0];
} else {
throw new Error("Not found ignore char.");
}
case CheckboxState.NoCheckbox:
throw new Error(`Unexpected value for parameter [state] = ${state}. "NoCheckbox".`);
default:
throw new Error(`Unexpected value for parameter [state] = ${state}.`);
}
}
}

View file

@ -1,3 +1,4 @@
import { CheckboxState } from "src/types";
import { AbstractLine } from "./AbstractLine";
export class ListLine extends AbstractLine {
@ -5,8 +6,8 @@ export class ListLine extends AbstractLine {
// символы перед текстом
private marker: string;
constructor(indent: number, marker: string, listText: string) {
super(indent, listText);
constructor(indentString: string, marker: string, listText: string, tabSize: number) {
super(indentString, listText, tabSize);
this.marker = marker;
}
@ -19,9 +20,12 @@ export class ListLine extends AbstractLine {
}
toResultText(): string {
const spaces = ' '.repeat(this.indent);
const resultText = spaces + this.marker + " " + this.listText;
// const spaces = ' '.repeat(this.indent);
const resultText = this.indentString + this.marker + " " + this.listText;
return resultText;
}
getState(): CheckboxState {
return CheckboxState.NoCheckbox;
}
}

View file

@ -1,9 +1,14 @@
import { CheckboxState } from "src/types";
import { AbstractLine } from "./AbstractLine";
export class TextLine extends AbstractLine {
toResultText(): string {
const spaces = ' '.repeat(this.indent);
const resultText = spaces + this.listText;
// const spaces = ' '.repeat(this.indent);
const resultText = this.indentString + this.listText;
return resultText;
}
getState(): CheckboxState {
return CheckboxState.NoCheckbox;
}
}

View file

@ -2,6 +2,7 @@ import { CheckboxState } from "src/types";
import { CheckboxProcess } from "../interface/CheckboxProcess";
import { Context } from "../model/Context";
import { TreeNode } from "../model/TreeNode";
import { CheckboxLine } from "../model/line/CheckboxLine";
export class PropagateStateToChildrenProcess implements CheckboxProcess {
@ -21,46 +22,51 @@ export class PropagateStateToChildrenProcess implements CheckboxProcess {
const nodes = view.getTreeNodes();
for (const node of nodes) {
this.propageteStateToChildrenFromChangeLineNode(node);
this.propagateStateToChildrenFromChangeLineNode(node);
}
}
// находит изменённую Line, если она существует и вызывает от неё propagateStateToChildren
propageteStateToChildrenFromChangeLineNode(node: TreeNode) {
propagateStateToChildrenFromChangeLineNode(node: TreeNode) {
if (!node.isModify()) {
return;
}
const line = node.getLine();
// если line изменён
if (line.isChange()) {
if (line instanceof CheckboxLine && line.isChange()) {
// значит мы нашли нужную ноду
this.propagateStateToChildren(node);
this.propagateStateToChildrenFromNode(node);
} else {
// иначе ищем вглубь среди детей
const childrens = node.getChildrenNodes();
for (const children of childrens) {
this.propageteStateToChildrenFromChangeLineNode(children);
this.propagateStateToChildrenFromChangeLineNode(children);
}
}
}
propagateStateToChildren(modifiedNode: TreeNode) {
const state = modifiedNode.getLine().getState();
propagateStateToChildrenFromNode(modifiedNode: TreeNode) {
const line = modifiedNode.getLine();
if (!(line instanceof CheckboxLine)) {
console.warn(`propagateStateToChildren from !CheckboxLine.`);
return;
}
const state = line.getState();
if (state === CheckboxState.Ignore || state === CheckboxState.NoCheckbox) {
console.warn(`propagateStateToChildren ${state.toString()}`);
return;
}
this.propagateToChildren(modifiedNode, state);
this.propagateStateToChildren(modifiedNode, state);
}
propagateToChildren(node: TreeNode, state: CheckboxState) {
propagateStateToChildren(node: TreeNode, state: CheckboxState) {
const line = node.getLine();
if (line.isCheckbox()) {
if (line instanceof CheckboxLine) {
line.setStateIfNotEquals(state);
}
for (const childrenNode of node.getChildrenNodes()) {
this.propagateToChildren(childrenNode, state);
this.propagateStateToChildren(childrenNode, state);
}
}
}

View file

@ -2,6 +2,7 @@ import { CheckboxState } from "src/types";
import { CheckboxProcess } from "../interface/CheckboxProcess";
import { Context } from "../model/Context";
import { TreeNode } from "../model/TreeNode";
import { CheckboxLine } from "../model/line/CheckboxLine";
export class PropagateStateToParentProcess implements CheckboxProcess {
@ -16,24 +17,42 @@ export class PropagateStateToParentProcess implements CheckboxProcess {
const nodes = view.getTreeNodes();
for (const node of nodes) {
this.propagateStateFromChildrens(node);
this.propagateStateToParent(node);
}
}
// возврат значения нужен для того, чтобы правильно обрабатывать случаи с "не чекбоксам", чтобы они передавали значения детей
propagateStateFromChildrens(node: TreeNode): CheckboxState {
propagateStateToParent(node: TreeNode): CheckboxState {
const line = node.getLine();
if (!node.hasChildren()) {
return line.getState();
// обходим всех детей
const childrenStates: CheckboxState[] = [];
for (const childrenNode of node.getChildrenNodes()) {
const childrenState = this.propagateStateToParent(childrenNode);
childrenStates.push(childrenState);
}
const newState = this.getNewState(line.getState(), childrenStates);
if (line instanceof CheckboxLine){
line.setStateIfNotEquals(newState);
}
return newState;
}
getNewState(actualState: CheckboxState, childrenStates: CheckboxState[]): CheckboxState {
if (actualState === CheckboxState.Ignore) {
return actualState;
}
// обходим всех детей
// если есть релевантные дети, то помечаем это и обновляем "состояние" Line
// "состояние" - потому что даже не чекбоксы могут иметь состояние через своих детей
let resultIfHasRelevantChildren = CheckboxState.Checked;
let hasRelevantChildren = false;
for (const childrenNode of node.getChildrenNodes()) {
const childrenState = this.propagateStateFromChildrens(childrenNode);
if (childrenState === CheckboxState.Checked || childrenState === CheckboxState.Unchecked) {
for (const childrenState of childrenStates) {
if (childrenState !== CheckboxState.Ignore && childrenState !== CheckboxState.NoCheckbox) {
hasRelevantChildren = true;
}
@ -41,17 +60,12 @@ export class PropagateStateToParentProcess implements CheckboxProcess {
resultIfHasRelevantChildren = CheckboxState.Unchecked;
}
}
// делаем эту проверку только после обработки детей
if (line.getState() == CheckboxState.Ignore) {
return line.getState();
}
// проверка для нод, все дети которых не релевантны
if (!hasRelevantChildren) {
return line.getState();
}
line.setStateIfNotEquals(resultIfHasRelevantChildren);
if (hasRelevantChildren) {
return resultIfHasRelevantChildren;
} else {
return actualState;
}
}
}

View file

@ -1,5 +1,4 @@
import { Plugin, TFile } from "obsidian";
import { CheckboxUtils } from "./checkboxUtils";
import { FileChangeEventHandler } from "./events/FileChangeEventHandler";
import { FileLoadEventHandler } from "./events/FileLoadEventHandler";
import FileStateHolder from "./FileStateHolder";
@ -8,6 +7,8 @@ import SyncController from "./SyncController";
import { CheckboxSyncPluginSettings, DEFAULT_SETTINGS } from "./types";
import { FileFilter } from "./FileFilter";
import TextSyncPipeline from "./TextSyncPipeline";
import { ICheckboxUtils } from "./core/interface/ICheckboxUtils";
import { CheckboxUtils2 } from "./core/CheckboxUtils2";
const DEBUG_FLAG_NAME = 'CHECKBOX_SYNC_DEBUG';
@ -23,7 +24,7 @@ export default class CheckboxSyncPlugin extends Plugin {
private _settings: CheckboxSyncPluginSettings;
private syncController: SyncController;
private checkboxUtils: CheckboxUtils;
private checkboxUtils: ICheckboxUtils;
private fileStateHolder: FileStateHolder;
private fileLoadEventHandler: FileLoadEventHandler;
private fileChangeEventHandler: FileChangeEventHandler;
@ -35,9 +36,9 @@ export default class CheckboxSyncPlugin extends Plugin {
this.fileFilter = new FileFilter(this.settings);
this.fileStateHolder = new FileStateHolder(this.app.vault);
this.checkboxUtils = new CheckboxUtils(this.settings);
this.checkboxUtils = new CheckboxUtils2(this.settings);
this.textSyncPipeline = new TextSyncPipeline(this.checkboxUtils,this.fileStateHolder, this.fileFilter);
this.syncController = new SyncController(this.app.vault, this.checkboxUtils, this.textSyncPipeline);
this.syncController = new SyncController(this.app.vault, this.textSyncPipeline);
this.fileLoadEventHandler = new FileLoadEventHandler(this, this.app, this.syncController, this.fileStateHolder);
this.fileChangeEventHandler = new FileChangeEventHandler(this, this.app, this.syncController, this.fileStateHolder);

View file

@ -1,4 +1,5 @@
export interface CheckboxSyncPluginSettings {
tabSize: number,
enableAutomaticParentState: boolean;
enableAutomaticChildState: boolean;
checkedSymbols: string[];
@ -18,6 +19,7 @@ export enum CheckboxState {
}
export const DEFAULT_SETTINGS: CheckboxSyncPluginSettings = {
tabSize: 4,
enableAutomaticParentState: true,
enableAutomaticChildState: true,
checkedSymbols: ['x'],