From a7f33e676de21c6021feac69891c2a5bf4c6194f Mon Sep 17 00:00:00 2001 From: Grol Grol Date: Thu, 5 Jun 2025 11:56:36 +0300 Subject: [PATCH] init state --- package-lock.json | 20 +-- src/core/CheckboxUtils2.ts | 37 ++++++ src/core/interface/CheckboxProcess.ts | 5 + src/core/interface/ICheckboxUtils.ts | 3 + src/core/model/Context.ts | 75 +++++++++++ src/core/model/ContextFactory.ts | 121 ++++++++++++++++++ src/core/model/Line.ts | 8 ++ src/core/model/TreeNode.ts | 59 +++++++++ src/core/model/View.ts | 27 ++++ src/core/model/line/AbstractLine.ts | 29 +++++ src/core/model/line/CheckboxLine.ts | 84 ++++++++++++ src/core/model/line/ListLine.ts | 27 ++++ src/core/model/line/TextLine.ts | 9 ++ .../PropagateStateToChildrenProcess.ts | 66 ++++++++++ .../process/PropagateStateToParentProcess.ts | 57 +++++++++ 15 files changed, 617 insertions(+), 10 deletions(-) create mode 100644 src/core/CheckboxUtils2.ts create mode 100644 src/core/interface/CheckboxProcess.ts create mode 100644 src/core/interface/ICheckboxUtils.ts create mode 100644 src/core/model/Context.ts create mode 100644 src/core/model/ContextFactory.ts create mode 100644 src/core/model/Line.ts create mode 100644 src/core/model/TreeNode.ts create mode 100644 src/core/model/View.ts create mode 100644 src/core/model/line/AbstractLine.ts create mode 100644 src/core/model/line/CheckboxLine.ts create mode 100644 src/core/model/line/ListLine.ts create mode 100644 src/core/model/line/TextLine.ts create mode 100644 src/core/process/PropagateStateToChildrenProcess.ts create mode 100644 src/core/process/PropagateStateToParentProcess.ts diff --git a/package-lock.json b/package-lock.json index 7c4c947..6d81217 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2741,16 +2741,6 @@ "node": ">=8" } }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -5673,6 +5663,16 @@ } } }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/tslib": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", diff --git a/src/core/CheckboxUtils2.ts b/src/core/CheckboxUtils2.ts new file mode 100644 index 0000000..1131a03 --- /dev/null +++ b/src/core/CheckboxUtils2.ts @@ -0,0 +1,37 @@ +import { CheckboxState, 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; + private propagateStateToChildrenProcces: PropagateStateToChildrenProcess; + private propagateStateToParentProcces: PropagateStateToParentProcess; + + constructor(settings: Readonly) { + this.settings = settings; + } + + syncText(text: string, textBefore: string | undefined): string { + if (text === textBefore) { + return text; + } + const context = ContextFactory.createContext(text, textBefore, this.settings); + + this.propagateStateToChildrenProcces.process(context); + this.propagateStateToParentProcces.process(context); + + return context.getResultText(); + } +} diff --git a/src/core/interface/CheckboxProcess.ts b/src/core/interface/CheckboxProcess.ts new file mode 100644 index 0000000..5523b83 --- /dev/null +++ b/src/core/interface/CheckboxProcess.ts @@ -0,0 +1,5 @@ +import { Context } from "../model/Context"; + +export interface CheckboxProcess { + process(context: Context): void; +} diff --git a/src/core/interface/ICheckboxUtils.ts b/src/core/interface/ICheckboxUtils.ts new file mode 100644 index 0000000..63ad24f --- /dev/null +++ b/src/core/interface/ICheckboxUtils.ts @@ -0,0 +1,3 @@ +export interface ICheckboxUtils { + syncText(text: string, textBefore: string | undefined): string; +} diff --git a/src/core/model/Context.ts b/src/core/model/Context.ts new file mode 100644 index 0000000..92141ce --- /dev/null +++ b/src/core/model/Context.ts @@ -0,0 +1,75 @@ +import { CheckboxSyncPluginSettings } from "src/types"; +import { View } from "./View"; +import { Line } from "./Line"; +import { ContextFactory } from "./ContextFactory"; + +export class Context { + + private settings: Readonly; + + private text: string; + private textLines?: string[]; + + private textBeforeChange?: string; + private textBeforeChangeLines?: string[]; + + private lines?: Line[]; + private view?: View; + + constructor(text: string, textBeforeChange: string | undefined, settings: Readonly) { + this.text = text; + this.textBeforeChange = textBeforeChange; + this.settings = settings; + } + + getSettings(): Readonly { + return this.settings; + } + + textBeforeChangeIsPresent(): boolean { + if (this.textBeforeChange) { + return true; + } + return false; + } + + getText() { + return this.text; + } + + getTextLines() { + if (!this.textLines) { + this.textLines = this.getText().split("\n"); + } + return this.textLines; + } + + getTextBeforeChange() { + return this.textBeforeChange; + } + + getTextBeforeChangeLines() { + if (!this.textBeforeChangeLines) { + this.textBeforeChangeLines = this.textBeforeChange?.split("\n"); + } + return this.textBeforeChangeLines; + } + + getLines() { + if (!this.lines) { + this.lines = ContextFactory.createLines(this); + } + return this.lines; + } + + getView(): View { + if (!this.view) { + this.view = ContextFactory.createView(this); + } + return this.view; + } + + getResultText(): string { + return this.getView().toResultText(); + } +} diff --git a/src/core/model/ContextFactory.ts b/src/core/model/ContextFactory.ts new file mode 100644 index 0000000..4aace2d --- /dev/null +++ b/src/core/model/ContextFactory.ts @@ -0,0 +1,121 @@ +import { CheckboxSyncPluginSettings } from "src/types"; +import { Context } from "./Context"; +import { TreeNode } from "./TreeNode"; +import { Line } from "./Line"; +import { View } from "./View"; + +export class ContextFactory { + + private constructor() { + } + + public static createContext(text: string, textBefore: string | undefined, settings: Readonly): 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(); + } + + return new View(treeNodes, isModify); + } + + private static createNodesFromLines(textLines: Line[]): TreeNode[] { + const nodes: TreeNode[] = []; + const stack: TreeNode[] = []; + for (let index = 0; index < textLines.length; index++) { + const line = textLines[index]; + const node = new TreeNode(line); + + // найти родителя + let parent = undefined; + while (stack.length > 0) { + const previousNode = stack[stack.length - 1]; + const previousLine = previousNode.getLine(); + if (previousLine.getIndent() < line.getIndent()) { + parent = previousNode; + break; + } else { + stack.pop(); + } + } + // добавить родителю, если есть + parent?.addChildren(node); + + nodes.push(node); + stack.push(node); + } + return nodes; + } + + + + // создаёт Line[] из текста + private static createLinesFromTextLines(textLines: string[], settings: Readonly): Line[] { + return textLines.map(line => { + return new Line(line, settings); + }); + } + + private static findSingleDiffLineIndex(lines1: string[], lines2: string[]): number | undefined { + const diffLines = this.findDifferentLineIndexes(lines1, lines2); + if (diffLines.length == 1) { + return diffLines[0]; + } + return undefined; + } + + private static 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; + } +} + diff --git a/src/core/model/Line.ts b/src/core/model/Line.ts new file mode 100644 index 0000000..dce5e2b --- /dev/null +++ b/src/core/model/Line.ts @@ -0,0 +1,8 @@ +export interface Line { + getIndent(): number; + + getText(): string; + + toResultText(): string; + +} \ No newline at end of file diff --git a/src/core/model/TreeNode.ts b/src/core/model/TreeNode.ts new file mode 100644 index 0000000..83fb4de --- /dev/null +++ b/src/core/model/TreeNode.ts @@ -0,0 +1,59 @@ +import { Line } from "./Line"; + +export class TreeNode { + private parent?: TreeNode; + private childrens: TreeNode[]; + + private line: Line; + private modify = false; + + constructor(line: Line) { + this.line = line; + if (line.isChange()) { + this.modify = true; + } + } + + getLine(): Line { + return this.line; + } + + isModify(): boolean { + return this.modify; + } + + private setParent(parent: TreeNode) { + this.parent = parent; + } + + getParent(): TreeNode | undefined { + return this.parent; + } + + addChildren(children: TreeNode) { + this.childrens.push(children); + children.setParent(this); + + this.modify = this.modify || children.isModify(); + } + + hasChildren(): boolean { + return this.childrens.length > 0; + } + + getChildrenNodes(): TreeNode[] { + return [...this.childrens]; + } + + toResultText(): string { + const parts: string[] = []; + + parts.push(this.line.toResultText()); + + for (const children of this.childrens) { + parts.push(children.toResultText()); + } + + return parts.join("\n"); + } +} diff --git a/src/core/model/View.ts b/src/core/model/View.ts new file mode 100644 index 0000000..3d9b203 --- /dev/null +++ b/src/core/model/View.ts @@ -0,0 +1,27 @@ +import { TreeNode } from "./TreeNode"; + +export class View { + private treeNodes: TreeNode[]; + private modify: boolean; + + constructor(treeNodes: TreeNode[], isModify: boolean) { + this.treeNodes = treeNodes; + this.modify = isModify; + } + + isModify() { + return this.modify; + } + + getTreeNodes(): TreeNode[] { + return this.treeNodes; + } + + toResultText(): string { + const parts: string[] = []; + for (const treeNode of this.treeNodes) { + parts.push(treeNode.toResultText()); + } + return parts.join("\n"); + } +} diff --git a/src/core/model/line/AbstractLine.ts b/src/core/model/line/AbstractLine.ts new file mode 100644 index 0000000..6b2a90f --- /dev/null +++ b/src/core/model/line/AbstractLine.ts @@ -0,0 +1,29 @@ +import { Line } from "../Line"; + +export abstract class AbstractLine implements Line { + + // длина отступа + protected indent: number; + // текст после чекбокса + protected listText: string; + + constructor(indent:number, lineText: string){ + this.indent = indent; + this.listText = lineText; + + } + + getIndent(): number { + return this.indent; + } + + getText(): string { + return this.listText; + } + + setText(lineText: string){ + this.listText = lineText; + } + + abstract toResultText(): string; +} \ No newline at end of file diff --git a/src/core/model/line/CheckboxLine.ts b/src/core/model/line/CheckboxLine.ts new file mode 100644 index 0000000..3d71851 --- /dev/null +++ b/src/core/model/line/CheckboxLine.ts @@ -0,0 +1,84 @@ +import { CheckboxState, CheckboxSyncPluginSettings } from "src/types"; +import { AbstractLine } from "./AbstractLine"; + +export class CheckboxLine extends AbstractLine { + + // символы перед чекбоксом + private marker: string; + // символ в чекбоксе + private checkChar: string; + // позиция символа чекбокса в исходной строке + private checkboxCharPosition: number; + // интерпретация символа чекбокса(или его отсутствия) + private checkboxState: CheckboxState; + // интерпретация состояния чекбокса(или его отсутствия) + // private isChecked?: boolean | undefined; + + + private hasChange = false; + + protected settings: Readonly; + + constructor(indent: number, marker: string, checkChar: string, lineText: string, settings: Readonly) { + super(indent, lineText); + this.marker = marker; + this.checkChar = checkChar; + this.settings = settings; + + this.checkboxCharPosition = ; + this.checkboxState = ; + throw new Error("Constructor not implemented."); + } + + isChange(): boolean { + return this.hasChange; + } + + setChange(hasChange: boolean) { + this.hasChange = hasChange; + } + + + + 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}.`); + } + } + + setStateIfNotEquals(newState: CheckboxState) { + if (newState !== this.checkboxState) { + this.setState(newState); + } + } + + getState(): CheckboxState { + return this.checkboxState; + } + + +} \ No newline at end of file diff --git a/src/core/model/line/ListLine.ts b/src/core/model/line/ListLine.ts new file mode 100644 index 0000000..66a05c8 --- /dev/null +++ b/src/core/model/line/ListLine.ts @@ -0,0 +1,27 @@ +import { AbstractLine } from "./AbstractLine"; + +export class ListLine extends AbstractLine { + + // символы перед текстом + private marker: string; + + constructor(indent: number, marker: string, listText: string) { + super(indent, listText); + this.marker = marker; + } + + getMarker(): string { + return this.marker; + } + + setMarker(marker: string) { + this.marker = marker; + } + + toResultText(): string { + const spaces = ' '.repeat(this.indent); + + const resultText = spaces + this.marker + " " + this.listText; + return resultText; + } +} \ No newline at end of file diff --git a/src/core/model/line/TextLine.ts b/src/core/model/line/TextLine.ts new file mode 100644 index 0000000..8d51a04 --- /dev/null +++ b/src/core/model/line/TextLine.ts @@ -0,0 +1,9 @@ +import { AbstractLine } from "./AbstractLine"; + +export class TextLine extends AbstractLine { + toResultText(): string { + const spaces = ' '.repeat(this.indent); + const resultText = spaces + this.listText; + return resultText; + } +} \ No newline at end of file diff --git a/src/core/process/PropagateStateToChildrenProcess.ts b/src/core/process/PropagateStateToChildrenProcess.ts new file mode 100644 index 0000000..fb3f93c --- /dev/null +++ b/src/core/process/PropagateStateToChildrenProcess.ts @@ -0,0 +1,66 @@ +import { CheckboxState } from "src/types"; +import { CheckboxProcess } from "../interface/CheckboxProcess"; +import { Context } from "../model/Context"; +import { TreeNode } from "../model/TreeNode"; + +export class PropagateStateToChildrenProcess implements CheckboxProcess { + + process(context: Context): void { + if (!context.getSettings().enableAutomaticChildState) { + return; + } + if (!context.textBeforeChangeIsPresent()) { + return; + } + // получить представление текста + const view = context.getView(); + + if (!view.isModify()) { + return; + } + + const nodes = view.getTreeNodes(); + for (const node of nodes) { + this.propageteStateToChildrenFromChangeLineNode(node); + } + } + + // находит изменённую Line, если она существует и вызывает от неё propagateStateToChildren + propageteStateToChildrenFromChangeLineNode(node: TreeNode) { + if (!node.isModify()) { + return; + } + const line = node.getLine(); + // если line изменён + if (line.isChange()) { + // значит мы нашли нужную ноду + this.propagateStateToChildren(node); + } else { + // иначе ищем вглубь среди детей + const childrens = node.getChildrenNodes(); + for (const children of childrens) { + this.propageteStateToChildrenFromChangeLineNode(children); + } + } + } + + + propagateStateToChildren(modifiedNode: TreeNode) { + const state = modifiedNode.getLine().getState(); + if (state === CheckboxState.Ignore || state === CheckboxState.NoCheckbox) { + console.warn(`propagateStateToChildren ${state.toString()}`); + return; + } + this.propagateToChildren(modifiedNode, state); + } + + propagateToChildren(node: TreeNode, state: CheckboxState) { + const line = node.getLine(); + if (line.isCheckbox()) { + line.setStateIfNotEquals(state); + } + for (const childrenNode of node.getChildrenNodes()) { + this.propagateToChildren(childrenNode, state); + } + } +} diff --git a/src/core/process/PropagateStateToParentProcess.ts b/src/core/process/PropagateStateToParentProcess.ts new file mode 100644 index 0000000..214a649 --- /dev/null +++ b/src/core/process/PropagateStateToParentProcess.ts @@ -0,0 +1,57 @@ +import { CheckboxState } from "src/types"; +import { CheckboxProcess } from "../interface/CheckboxProcess"; +import { Context } from "../model/Context"; +import { TreeNode } from "../model/TreeNode"; + +export class PropagateStateToParentProcess implements CheckboxProcess { + + process(context: Context): void { + if (!context.getSettings().enableAutomaticParentState) { + return; + } + // получить представление текста + const view = context.getView(); + + + const nodes = view.getTreeNodes(); + + for (const node of nodes) { + this.propagateStateFromChildrens(node); + } + } + + // возврат значения нужен для того, чтобы правильно обрабатывать случаи с "не чекбоксам", чтобы они передавали значения детей + propagateStateFromChildrens(node: TreeNode): CheckboxState { + const line = node.getLine(); + + if (!node.hasChildren()) { + return line.getState(); + } + + 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) { + hasRelevantChildren = true; + } + + if (childrenState === CheckboxState.Unchecked) { + resultIfHasRelevantChildren = CheckboxState.Unchecked; + } + } + // делаем эту проверку только после обработки детей + if (line.getState() == CheckboxState.Ignore) { + return line.getState(); + } + // проверка для нод, все дети которых не релевантны + if (!hasRelevantChildren) { + return line.getState(); + } + + line.setStateIfNotEquals(resultIfHasRelevantChildren); + + return resultIfHasRelevantChildren; + } +}