init state

This commit is contained in:
Grol Grol 2025-06-05 11:56:36 +03:00
parent b0d58d6447
commit a7f33e676d
15 changed files with 617 additions and 10 deletions

20
package-lock.json generated
View file

@ -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",

View file

@ -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<CheckboxSyncPluginSettings>;
private propagateStateToChildrenProcces: PropagateStateToChildrenProcess;
private propagateStateToParentProcces: PropagateStateToParentProcess;
constructor(settings: Readonly<CheckboxSyncPluginSettings>) {
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();
}
}

View file

@ -0,0 +1,5 @@
import { Context } from "../model/Context";
export interface CheckboxProcess {
process(context: Context): void;
}

View file

@ -0,0 +1,3 @@
export interface ICheckboxUtils {
syncText(text: string, textBefore: string | undefined): string;
}

75
src/core/model/Context.ts Normal file
View file

@ -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<CheckboxSyncPluginSettings>;
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<CheckboxSyncPluginSettings>) {
this.text = text;
this.textBeforeChange = textBeforeChange;
this.settings = settings;
}
getSettings(): Readonly<CheckboxSyncPluginSettings> {
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();
}
}

View file

@ -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<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();
}
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<CheckboxSyncPluginSettings>): 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;
}
}

8
src/core/model/Line.ts Normal file
View file

@ -0,0 +1,8 @@
export interface Line {
getIndent(): number;
getText(): string;
toResultText(): string;
}

View file

@ -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");
}
}

27
src/core/model/View.ts Normal file
View file

@ -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");
}
}

View file

@ -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;
}

View file

@ -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<CheckboxSyncPluginSettings>;
constructor(indent: number, marker: string, checkChar: string, lineText: string, settings: Readonly<CheckboxSyncPluginSettings>) {
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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}