Skip to content

Commit dc8e322

Browse files
authored
Merge pull request #3329 from ilandikov/refactor-check-or-uncheck-list-items
refactor: check or uncheck list items
2 parents 0bd5771 + f55d47f commit dc8e322

File tree

2 files changed

+115
-1
lines changed

2 files changed

+115
-1
lines changed

src/Task/ListItem.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export class ListItem {
99

1010
public readonly parent: ListItem | null = null;
1111
public readonly children: ListItem[] = [];
12+
public readonly indentation: string = '';
13+
public readonly listMarker: string = '';
1214
public readonly description: string;
1315
public readonly statusCharacter: string | null = null;
1416

@@ -18,6 +20,8 @@ export class ListItem {
1820
this.description = originalMarkdown.replace(TaskRegularExpressions.listItemRegex, '').trim();
1921
const nonTaskMatch = RegExp(TaskRegularExpressions.nonTaskRegex).exec(originalMarkdown);
2022
if (nonTaskMatch) {
23+
this.indentation = nonTaskMatch[1];
24+
this.listMarker = nonTaskMatch[2];
2125
this.description = nonTaskMatch[5].trim();
2226
this.statusCharacter = nonTaskMatch[4] ?? null;
2327
}
@@ -172,4 +176,19 @@ export class ListItem {
172176
public get precedingHeader(): string | null {
173177
return this.taskLocation.precedingHeader;
174178
}
179+
180+
public checkOrUncheck(): ListItem {
181+
const newStatusCharacter = this.statusCharacter === ' ' ? 'x' : ' ';
182+
const newMarkdown = this.originalMarkdown.replace(
183+
RegExp(TaskRegularExpressions.checkboxRegex),
184+
`[${newStatusCharacter}]`,
185+
);
186+
187+
return new ListItem(newMarkdown, null, this.taskLocation);
188+
}
189+
190+
public toFileLineString(): string {
191+
const statusCharacterToString = this.statusCharacter ? `[${this.statusCharacter}] ` : '';
192+
return `${this.indentation}${this.listMarker} ${statusCharacterToString}${this.description}`;
193+
}
175194
}

tests/Task/ListItem.test.ts

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
*/
44
import moment from 'moment/moment';
55
import { TasksFile } from '../../src/Scripting/TasksFile';
6+
import { ListItem } from '../../src/Task/ListItem';
67
import { Task } from '../../src/Task/Task';
78
import { TaskLocation } from '../../src/Task/TaskLocation';
8-
import { ListItem } from '../../src/Task/ListItem';
99
import { TaskBuilder } from '../TestingTools/TaskBuilder';
1010
import { fromLine } from '../TestingTools/TestHelpers';
1111
import { createChildListItem } from './ListItemHelpers';
@@ -115,6 +115,8 @@ describe('list item parsing', () => {
115115
expect(item.description).toEqual('without checkbox');
116116
expect(item.originalMarkdown).toEqual('- without checkbox');
117117
expect(item.statusCharacter).toEqual(null);
118+
expect(item.indentation).toEqual('');
119+
expect(item.listMarker).toEqual('-');
118120
});
119121

120122
it('should read a list item with checkbox', () => {
@@ -133,6 +135,20 @@ describe('list item parsing', () => {
133135
expect(item.statusCharacter).toEqual('x');
134136
});
135137

138+
it('should read a list item with indentation', () => {
139+
const item = new ListItem(' - indented', null, taskLocation);
140+
141+
expect(item.description).toEqual('indented');
142+
expect(item.indentation).toEqual(' ');
143+
});
144+
145+
it('should read a list marker', () => {
146+
expect(new ListItem('* xxx', null, taskLocation).listMarker).toEqual('*');
147+
expect(new ListItem('- xxx', null, taskLocation).listMarker).toEqual('-');
148+
expect(new ListItem('+ xxx', null, taskLocation).listMarker).toEqual('+');
149+
expect(new ListItem('2. xxx', null, taskLocation).listMarker).toEqual('2.');
150+
});
151+
136152
it('should accept a non list item', () => {
137153
// we tried making the constructor throw if given a non list item
138154
// but it broke lots of normal Task uses in the tests (TaskBuilder)
@@ -144,6 +160,32 @@ describe('list item parsing', () => {
144160
});
145161
});
146162

163+
describe('list item writing', () => {
164+
it('should write a simple list item', () => {
165+
const item = new ListItem('- simple', null, taskLocation);
166+
167+
expect(item.toFileLineString()).toEqual('- simple');
168+
});
169+
170+
it('should write a simple check list item', () => {
171+
const item = new ListItem('- [ ] simple checklist', null, taskLocation);
172+
173+
expect(item.toFileLineString()).toEqual('- [ ] simple checklist');
174+
});
175+
176+
it('should write an indented list item', () => {
177+
const item = new ListItem(' - indented', null, taskLocation);
178+
179+
expect(item.toFileLineString()).toEqual(' - indented');
180+
});
181+
182+
it('should write a list item with a different list marker', () => {
183+
const item = new ListItem('* star', null, taskLocation);
184+
185+
expect(item.toFileLineString()).toEqual('* star');
186+
});
187+
});
188+
147189
describe('related items', () => {
148190
it('should detect if no closest parent task', () => {
149191
const task = fromLine({ line: '- [ ] task' });
@@ -283,3 +325,56 @@ describe('checking if mixed lists are identical', () => {
283325
expect(ListItem.listsAreIdentical(list2, list2)).toEqual(true);
284326
});
285327
});
328+
329+
describe('list item checking and unchecking', () => {
330+
it('should create a checked list item', () => {
331+
const listItem = new ListItem(
332+
'- [ ] description',
333+
new ListItem('- [ ] parent', null, taskLocation),
334+
taskLocation,
335+
);
336+
337+
const checkedListItem = listItem.checkOrUncheck();
338+
339+
expect(checkedListItem.parent).toEqual(null);
340+
expect(checkedListItem.taskLocation).toBe(taskLocation);
341+
expect(checkedListItem.statusCharacter).toEqual('x');
342+
expect(checkedListItem.originalMarkdown).toEqual('- [x] description');
343+
});
344+
345+
it('should create a checked list item and preserve the list marker', () => {
346+
const listItem = new ListItem('* [ ] check me', null, taskLocation);
347+
348+
const checkedListItem = listItem.checkOrUncheck();
349+
350+
expect(checkedListItem.statusCharacter).toEqual('x');
351+
expect(checkedListItem.originalMarkdown).toEqual('* [x] check me');
352+
});
353+
354+
it('should create an unchecked list item', () => {
355+
const listItem = new ListItem('4. [#] uncheck me', null, taskLocation);
356+
357+
const checkedListItem = listItem.checkOrUncheck();
358+
359+
expect(checkedListItem.statusCharacter).toEqual(' ');
360+
expect(checkedListItem.originalMarkdown).toEqual('4. [ ] uncheck me');
361+
});
362+
363+
it('should preserve a non-checklist item', () => {
364+
const listItem = new ListItem('- no checkbox', null, taskLocation);
365+
366+
const newListItem = listItem.checkOrUncheck();
367+
368+
expect(newListItem.statusCharacter).toEqual(null);
369+
expect(newListItem.originalMarkdown).toEqual('- no checkbox');
370+
});
371+
372+
it.failing('should preserve a non-checklist item with checkbox-like string in description', () => {
373+
const listItem = new ListItem('- this looks like a checkbox [f]', null, taskLocation);
374+
375+
const newListItem = listItem.checkOrUncheck();
376+
377+
expect(newListItem.statusCharacter).toEqual(null);
378+
expect(newListItem.originalMarkdown).toEqual('- this looks like a checkbox [f]');
379+
});
380+
});

0 commit comments

Comments
 (0)