mirror of
https://github.com/groldsf/obsidian_check_plugin.git
synced 2026-07-22 05:37:48 +00:00
implement feature
This commit is contained in:
parent
b18acc10b1
commit
5115988918
6 changed files with 1531 additions and 954 deletions
932
__tests__/checkboxUtils.base.test.ts
Normal file
932
__tests__/checkboxUtils.base.test.ts
Normal file
|
|
@ -0,0 +1,932 @@
|
|||
import { CheckboxLineInfo, CheckboxUtils } from '../src/checkboxUtils'; // Путь к вашему файлу
|
||||
import { CheckboxState, CheckboxSyncPluginSettings, DEFAULT_SETTINGS } from '../src/types'; // Путь к вашему файлу типов
|
||||
|
||||
// Вспомогательная функция для создания настроек с переопределениями
|
||||
const createSettings = (overrides: Partial<CheckboxSyncPluginSettings> = {}): Readonly<CheckboxSyncPluginSettings> => {
|
||||
return { ...DEFAULT_SETTINGS, ...overrides } as Readonly<CheckboxSyncPluginSettings>;
|
||||
};
|
||||
|
||||
describe('CheckboxUtils', () => {
|
||||
let checkboxUtils: CheckboxUtils;
|
||||
let settings: Readonly<CheckboxSyncPluginSettings>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Используем настройки по умолчанию для большинства тестов
|
||||
settings = createSettings();
|
||||
checkboxUtils = new CheckboxUtils(settings);
|
||||
});
|
||||
|
||||
// --- Тесты для matchCheckboxLine ---
|
||||
describe('matchCheckboxLine', () => {
|
||||
it('should match standard unchecked checkbox', () => {
|
||||
const line = '- [ ] Task';
|
||||
const result = checkboxUtils.matchCheckboxLine(line);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toEqual({
|
||||
indent: 0,
|
||||
marker: '-',
|
||||
checkChar: ' ',
|
||||
checkboxCharPosition: 3,
|
||||
checkboxState: CheckboxState.Unchecked,
|
||||
isChecked: false,
|
||||
listItemText: "Task",
|
||||
});
|
||||
});
|
||||
|
||||
it('should match standard checked checkbox', () => {
|
||||
const line = '* [x] Done';
|
||||
const result = checkboxUtils.matchCheckboxLine(line);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toEqual({
|
||||
indent: 0,
|
||||
marker: '*',
|
||||
checkChar: 'x',
|
||||
checkboxCharPosition: 3,
|
||||
checkboxState: CheckboxState.Checked,
|
||||
isChecked: true,
|
||||
listItemText: "Done",
|
||||
});
|
||||
});
|
||||
|
||||
it('should match checkbox with indentation', () => {
|
||||
const line = ' + [x] Indented Task';
|
||||
const result = checkboxUtils.matchCheckboxLine(line);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toEqual({
|
||||
indent: 2,
|
||||
marker: '+',
|
||||
checkChar: 'x',
|
||||
checkboxCharPosition: 5, // 2 spaces + 1 marker + 1 space + 1 [ = 5
|
||||
checkboxState: CheckboxState.Checked,
|
||||
isChecked: true,
|
||||
listItemText: "Indented Task",
|
||||
});
|
||||
});
|
||||
|
||||
it('should match numbered list checkbox', () => {
|
||||
const line = '1. [ ] Numbered';
|
||||
const result = checkboxUtils.matchCheckboxLine(line);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toEqual({
|
||||
indent: 0,
|
||||
marker: '1.',
|
||||
checkChar: ' ',
|
||||
checkboxCharPosition: 4, // 2 marker + 1 space + 1 [ = 4
|
||||
checkboxState: CheckboxState.Unchecked,
|
||||
isChecked: false,
|
||||
listItemText: "Numbered",
|
||||
});
|
||||
});
|
||||
|
||||
it('should match checkbox with multi-digit numbered list', () => {
|
||||
const line = '10. [x] Double Digit';
|
||||
const result = checkboxUtils.matchCheckboxLine(line);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toEqual({
|
||||
indent: 0,
|
||||
marker: '10.',
|
||||
checkChar: 'x',
|
||||
checkboxCharPosition: 5, // 3 marker + 1 space + 1 [ = 5
|
||||
checkboxState: CheckboxState.Checked,
|
||||
isChecked: true,
|
||||
listItemText: "Double Digit",
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null for lines without checkbox format', () => {
|
||||
expect(checkboxUtils.matchCheckboxLine('Just text')).toBeNull();
|
||||
expect(checkboxUtils.matchCheckboxLine('- [ ]')).toBeNull(); // No space after ]
|
||||
expect(checkboxUtils.matchCheckboxLine('- [] ')).toBeNull(); // No char inside
|
||||
expect(checkboxUtils.matchCheckboxLine('[ ] Task')).toBeNull(); // No marker
|
||||
expect(checkboxUtils.matchCheckboxLine('-[ ] Task')).toBeNull(); // No space after marker
|
||||
});
|
||||
|
||||
it('should match custom symbols based on settings', () => {
|
||||
const customSettings = createSettings({
|
||||
checkedSymbols: ['X', 'V'],
|
||||
uncheckedSymbols: ['O', '-'],
|
||||
ignoreSymbols: ['~'],
|
||||
});
|
||||
const customUtils = new CheckboxUtils(customSettings);
|
||||
|
||||
const checkedLine = '- [V] Custom Checked';
|
||||
const checkedResult = customUtils.matchCheckboxLine(checkedLine);
|
||||
expect(checkedResult?.checkboxState).toBe(CheckboxState.Checked);
|
||||
expect(checkedResult?.isChecked).toBe(true);
|
||||
expect(checkedResult?.checkChar).toBe('V');
|
||||
|
||||
const uncheckedLine = '* [O] Custom Unchecked';
|
||||
const uncheckedResult = customUtils.matchCheckboxLine(uncheckedLine);
|
||||
expect(uncheckedResult?.checkboxState).toBe(CheckboxState.Unchecked);
|
||||
expect(uncheckedResult?.isChecked).toBe(false);
|
||||
expect(uncheckedResult?.checkChar).toBe('O');
|
||||
|
||||
const ignoredLine = '+ [~] Custom Ignored';
|
||||
const ignoredResult = customUtils.matchCheckboxLine(ignoredLine);
|
||||
expect(ignoredResult?.checkboxState).toBe(CheckboxState.Ignore);
|
||||
expect(ignoredResult?.isChecked).toBeUndefined();
|
||||
expect(ignoredResult?.checkChar).toBe('~');
|
||||
});
|
||||
});
|
||||
|
||||
// --- Тесты для getCheckboxState ---
|
||||
describe('getCheckboxState', () => {
|
||||
it('should return Checked for symbols in checkedSymbols', () => {
|
||||
const customSettings = createSettings({ checkedSymbols: ['x', 'X', '✓'] });
|
||||
const customUtils = new CheckboxUtils(customSettings);
|
||||
expect(customUtils.getCheckboxState('x')).toBe(CheckboxState.Checked);
|
||||
expect(customUtils.getCheckboxState('X')).toBe(CheckboxState.Checked);
|
||||
expect(customUtils.getCheckboxState('✓')).toBe(CheckboxState.Checked);
|
||||
});
|
||||
|
||||
it('should return Unchecked for symbols in uncheckedSymbols', () => {
|
||||
const customSettings = createSettings({ uncheckedSymbols: [' ', '_', '?'] });
|
||||
const customUtils = new CheckboxUtils(customSettings);
|
||||
expect(customUtils.getCheckboxState(' ')).toBe(CheckboxState.Unchecked);
|
||||
expect(customUtils.getCheckboxState('_')).toBe(CheckboxState.Unchecked);
|
||||
expect(customUtils.getCheckboxState('?')).toBe(CheckboxState.Unchecked);
|
||||
});
|
||||
|
||||
it('should return Ignore for symbols in ignoreSymbols', () => {
|
||||
const customSettings = createSettings({ ignoreSymbols: ['-', '~', '>'] });
|
||||
const customUtils = new CheckboxUtils(customSettings);
|
||||
expect(customUtils.getCheckboxState('-')).toBe(CheckboxState.Ignore);
|
||||
expect(customUtils.getCheckboxState('~')).toBe(CheckboxState.Ignore);
|
||||
expect(customUtils.getCheckboxState('>')).toBe(CheckboxState.Ignore);
|
||||
});
|
||||
|
||||
it('should use unknownSymbolPolicy for symbols not in any list', () => {
|
||||
const settingsChecked = createSettings({ unknownSymbolPolicy: CheckboxState.Checked });
|
||||
const utilsChecked = new CheckboxUtils(settingsChecked);
|
||||
expect(utilsChecked.getCheckboxState('?')).toBe(CheckboxState.Checked);
|
||||
|
||||
const settingsUnchecked = createSettings({ unknownSymbolPolicy: CheckboxState.Unchecked });
|
||||
const utilsUnchecked = new CheckboxUtils(settingsUnchecked);
|
||||
expect(utilsUnchecked.getCheckboxState('?')).toBe(CheckboxState.Unchecked);
|
||||
|
||||
const settingsIgnore = createSettings({ unknownSymbolPolicy: CheckboxState.Ignore });
|
||||
const utilsIgnore = new CheckboxUtils(settingsIgnore);
|
||||
expect(utilsIgnore.getCheckboxState('?')).toBe(CheckboxState.Ignore);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Тесты для updateLineCheckboxStateWithInfo ---
|
||||
describe('updateLineCheckboxStateWithInfo', () => {
|
||||
it('should update checkbox state from unchecked to checked', () => {
|
||||
const line = '- [ ] Task';
|
||||
const lineInfo = checkboxUtils.matchCheckboxLine(line)!; // Assume valid line
|
||||
const updatedLine = checkboxUtils.updateLineCheckboxStateWithInfo(line, true, lineInfo);
|
||||
// Uses the first symbol from settings.checkedSymbols ('x' by default)
|
||||
expect(updatedLine).toBe('- [x] Task');
|
||||
});
|
||||
|
||||
it('should update checkbox state from checked to unchecked', () => {
|
||||
const line = '* [x] Done';
|
||||
const lineInfo = checkboxUtils.matchCheckboxLine(line)!;
|
||||
const updatedLine = checkboxUtils.updateLineCheckboxStateWithInfo(line, false, lineInfo);
|
||||
// Uses the first symbol from settings.uncheckedSymbols (' ' by default)
|
||||
expect(updatedLine).toBe('* [ ] Done');
|
||||
});
|
||||
|
||||
it('should use first symbol from settings for update', () => {
|
||||
const customSettings = createSettings({ checkedSymbols: ['V', 'X'], uncheckedSymbols: ['O', ' '] });
|
||||
const customUtils = new CheckboxUtils(customSettings);
|
||||
|
||||
const lineToCheck = '- [O] Task';
|
||||
const infoToCheck = customUtils.matchCheckboxLine(lineToCheck)!;
|
||||
const updatedToCheck = customUtils.updateLineCheckboxStateWithInfo(lineToCheck, true, infoToCheck);
|
||||
expect(updatedToCheck).toBe('- [V] Task'); // Should use 'V'
|
||||
|
||||
const lineToUncheck = '* [V] Done';
|
||||
const infoToUncheck = customUtils.matchCheckboxLine(lineToUncheck)!;
|
||||
const updatedToUncheck = customUtils.updateLineCheckboxStateWithInfo(lineToUncheck, false, infoToUncheck);
|
||||
expect(updatedToUncheck).toBe('* [O] Done'); // Should use 'O'
|
||||
});
|
||||
|
||||
it('should handle indented lines correctly', () => {
|
||||
const line = ' - [ ] Indented';
|
||||
const lineInfo = checkboxUtils.matchCheckboxLine(line)!;
|
||||
const updatedLine = checkboxUtils.updateLineCheckboxStateWithInfo(line, true, lineInfo);
|
||||
expect(updatedLine).toBe(' - [x] Indented');
|
||||
});
|
||||
|
||||
it('should return original line if position is invalid (simulated)', () => {
|
||||
const line = '- [ ] Task';
|
||||
const lineInfo = checkboxUtils.matchCheckboxLine(line)!;
|
||||
// Simulate invalid position
|
||||
const invalidInfo = { ...lineInfo, checkboxCharPosition: 100 };
|
||||
const updatedLine = checkboxUtils.updateLineCheckboxStateWithInfo(line, true, invalidInfo);
|
||||
expect(updatedLine).toBe(line); // Should not change
|
||||
});
|
||||
|
||||
it('should use default checked symbol "x" if checkedSymbols setting is empty', () => {
|
||||
// Создаем настройки с пустым списком checkedSymbols
|
||||
const customSettings = createSettings({ checkedSymbols: [] });
|
||||
const customUtils = new CheckboxUtils(customSettings);
|
||||
const line = '- [ ] Task';
|
||||
const lineInfo = customUtils.matchCheckboxLine(line)!;
|
||||
|
||||
// Пытаемся отметить чекбокс
|
||||
const updatedLine = customUtils.updateLineCheckboxStateWithInfo(line, true, lineInfo);
|
||||
expect(updatedLine).toBe('- [x] Task'); // Ожидаем дефолтный 'x'
|
||||
});
|
||||
|
||||
it('should use default unchecked symbol " " if uncheckedSymbols setting is empty', () => {
|
||||
// Создаем настройки с пустым списком uncheckedSymbols
|
||||
const customSettings = createSettings({ uncheckedSymbols: [] });
|
||||
const customUtils = new CheckboxUtils(customSettings);
|
||||
const line = '- [x] Task'; // Используем 'x' из дефолтных checkedSymbols
|
||||
const lineInfo = customUtils.matchCheckboxLine(line)!;
|
||||
|
||||
// Пытаемся снять отметку
|
||||
const updatedLine = customUtils.updateLineCheckboxStateWithInfo(line, false, lineInfo);
|
||||
expect(updatedLine).toBe('- [ ] Task'); // Ожидаем дефолтный ' ' (пробел)
|
||||
});
|
||||
|
||||
// Усиленный тест для невалидной позиции (с проверкой console.warn)
|
||||
it('should return original line and warn if position is invalid', () => {
|
||||
const line = '- [ ] Task';
|
||||
const lineInfo = checkboxUtils.matchCheckboxLine(line)!;
|
||||
// Мокаем console.warn
|
||||
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
|
||||
|
||||
// Simulate invalid position
|
||||
const invalidInfo = { ...lineInfo, checkboxCharPosition: -1 }; // Невалидная позиция
|
||||
const updatedLineNegative = checkboxUtils.updateLineCheckboxStateWithInfo(line, true, invalidInfo);
|
||||
expect(updatedLineNegative).toBe(line); // Должен вернуть оригинальную строку
|
||||
expect(warnSpy).toHaveBeenCalledTimes(1); // Проверяем вызов warn
|
||||
|
||||
const invalidInfo2 = { ...lineInfo, checkboxCharPosition: 100 }; // Другая невалидная позиция
|
||||
const updatedLineOob = checkboxUtils.updateLineCheckboxStateWithInfo(line, true, invalidInfo2);
|
||||
expect(updatedLineOob).toBe(line); // Должен вернуть оригинальную строку
|
||||
expect(warnSpy).toHaveBeenCalledTimes(2); // Проверяем вызов warn еще раз
|
||||
|
||||
// Восстанавливаем оригинальную функцию console.warn
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should return original line and warn if checkboxCharPosition is invalid for a valid checkbox type', () => {
|
||||
const line = '- [ ] Task';
|
||||
const lineInfo = checkboxUtils.matchCheckboxLine(line)!; // Это валидный чекбокс
|
||||
const invalidPosInfo = { ...lineInfo, checkboxCharPosition: -1 }; // Невалидная позиция
|
||||
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
|
||||
const updatedLine = checkboxUtils.updateLineCheckboxStateWithInfo(line, true, invalidPosInfo);
|
||||
expect(updatedLine).toBe(line);
|
||||
expect(warnSpy).toHaveBeenCalledWith("updateLineCheckboxStateWithInfo: Invalid checkbox position in lineInfo for line:", line);
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('UPDATE_LINE: should return original line and warn if lineInfo is for NoCheckbox', () => {
|
||||
const line = "- Plain item";
|
||||
// Создаем lineInfo, как будто он пришел от plain list item
|
||||
const plainLineInfo: CheckboxLineInfo = {
|
||||
indent: 0,
|
||||
marker: '-',
|
||||
checkboxState: CheckboxState.NoCheckbox, // Ключевой момент
|
||||
listItemText: 'Plain item',
|
||||
// checkboxCharPosition не важен, т.к. до него не дойдет
|
||||
};
|
||||
|
||||
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
|
||||
|
||||
const updatedLine = checkboxUtils.updateLineCheckboxStateWithInfo(line, true, plainLineInfo);
|
||||
|
||||
expect(updatedLine).toBe(line); // Строка не должна измениться
|
||||
expect(warnSpy).toHaveBeenCalledWith("updateLineCheckboxStateWithInfo: Invalid lineInfo(no checkbox) for line:", line);
|
||||
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
// --- Тесты для propagateStateToChildren ---
|
||||
describe('propagateStateToChildren', () => {
|
||||
const setupUtils = (customSettings?: Partial<CheckboxSyncPluginSettings>) => {
|
||||
return new CheckboxUtils(createSettings(customSettings));
|
||||
}
|
||||
|
||||
it('should check children when parent is checked', () => {
|
||||
const text = [
|
||||
'- [x] Parent', // line 0
|
||||
' - [ ] Child 1',
|
||||
' - [ ] Grandchild',
|
||||
' - [ ] Child 2',
|
||||
'- [ ] Sibling',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [x] Parent',
|
||||
' - [x] Child 1',
|
||||
' - [x] Grandchild',
|
||||
' - [x] Child 2',
|
||||
'- [ ] Sibling', // Sibling should not change
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateToChildren(text, 0)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should uncheck children when parent is unchecked', () => {
|
||||
const text = [
|
||||
'- [ ] Parent', // line 0
|
||||
' - [x] Child 1',
|
||||
' - [x] Grandchild',
|
||||
' - [ ] Child 2', // Already unchecked
|
||||
'- [x] Sibling',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [ ] Parent',
|
||||
' - [ ] Child 1',
|
||||
' - [ ] Grandchild',
|
||||
' - [ ] Child 2',
|
||||
'- [x] Sibling', // Sibling should not change
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateToChildren(text, 0)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should stop propagation at siblings or less indented lines', () => {
|
||||
const text = [
|
||||
' - [x] Parent', // line 0
|
||||
' - [ ] Child',
|
||||
' - [ ] Sibling', // Same indent level
|
||||
'- [ ] Less Indented',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
' - [x] Parent',
|
||||
' - [x] Child', // Changed
|
||||
' - [ ] Sibling', // Not changed
|
||||
'- [ ] Less Indented', // Not changed
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateToChildren(text, 0)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should skip ignored children and their subtrees', () => {
|
||||
const utils = setupUtils({ ignoreSymbols: ['~'] });
|
||||
const text = [
|
||||
'- [x] Parent', // 0
|
||||
' - [ ] Child 1', // 1
|
||||
' - [~] Ignored Child',// 2 (ignore)
|
||||
' - [ ] Skipped GC', // 3 (skipped because parent ignored)
|
||||
' - [ ] Child 2', // 4
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [x] Parent',
|
||||
' - [x] Child 1', // Changed
|
||||
' - [~] Ignored Child',// Unchanged
|
||||
' - [ ] Skipped GC', // Unchanged
|
||||
' - [x] Child 2', // Changed
|
||||
].join('\n');
|
||||
expect(utils.propagateStateToChildren(text, 0)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should do nothing if the parent line is not a checkbox', () => {
|
||||
const text = [
|
||||
'Parent Line',
|
||||
' - [ ] Child',
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateToChildren(text, 0)).toBe(text);
|
||||
});
|
||||
|
||||
it('should do nothing if the parent checkbox is ignored', () => {
|
||||
const utils = setupUtils({ ignoreSymbols: ['~'] });
|
||||
const text = [
|
||||
'- [~] Ignored Parent', // 0
|
||||
' - [ ] Child',
|
||||
].join('\n');
|
||||
expect(utils.propagateStateToChildren(text, 0)).toBe(text);
|
||||
});
|
||||
|
||||
it('should return original text and warn if parent line index does not contain a list item', () => {
|
||||
const text = "Not a list item\n - [ ] Child";
|
||||
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
|
||||
const result = checkboxUtils.propagateStateToChildren(text, 0);
|
||||
expect(result).toBe(text);
|
||||
expect(warnSpy).toHaveBeenCalledWith("checkbox not found in line 0");
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should stop propagation if a non-list-item line is encountered among children', () => {
|
||||
const text = [
|
||||
'- [x] Parent',
|
||||
' Not a child list item', // This will make childLineInfo null
|
||||
' - [ ] Grandchild (should not be processed)',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [x] Parent',
|
||||
' Not a child list item',
|
||||
' - [ ] Grandchild (should not be processed)',
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateToChildren(text, 0)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should correctly stop processing sub-children of an ignored node if a non-list-item is found', () => {
|
||||
const utils = new CheckboxUtils(createSettings({ ignoreSymbols: ['~'] }));
|
||||
const text = [
|
||||
'- [x] Parent',
|
||||
' - [~] Ignored Child',
|
||||
' - [ ] Valid Sub-Child (will be skipped by inner loop)', // j инкрементируется
|
||||
' Not a sub-child list item', // subChildLineInfo = null, inner loop breaks
|
||||
' - [ ] Another Sub-Child (SHOULD NOT BE SKIPPED by inner loop, as it breaks, and also not processed by outer loop)',
|
||||
' - [ ] Next Sibling of Ignored', // This should also not be processed due to outer loop break
|
||||
].join('\n');
|
||||
|
||||
// Ожидаем, что Parent и Ignored Child останутся, а остальное не изменится,
|
||||
// потому что внешний цикл прервется после обработки "Not a sub-child list item"
|
||||
const expected = [
|
||||
'- [x] Parent',
|
||||
' - [~] Ignored Child',
|
||||
' - [ ] Valid Sub-Child (will be skipped by inner loop)',
|
||||
' Not a sub-child list item',
|
||||
' - [ ] Another Sub-Child (SHOULD NOT BE SKIPPED by inner loop, as it breaks, and also not processed by outer loop)',
|
||||
' - [ ] Next Sibling of Ignored',
|
||||
].join('\n');
|
||||
|
||||
const result = utils.propagateStateToChildren(text, 0);
|
||||
expect(result).toBe(expected);
|
||||
// В данном случае, поскольку Parent [x], и мы ничего не меняем, результат будет равен text.
|
||||
// Важно то, что Next Sibling of Ignored не стал [x].
|
||||
});
|
||||
});
|
||||
|
||||
// --- Тесты для propagateStateFromChildren ---
|
||||
describe('propagateStateFromChildren', () => {
|
||||
const setupUtils = (customSettings?: Partial<CheckboxSyncPluginSettings>) => {
|
||||
return new CheckboxUtils(createSettings(customSettings));
|
||||
}
|
||||
|
||||
it('should check parent if all children are checked', () => {
|
||||
const text = [
|
||||
'- [ ] Parent', // line 0
|
||||
' - [x] Child 1',
|
||||
' - [x] Child 2',
|
||||
' - [x] Grandchild', // Needs parent (Child 2) to be checked first
|
||||
].join('\n');
|
||||
// Expected: Grandchild affects Child 2 -> Child 2 and Child 1 affect Parent
|
||||
const expectedPass1 = [ // After processing Grandchild and Child 2
|
||||
'- [ ] Parent',
|
||||
' - [x] Child 1',
|
||||
' - [x] Child 2', // Stays checked
|
||||
' - [x] Grandchild',
|
||||
].join('\n');
|
||||
const expectedPass2 = [ // After processing Child 1 and Parent
|
||||
'- [x] Parent', // Changes to checked
|
||||
' - [x] Child 1',
|
||||
' - [x] Child 2',
|
||||
' - [x] Grandchild',
|
||||
].join('\n');
|
||||
// Since it processes bottom-up, Child 2 should already be evaluated correctly
|
||||
expect(checkboxUtils.propagateStateFromChildren(text)).toBe(expectedPass2);
|
||||
});
|
||||
|
||||
it('should uncheck parent if any child is unchecked', () => {
|
||||
const text = [
|
||||
'- [x] Parent', // line 0
|
||||
' - [x] Child 1',
|
||||
' - [ ] Child 2', // This one makes the parent unchecked
|
||||
' - [ ] Grandchild',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [ ] Parent', // Changes to unchecked
|
||||
' - [x] Child 1',
|
||||
' - [ ] Child 2',
|
||||
' - [ ] Grandchild',
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateFromChildren(text)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle nested updates correctly (bottom-up)', () => {
|
||||
const text = [
|
||||
'- [ ] Grandparent', // 0
|
||||
' - [ ] Parent 1', // 1
|
||||
' - [x] Child 1.1',// 2
|
||||
' - [x] Child 1.2',// 3
|
||||
' - [x] Parent 2', // 4
|
||||
' - [ ] Child 2.1',// 5 (Makes Parent 2 unchecked)
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [ ] Grandparent', // Becomes unchecked (because Parent 2 becomes unchecked)
|
||||
' - [x] Parent 1', // Becomes checked (Child 1.1, 1.2 are checked)
|
||||
' - [x] Child 1.1',
|
||||
' - [x] Child 1.2',
|
||||
' - [ ] Parent 2', // Becomes unchecked (Child 2.1 is unchecked)
|
||||
' - [ ] Child 2.1',
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateFromChildren(text)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should not change parent state if it has no relevant children', () => {
|
||||
const text = [
|
||||
'- [ ] Parent',
|
||||
'Sibling', // Not a child checkbox
|
||||
' Not indented correctly',
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateFromChildren(text)).toBe(text);
|
||||
});
|
||||
|
||||
it('should ignore non-checkbox lines when determining parent state', () => {
|
||||
const text = [
|
||||
'- [ ] Parent',
|
||||
' - [x] Child 1',
|
||||
' Just text',
|
||||
' - [x] Child 2',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [x] Parent', // Should become checked
|
||||
' - [x] Child 1',
|
||||
' Just text',
|
||||
' - [x] Child 2',
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateFromChildren(text)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should skip ignored children when determining parent state', () => {
|
||||
const utils = setupUtils({ ignoreSymbols: ['~'] });
|
||||
const text = [
|
||||
'- [ ] Parent', // 0
|
||||
' - [x] Child 1', // 1
|
||||
' - [~] Ignored Child',// 2 (skipped)
|
||||
' - [ ] Skipped GC', // 3 (doesn't affect parent)
|
||||
' - [x] Child 2', // 4
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [x] Parent', // Becomes checked (based on Child 1 and Child 2 only)
|
||||
' - [x] Child 1',
|
||||
' - [~] Ignored Child',
|
||||
' - [ ] Skipped GC',
|
||||
' - [x] Child 2',
|
||||
].join('\n');
|
||||
expect(utils.propagateStateFromChildren(text)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should not change parent state if all direct children are ignored', () => {
|
||||
const utils = setupUtils({ ignoreSymbols: ['~'] });
|
||||
const text = [
|
||||
'- [ ] Parent', // 0
|
||||
' - [~] Ignored 1', // 1
|
||||
' - [~] Ignored 2', // 2
|
||||
].join('\n');
|
||||
expect(utils.propagateStateFromChildren(text)).toBe(text); // Parent stays unchecked
|
||||
const text2 = [
|
||||
'- [x] Parent', // 0
|
||||
' - [~] Ignored 1', // 1
|
||||
' - [~] Ignored 2', // 2
|
||||
].join('\n');
|
||||
expect(utils.propagateStateFromChildren(text2)).toBe(text2); // Parent stays checked
|
||||
});
|
||||
|
||||
it('should not update ignored parents', () => {
|
||||
const utils = setupUtils({ ignoreSymbols: ['~'] });
|
||||
const text = [
|
||||
'- [~] Ignored Parent', // 0
|
||||
' - [x] Child 1',
|
||||
' - [x] Child 2',
|
||||
].join('\n');
|
||||
expect(utils.propagateStateFromChildren(text)).toBe(text); // Parent remains ignored
|
||||
});
|
||||
|
||||
it('should stop searching children when a line with equal or lesser indent is found', () => {
|
||||
const text = [
|
||||
'- [ ] Parent 1', // indent 0
|
||||
'Some regular text', // indent 0. Stops search for Parent 1 children.
|
||||
' - [x] Child 1.1', // indent 2. Should be ignored for Parent 1.
|
||||
'- [ ] Parent 2', // indent 0. Processed independently.
|
||||
' - [x] Child 2.1', // indent 2. Child of Parent 2.
|
||||
].join('\n');
|
||||
|
||||
const result = checkboxUtils.propagateStateFromChildren(text);
|
||||
|
||||
const expected = [
|
||||
'- [ ] Parent 1', // Unchanged (no children found before stop)
|
||||
'Some regular text',
|
||||
' - [x] Child 1.1',
|
||||
'- [x] Parent 2', // Updated by Child 2.1
|
||||
' - [x] Child 2.1',
|
||||
].join('\n');
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
|
||||
it('should stop searching children when a sibling checkbox (equal/lesser indent) is found', () => {
|
||||
const text = [
|
||||
'- [ ] Parent 1', // indent 0
|
||||
'- [ ] Sibling Parent', // indent 0. Stops search for Parent 1 children.
|
||||
' - [x] Child SP.1', // indent 2. Belongs to Sibling Parent.
|
||||
].join('\n');
|
||||
|
||||
const result = checkboxUtils.propagateStateFromChildren(text);
|
||||
|
||||
const expected = [
|
||||
'- [ ] Parent 1', // Unchanged (no children found before stop)
|
||||
'- [x] Sibling Parent', // Updated by Child SP.1
|
||||
' - [x] Child SP.1',
|
||||
].join('\n');
|
||||
expect(result).toBe(expected);
|
||||
|
||||
const text2 = [
|
||||
' - [ ] Parent 1', // indent 2
|
||||
' - [x] Child 1.1', // indent 4
|
||||
' - [ ] Sibling Parent', // indent 2. Stops search for Parent 1 children.
|
||||
' - [ ] Child SP.1', // indent 4. Belongs to Sibling Parent.
|
||||
].join('\n');
|
||||
const result2 = checkboxUtils.propagateStateFromChildren(text2);
|
||||
const expected2 = [
|
||||
' - [x] Parent 1', // Updated by Child 1.1
|
||||
' - [x] Child 1.1',
|
||||
' - [ ] Sibling Parent', // Unchanged (Child SP.1 is unchecked)
|
||||
' - [ ] Child SP.1',
|
||||
].join('\n');
|
||||
expect(result2).toBe(expected2);
|
||||
});
|
||||
|
||||
it('should ignore non-list-item lines when iterating upwards', () => {
|
||||
const text = [
|
||||
'- [ ] Parent',
|
||||
' - [x] Child',
|
||||
'Just some random text in between', // parentLineInfo will be null for this
|
||||
'- [ ] Another Parent',
|
||||
' - [x] Another Child',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [x] Parent',
|
||||
' - [x] Child',
|
||||
'Just some random text in between',
|
||||
'- [x] Another Parent',
|
||||
' - [x] Another Child',
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateFromChildren(text)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Тесты для syncText ---
|
||||
describe('syncText', () => {
|
||||
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');
|
||||
|
||||
it('should propagate down only if child sync enabled and diff matches', () => {
|
||||
const utils = new CheckboxUtils(createSettings({
|
||||
enableAutomaticChildState: true,
|
||||
enableAutomaticParentState: false, // Parent sync off
|
||||
}));
|
||||
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 CheckboxUtils(createSettings({
|
||||
enableAutomaticChildState: false, // Child sync off
|
||||
enableAutomaticParentState: true,
|
||||
}));
|
||||
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 CheckboxUtils(createSettings({
|
||||
enableAutomaticChildState: true,
|
||||
enableAutomaticParentState: true,
|
||||
}));
|
||||
// 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);
|
||||
|
||||
// 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 CheckboxUtils(createSettings({
|
||||
enableAutomaticChildState: false,
|
||||
enableAutomaticParentState: false,
|
||||
}));
|
||||
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 CheckboxUtils(createSettings({
|
||||
enableAutomaticChildState: true, // Down propagation enabled
|
||||
enableAutomaticParentState: false,
|
||||
}));
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Тесты для propagateStateToChildrenFromSingleDiff ---
|
||||
describe('propagateStateToChildrenFromSingleDiff', () => {
|
||||
const textBefore = [
|
||||
'- [ ] Parent',
|
||||
' - [ ] Child',
|
||||
].join('\n');
|
||||
|
||||
it('should propagate if only checkbox state changed', () => {
|
||||
const textAfter = [
|
||||
'- [x] Parent', // Only state changed
|
||||
' - [ ] Child',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [x] Parent',
|
||||
' - [x] Child', // Propagated
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should propagate if text content also changed', () => {
|
||||
const textAfter = [
|
||||
'- [x] Parent Changed Text', // State and text changed
|
||||
' - [ ] Child',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [x] Parent Changed Text',
|
||||
' - [x] Child', // Propagated
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should NOT propagate if marker changed', () => {
|
||||
const textAfter = [
|
||||
'* [x] Parent', // Marker changed from - to *
|
||||
' - [ ] Child',
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(textAfter);
|
||||
});
|
||||
|
||||
it('should NOT propagate if indentation changed', () => {
|
||||
const textAfter = [
|
||||
' - [x] Parent', // Indentation changed
|
||||
' - [ ] Child',
|
||||
].join('\n');
|
||||
// This will be detected as > 1 line difference by findDifferentLineIndexes
|
||||
expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(textAfter);
|
||||
});
|
||||
|
||||
|
||||
it('should NOT propagate if multiple lines changed', () => {
|
||||
const textAfter = [
|
||||
'- [x] Parent',
|
||||
' - [x] Child', // Second line also changed
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(textAfter);
|
||||
});
|
||||
|
||||
it('should NOT propagate if changed line is not a checkbox', () => {
|
||||
const textBeforeNonCheck = "Line 1\nLine 2";
|
||||
const textAfterNonCheck = "Line 1 changed\nLine 2";
|
||||
expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfterNonCheck, textBeforeNonCheck)).toBe(textAfterNonCheck);
|
||||
});
|
||||
|
||||
it('should propagate if changed line is an ignored checkbox', () => {
|
||||
const utils = new CheckboxUtils(createSettings({ ignoreSymbols: ['~'] }));
|
||||
const textBeforeIgnored = [
|
||||
'- [~] Parent',
|
||||
' - [ ] Child',
|
||||
].join('\n');
|
||||
const textAfterIgnored = [
|
||||
'- [x] Parent', // Changed FROM ignored TO checked
|
||||
' - [ ] Child',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [x] Parent',
|
||||
' - [x] Child', // Propagated
|
||||
].join('\n');
|
||||
|
||||
expect(utils.propagateStateToChildrenFromSingleDiff(textAfterIgnored, textBeforeIgnored)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should return original text if textBefore is undefined', () => {
|
||||
const text = '- [ ] Line';
|
||||
expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(text, undefined)).toBe(text);
|
||||
});
|
||||
|
||||
it('should return original text if line counts differ', () => {
|
||||
const text1 = '- [ ] Line1\n- [ ] Line2';
|
||||
const text2 = '- [ ] Line1';
|
||||
expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(text1, text2)).toBe(text1);
|
||||
});
|
||||
|
||||
it('should return original text if textBefore and text have different line counts', () => {
|
||||
const textBefore = "- [ ] Line 1";
|
||||
const text = "- [ ] Line 1\n- [ ] Line 2";
|
||||
expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(text, textBefore)).toBe(text);
|
||||
});
|
||||
|
||||
it('should return original text if no lines changed (diffIndexes.length === 0)', () => {
|
||||
const text = "- [ ] Task";
|
||||
expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(text, text)).toBe(text);
|
||||
});
|
||||
|
||||
it('should return original text if multiple lines changed (diffIndexes.length > 1)', () => {
|
||||
const textBefore = "- [ ] Task1\n- [ ] Task2";
|
||||
const textAfter = "- [x] Task1\n- [x] Task2"; // 2 diffs
|
||||
expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(textAfter);
|
||||
});
|
||||
|
||||
it('should not propagate if changed line was not a list item before', () => {
|
||||
const textBefore = "Not a list item";
|
||||
const textAfter = "- [x] Became a list item";
|
||||
expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(textAfter);
|
||||
});
|
||||
|
||||
it('should not propagate if changed line is not a list item after', () => {
|
||||
const textBefore = "- [ ] Was a list item";
|
||||
const textAfter = "Not a list item anymore";
|
||||
expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(textAfter);
|
||||
});
|
||||
|
||||
it('should not propagate if indent changed', () => {
|
||||
const textBeforeIndent = [
|
||||
"- [ ] Parent",
|
||||
" - [ ] Child",
|
||||
].join('\n');
|
||||
const textAfterIndent = [
|
||||
" - [ ] Parent",
|
||||
" - [ ] Child",
|
||||
].join('\n'); // Parent indent changed
|
||||
expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfterIndent, textBeforeIndent)).toBe(textAfterIndent);
|
||||
});
|
||||
|
||||
it('should not propagate if marker changed', () => {
|
||||
const textBefore = "- [ ] Item\n - [ ] Child";
|
||||
const textAfter = "* [ ] Item\n - [ ] Child"; // Marker changed
|
||||
expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(textAfter);
|
||||
});
|
||||
|
||||
it('should not propagate if checkbox state did not change (e.g., only text content)', () => {
|
||||
const textBefore = "- [x] Task Alpha\n - [ ] Child";
|
||||
const textAfter = "- [x] Task Bravo\n - [ ] Child"; // Only text changed
|
||||
expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(textAfter);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Тесты для findDifferentLineIndexes ---
|
||||
describe('findDifferentLineIndexes', () => {
|
||||
it('should return empty array for identical lines', () => {
|
||||
const lines1 = ['a', 'b', 'c'];
|
||||
const lines2 = ['a', 'b', 'c'];
|
||||
expect(checkboxUtils.findDifferentLineIndexes(lines1, lines2)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return index of the single different line', () => {
|
||||
const lines1 = ['a', 'b', 'c'];
|
||||
const lines2 = ['a', 'B', 'c'];
|
||||
expect(checkboxUtils.findDifferentLineIndexes(lines1, lines2)).toEqual([1]);
|
||||
});
|
||||
|
||||
it('should return indexes of multiple different lines', () => {
|
||||
const lines1 = ['a', 'b', 'c', 'd'];
|
||||
const lines2 = ['A', 'b', 'C', 'd'];
|
||||
expect(checkboxUtils.findDifferentLineIndexes(lines1, lines2)).toEqual([0, 2]);
|
||||
});
|
||||
|
||||
it('should detect differences at start and end', () => {
|
||||
const lines1 = ['a', 'b', 'c'];
|
||||
const lines2 = ['X', 'b', 'Y'];
|
||||
expect(checkboxUtils.findDifferentLineIndexes(lines1, lines2)).toEqual([0, 2]);
|
||||
});
|
||||
|
||||
it('should throw error if line arrays have different lengths', () => {
|
||||
const lines1 = ['a', 'b'];
|
||||
const lines2 = ['a', 'b', 'c'];
|
||||
expect(() => checkboxUtils.findDifferentLineIndexes(lines1, lines2))
|
||||
.toThrow("the length of the lines must be equal");
|
||||
expect(() => checkboxUtils.findDifferentLineIndexes(lines2, lines1))
|
||||
.toThrow("the length of the lines must be equal");
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
383
__tests__/checkboxUtils.plainLists.test.ts
Normal file
383
__tests__/checkboxUtils.plainLists.test.ts
Normal file
|
|
@ -0,0 +1,383 @@
|
|||
// checkboxUtils.plainLists.test.ts
|
||||
|
||||
import { CheckboxUtils } from '../src/checkboxUtils';
|
||||
import { CheckboxState, CheckboxSyncPluginSettings, DEFAULT_SETTINGS } from '../src/types';
|
||||
|
||||
// Вспомогательная функция для создания настроек с переопределениями
|
||||
const createSettings = (overrides: Partial<CheckboxSyncPluginSettings> = {}): Readonly<CheckboxSyncPluginSettings> => {
|
||||
return { ...DEFAULT_SETTINGS, ...overrides } as Readonly<CheckboxSyncPluginSettings>;
|
||||
};
|
||||
|
||||
describe('CheckboxUtils - Plain List Item Support', () => {
|
||||
let checkboxUtils: CheckboxUtils;
|
||||
let settings: Readonly<CheckboxSyncPluginSettings>;
|
||||
|
||||
beforeEach(() => {
|
||||
settings = createSettings();
|
||||
checkboxUtils = new CheckboxUtils(settings);
|
||||
});
|
||||
|
||||
// --- Тесты для matchCheckboxLine с поддержкой plain list items ---
|
||||
describe('matchCheckboxLine with Plain List Items', () => {
|
||||
it('should match a plain bullet list item', () => {
|
||||
const line = '- Plain item text';
|
||||
const result = checkboxUtils.matchCheckboxLine(line);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toEqual({
|
||||
indent: 0,
|
||||
marker: '-',
|
||||
checkboxState: CheckboxState.NoCheckbox,
|
||||
listItemText: 'Plain item text',
|
||||
// Опциональные поля для чекбокса должны быть undefined
|
||||
checkChar: undefined,
|
||||
checkboxCharPosition: undefined,
|
||||
isChecked: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should match a plain asterisk list item with indentation', () => {
|
||||
const line = ' * Indented plain item';
|
||||
const result = checkboxUtils.matchCheckboxLine(line);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toEqual({
|
||||
indent: 2,
|
||||
marker: '*',
|
||||
checkboxState: CheckboxState.NoCheckbox,
|
||||
listItemText: 'Indented plain item',
|
||||
});
|
||||
});
|
||||
|
||||
it('should match a plain plus list item', () => {
|
||||
const line = '+ Another plain item';
|
||||
const result = checkboxUtils.matchCheckboxLine(line);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toEqual({
|
||||
indent: 0,
|
||||
marker: '+',
|
||||
checkboxState: CheckboxState.NoCheckbox,
|
||||
listItemText: 'Another plain item',
|
||||
});
|
||||
});
|
||||
|
||||
it('should match a plain numbered list item', () => {
|
||||
const line = '1. Numbered plain item';
|
||||
const result = checkboxUtils.matchCheckboxLine(line);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toEqual({
|
||||
indent: 0,
|
||||
marker: '1.',
|
||||
checkboxState: CheckboxState.NoCheckbox,
|
||||
listItemText: 'Numbered plain item',
|
||||
});
|
||||
});
|
||||
|
||||
it('should match a plain multi-digit numbered list item', () => {
|
||||
const line = '10. Long numbered plain item';
|
||||
const result = checkboxUtils.matchCheckboxLine(line);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toEqual({
|
||||
indent: 0,
|
||||
marker: '10.',
|
||||
checkboxState: CheckboxState.NoCheckbox,
|
||||
listItemText: 'Long numbered plain item',
|
||||
});
|
||||
});
|
||||
|
||||
it('should still match a standard checkbox item', () => {
|
||||
const line = '- [x] A checkbox item';
|
||||
const result = checkboxUtils.matchCheckboxLine(line);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.checkboxState).toBe(CheckboxState.Checked);
|
||||
expect(result?.isChecked).toBe(true);
|
||||
expect(result?.checkChar).toBe('x');
|
||||
expect(result?.listItemText).toBe('A checkbox item');
|
||||
});
|
||||
|
||||
it('should match a plain list item with no text after marker', () => {
|
||||
const line = '- '; // Marker and space
|
||||
const result = checkboxUtils.matchCheckboxLine(line);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toEqual({
|
||||
indent: 0,
|
||||
marker: '-',
|
||||
checkboxState: CheckboxState.NoCheckbox,
|
||||
listItemText: '', // Empty string for text
|
||||
});
|
||||
});
|
||||
|
||||
it('should match a plain list item with only spaces after marker', () => {
|
||||
const line = '* '; // Marker and spaces
|
||||
const result = checkboxUtils.matchCheckboxLine(line);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toEqual({
|
||||
indent: 0,
|
||||
marker: '*',
|
||||
checkboxState: CheckboxState.NoCheckbox,
|
||||
listItemText: '', // Text is trimmed
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly parse listItemText for checkboxes', () => {
|
||||
const line = '- [ ] Task with text';
|
||||
const result = checkboxUtils.matchCheckboxLine(line);
|
||||
expect(result?.listItemText).toBe('Task with text');
|
||||
|
||||
const lineNoText = '- [x] ';
|
||||
const resultNoText = checkboxUtils.matchCheckboxLine(lineNoText);
|
||||
expect(resultNoText?.listItemText).toBe('');
|
||||
});
|
||||
|
||||
it('should return null for lines that are not list items or malformed', () => {
|
||||
expect(checkboxUtils.matchCheckboxLine('Just some text')).toBeNull();
|
||||
expect(checkboxUtils.matchCheckboxLine('-NoSpaceAfterMarker')).toBeNull();
|
||||
expect(checkboxUtils.matchCheckboxLine('1.NoSpaceAfterMarker')).toBeNull();
|
||||
expect(checkboxUtils.matchCheckboxLine('> Not a list marker')).toBeNull();
|
||||
// Это все еще НЕ чекбокс и НЕ plain list item по текущим правилам (нет пробела после маркера)
|
||||
expect(checkboxUtils.matchCheckboxLine('-[ ] No space after marker for checkbox')).toBeNull();
|
||||
// Это plain list item, а не чекбокс, т.к. нет пробела перед [ ]
|
||||
const trickyLine = "-[ ] Not a checkbox, but could be a plain item if regex allowed marker immediately followed by non-space";
|
||||
// По текущему regex ^(\s*)([*+-]|\d+\.)\s+(?!\[.?\])(.*)$ для plain list, это будет null.
|
||||
// Если бы regex для plain list был ^(\s*)([*+-]|\d+\.)\s*(.*)$, и мы бы отсекали чекбоксы отдельно,
|
||||
// то "-[ ] text" мог бы считаться plain. Но текущий regex для plain требует \s+ после маркера.
|
||||
// А regex для чекбокса требует " \[" . Так что "-[ ] text" не матчится ни тем, ни другим.
|
||||
expect(checkboxUtils.matchCheckboxLine(trickyLine)).toBeNull();
|
||||
|
||||
// A line that looks like a checkbox but is missing the char inside brackets
|
||||
expect(checkboxUtils.matchCheckboxLine('- [] Text')).toBeNull(); // Checkbox regex expects a char
|
||||
// A line that looks like a checkbox but missing space after brackets (and thus missing listItemText part)
|
||||
expect(checkboxUtils.matchCheckboxLine('- [x]')).toBeNull(); // Checkbox regex expects \s after [.]
|
||||
});
|
||||
});
|
||||
|
||||
// --- Тесты для propagateStateToChildren с Plain List Items ---
|
||||
describe('propagateStateToChildren with Plain List Items', () => {
|
||||
it('should not change plain list item children', () => {
|
||||
const text = [
|
||||
'- [x] Parent Checkbox',
|
||||
' - Plain Child Item',
|
||||
' * [ ] Another Checkbox Child',
|
||||
' - Deep Plain Child',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [x] Parent Checkbox',
|
||||
' - Plain Child Item', // Unchanged
|
||||
' * [x] Another Checkbox Child', // Changed
|
||||
' - Deep Plain Child', // Unchanged
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateToChildren(text, 0)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should not propagate from a plain list item parent', () => {
|
||||
const text = [
|
||||
'- Parent Plain Item', // line 0
|
||||
' - [ ] Child Checkbox',
|
||||
' * Plain Grandchild',
|
||||
].join('\n');
|
||||
// No changes expected as parent is NoCheckbox
|
||||
expect(checkboxUtils.propagateStateToChildren(text, 0)).toBe(text);
|
||||
});
|
||||
|
||||
it('should skip ignored children and not skip childs plain list items correctly', () => {
|
||||
const customSettings = createSettings({ ignoreSymbols: ['~'] });
|
||||
const utils = new CheckboxUtils(customSettings);
|
||||
const text = [
|
||||
'- [x] Parent', // 0
|
||||
' - Plain Child', // 1 (NoCheckbox)
|
||||
' - [ ] Grandchild CB',// 2 (Checkbox under NoCheckbox, should be affected by Parent)
|
||||
' - [~] Ignored Child', // 3 (Ignore)
|
||||
' - [ ] Skipped GC', // 4 (Checkbox under Ignore, should not be affected)
|
||||
' - [ ] Checkbox Child', // 5 (Checkbox)
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [x] Parent',
|
||||
' - Plain Child', // Unchanged
|
||||
' - [x] Grandchild CB',// Changed
|
||||
' - [~] Ignored Child', // Unchanged
|
||||
' - [ ] Skipped GC', // Unchanged
|
||||
' - [x] Checkbox Child', // Changed
|
||||
].join('\n');
|
||||
expect(utils.propagateStateToChildren(text, 0)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should stop propagation if a non-list-item line is encountered (indent > parent)', () => {
|
||||
const text = [
|
||||
'- [x] Parent',
|
||||
' This is just text, not a list item.', // childLineInfo будет null, indent > parent.indent. Цикл прервется.
|
||||
' - [ ] Grandchild (should not be processed as loop breaks)',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [x] Parent',
|
||||
' This is just text, not a list item.',
|
||||
' - [ ] Grandchild (should not be processed as loop breaks)',
|
||||
].join('\n');
|
||||
// Проверяем, что Grandchild не изменился
|
||||
const result = checkboxUtils.propagateStateToChildren(text, 0);
|
||||
expect(result).toBe(expected);
|
||||
// Дополнительно можно проверить, что matchCheckboxLine для "Grandchild" не вызывался бы
|
||||
// (если это важно, но для покрытия достаточно проверки результата)
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// --- Тесты для propagateStateFromChildren с Plain List Items ---
|
||||
describe('propagateStateFromChildren with Plain List Items', () => {
|
||||
it('should not update a plain list item parent based on children', () => {
|
||||
const text = [
|
||||
'- Parent Plain Item',
|
||||
' - [x] Child Checkbox 1',
|
||||
' - [x] Child Checkbox 2',
|
||||
].join('\n');
|
||||
// Parent is NoCheckbox, so it should not change
|
||||
expect(checkboxUtils.propagateStateFromChildren(text)).toBe(text);
|
||||
});
|
||||
|
||||
it('should not ignore plain list item children when determining parent checkbox state', () => {
|
||||
const text = [
|
||||
'- [ ] Parent Checkbox',
|
||||
' - Plain Child 1',
|
||||
' - [x] Actual Checkbox Child',
|
||||
' * Plain Child 2',
|
||||
' - [ ] Grandchild (under plain, ignored for Parent Checkbox)',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [ ] Parent Checkbox', // Becomes checked because "Actual Checkbox Child" is checked
|
||||
' - Plain Child 1',
|
||||
' - [x] Actual Checkbox Child',
|
||||
' * Plain Child 2',
|
||||
' - [ ] Grandchild (under plain, ignored for Parent Checkbox)',
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateFromChildren(text)).toBe(expected);
|
||||
});
|
||||
|
||||
it('parent checkbox remains unchecked if all relevant children are plain or ignored', () => {
|
||||
const customSettings = createSettings({ ignoreSymbols: ['~'] });
|
||||
const utils = new CheckboxUtils(customSettings);
|
||||
const text = [
|
||||
'- [ ] Parent Checkbox',
|
||||
' - Plain Child',
|
||||
' - [~] Ignored Child',
|
||||
].join('\n');
|
||||
// No actual checkbox children to influence the parent
|
||||
expect(utils.propagateStateFromChildren(text)).toBe(text);
|
||||
|
||||
const text2 = [
|
||||
'- [x] Parent Checkbox Initially Checked',
|
||||
' - Plain Child',
|
||||
' - [~] Ignored Child',
|
||||
].join('\n');
|
||||
// Parent should remain checked as no relevant children to change it
|
||||
expect(utils.propagateStateFromChildren(text2)).toBe(text2);
|
||||
});
|
||||
|
||||
it('parent checkbox becomes unchecked if it has one unchecked checkbox child and other plain/ignored children', () => {
|
||||
const text = [
|
||||
'- [x] Parent Checkbox',
|
||||
' - Plain Child',
|
||||
' - [ ] Unchecked Checkbox Child', // This will make parent unchecked
|
||||
' - [x] Checked Checkbox Child (but one is enough)',
|
||||
].join('\n');
|
||||
// Note: propagateStateFromChildren processes from bottom up.
|
||||
// The order of children doesn't matter as much as their collective state.
|
||||
// Let's simplify to ensure clarity:
|
||||
const simplerText = [
|
||||
'- [x] Parent Checkbox',
|
||||
' - Plain Child',
|
||||
' - [ ] Unchecked Checkbox Child',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [ ] Parent Checkbox', // Becomes unchecked
|
||||
' - Plain Child',
|
||||
' - [ ] Unchecked Checkbox Child',
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateFromChildren(simplerText)).toBe(expected);
|
||||
});
|
||||
|
||||
it('handles mixed children correctly, bottom-up', () => {
|
||||
const text = [
|
||||
'- [ ] GP', // 0
|
||||
' - Plain Child of GP', // 1
|
||||
' - [ ] P1 (Checkbox)', // 2
|
||||
' - [x] C1.1 (Checkbox)', // 3
|
||||
' - Plain Grandchild', // 4
|
||||
' - [x] P2 (Checkbox)', // 5 (initially checked)
|
||||
' - Plain Grandchild 2', // 6
|
||||
' - [ ] C2.1 (Unchecked CB)',// 7
|
||||
].join('\n');
|
||||
|
||||
const expected = [
|
||||
'- [ ] GP', // P1 becomes [x], P2 becomes [ ]. So GP is [ ]
|
||||
' - Plain Child of GP',
|
||||
' - [x] P1 (Checkbox)', // Becomes [x] due to C1.1
|
||||
' - [x] C1.1 (Checkbox)',
|
||||
' - Plain Grandchild',
|
||||
' - [ ] P2 (Checkbox)', // Becomes [ ] due to C2.1
|
||||
' - Plain Grandchild 2',
|
||||
' - [ ] C2.1 (Unchecked CB)',
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateFromChildren(text)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Тесты для syncText с Plain List Items (влияние на propagateStateToChildrenFromSingleDiff) ---
|
||||
describe('syncText (and propagateStateToChildrenFromSingleDiff) with Plain List Items', () => {
|
||||
const utils = new CheckboxUtils(createSettings({
|
||||
enableAutomaticChildState: true,
|
||||
enableAutomaticParentState: true, // Keep true to see full effect if needed, though child is main focus
|
||||
}));
|
||||
|
||||
it('should propagate down if a line changes from Plain to Checked Checkbox', () => {
|
||||
const textBefore = [
|
||||
'- Parent Plain Item', // Was plain
|
||||
' - [ ] Child Checkbox',
|
||||
' - Plain Grandchild',
|
||||
].join('\n');
|
||||
const textAfter = [
|
||||
'- [x] Parent Plain Item', // Now a checked checkbox
|
||||
' - [ ] Child Checkbox',
|
||||
' - Plain Grandchild',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [x] Parent Plain Item',
|
||||
' - [x] Child Checkbox', // Propagated
|
||||
' - Plain Grandchild', // Unchanged (it's plain)
|
||||
].join('\n');
|
||||
expect(utils.syncText(textAfter, textBefore)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should NOT propagate down if a line changes from Checked Checkbox to Plain', () => {
|
||||
const textBefore = [
|
||||
'- [x] Parent Checkbox', // Was checkbox
|
||||
' - [ ] Child Checkbox', // This would have been checked by propagation
|
||||
' - Plain Grandchild',
|
||||
].join('\n');
|
||||
const textAfter = [
|
||||
'- Parent Checkbox', // Now plain
|
||||
' - [ ] Child Checkbox',
|
||||
' - Plain Grandchild',
|
||||
].join('\n');
|
||||
// propagateStateToChildrenFromSingleDiff will call propagateStateToChildren
|
||||
// propagateStateToChildren will see the parent is NoCheckbox and do nothing.
|
||||
// Then propagateStateFromChildren runs, but parent is NoCheckbox.
|
||||
expect(utils.syncText(textAfter, textBefore)).toBe(textAfter);
|
||||
});
|
||||
|
||||
it('should correctly propagate up if a child changes, parent is checkbox, and siblings are plain', () => {
|
||||
const textBefore = [
|
||||
'- [ ] Parent Checkbox',
|
||||
' - Plain Sibling',
|
||||
' - [ ] Child Checkbox that changes',
|
||||
].join('\n');
|
||||
const textAfter = [
|
||||
'- [ ] Parent Checkbox',
|
||||
' - Plain Sibling',
|
||||
' - [x] Child Checkbox that changes', // User checks this
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [x] Parent Checkbox', // Propagates up
|
||||
' - Plain Sibling',
|
||||
' - [x] Child Checkbox that changes',
|
||||
].join('\n');
|
||||
expect(utils.syncText(textAfter, textBefore)).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,774 +0,0 @@
|
|||
import { CheckboxUtils } from '../src/checkboxUtils'; // Путь к вашему файлу
|
||||
import { CheckboxState, CheckboxSyncPluginSettings, DEFAULT_SETTINGS } from '../src/types'; // Путь к вашему файлу типов
|
||||
|
||||
// Вспомогательная функция для создания настроек с переопределениями
|
||||
const createSettings = (overrides: Partial<CheckboxSyncPluginSettings> = {}): Readonly<CheckboxSyncPluginSettings> => {
|
||||
return { ...DEFAULT_SETTINGS, ...overrides } as Readonly<CheckboxSyncPluginSettings>;
|
||||
};
|
||||
|
||||
describe('CheckboxUtils', () => {
|
||||
let checkboxUtils: CheckboxUtils;
|
||||
let settings: Readonly<CheckboxSyncPluginSettings>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Используем настройки по умолчанию для большинства тестов
|
||||
settings = createSettings();
|
||||
checkboxUtils = new CheckboxUtils(settings);
|
||||
});
|
||||
|
||||
// --- Тесты для matchCheckboxLine ---
|
||||
describe('matchCheckboxLine', () => {
|
||||
it('should match standard unchecked checkbox', () => {
|
||||
const line = '- [ ] Task';
|
||||
const result = checkboxUtils.matchCheckboxLine(line);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toEqual({
|
||||
indent: 0,
|
||||
marker: '-',
|
||||
checkChar: ' ',
|
||||
checkboxCharPosition: 3,
|
||||
checkboxState: CheckboxState.Unchecked,
|
||||
isChecked: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should match standard checked checkbox', () => {
|
||||
const line = '* [x] Done';
|
||||
const result = checkboxUtils.matchCheckboxLine(line);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toEqual({
|
||||
indent: 0,
|
||||
marker: '*',
|
||||
checkChar: 'x',
|
||||
checkboxCharPosition: 3,
|
||||
checkboxState: CheckboxState.Checked,
|
||||
isChecked: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should match checkbox with indentation', () => {
|
||||
const line = ' + [x] Indented Task';
|
||||
const result = checkboxUtils.matchCheckboxLine(line);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toEqual({
|
||||
indent: 2,
|
||||
marker: '+',
|
||||
checkChar: 'x',
|
||||
checkboxCharPosition: 5, // 2 spaces + 1 marker + 1 space + 1 [ = 5
|
||||
checkboxState: CheckboxState.Checked,
|
||||
isChecked: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should match numbered list checkbox', () => {
|
||||
const line = '1. [ ] Numbered';
|
||||
const result = checkboxUtils.matchCheckboxLine(line);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toEqual({
|
||||
indent: 0,
|
||||
marker: '1.',
|
||||
checkChar: ' ',
|
||||
checkboxCharPosition: 4, // 2 marker + 1 space + 1 [ = 4
|
||||
checkboxState: CheckboxState.Unchecked,
|
||||
isChecked: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should match checkbox with multi-digit numbered list', () => {
|
||||
const line = '10. [x] Double Digit';
|
||||
const result = checkboxUtils.matchCheckboxLine(line);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toEqual({
|
||||
indent: 0,
|
||||
marker: '10.',
|
||||
checkChar: 'x',
|
||||
checkboxCharPosition: 5, // 3 marker + 1 space + 1 [ = 5
|
||||
checkboxState: CheckboxState.Checked,
|
||||
isChecked: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null for lines without checkbox format', () => {
|
||||
expect(checkboxUtils.matchCheckboxLine('Just text')).toBeNull();
|
||||
expect(checkboxUtils.matchCheckboxLine('- Not a checkbox')).toBeNull();
|
||||
expect(checkboxUtils.matchCheckboxLine('- [ ]')).toBeNull(); // No space after ]
|
||||
expect(checkboxUtils.matchCheckboxLine('- [] ')).toBeNull(); // No char inside
|
||||
expect(checkboxUtils.matchCheckboxLine('[ ] Task')).toBeNull(); // No marker
|
||||
expect(checkboxUtils.matchCheckboxLine('-[ ] Task')).toBeNull(); // No space after marker
|
||||
});
|
||||
|
||||
it('should match custom symbols based on settings', () => {
|
||||
const customSettings = createSettings({
|
||||
checkedSymbols: ['X', 'V'],
|
||||
uncheckedSymbols: ['O', '-'],
|
||||
ignoreSymbols: ['~'],
|
||||
});
|
||||
const customUtils = new CheckboxUtils(customSettings);
|
||||
|
||||
const checkedLine = '- [V] Custom Checked';
|
||||
const checkedResult = customUtils.matchCheckboxLine(checkedLine);
|
||||
expect(checkedResult?.checkboxState).toBe(CheckboxState.Checked);
|
||||
expect(checkedResult?.isChecked).toBe(true);
|
||||
expect(checkedResult?.checkChar).toBe('V');
|
||||
|
||||
const uncheckedLine = '* [O] Custom Unchecked';
|
||||
const uncheckedResult = customUtils.matchCheckboxLine(uncheckedLine);
|
||||
expect(uncheckedResult?.checkboxState).toBe(CheckboxState.Unchecked);
|
||||
expect(uncheckedResult?.isChecked).toBe(false);
|
||||
expect(uncheckedResult?.checkChar).toBe('O');
|
||||
|
||||
const ignoredLine = '+ [~] Custom Ignored';
|
||||
const ignoredResult = customUtils.matchCheckboxLine(ignoredLine);
|
||||
expect(ignoredResult?.checkboxState).toBe(CheckboxState.Ignore);
|
||||
expect(ignoredResult?.isChecked).toBeUndefined();
|
||||
expect(ignoredResult?.checkChar).toBe('~');
|
||||
});
|
||||
});
|
||||
|
||||
// --- Тесты для getCheckboxState ---
|
||||
describe('getCheckboxState', () => {
|
||||
it('should return Checked for symbols in checkedSymbols', () => {
|
||||
const customSettings = createSettings({ checkedSymbols: ['x', 'X', '✓'] });
|
||||
const customUtils = new CheckboxUtils(customSettings);
|
||||
expect(customUtils.getCheckboxState('x')).toBe(CheckboxState.Checked);
|
||||
expect(customUtils.getCheckboxState('X')).toBe(CheckboxState.Checked);
|
||||
expect(customUtils.getCheckboxState('✓')).toBe(CheckboxState.Checked);
|
||||
});
|
||||
|
||||
it('should return Unchecked for symbols in uncheckedSymbols', () => {
|
||||
const customSettings = createSettings({ uncheckedSymbols: [' ', '_', '?'] });
|
||||
const customUtils = new CheckboxUtils(customSettings);
|
||||
expect(customUtils.getCheckboxState(' ')).toBe(CheckboxState.Unchecked);
|
||||
expect(customUtils.getCheckboxState('_')).toBe(CheckboxState.Unchecked);
|
||||
expect(customUtils.getCheckboxState('?')).toBe(CheckboxState.Unchecked);
|
||||
});
|
||||
|
||||
it('should return Ignore for symbols in ignoreSymbols', () => {
|
||||
const customSettings = createSettings({ ignoreSymbols: ['-', '~', '>'] });
|
||||
const customUtils = new CheckboxUtils(customSettings);
|
||||
expect(customUtils.getCheckboxState('-')).toBe(CheckboxState.Ignore);
|
||||
expect(customUtils.getCheckboxState('~')).toBe(CheckboxState.Ignore);
|
||||
expect(customUtils.getCheckboxState('>')).toBe(CheckboxState.Ignore);
|
||||
});
|
||||
|
||||
it('should use unknownSymbolPolicy for symbols not in any list', () => {
|
||||
const settingsChecked = createSettings({ unknownSymbolPolicy: CheckboxState.Checked });
|
||||
const utilsChecked = new CheckboxUtils(settingsChecked);
|
||||
expect(utilsChecked.getCheckboxState('?')).toBe(CheckboxState.Checked);
|
||||
|
||||
const settingsUnchecked = createSettings({ unknownSymbolPolicy: CheckboxState.Unchecked });
|
||||
const utilsUnchecked = new CheckboxUtils(settingsUnchecked);
|
||||
expect(utilsUnchecked.getCheckboxState('?')).toBe(CheckboxState.Unchecked);
|
||||
|
||||
const settingsIgnore = createSettings({ unknownSymbolPolicy: CheckboxState.Ignore });
|
||||
const utilsIgnore = new CheckboxUtils(settingsIgnore);
|
||||
expect(utilsIgnore.getCheckboxState('?')).toBe(CheckboxState.Ignore);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Тесты для updateLineCheckboxStateWithInfo ---
|
||||
describe('updateLineCheckboxStateWithInfo', () => {
|
||||
it('should update checkbox state from unchecked to checked', () => {
|
||||
const line = '- [ ] Task';
|
||||
const lineInfo = checkboxUtils.matchCheckboxLine(line)!; // Assume valid line
|
||||
const updatedLine = checkboxUtils.updateLineCheckboxStateWithInfo(line, true, lineInfo);
|
||||
// Uses the first symbol from settings.checkedSymbols ('x' by default)
|
||||
expect(updatedLine).toBe('- [x] Task');
|
||||
});
|
||||
|
||||
it('should update checkbox state from checked to unchecked', () => {
|
||||
const line = '* [x] Done';
|
||||
const lineInfo = checkboxUtils.matchCheckboxLine(line)!;
|
||||
const updatedLine = checkboxUtils.updateLineCheckboxStateWithInfo(line, false, lineInfo);
|
||||
// Uses the first symbol from settings.uncheckedSymbols (' ' by default)
|
||||
expect(updatedLine).toBe('* [ ] Done');
|
||||
});
|
||||
|
||||
it('should use first symbol from settings for update', () => {
|
||||
const customSettings = createSettings({ checkedSymbols: ['V', 'X'], uncheckedSymbols: ['O', ' '] });
|
||||
const customUtils = new CheckboxUtils(customSettings);
|
||||
|
||||
const lineToCheck = '- [O] Task';
|
||||
const infoToCheck = customUtils.matchCheckboxLine(lineToCheck)!;
|
||||
const updatedToCheck = customUtils.updateLineCheckboxStateWithInfo(lineToCheck, true, infoToCheck);
|
||||
expect(updatedToCheck).toBe('- [V] Task'); // Should use 'V'
|
||||
|
||||
const lineToUncheck = '* [V] Done';
|
||||
const infoToUncheck = customUtils.matchCheckboxLine(lineToUncheck)!;
|
||||
const updatedToUncheck = customUtils.updateLineCheckboxStateWithInfo(lineToUncheck, false, infoToUncheck);
|
||||
expect(updatedToUncheck).toBe('* [O] Done'); // Should use 'O'
|
||||
});
|
||||
|
||||
it('should handle indented lines correctly', () => {
|
||||
const line = ' - [ ] Indented';
|
||||
const lineInfo = checkboxUtils.matchCheckboxLine(line)!;
|
||||
const updatedLine = checkboxUtils.updateLineCheckboxStateWithInfo(line, true, lineInfo);
|
||||
expect(updatedLine).toBe(' - [x] Indented');
|
||||
});
|
||||
|
||||
it('should return original line if position is invalid (simulated)', () => {
|
||||
const line = '- [ ] Task';
|
||||
const lineInfo = checkboxUtils.matchCheckboxLine(line)!;
|
||||
// Simulate invalid position
|
||||
const invalidInfo = { ...lineInfo, checkboxCharPosition: 100 };
|
||||
const updatedLine = checkboxUtils.updateLineCheckboxStateWithInfo(line, true, invalidInfo);
|
||||
expect(updatedLine).toBe(line); // Should not change
|
||||
});
|
||||
|
||||
it('should use default checked symbol "x" if checkedSymbols setting is empty', () => {
|
||||
// Создаем настройки с пустым списком checkedSymbols
|
||||
const customSettings = createSettings({ checkedSymbols: [] });
|
||||
const customUtils = new CheckboxUtils(customSettings);
|
||||
const line = '- [ ] Task';
|
||||
const lineInfo = customUtils.matchCheckboxLine(line)!;
|
||||
|
||||
// Пытаемся отметить чекбокс
|
||||
const updatedLine = customUtils.updateLineCheckboxStateWithInfo(line, true, lineInfo);
|
||||
expect(updatedLine).toBe('- [x] Task'); // Ожидаем дефолтный 'x'
|
||||
});
|
||||
|
||||
it('should use default unchecked symbol " " if uncheckedSymbols setting is empty', () => {
|
||||
// Создаем настройки с пустым списком uncheckedSymbols
|
||||
const customSettings = createSettings({ uncheckedSymbols: [] });
|
||||
const customUtils = new CheckboxUtils(customSettings);
|
||||
const line = '- [x] Task'; // Используем 'x' из дефолтных checkedSymbols
|
||||
const lineInfo = customUtils.matchCheckboxLine(line)!;
|
||||
|
||||
// Пытаемся снять отметку
|
||||
const updatedLine = customUtils.updateLineCheckboxStateWithInfo(line, false, lineInfo);
|
||||
expect(updatedLine).toBe('- [ ] Task'); // Ожидаем дефолтный ' ' (пробел)
|
||||
});
|
||||
|
||||
// Усиленный тест для невалидной позиции (с проверкой console.warn)
|
||||
it('should return original line and warn if position is invalid', () => {
|
||||
const line = '- [ ] Task';
|
||||
const lineInfo = checkboxUtils.matchCheckboxLine(line)!;
|
||||
// Мокаем console.warn
|
||||
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
|
||||
|
||||
// Simulate invalid position
|
||||
const invalidInfo = { ...lineInfo, checkboxCharPosition: -1 }; // Невалидная позиция
|
||||
const updatedLineNegative = checkboxUtils.updateLineCheckboxStateWithInfo(line, true, invalidInfo);
|
||||
expect(updatedLineNegative).toBe(line); // Должен вернуть оригинальную строку
|
||||
expect(warnSpy).toHaveBeenCalledTimes(1); // Проверяем вызов warn
|
||||
|
||||
const invalidInfo2 = { ...lineInfo, checkboxCharPosition: 100 }; // Другая невалидная позиция
|
||||
const updatedLineOob = checkboxUtils.updateLineCheckboxStateWithInfo(line, true, invalidInfo2);
|
||||
expect(updatedLineOob).toBe(line); // Должен вернуть оригинальную строку
|
||||
expect(warnSpy).toHaveBeenCalledTimes(2); // Проверяем вызов warn еще раз
|
||||
|
||||
// Восстанавливаем оригинальную функцию console.warn
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
// --- Тесты для propagateStateToChildren ---
|
||||
describe('propagateStateToChildren', () => {
|
||||
const setupUtils = (customSettings?: Partial<CheckboxSyncPluginSettings>) => {
|
||||
return new CheckboxUtils(createSettings(customSettings));
|
||||
}
|
||||
|
||||
it('should check children when parent is checked', () => {
|
||||
const text = [
|
||||
'- [x] Parent', // line 0
|
||||
' - [ ] Child 1',
|
||||
' - [ ] Grandchild',
|
||||
' - [ ] Child 2',
|
||||
'- [ ] Sibling',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [x] Parent',
|
||||
' - [x] Child 1',
|
||||
' - [x] Grandchild',
|
||||
' - [x] Child 2',
|
||||
'- [ ] Sibling', // Sibling should not change
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateToChildren(text, 0)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should uncheck children when parent is unchecked', () => {
|
||||
const text = [
|
||||
'- [ ] Parent', // line 0
|
||||
' - [x] Child 1',
|
||||
' - [x] Grandchild',
|
||||
' - [ ] Child 2', // Already unchecked
|
||||
'- [x] Sibling',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [ ] Parent',
|
||||
' - [ ] Child 1',
|
||||
' - [ ] Grandchild',
|
||||
' - [ ] Child 2',
|
||||
'- [x] Sibling', // Sibling should not change
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateToChildren(text, 0)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should stop propagation at siblings or less indented lines', () => {
|
||||
const text = [
|
||||
' - [x] Parent', // line 0
|
||||
' - [ ] Child',
|
||||
' - [ ] Sibling', // Same indent level
|
||||
'- [ ] Less Indented',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
' - [x] Parent',
|
||||
' - [x] Child', // Changed
|
||||
' - [ ] Sibling', // Not changed
|
||||
'- [ ] Less Indented', // Not changed
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateToChildren(text, 0)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should skip ignored children and their subtrees', () => {
|
||||
const utils = setupUtils({ ignoreSymbols: ['~'] });
|
||||
const text = [
|
||||
'- [x] Parent', // 0
|
||||
' - [ ] Child 1', // 1
|
||||
' - [~] Ignored Child',// 2 (ignore)
|
||||
' - [ ] Skipped GC', // 3 (skipped because parent ignored)
|
||||
' - [ ] Child 2', // 4
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [x] Parent',
|
||||
' - [x] Child 1', // Changed
|
||||
' - [~] Ignored Child',// Unchanged
|
||||
' - [ ] Skipped GC', // Unchanged
|
||||
' - [x] Child 2', // Changed
|
||||
].join('\n');
|
||||
expect(utils.propagateStateToChildren(text, 0)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should do nothing if the parent line is not a checkbox', () => {
|
||||
const text = [
|
||||
'Parent Line',
|
||||
' - [ ] Child',
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateToChildren(text, 0)).toBe(text);
|
||||
});
|
||||
|
||||
it('should do nothing if the parent checkbox is ignored', () => {
|
||||
const utils = setupUtils({ ignoreSymbols: ['~'] });
|
||||
const text = [
|
||||
'- [~] Ignored Parent', // 0
|
||||
' - [ ] Child',
|
||||
].join('\n');
|
||||
expect(utils.propagateStateToChildren(text, 0)).toBe(text);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Тесты для propagateStateFromChildren ---
|
||||
describe('propagateStateFromChildren', () => {
|
||||
const setupUtils = (customSettings?: Partial<CheckboxSyncPluginSettings>) => {
|
||||
return new CheckboxUtils(createSettings(customSettings));
|
||||
}
|
||||
|
||||
it('should check parent if all children are checked', () => {
|
||||
const text = [
|
||||
'- [ ] Parent', // line 0
|
||||
' - [x] Child 1',
|
||||
' - [x] Child 2',
|
||||
' - [x] Grandchild', // Needs parent (Child 2) to be checked first
|
||||
].join('\n');
|
||||
// Expected: Grandchild affects Child 2 -> Child 2 and Child 1 affect Parent
|
||||
const expectedPass1 = [ // After processing Grandchild and Child 2
|
||||
'- [ ] Parent',
|
||||
' - [x] Child 1',
|
||||
' - [x] Child 2', // Stays checked
|
||||
' - [x] Grandchild',
|
||||
].join('\n');
|
||||
const expectedPass2 = [ // After processing Child 1 and Parent
|
||||
'- [x] Parent', // Changes to checked
|
||||
' - [x] Child 1',
|
||||
' - [x] Child 2',
|
||||
' - [x] Grandchild',
|
||||
].join('\n');
|
||||
// Since it processes bottom-up, Child 2 should already be evaluated correctly
|
||||
expect(checkboxUtils.propagateStateFromChildren(text)).toBe(expectedPass2);
|
||||
});
|
||||
|
||||
it('should uncheck parent if any child is unchecked', () => {
|
||||
const text = [
|
||||
'- [x] Parent', // line 0
|
||||
' - [x] Child 1',
|
||||
' - [ ] Child 2', // This one makes the parent unchecked
|
||||
' - [ ] Grandchild',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [ ] Parent', // Changes to unchecked
|
||||
' - [x] Child 1',
|
||||
' - [ ] Child 2',
|
||||
' - [ ] Grandchild',
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateFromChildren(text)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle nested updates correctly (bottom-up)', () => {
|
||||
const text = [
|
||||
'- [ ] Grandparent', // 0
|
||||
' - [ ] Parent 1', // 1
|
||||
' - [x] Child 1.1',// 2
|
||||
' - [x] Child 1.2',// 3
|
||||
' - [x] Parent 2', // 4
|
||||
' - [ ] Child 2.1',// 5 (Makes Parent 2 unchecked)
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [ ] Grandparent', // Becomes unchecked (because Parent 2 becomes unchecked)
|
||||
' - [x] Parent 1', // Becomes checked (Child 1.1, 1.2 are checked)
|
||||
' - [x] Child 1.1',
|
||||
' - [x] Child 1.2',
|
||||
' - [ ] Parent 2', // Becomes unchecked (Child 2.1 is unchecked)
|
||||
' - [ ] Child 2.1',
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateFromChildren(text)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should not change parent state if it has no relevant children', () => {
|
||||
const text = [
|
||||
'- [ ] Parent',
|
||||
'Sibling', // Not a child checkbox
|
||||
' Not indented correctly',
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateFromChildren(text)).toBe(text);
|
||||
});
|
||||
|
||||
it('should ignore non-checkbox lines when determining parent state', () => {
|
||||
const text = [
|
||||
'- [ ] Parent',
|
||||
' - [x] Child 1',
|
||||
' Just text',
|
||||
' - [x] Child 2',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [x] Parent', // Should become checked
|
||||
' - [x] Child 1',
|
||||
' Just text',
|
||||
' - [x] Child 2',
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateFromChildren(text)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should skip ignored children when determining parent state', () => {
|
||||
const utils = setupUtils({ ignoreSymbols: ['~'] });
|
||||
const text = [
|
||||
'- [ ] Parent', // 0
|
||||
' - [x] Child 1', // 1
|
||||
' - [~] Ignored Child',// 2 (skipped)
|
||||
' - [ ] Skipped GC', // 3 (doesn't affect parent)
|
||||
' - [x] Child 2', // 4
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [x] Parent', // Becomes checked (based on Child 1 and Child 2 only)
|
||||
' - [x] Child 1',
|
||||
' - [~] Ignored Child',
|
||||
' - [ ] Skipped GC',
|
||||
' - [x] Child 2',
|
||||
].join('\n');
|
||||
expect(utils.propagateStateFromChildren(text)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should not change parent state if all direct children are ignored', () => {
|
||||
const utils = setupUtils({ ignoreSymbols: ['~'] });
|
||||
const text = [
|
||||
'- [ ] Parent', // 0
|
||||
' - [~] Ignored 1', // 1
|
||||
' - [~] Ignored 2', // 2
|
||||
].join('\n');
|
||||
expect(utils.propagateStateFromChildren(text)).toBe(text); // Parent stays unchecked
|
||||
const text2 = [
|
||||
'- [x] Parent', // 0
|
||||
' - [~] Ignored 1', // 1
|
||||
' - [~] Ignored 2', // 2
|
||||
].join('\n');
|
||||
expect(utils.propagateStateFromChildren(text2)).toBe(text2); // Parent stays checked
|
||||
});
|
||||
|
||||
it('should not update ignored parents', () => {
|
||||
const utils = setupUtils({ ignoreSymbols: ['~'] });
|
||||
const text = [
|
||||
'- [~] Ignored Parent', // 0
|
||||
' - [x] Child 1',
|
||||
' - [x] Child 2',
|
||||
].join('\n');
|
||||
expect(utils.propagateStateFromChildren(text)).toBe(text); // Parent remains ignored
|
||||
});
|
||||
|
||||
it('should stop searching children when a line with equal or lesser indent is found', () => {
|
||||
const text = [
|
||||
'- [ ] Parent 1', // indent 0
|
||||
'Some regular text', // indent 0. Stops search for Parent 1 children.
|
||||
' - [x] Child 1.1', // indent 2. Should be ignored for Parent 1.
|
||||
'- [ ] Parent 2', // indent 0. Processed independently.
|
||||
' - [x] Child 2.1', // indent 2. Child of Parent 2.
|
||||
].join('\n');
|
||||
|
||||
const result = checkboxUtils.propagateStateFromChildren(text);
|
||||
|
||||
const expected = [
|
||||
'- [ ] Parent 1', // Unchanged (no children found before stop)
|
||||
'Some regular text',
|
||||
' - [x] Child 1.1',
|
||||
'- [x] Parent 2', // Updated by Child 2.1
|
||||
' - [x] Child 2.1',
|
||||
].join('\n');
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
|
||||
it('should stop searching children when a sibling checkbox (equal/lesser indent) is found', () => {
|
||||
const text = [
|
||||
'- [ ] Parent 1', // indent 0
|
||||
'- [ ] Sibling Parent', // indent 0. Stops search for Parent 1 children.
|
||||
' - [x] Child SP.1', // indent 2. Belongs to Sibling Parent.
|
||||
].join('\n');
|
||||
|
||||
const result = checkboxUtils.propagateStateFromChildren(text);
|
||||
|
||||
const expected = [
|
||||
'- [ ] Parent 1', // Unchanged (no children found before stop)
|
||||
'- [x] Sibling Parent', // Updated by Child SP.1
|
||||
' - [x] Child SP.1',
|
||||
].join('\n');
|
||||
expect(result).toBe(expected);
|
||||
|
||||
const text2 = [
|
||||
' - [ ] Parent 1', // indent 2
|
||||
' - [x] Child 1.1', // indent 4
|
||||
' - [ ] Sibling Parent', // indent 2. Stops search for Parent 1 children.
|
||||
' - [ ] Child SP.1', // indent 4. Belongs to Sibling Parent.
|
||||
].join('\n');
|
||||
const result2 = checkboxUtils.propagateStateFromChildren(text2);
|
||||
const expected2 = [
|
||||
' - [x] Parent 1', // Updated by Child 1.1
|
||||
' - [x] Child 1.1',
|
||||
' - [ ] Sibling Parent', // Unchanged (Child SP.1 is unchecked)
|
||||
' - [ ] Child SP.1',
|
||||
].join('\n');
|
||||
expect(result2).toBe(expected2);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Тесты для syncText ---
|
||||
describe('syncText', () => {
|
||||
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');
|
||||
|
||||
it('should propagate down only if child sync enabled and diff matches', () => {
|
||||
const utils = new CheckboxUtils(createSettings({
|
||||
enableAutomaticChildState: true,
|
||||
enableAutomaticParentState: false, // Parent sync off
|
||||
}));
|
||||
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 CheckboxUtils(createSettings({
|
||||
enableAutomaticChildState: false, // Child sync off
|
||||
enableAutomaticParentState: true,
|
||||
}));
|
||||
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 CheckboxUtils(createSettings({
|
||||
enableAutomaticChildState: true,
|
||||
enableAutomaticParentState: true,
|
||||
}));
|
||||
// 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);
|
||||
|
||||
// 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 CheckboxUtils(createSettings({
|
||||
enableAutomaticChildState: false,
|
||||
enableAutomaticParentState: false,
|
||||
}));
|
||||
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 CheckboxUtils(createSettings({
|
||||
enableAutomaticChildState: true, // Down propagation enabled
|
||||
enableAutomaticParentState: false,
|
||||
}));
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Тесты для propagateStateToChildrenFromSingleDiff ---
|
||||
describe('propagateStateToChildrenFromSingleDiff', () => {
|
||||
const textBefore = [
|
||||
'- [ ] Parent',
|
||||
' - [ ] Child',
|
||||
].join('\n');
|
||||
|
||||
it('should propagate if only checkbox state changed', () => {
|
||||
const textAfter = [
|
||||
'- [x] Parent', // Only state changed
|
||||
' - [ ] Child',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [x] Parent',
|
||||
' - [x] Child', // Propagated
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should propagate if text content also changed', () => {
|
||||
const textAfter = [
|
||||
'- [x] Parent Changed Text', // State and text changed
|
||||
' - [ ] Child',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [x] Parent Changed Text',
|
||||
' - [x] Child', // Propagated
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should NOT propagate if marker changed', () => {
|
||||
const textAfter = [
|
||||
'* [x] Parent', // Marker changed from - to *
|
||||
' - [ ] Child',
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(textAfter);
|
||||
});
|
||||
|
||||
it('should NOT propagate if indentation changed', () => {
|
||||
const textAfter = [
|
||||
' - [x] Parent', // Indentation changed
|
||||
' - [ ] Child',
|
||||
].join('\n');
|
||||
// This will be detected as > 1 line difference by findDifferentLineIndexes
|
||||
expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(textAfter);
|
||||
});
|
||||
|
||||
|
||||
it('should NOT propagate if multiple lines changed', () => {
|
||||
const textAfter = [
|
||||
'- [x] Parent',
|
||||
' - [x] Child', // Second line also changed
|
||||
].join('\n');
|
||||
expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(textAfter);
|
||||
});
|
||||
|
||||
it('should NOT propagate if changed line is not a checkbox', () => {
|
||||
const textBeforeNonCheck = "Line 1\nLine 2";
|
||||
const textAfterNonCheck = "Line 1 changed\nLine 2";
|
||||
expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfterNonCheck, textBeforeNonCheck)).toBe(textAfterNonCheck);
|
||||
});
|
||||
|
||||
it('should propagate if changed line is an ignored checkbox', () => {
|
||||
const utils = new CheckboxUtils(createSettings({ ignoreSymbols: ['~'] }));
|
||||
const textBeforeIgnored = [
|
||||
'- [~] Parent',
|
||||
' - [ ] Child',
|
||||
].join('\n');
|
||||
const textAfterIgnored = [
|
||||
'- [x] Parent', // Changed FROM ignored TO checked
|
||||
' - [ ] Child',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'- [x] Parent',
|
||||
' - [x] Child', // Propagated
|
||||
].join('\n');
|
||||
|
||||
expect(utils.propagateStateToChildrenFromSingleDiff(textAfterIgnored, textBeforeIgnored)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should return original text if textBefore is undefined', () => {
|
||||
const text = '- [ ] Line';
|
||||
expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(text, undefined)).toBe(text);
|
||||
});
|
||||
|
||||
it('should return original text if line counts differ', () => {
|
||||
const text1 = '- [ ] Line1\n- [ ] Line2';
|
||||
const text2 = '- [ ] Line1';
|
||||
expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(text1, text2)).toBe(text1);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Тесты для findDifferentLineIndexes ---
|
||||
describe('findDifferentLineIndexes', () => {
|
||||
it('should return empty array for identical lines', () => {
|
||||
const lines1 = ['a', 'b', 'c'];
|
||||
const lines2 = ['a', 'b', 'c'];
|
||||
expect(checkboxUtils.findDifferentLineIndexes(lines1, lines2)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return index of the single different line', () => {
|
||||
const lines1 = ['a', 'b', 'c'];
|
||||
const lines2 = ['a', 'B', 'c'];
|
||||
expect(checkboxUtils.findDifferentLineIndexes(lines1, lines2)).toEqual([1]);
|
||||
});
|
||||
|
||||
it('should return indexes of multiple different lines', () => {
|
||||
const lines1 = ['a', 'b', 'c', 'd'];
|
||||
const lines2 = ['A', 'b', 'C', 'd'];
|
||||
expect(checkboxUtils.findDifferentLineIndexes(lines1, lines2)).toEqual([0, 2]);
|
||||
});
|
||||
|
||||
it('should detect differences at start and end', () => {
|
||||
const lines1 = ['a', 'b', 'c'];
|
||||
const lines2 = ['X', 'b', 'Y'];
|
||||
expect(checkboxUtils.findDifferentLineIndexes(lines1, lines2)).toEqual([0, 2]);
|
||||
});
|
||||
|
||||
it('should throw error if line arrays have different lengths', () => {
|
||||
const lines1 = ['a', 'b'];
|
||||
const lines2 = ['a', 'b', 'c'];
|
||||
expect(() => checkboxUtils.findDifferentLineIndexes(lines1, lines2))
|
||||
.toThrow("the length of the lines must be equal");
|
||||
expect(() => checkboxUtils.findDifferentLineIndexes(lines2, lines1))
|
||||
.toThrow("the length of the lines must be equal");
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -17,7 +17,8 @@ const config: Config = {
|
|||
coverageDirectory: "coverage",
|
||||
|
||||
// Indicates which provider should be used to instrument code for coverage
|
||||
coverageProvider: "v8",
|
||||
// coverageProvider: "v8",
|
||||
coverageProvider: "babel",
|
||||
|
||||
// An array of file extensions your modules use
|
||||
moduleFileExtensions: [
|
||||
|
|
|
|||
|
|
@ -1,212 +1,246 @@
|
|||
import { CheckboxState, CheckboxSyncPluginSettings } from "./types";
|
||||
|
||||
export interface CheckboxLineInfo {
|
||||
indent: number;
|
||||
marker: string;
|
||||
checkChar: string;
|
||||
checkboxCharPosition: number;
|
||||
checkboxState: CheckboxState;
|
||||
isChecked: boolean | undefined;
|
||||
indent: number;
|
||||
marker: string;
|
||||
checkChar?: string;
|
||||
checkboxCharPosition?: number;
|
||||
checkboxState: CheckboxState;
|
||||
isChecked?: boolean | undefined;
|
||||
listItemText?: string;
|
||||
}
|
||||
|
||||
export class CheckboxUtils {
|
||||
private settings: Readonly<CheckboxSyncPluginSettings>;
|
||||
constructor(settings: Readonly<CheckboxSyncPluginSettings>) {
|
||||
this.settings = settings;
|
||||
}
|
||||
private settings: Readonly<CheckboxSyncPluginSettings>;
|
||||
constructor(settings: Readonly<CheckboxSyncPluginSettings>) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
matchCheckboxLine(line: string): CheckboxLineInfo | null {
|
||||
const match = line.match(/^(\s*)([*+-]|\d+\.) \[(.)\]\s/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const indent = match[1].length;
|
||||
const marker = match[2];
|
||||
const checkChar = match[3];
|
||||
const checkboxCharPosition = indent + marker.length + 2;
|
||||
const checkboxState = this.getCheckboxState(checkChar);
|
||||
let isChecked = checkboxState === CheckboxState.Ignore ? undefined : checkboxState === CheckboxState.Checked;
|
||||
matchCheckboxLine(line: string): CheckboxLineInfo | null {
|
||||
// const checkboxMatch = line.match(/^(\s*)([*+-]|\d+\.) \[(.)\](\s.*)?$/);
|
||||
// const checkboxMatch = line.match(/^(\s*)([*+-]|\d+\.) \[(.)\](\s|$)(.*)$/);
|
||||
const checkboxMatch = line.match(/^(\s*)([*+-]|\d+\.) \[(.)\]\s(.*)$/);
|
||||
if (checkboxMatch) {
|
||||
const indent = checkboxMatch[1].length;
|
||||
const marker = checkboxMatch[2];
|
||||
const checkChar = checkboxMatch[3];
|
||||
const checkboxCharPosition = indent + marker.length + 2;
|
||||
const checkboxState = this.getCheckboxState(checkChar);
|
||||
const isChecked = checkboxState === CheckboxState.Ignore ? undefined : checkboxState === CheckboxState.Checked;
|
||||
const listItemText = (checkboxMatch[4] || "").trimStart();
|
||||
|
||||
return {
|
||||
indent,
|
||||
marker,
|
||||
checkChar,
|
||||
checkboxCharPosition,
|
||||
checkboxState,
|
||||
isChecked
|
||||
};
|
||||
}
|
||||
return {
|
||||
indent,
|
||||
marker,
|
||||
checkChar,
|
||||
checkboxCharPosition,
|
||||
checkboxState,
|
||||
isChecked,
|
||||
listItemText
|
||||
};
|
||||
}
|
||||
const listItemMatch = line.match(/^(\s*)([*+-]|\d+\.)\s+(?!\[.?\])(.*)$/);
|
||||
if (listItemMatch) {
|
||||
const indent = listItemMatch[1].length;
|
||||
const marker = listItemMatch[2];
|
||||
const listItemText = listItemMatch[3].trimStart();
|
||||
|
||||
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;
|
||||
}
|
||||
return {
|
||||
indent,
|
||||
marker,
|
||||
// checkChar, checkboxCharPosition, isChecked - не применимы
|
||||
checkboxState: CheckboxState.NoCheckbox,
|
||||
listItemText
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
updateLineCheckboxStateWithInfo(line: string, shouldBeChecked: boolean, lineInfo: CheckboxLineInfo): string {
|
||||
const checkedChar = this.settings.checkedSymbols.length > 0 ? this.settings.checkedSymbols[0] : 'x'; // 'x' как дефолт
|
||||
const uncheckedChar = this.settings.uncheckedSymbols.length > 0 ? this.settings.uncheckedSymbols[0] : ' '; // ' ' как дефолт
|
||||
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;
|
||||
}
|
||||
|
||||
const newCheckChar = shouldBeChecked ? checkedChar : uncheckedChar;
|
||||
const pos = lineInfo.checkboxCharPosition;
|
||||
if (pos >= 0 && pos < line.length) {
|
||||
return line.substring(0, pos) + newCheckChar + line.substring(pos + 1);
|
||||
}
|
||||
console.warn("updateLineCheckboxStateWithInfo: Invalid checkbox position in lineInfo for line:", line);
|
||||
return line;
|
||||
}
|
||||
updateLineCheckboxStateWithInfo(line: string, shouldBeChecked: boolean, lineInfo: CheckboxLineInfo): string {
|
||||
if (lineInfo.checkboxState !== CheckboxState.NoCheckbox) {
|
||||
const checkedChar = this.settings.checkedSymbols.length > 0 ? this.settings.checkedSymbols[0] : 'x'; // 'x' как дефолт
|
||||
const uncheckedChar = this.settings.uncheckedSymbols.length > 0 ? this.settings.uncheckedSymbols[0] : ' '; // ' ' как дефолт
|
||||
|
||||
propagateStateToChildren(text: string, parentLine: number): string {
|
||||
// console.log("propagateStateToChildren");
|
||||
const newCheckChar = shouldBeChecked ? checkedChar : uncheckedChar;
|
||||
const pos = lineInfo.checkboxCharPosition!;
|
||||
if (pos >= 0 && pos < line.length) {
|
||||
return line.substring(0, pos) + newCheckChar + line.substring(pos + 1);
|
||||
} else {
|
||||
console.warn("updateLineCheckboxStateWithInfo: Invalid checkbox position in lineInfo for line:", line);
|
||||
return line;
|
||||
}
|
||||
}
|
||||
console.warn("updateLineCheckboxStateWithInfo: Invalid lineInfo(no checkbox) for line:", line);
|
||||
return line;
|
||||
}
|
||||
|
||||
const lines = text.split("\n");
|
||||
propagateStateToChildren(text: string, parentLine: number): string {
|
||||
const lines = text.split("\n");
|
||||
const parentLineInfo = this.matchCheckboxLine(lines[parentLine]);
|
||||
|
||||
const parentLineInfo = this.matchCheckboxLine(lines[parentLine]);
|
||||
if (!parentLineInfo) {
|
||||
console.warn(`checkbox not found in line ${parentLine}`)
|
||||
return text;
|
||||
}
|
||||
if (!parentLineInfo) {
|
||||
console.warn(`checkbox not found in line ${parentLine}`)
|
||||
return text;
|
||||
}
|
||||
|
||||
if (parentLineInfo.checkboxState === CheckboxState.Ignore) {
|
||||
return text;
|
||||
}
|
||||
let parentIsChecked = parentLineInfo.isChecked!;
|
||||
if (parentLineInfo.checkboxState === CheckboxState.Ignore ||
|
||||
parentLineInfo.checkboxState === CheckboxState.NoCheckbox ||
|
||||
parentLineInfo.isChecked === undefined) {
|
||||
return text;
|
||||
}
|
||||
const parentIsChecked = parentLineInfo.isChecked;
|
||||
|
||||
let j = parentLine + 1;
|
||||
let j = parentLine + 1;
|
||||
|
||||
while (j < lines.length) {
|
||||
const childText = lines[j];
|
||||
const childLineInfo = this.matchCheckboxLine(childText);
|
||||
if (!childLineInfo || childLineInfo.indent <= parentLineInfo.indent) break;
|
||||
while (j < lines.length) {
|
||||
const childText = lines[j];
|
||||
const childLineInfo = this.matchCheckboxLine(childText);
|
||||
if (!childLineInfo || childLineInfo.indent <= parentLineInfo.indent) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (childLineInfo.checkboxState !== CheckboxState.Ignore) {
|
||||
lines[j] = this.updateLineCheckboxStateWithInfo(lines[j], parentIsChecked, childLineInfo);
|
||||
j++;
|
||||
} else {
|
||||
//skip children ignore node
|
||||
j++;
|
||||
while (j < lines.length) {
|
||||
const subChildLineInfo = this.matchCheckboxLine(lines[j]);
|
||||
if (!subChildLineInfo || subChildLineInfo.indent <= childLineInfo.indent) {
|
||||
break;
|
||||
};
|
||||
j++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
if (childLineInfo.checkboxState === CheckboxState.Ignore) {
|
||||
//skip children ignore node
|
||||
j++;
|
||||
while (j < lines.length) {
|
||||
const subChildLineInfo = this.matchCheckboxLine(lines[j]);
|
||||
if (!subChildLineInfo || subChildLineInfo.indent <= childLineInfo.indent) {
|
||||
break;
|
||||
};
|
||||
j++;
|
||||
}
|
||||
} else if (childLineInfo.checkboxState === CheckboxState.NoCheckbox) {
|
||||
j++;
|
||||
} else {
|
||||
lines[j] = this.updateLineCheckboxStateWithInfo(lines[j], parentIsChecked, childLineInfo);
|
||||
j++;
|
||||
}
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
propagateStateFromChildren(text: string): string {
|
||||
const lines = text.split("\n");
|
||||
propagateStateFromChildren(text: string): string {
|
||||
const lines = text.split("\n");
|
||||
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const parentLineInfo = this.matchCheckboxLine(lines[i]);
|
||||
if (!parentLineInfo) continue;
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const parentLineInfo = this.matchCheckboxLine(lines[i]);
|
||||
if (!parentLineInfo) continue;
|
||||
|
||||
if (parentLineInfo.checkboxState === CheckboxState.Ignore) {
|
||||
continue;
|
||||
}
|
||||
const parentIsChecked = parentLineInfo.isChecked!;
|
||||
let allChildrenChecked = true;
|
||||
let hasChildren = false;
|
||||
if (
|
||||
parentLineInfo.checkboxState === CheckboxState.Ignore ||
|
||||
parentLineInfo.checkboxState === CheckboxState.NoCheckbox
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const parentIsChecked = parentLineInfo.isChecked!;
|
||||
let allRelevantChildrenChecked = true;
|
||||
let hasRelevantChildren = false;
|
||||
|
||||
for (let j = i + 1; j < lines.length; j++) {
|
||||
const childLineInfo = this.matchCheckboxLine(lines[j]);
|
||||
|
||||
if (!childLineInfo || childLineInfo.indent <= parentLineInfo.indent) break;
|
||||
for (let j = i + 1; j < lines.length; j++) {
|
||||
const childLineInfo = this.matchCheckboxLine(lines[j]);
|
||||
|
||||
if (childLineInfo.checkboxState === CheckboxState.Ignore) {
|
||||
while (j + 1 < lines.length) {
|
||||
const subChildLineInfo = this.matchCheckboxLine(lines[j + 1]);
|
||||
if (!subChildLineInfo || subChildLineInfo.indent <= childLineInfo.indent) {
|
||||
break;
|
||||
};
|
||||
j++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
hasChildren = true;
|
||||
const childrenIsChecked = childLineInfo.isChecked!;
|
||||
if (!childrenIsChecked) {
|
||||
allChildrenChecked = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!childLineInfo || childLineInfo.indent <= parentLineInfo.indent) break;
|
||||
|
||||
if (hasChildren && parentIsChecked !== allChildrenChecked) {
|
||||
lines[i] = this.updateLineCheckboxStateWithInfo(lines[i], allChildrenChecked, parentLineInfo);
|
||||
}
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
if (childLineInfo.checkboxState === CheckboxState.Ignore) {
|
||||
while (j + 1 < lines.length) {
|
||||
const subChildLineInfo = this.matchCheckboxLine(lines[j + 1]);
|
||||
if (!subChildLineInfo || subChildLineInfo.indent <= childLineInfo.indent) {
|
||||
break;
|
||||
};
|
||||
j++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (childLineInfo.checkboxState === CheckboxState.NoCheckbox) {
|
||||
continue;
|
||||
}
|
||||
hasRelevantChildren = true;
|
||||
const childrenIsChecked = childLineInfo.isChecked!;
|
||||
if (!childrenIsChecked) {
|
||||
allRelevantChildrenChecked = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
syncText(text: string, textBefore: string | undefined): string {
|
||||
let newText = text;
|
||||
if (this.settings.enableAutomaticChildState) {
|
||||
newText = this.propagateStateToChildrenFromSingleDiff(text, textBefore);
|
||||
}
|
||||
if (this.settings.enableAutomaticParentState) {
|
||||
newText = this.propagateStateFromChildren(newText);
|
||||
}
|
||||
return newText;
|
||||
}
|
||||
if (hasRelevantChildren && parentIsChecked !== allRelevantChildrenChecked) {
|
||||
lines[i] = this.updateLineCheckboxStateWithInfo(lines[i], allRelevantChildrenChecked, parentLineInfo);
|
||||
}
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
propagateStateToChildrenFromSingleDiff(text: string, textBefore: string | undefined): string {
|
||||
if (!textBefore) return text;
|
||||
const textBeforeLines = textBefore.split('\n');
|
||||
const textLines = text.split('\n');
|
||||
syncText(text: string, textBefore: string | undefined): string {
|
||||
let newText = text;
|
||||
if (this.settings.enableAutomaticChildState) {
|
||||
newText = this.propagateStateToChildrenFromSingleDiff(text, textBefore);
|
||||
}
|
||||
if (this.settings.enableAutomaticParentState) {
|
||||
newText = this.propagateStateFromChildren(newText);
|
||||
}
|
||||
return newText;
|
||||
}
|
||||
|
||||
if (textBeforeLines.length !== textLines.length) return text;
|
||||
propagateStateToChildrenFromSingleDiff(text: string, textBefore: string | undefined): string {
|
||||
if (!textBefore) return text;
|
||||
const textBeforeLines = textBefore.split('\n');
|
||||
const textLines = text.split('\n');
|
||||
|
||||
const diffIndexes = this.findDifferentLineIndexes(textBeforeLines, textLines);
|
||||
if (diffIndexes.length !== 1) {
|
||||
return text;
|
||||
}
|
||||
if (textBeforeLines.length !== textLines.length) return text;
|
||||
|
||||
const index = diffIndexes[0];
|
||||
const diffIndexes = this.findDifferentLineIndexes(textBeforeLines, textLines);
|
||||
if (diffIndexes.length !== 1) {
|
||||
return text;
|
||||
}
|
||||
|
||||
const lineBefore = textBeforeLines[index];
|
||||
const lineAfter = textLines[index];
|
||||
// Получаем информацию о чекбоксе ДО и ПОСЛЕ изменения
|
||||
const lineInfoBefore = this.matchCheckboxLine(lineBefore);
|
||||
const lineInfoAfter = this.matchCheckboxLine(lineAfter);
|
||||
// Проверяем, что:
|
||||
// 1. Обе строки (до и после) являются валидными строками с чекбоксом.
|
||||
// 2. Отступ и маркер списка не изменились (т.е. структура строки та же).
|
||||
// 3. Изменился именно символ внутри скобок [ ].
|
||||
if (
|
||||
lineInfoBefore && lineInfoAfter &&
|
||||
lineInfoBefore.indent === lineInfoAfter.indent &&
|
||||
lineInfoBefore.marker === lineInfoAfter.marker &&
|
||||
lineInfoBefore.checkboxState !== lineInfoAfter.checkboxState
|
||||
) {
|
||||
return this.propagateStateToChildren(text, diffIndexes[0]);
|
||||
}
|
||||
return text;
|
||||
const index = diffIndexes[0];
|
||||
|
||||
}
|
||||
const lineBefore = textBeforeLines[index];
|
||||
const lineAfter = textLines[index];
|
||||
// Получаем информацию о чекбоксе ДО и ПОСЛЕ изменения
|
||||
const lineInfoBefore = this.matchCheckboxLine(lineBefore);
|
||||
const lineInfoAfter = this.matchCheckboxLine(lineAfter);
|
||||
// Проверяем, что:
|
||||
// 1. Обе строки (до и после) являются валидными строками с чекбоксом.
|
||||
// 2. Отступ и маркер списка не изменились (т.е. структура строки та же).
|
||||
// 3. Изменился именно символ внутри скобок [ ].
|
||||
if (
|
||||
lineInfoBefore && lineInfoAfter &&
|
||||
lineInfoBefore.indent === lineInfoAfter.indent &&
|
||||
lineInfoBefore.marker === lineInfoAfter.marker &&
|
||||
lineInfoBefore.checkboxState !== lineInfoAfter.checkboxState
|
||||
) {
|
||||
return this.propagateStateToChildren(text, diffIndexes[0]);
|
||||
}
|
||||
return text;
|
||||
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ export interface CheckboxSyncPluginSettings {
|
|||
export enum CheckboxState {
|
||||
Checked = 'checked',
|
||||
Unchecked = 'unchecked',
|
||||
Ignore = 'ignore'
|
||||
Ignore = 'ignore',
|
||||
NoCheckbox = "noCheckbox"
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: CheckboxSyncPluginSettings = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue