Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions src/__tests__/tasks/data-view-collection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,17 +340,24 @@ describe('DataView task collection', () => {
expect(result).toContain(' - [ ] Child level 2');
});

it('handles orphaned sub-tasks when parent indent is skipped', () => {
// Task at level 2 without a level 1 parent - should be added as top-level
sut = new DataViewTaskCollection(
'## Header 1\n\n- [ ] Parent\n\t\t- [ ] Orphaned child at level 2\n'
);
it('finds nearest parent when indent levels are skipped', () => {
// Task at level 2 without a level 1 parent - should find level 0 parent
sut = new DataViewTaskCollection('## Header 1\n\n- [ ] Parent\n\t\t- [ ] Child at level 2\n');

const tasks = sut.getAllTasks();
// The orphaned task should be added as a top-level task since there's no level 1 parent
expect(tasks.length).toEqual(2);
expect(tasks.length).toEqual(1);
expect(tasks[0].getName()).toEqual('Parent');
expect(tasks[1].getName()).toEqual('Orphaned child at level 2');
expect(tasks[0].hasChildren()).toBe(true);
expect(tasks[0].getChildren()[0].getName()).toEqual('Child at level 2');
});

it('handles truly orphaned sub-tasks with no parent available', () => {
// Indented task at start of section with no parent - should be added as top-level
sut = new DataViewTaskCollection('## Header 1\n\n\t- [ ] Orphaned task with no parent\n');

const tasks = sut.getAllTasks();
expect(tasks.length).toEqual(1);
expect(tasks[0].getName()).toEqual('Orphaned task with no parent');
});

it('handles task with due date in toString when dueDate is set', () => {
Expand Down
49 changes: 43 additions & 6 deletions src/__tests__/tasks/emoji-collection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,17 +394,54 @@ describe('Emoji task collection', () => {
expect(result).toContain(' - [ ] Child level 2');
});

it('handles orphaned sub-tasks when parent indent is skipped', () => {
// Task at level 2 without a level 1 parent - should be added as top-level
it('parses sub-tasks with 4-space indentation (issue #63)', () => {
// 4-space indentation calculates as level 2, but should still find level 0 parent
sut = new EmojiTaskCollection(
'## Header 1\n\n- [ ] Parent\n\t\t- [ ] Orphaned child at level 2\n'
'## Header 1\n\n- [ ] Perl Training setup\n - [x] Initial build ✅ 2026-01-15\n - [ ] Add in extra features\n - [ ] Decide upon the activities\n'
);

const tasks = sut.getAllTasks();
// The orphaned task should be added as a top-level task since there's no level 1 parent
expect(tasks.length).toEqual(2);
expect(tasks.length).toEqual(1);
expect(tasks[0].getName()).toEqual('Perl Training setup');
expect(tasks[0].hasChildren()).toBe(true);
expect(tasks[0].getChildren().length).toEqual(3);
expect(tasks[0].getChildren()[0].getName()).toEqual('Initial build');
expect(tasks[0].getChildren()[1].getName()).toEqual('Add in extra features');
expect(tasks[0].getChildren()[2].getName()).toEqual('Decide upon the activities');
});

it('preserves 4-space indentation pattern when outputting', () => {
sut = new EmojiTaskCollection(
'## Header 1\n\n- [ ] Parent\n - [ ] Child with 4-space indent\n'
);

const tasks = sut.getAllTasks();
expect(tasks.length).toEqual(1);
expect(tasks[0].hasChildren()).toBe(true);

const result = tasks[0].toString();
expect(result).toContain('- [ ] Parent');
expect(result).toContain(' - [ ] Child with 4-space indent');
});

it('finds nearest parent when indent levels are skipped', () => {
// Task at level 2 without a level 1 parent - should find level 0 parent
sut = new EmojiTaskCollection('## Header 1\n\n- [ ] Parent\n\t\t- [ ] Child at level 2\n');

const tasks = sut.getAllTasks();
expect(tasks.length).toEqual(1);
expect(tasks[0].getName()).toEqual('Parent');
expect(tasks[1].getName()).toEqual('Orphaned child at level 2');
expect(tasks[0].hasChildren()).toBe(true);
expect(tasks[0].getChildren()[0].getName()).toEqual('Child at level 2');
});

it('handles truly orphaned sub-tasks with no parent available', () => {
// Indented task at start of section with no parent - should be added as top-level
sut = new EmojiTaskCollection('## Header 1\n\n\t- [ ] Orphaned task with no parent\n');

const tasks = sut.getAllTasks();
expect(tasks.length).toEqual(1);
expect(tasks[0].getName()).toEqual('Orphaned task with no parent');
});

it('handles task with due date in toString when dueDate is set', () => {
Expand Down
7 changes: 5 additions & 2 deletions src/tasks/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ export abstract class TaskCollection {
parentStack[0] = task;
} else {
// Nested task: find the appropriate parent
// Parent is at indentLevel - 1 in the stack
const parentIndex = indentLevel - 1;
// Search backwards from indentLevel - 1 to find the nearest parent
let parentIndex = indentLevel - 1;
while (parentIndex >= 0 && !parentStack[parentIndex]) {
parentIndex--;
}
if (parentIndex >= 0 && parentStack[parentIndex]) {
parentStack[parentIndex].addChild(task);
} else {
Expand Down
6 changes: 3 additions & 3 deletions src/tasks/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ export abstract class Task {
const match = this.line.match(/^(\s*)/);
if (match && match[1]) {
this.indent = match[1];
// Calculate indent level: count tabs as 1, or every 2-4 spaces as 1
// Calculate indent level: count tabs as 1, or spaces as levels
if (this.indent.includes('\t')) {
this.indentLevel = (this.indent.match(/\t/g) || []).length;
} else {
// Assume 2-space or 4-space indentation, use 2 as minimum
this.indentLevel = Math.floor(this.indent.length / 2);
// Any non-zero indentation is at least level 1, then add levels for every 2 spaces
this.indentLevel = Math.max(1, Math.floor(this.indent.length / 2));
}
}
}
Expand Down