mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 10:45:57 +00:00
Merge branch 'fix/sync-md-header'
* fix/sync-md-header: feat(sync-md): add support for markdown content before tasks as needed for joplin #4751
This commit is contained in:
commit
fa5e5325df
16 changed files with 548 additions and 148 deletions
|
|
@ -1,21 +1,18 @@
|
|||
import {
|
||||
TaskBuilder,
|
||||
ParsedTaskBuilder,
|
||||
MarkdownBuilder,
|
||||
createMockConfig,
|
||||
createMockPluginAPI,
|
||||
createTaskHierarchy,
|
||||
generateLargeMarkdown,
|
||||
MarkdownBuilder,
|
||||
measureExecutionTime,
|
||||
ParsedTaskBuilder,
|
||||
TaskBuilder,
|
||||
} from '../test-utils';
|
||||
import { parseMarkdown } from '../../sync/markdown-parser';
|
||||
import { parseMarkdown, parseMarkdownWithHeader } from '../../sync/markdown-parser';
|
||||
import { generateTaskOperations } from '../../sync/generate-task-operations';
|
||||
import { convertTasksToMarkdown } from '../../sync/sp-to-md';
|
||||
import { mdToSp } from '../../sync/md-to-sp';
|
||||
import { spToMd } from '../../sync/sp-to-md';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('../../sync/markdown-parser');
|
||||
jest.mock('../../sync/generate-task-operations');
|
||||
jest.mock('../../helper/file-utils');
|
||||
|
||||
|
|
@ -117,44 +114,7 @@ describe('Comprehensive Sync Tests', () => {
|
|||
.addTask('Add animations', { id: 'animations', indent: 4 })
|
||||
.build();
|
||||
|
||||
// Mock parser to return appropriate structure
|
||||
(parseMarkdown as jest.Mock).mockReturnValue([
|
||||
new ParsedTaskBuilder()
|
||||
.withId('overview')
|
||||
.withTitle('Project Overview')
|
||||
.withNotes('This project contains multiple features')
|
||||
.build(),
|
||||
new ParsedTaskBuilder().withId('feature1').withTitle('Feature 1').build(),
|
||||
new ParsedTaskBuilder()
|
||||
.withId('api')
|
||||
.withTitle('Implement API')
|
||||
.withParentId('feature1')
|
||||
.withIsSubtask(true)
|
||||
.build(),
|
||||
new ParsedTaskBuilder()
|
||||
.withId('tests')
|
||||
.withTitle('Write tests')
|
||||
.withParentId('feature1')
|
||||
.withIsSubtask(true)
|
||||
.withCompleted(true)
|
||||
.withNotes('Unit and integration tests')
|
||||
.build(),
|
||||
new ParsedTaskBuilder().withId('feature2').withTitle('Feature 2').build(),
|
||||
new ParsedTaskBuilder()
|
||||
.withId('ui')
|
||||
.withTitle('Design UI')
|
||||
.withParentId('feature2')
|
||||
.withIsSubtask(true)
|
||||
.build(),
|
||||
new ParsedTaskBuilder()
|
||||
.withId('animations')
|
||||
.withTitle('Add animations')
|
||||
.withParentId('ui')
|
||||
.withIsSubtask(true)
|
||||
.build(),
|
||||
]);
|
||||
|
||||
// Mock operations
|
||||
// Mock operations instead of parser since we're testing integration
|
||||
(generateTaskOperations as jest.Mock).mockReturnValue([
|
||||
{
|
||||
type: 'create',
|
||||
|
|
@ -165,7 +125,6 @@ describe('Comprehensive Sync Tests', () => {
|
|||
|
||||
await mdToSp(markdown, 'test-project');
|
||||
|
||||
expect(parseMarkdown).toHaveBeenCalledWith(markdown);
|
||||
expect(generateTaskOperations).toHaveBeenCalled();
|
||||
expect(mockPluginAPI.batchUpdateForProject).toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -175,20 +134,12 @@ describe('Comprehensive Sync Tests', () => {
|
|||
it('should handle large markdown efficiently', async () => {
|
||||
const largeMarkdown = generateLargeMarkdown(1000);
|
||||
|
||||
// Mock parseMarkdown to simulate parsing
|
||||
(parseMarkdown as jest.Mock).mockImplementation((content) => {
|
||||
// Simulate parsing time
|
||||
const lines = content.split('\n');
|
||||
return lines
|
||||
.filter((line: string) => line.match(/^[\s]*- \[/))
|
||||
.map((line: string, index: number) =>
|
||||
new ParsedTaskBuilder().withLine(index).withTitle(`Task ${index}`).build(),
|
||||
);
|
||||
});
|
||||
|
||||
// Test actual parsing performance without mocking
|
||||
const time = await measureExecutionTime(() => {
|
||||
const result = parseMarkdown(largeMarkdown);
|
||||
expect(result.length).toBeGreaterThan(900); // Some have notes
|
||||
// The parser only returns tasks at depth 0 and 1 (parents and subtasks)
|
||||
// Tasks with indent >= 4 (depth >= 2) are converted to notes
|
||||
expect(result.length).toBeGreaterThan(200); // Should have many tasks
|
||||
});
|
||||
|
||||
expect(time).toBeLessThan(100); // Should parse in under 100ms
|
||||
|
|
@ -201,9 +152,9 @@ describe('Comprehensive Sync Tests', () => {
|
|||
});
|
||||
|
||||
// Convert to markdown
|
||||
const markdownTime = await measureExecutionTime(() =>
|
||||
convertTasksToMarkdown(tasks),
|
||||
);
|
||||
const markdownTime = await measureExecutionTime(() => {
|
||||
convertTasksToMarkdown(tasks);
|
||||
});
|
||||
|
||||
expect(markdownTime).toBeLessThan(50);
|
||||
|
||||
|
|
@ -216,9 +167,9 @@ describe('Comprehensive Sync Tests', () => {
|
|||
})),
|
||||
);
|
||||
|
||||
const opsTime = await measureExecutionTime(() =>
|
||||
generateTaskOperations([], tasks, 'test-project'),
|
||||
);
|
||||
const opsTime = await measureExecutionTime(() => {
|
||||
generateTaskOperations([], tasks, 'test-project');
|
||||
});
|
||||
|
||||
expect(opsTime).toBeLessThan(50);
|
||||
});
|
||||
|
|
@ -323,20 +274,9 @@ describe('Comprehensive Sync Tests', () => {
|
|||
// First cycle: SP to MD
|
||||
const markdown1 = convertTasksToMarkdown(initialTasks);
|
||||
|
||||
// Mock parse to return equivalent structure
|
||||
(parseMarkdown as jest.Mock).mockReturnValue(
|
||||
initialTasks.map((t) =>
|
||||
new ParsedTaskBuilder()
|
||||
.withId(t.id)
|
||||
.withTitle(t.title)
|
||||
.withCompleted(t.isDone)
|
||||
.withParentId(t.parentId)
|
||||
.build(),
|
||||
),
|
||||
);
|
||||
|
||||
// Second cycle: MD to SP
|
||||
const parsed = parseMarkdown(markdown1);
|
||||
// Second cycle: MD to SP - parse the actual markdown
|
||||
const result = parseMarkdownWithHeader(markdown1);
|
||||
const parsed = result.tasks;
|
||||
|
||||
// Verify consistency
|
||||
expect(parsed).toHaveLength(initialTasks.length);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { parseMarkdown, parseMarkdownWithErrors } from '../../sync/markdown-parser';
|
||||
import { generateTaskOperations } from '../../sync/generate-task-operations';
|
||||
import { Task } from '@super-productivity/plugin-api';
|
||||
|
||||
describe('Error Scenarios and Boundary Conditions', () => {
|
||||
describe('Markdown Parser - Error Scenarios', () => {
|
||||
|
|
@ -313,7 +312,7 @@ describe('Error Scenarios and Boundary Conditions', () => {
|
|||
|
||||
maliciousPatterns.forEach((pattern) => {
|
||||
const start = Date.now();
|
||||
const tasks = parseMarkdown(pattern);
|
||||
parseMarkdown(pattern);
|
||||
const duration = Date.now() - start;
|
||||
|
||||
// Should not hang or take excessive time
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { startFileWatcher, stopFileWatcher } from '../../sync/file-watcher';
|
||||
import * as path from 'path';
|
||||
|
||||
// Set up Node.js environment for tests
|
||||
if (typeof setImmediate === 'undefined') {
|
||||
(global as any).setImmediate = (fn: Function) => setTimeout(fn, 0);
|
||||
(global as any).setImmediate = (fn: () => void) => setTimeout(fn, 0);
|
||||
}
|
||||
|
||||
// Mock PluginAPI globally
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { mdToSp } from '../../sync/md-to-sp';
|
||||
import { parseMarkdown } from '../../sync/markdown-parser';
|
||||
import { parseMarkdownWithHeader } from '../../sync/markdown-parser';
|
||||
import { generateTaskOperations } from '../../sync/generate-task-operations';
|
||||
import { Task } from '@super-productivity/plugin-api';
|
||||
|
||||
|
|
@ -12,6 +12,19 @@ const mockPluginAPI = {
|
|||
getTasks: jest.fn(),
|
||||
batchUpdateForProject: jest.fn(),
|
||||
getAllProjects: jest.fn(),
|
||||
log: {
|
||||
critical: jest.fn(),
|
||||
err: jest.fn(),
|
||||
error: jest.fn(),
|
||||
log: jest.fn(),
|
||||
normal: jest.fn(),
|
||||
info: jest.fn(),
|
||||
verbose: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
},
|
||||
persistDataSynced: jest.fn(),
|
||||
loadSyncedData: jest.fn(),
|
||||
};
|
||||
|
||||
(global as any).PluginAPI = mockPluginAPI;
|
||||
|
|
@ -71,12 +84,15 @@ describe('MD to SP Sync - Complete Tests', () => {
|
|||
},
|
||||
];
|
||||
|
||||
(parseMarkdown as jest.Mock).mockReturnValue(mockParsedTasks);
|
||||
(parseMarkdownWithHeader as jest.Mock).mockReturnValue({
|
||||
tasks: mockParsedTasks,
|
||||
errors: [],
|
||||
});
|
||||
(generateTaskOperations as jest.Mock).mockReturnValue(mockOperations);
|
||||
|
||||
await mdToSp(mockMarkdownContent, mockProjectId);
|
||||
|
||||
expect(parseMarkdown).toHaveBeenCalledWith(mockMarkdownContent);
|
||||
expect(parseMarkdownWithHeader).toHaveBeenCalledWith(mockMarkdownContent);
|
||||
expect(mockPluginAPI.getTasks).toHaveBeenCalledWith();
|
||||
expect(generateTaskOperations).toHaveBeenCalledWith(
|
||||
mockParsedTasks,
|
||||
|
|
@ -90,12 +106,12 @@ describe('MD to SP Sync - Complete Tests', () => {
|
|||
});
|
||||
|
||||
it('should handle empty markdown content', async () => {
|
||||
(parseMarkdown as jest.Mock).mockReturnValue([]);
|
||||
(parseMarkdownWithHeader as jest.Mock).mockReturnValue({ tasks: [], errors: [] });
|
||||
(generateTaskOperations as jest.Mock).mockReturnValue([]);
|
||||
|
||||
await mdToSp('', mockProjectId);
|
||||
|
||||
expect(parseMarkdown).toHaveBeenCalledWith('');
|
||||
expect(parseMarkdownWithHeader).toHaveBeenCalledWith('');
|
||||
// batchUpdateForProject won't be called with empty operations
|
||||
expect(mockPluginAPI.batchUpdateForProject).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -103,12 +119,12 @@ describe('MD to SP Sync - Complete Tests', () => {
|
|||
it('should handle markdown with only whitespace', async () => {
|
||||
const whitespaceContent = ' \n\n \t ';
|
||||
|
||||
(parseMarkdown as jest.Mock).mockReturnValue([]);
|
||||
(parseMarkdownWithHeader as jest.Mock).mockReturnValue({ tasks: [], errors: [] });
|
||||
(generateTaskOperations as jest.Mock).mockReturnValue([]);
|
||||
|
||||
await mdToSp(whitespaceContent, mockProjectId);
|
||||
|
||||
expect(parseMarkdown).toHaveBeenCalledWith(whitespaceContent);
|
||||
expect(parseMarkdownWithHeader).toHaveBeenCalledWith(whitespaceContent);
|
||||
// batchUpdateForProject won't be called with empty operations
|
||||
expect(mockPluginAPI.batchUpdateForProject).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -150,7 +166,10 @@ describe('MD to SP Sync - Complete Tests', () => {
|
|||
];
|
||||
|
||||
mockPluginAPI.getTasks.mockResolvedValue(existingTasks);
|
||||
(parseMarkdown as jest.Mock).mockReturnValue(mockParsedTasks);
|
||||
(parseMarkdownWithHeader as jest.Mock).mockReturnValue({
|
||||
tasks: mockParsedTasks,
|
||||
errors: [],
|
||||
});
|
||||
(generateTaskOperations as jest.Mock).mockReturnValue(mockOperations);
|
||||
|
||||
await mdToSp(mockMarkdownContent, mockProjectId);
|
||||
|
|
@ -190,7 +209,7 @@ describe('MD to SP Sync - Complete Tests', () => {
|
|||
it('should handle parseMarkdown error', async () => {
|
||||
const error = new Error('Parse error');
|
||||
|
||||
(parseMarkdown as jest.Mock).mockImplementation(() => {
|
||||
(parseMarkdownWithHeader as jest.Mock).mockImplementation(() => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
|
|
@ -211,7 +230,10 @@ describe('MD to SP Sync - Complete Tests', () => {
|
|||
},
|
||||
];
|
||||
|
||||
(parseMarkdown as jest.Mock).mockReturnValue(parsedTasks);
|
||||
(parseMarkdownWithHeader as jest.Mock).mockReturnValue({
|
||||
tasks: parsedTasks,
|
||||
errors: [],
|
||||
});
|
||||
(generateTaskOperations as jest.Mock).mockReturnValue([
|
||||
{ type: 'create', data: { title: 'test' } },
|
||||
]);
|
||||
|
|
@ -238,7 +260,10 @@ describe('MD to SP Sync - Complete Tests', () => {
|
|||
},
|
||||
}));
|
||||
|
||||
(parseMarkdown as jest.Mock).mockReturnValue(largeTasks);
|
||||
(parseMarkdownWithHeader as jest.Mock).mockReturnValue({
|
||||
tasks: largeTasks,
|
||||
errors: [],
|
||||
});
|
||||
(generateTaskOperations as jest.Mock).mockReturnValue(largeOperations);
|
||||
|
||||
await mdToSp('large content', mockProjectId);
|
||||
|
|
@ -277,7 +302,10 @@ describe('MD to SP Sync - Complete Tests', () => {
|
|||
},
|
||||
];
|
||||
|
||||
(parseMarkdown as jest.Mock).mockReturnValue(orderedTasks);
|
||||
(parseMarkdownWithHeader as jest.Mock).mockReturnValue({
|
||||
tasks: orderedTasks,
|
||||
errors: [],
|
||||
});
|
||||
(generateTaskOperations as jest.Mock).mockImplementation((tasks) => {
|
||||
// Verify tasks are passed in the correct order
|
||||
expect(tasks).toEqual(orderedTasks);
|
||||
|
|
@ -314,7 +342,10 @@ describe('MD to SP Sync - Complete Tests', () => {
|
|||
},
|
||||
];
|
||||
|
||||
(parseMarkdown as jest.Mock).mockReturnValue(specialTasks);
|
||||
(parseMarkdownWithHeader as jest.Mock).mockReturnValue({
|
||||
tasks: specialTasks,
|
||||
errors: [],
|
||||
});
|
||||
(generateTaskOperations as jest.Mock).mockReturnValue([]);
|
||||
|
||||
await mdToSp(mockMarkdownContent, mockProjectId);
|
||||
|
|
@ -352,7 +383,10 @@ describe('MD to SP Sync - Complete Tests', () => {
|
|||
},
|
||||
];
|
||||
|
||||
(parseMarkdown as jest.Mock).mockReturnValue(nestedTasks);
|
||||
(parseMarkdownWithHeader as jest.Mock).mockReturnValue({
|
||||
tasks: nestedTasks,
|
||||
errors: [],
|
||||
});
|
||||
(generateTaskOperations as jest.Mock).mockReturnValue([]);
|
||||
|
||||
await mdToSp(mockMarkdownContent, mockProjectId);
|
||||
|
|
@ -361,7 +395,7 @@ describe('MD to SP Sync - Complete Tests', () => {
|
|||
});
|
||||
|
||||
it('should skip sync when no operations are needed', async () => {
|
||||
(parseMarkdown as jest.Mock).mockReturnValue([]);
|
||||
(parseMarkdownWithHeader as jest.Mock).mockReturnValue({ tasks: [], errors: [] });
|
||||
(generateTaskOperations as jest.Mock).mockReturnValue([]);
|
||||
|
||||
await mdToSp(mockMarkdownContent, mockProjectId);
|
||||
|
|
|
|||
|
|
@ -5,18 +5,43 @@ import { Task } from '@super-productivity/plugin-api';
|
|||
|
||||
// Mock dependencies
|
||||
jest.mock('../../helper/file-utils');
|
||||
jest.mock('../../../shared/logger', () => ({
|
||||
log: {
|
||||
critical: jest.fn(),
|
||||
err: jest.fn(),
|
||||
error: jest.fn(),
|
||||
log: jest.fn(),
|
||||
normal: jest.fn(),
|
||||
info: jest.fn(),
|
||||
verbose: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock PluginAPI
|
||||
global.PluginAPI = {
|
||||
(global as any).PluginAPI = {
|
||||
getTasks: jest.fn(),
|
||||
getAllProjects: jest.fn(),
|
||||
log: {
|
||||
critical: jest.fn(),
|
||||
err: jest.fn(),
|
||||
error: jest.fn(),
|
||||
log: jest.fn(),
|
||||
normal: jest.fn(),
|
||||
info: jest.fn(),
|
||||
verbose: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
},
|
||||
persistDataSynced: jest.fn(),
|
||||
loadSyncedData: jest.fn(),
|
||||
} as any;
|
||||
|
||||
describe('spToMd', () => {
|
||||
const mockConfig: LocalUserCfg = {
|
||||
filePath: '/test/tasks.md',
|
||||
projectId: 'test-project',
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -66,8 +91,10 @@ describe('spToMd', () => {
|
|||
},
|
||||
];
|
||||
|
||||
(global.PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
(global.PluginAPI.getAllProjects as jest.Mock).mockResolvedValue(mockProjects);
|
||||
((global as any).PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
((global as any).PluginAPI.getAllProjects as jest.Mock).mockResolvedValue(
|
||||
mockProjects,
|
||||
);
|
||||
|
||||
await spToMd(mockConfig);
|
||||
|
||||
|
|
@ -131,8 +158,10 @@ describe('spToMd', () => {
|
|||
},
|
||||
];
|
||||
|
||||
(global.PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
(global.PluginAPI.getAllProjects as jest.Mock).mockResolvedValue(mockProjects);
|
||||
((global as any).PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
((global as any).PluginAPI.getAllProjects as jest.Mock).mockResolvedValue(
|
||||
mockProjects,
|
||||
);
|
||||
|
||||
await spToMd(mockConfig);
|
||||
|
||||
|
|
@ -185,8 +214,10 @@ describe('spToMd', () => {
|
|||
},
|
||||
];
|
||||
|
||||
(global.PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
(global.PluginAPI.getAllProjects as jest.Mock).mockResolvedValue(mockProjects);
|
||||
((global as any).PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
((global as any).PluginAPI.getAllProjects as jest.Mock).mockResolvedValue(
|
||||
mockProjects,
|
||||
);
|
||||
|
||||
await spToMd(mockConfig);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,19 @@ jest.mock('../../sync/sp-to-md');
|
|||
jest.mock('../../sync/md-to-sp');
|
||||
jest.mock('../../helper/file-utils');
|
||||
jest.mock('../../sync/verify-sync');
|
||||
jest.mock('../../../shared/logger', () => ({
|
||||
log: {
|
||||
critical: jest.fn(),
|
||||
err: jest.fn(),
|
||||
error: jest.fn(),
|
||||
log: jest.fn(),
|
||||
normal: jest.fn(),
|
||||
info: jest.fn(),
|
||||
verbose: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Import after mocking
|
||||
import { spToMd } from '../../sync/sp-to-md';
|
||||
|
|
@ -21,6 +34,23 @@ import {
|
|||
} from '../../helper/file-utils';
|
||||
import { verifySyncState, logSyncVerification } from '../../sync/verify-sync';
|
||||
|
||||
// Mock PluginAPI with log methods
|
||||
(global as any).PluginAPI = {
|
||||
log: {
|
||||
critical: jest.fn(),
|
||||
err: jest.fn(),
|
||||
error: jest.fn(),
|
||||
log: jest.fn(),
|
||||
normal: jest.fn(),
|
||||
info: jest.fn(),
|
||||
verbose: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
},
|
||||
persistDataSynced: jest.fn(),
|
||||
loadSyncedData: jest.fn(),
|
||||
};
|
||||
|
||||
// Mock console to reduce noise
|
||||
beforeAll(() => {
|
||||
jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
|
@ -55,8 +85,8 @@ describe('Sync Manager Debounce Behavior', () => {
|
|||
return Promise.resolve();
|
||||
});
|
||||
(mdToSp as jest.Mock).mockImplementation(() => {
|
||||
// Return a resolved promise
|
||||
return Promise.resolve();
|
||||
// Return a resolved promise with the expected structure
|
||||
return Promise.resolve({ header: undefined });
|
||||
});
|
||||
(verifySyncState as jest.Mock).mockResolvedValue({ isInSync: true, differences: [] });
|
||||
(logSyncVerification as jest.Mock).mockReturnValue(undefined);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import * as fileUtils from '../../helper/file-utils';
|
|||
import { spToMd } from '../../sync/sp-to-md';
|
||||
import { mdToSp } from '../../sync/md-to-sp';
|
||||
import { verifySyncState, logSyncVerification } from '../../sync/verify-sync';
|
||||
import { PluginHooks } from '@super-productivity/plugin-api';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('../../sync/file-watcher');
|
||||
|
|
@ -13,6 +12,38 @@ jest.mock('../../helper/file-utils');
|
|||
jest.mock('../../sync/sp-to-md');
|
||||
jest.mock('../../sync/md-to-sp');
|
||||
jest.mock('../../sync/verify-sync');
|
||||
jest.mock('../../../shared/logger', () => ({
|
||||
log: {
|
||||
critical: jest.fn(),
|
||||
err: jest.fn(),
|
||||
error: jest.fn(),
|
||||
log: jest.fn(),
|
||||
normal: jest.fn(),
|
||||
info: jest.fn(),
|
||||
verbose: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock PluginAPI
|
||||
(global as any).PluginAPI = {
|
||||
registerHook: jest.fn(),
|
||||
onWindowFocusChange: jest.fn(),
|
||||
log: {
|
||||
critical: jest.fn(),
|
||||
err: jest.fn(),
|
||||
error: jest.fn(),
|
||||
log: jest.fn(),
|
||||
normal: jest.fn(),
|
||||
info: jest.fn(),
|
||||
verbose: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
},
|
||||
persistDataSynced: jest.fn(),
|
||||
loadSyncedData: jest.fn(),
|
||||
};
|
||||
|
||||
describe('Sync Manager - Edge Cases', () => {
|
||||
const mockConfig: LocalUserCfg = {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { Task } from '@super-productivity/plugin-api';
|
|||
jest.mock('../../helper/file-utils');
|
||||
|
||||
// Mock PluginAPI
|
||||
global.PluginAPI = {
|
||||
(global as any).PluginAPI = {
|
||||
getTasks: jest.fn(),
|
||||
getAllProjects: jest.fn(),
|
||||
} as any;
|
||||
|
|
@ -20,7 +20,6 @@ describe('verifySyncState', () => {
|
|||
const mockConfig: LocalUserCfg = {
|
||||
filePath: '/test/tasks.md',
|
||||
projectId: 'test-project',
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -55,8 +54,8 @@ describe('verifySyncState', () => {
|
|||
const mockMarkdown = `- [ ] <!--task1--> Task 1
|
||||
- [x] <!--task2--> Task 2`;
|
||||
|
||||
(global.PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
(global.PluginAPI.getAllProjects as jest.Mock).mockResolvedValue([]);
|
||||
((global as any).PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
((global as any).PluginAPI.getAllProjects as jest.Mock).mockResolvedValue([]);
|
||||
(fileUtils.readTasksFile as jest.Mock).mockResolvedValue(mockMarkdown);
|
||||
|
||||
const result = await verifySyncState(mockConfig);
|
||||
|
|
@ -85,8 +84,8 @@ describe('verifySyncState', () => {
|
|||
|
||||
const mockMarkdown = `- [ ] <!--task1--> Task 1`;
|
||||
|
||||
(global.PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
(global.PluginAPI.getAllProjects as jest.Mock).mockResolvedValue([]);
|
||||
((global as any).PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
((global as any).PluginAPI.getAllProjects as jest.Mock).mockResolvedValue([]);
|
||||
(fileUtils.readTasksFile as jest.Mock).mockResolvedValue(mockMarkdown);
|
||||
|
||||
const result = await verifySyncState(mockConfig);
|
||||
|
|
@ -114,8 +113,8 @@ describe('verifySyncState', () => {
|
|||
const mockMarkdown = `- [ ] <!--task1--> Task 1
|
||||
- [ ] <!--task2--> Task 2`;
|
||||
|
||||
(global.PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
(global.PluginAPI.getAllProjects as jest.Mock).mockResolvedValue([]);
|
||||
((global as any).PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
((global as any).PluginAPI.getAllProjects as jest.Mock).mockResolvedValue([]);
|
||||
(fileUtils.readTasksFile as jest.Mock).mockResolvedValue(mockMarkdown);
|
||||
|
||||
const result = await verifySyncState(mockConfig);
|
||||
|
|
@ -142,8 +141,8 @@ describe('verifySyncState', () => {
|
|||
|
||||
const mockMarkdown = `- [ ] <!--task1--> Task 1 MD`;
|
||||
|
||||
(global.PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
(global.PluginAPI.getAllProjects as jest.Mock).mockResolvedValue([]);
|
||||
((global as any).PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
((global as any).PluginAPI.getAllProjects as jest.Mock).mockResolvedValue([]);
|
||||
(fileUtils.readTasksFile as jest.Mock).mockResolvedValue(mockMarkdown);
|
||||
|
||||
const result = await verifySyncState(mockConfig);
|
||||
|
|
@ -171,8 +170,8 @@ describe('verifySyncState', () => {
|
|||
|
||||
const mockMarkdown = `- [ ] <!--task1--> Task 1`;
|
||||
|
||||
(global.PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
(global.PluginAPI.getAllProjects as jest.Mock).mockResolvedValue([]);
|
||||
((global as any).PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
((global as any).PluginAPI.getAllProjects as jest.Mock).mockResolvedValue([]);
|
||||
(fileUtils.readTasksFile as jest.Mock).mockResolvedValue(mockMarkdown);
|
||||
|
||||
const result = await verifySyncState(mockConfig);
|
||||
|
|
@ -216,8 +215,10 @@ describe('verifySyncState', () => {
|
|||
const mockMarkdown = `- [ ] <!--task2--> Task 2
|
||||
- [ ] <!--task1--> Task 1`;
|
||||
|
||||
(global.PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
(global.PluginAPI.getAllProjects as jest.Mock).mockResolvedValue(mockProjects);
|
||||
((global as any).PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
((global as any).PluginAPI.getAllProjects as jest.Mock).mockResolvedValue(
|
||||
mockProjects,
|
||||
);
|
||||
(fileUtils.readTasksFile as jest.Mock).mockResolvedValue(mockMarkdown);
|
||||
|
||||
const result = await verifySyncState(mockConfig);
|
||||
|
|
@ -261,8 +262,8 @@ describe('verifySyncState', () => {
|
|||
- [ ] <!--child2--> Child 2
|
||||
- [ ] <!--child1--> Child 1`;
|
||||
|
||||
(global.PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
(global.PluginAPI.getAllProjects as jest.Mock).mockResolvedValue([]);
|
||||
((global as any).PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
((global as any).PluginAPI.getAllProjects as jest.Mock).mockResolvedValue([]);
|
||||
(fileUtils.readTasksFile as jest.Mock).mockResolvedValue(mockMarkdown);
|
||||
|
||||
const result = await verifySyncState(mockConfig);
|
||||
|
|
@ -288,8 +289,8 @@ describe('verifySyncState', () => {
|
|||
} as Task,
|
||||
];
|
||||
|
||||
(global.PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
(global.PluginAPI.getAllProjects as jest.Mock).mockResolvedValue([]);
|
||||
((global as any).PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
((global as any).PluginAPI.getAllProjects as jest.Mock).mockResolvedValue([]);
|
||||
(fileUtils.readTasksFile as jest.Mock).mockResolvedValue('');
|
||||
|
||||
const result = await verifySyncState(mockConfig);
|
||||
|
|
@ -323,8 +324,8 @@ describe('verifySyncState', () => {
|
|||
const mockMarkdown = `- [ ] <!--task1--> Task 1
|
||||
- [ ] <!--task2--> Task 2`;
|
||||
|
||||
(global.PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
(global.PluginAPI.getAllProjects as jest.Mock).mockResolvedValue([]);
|
||||
((global as any).PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
((global as any).PluginAPI.getAllProjects as jest.Mock).mockResolvedValue([]);
|
||||
(fileUtils.readTasksFile as jest.Mock).mockResolvedValue(mockMarkdown);
|
||||
|
||||
const result = await verifySyncState(mockConfig);
|
||||
|
|
@ -348,8 +349,8 @@ describe('verifySyncState', () => {
|
|||
const mockMarkdown = `- [ ] <!--task1--> Task 1
|
||||
MD notes`;
|
||||
|
||||
(global.PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
(global.PluginAPI.getAllProjects as jest.Mock).mockResolvedValue([]);
|
||||
((global as any).PluginAPI.getTasks as jest.Mock).mockResolvedValue(mockTasks);
|
||||
((global as any).PluginAPI.getAllProjects as jest.Mock).mockResolvedValue([]);
|
||||
(fileUtils.readTasksFile as jest.Mock).mockResolvedValue(mockMarkdown);
|
||||
|
||||
const result = await verifySyncState(mockConfig);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Task } from '@super-productivity/plugin-api';
|
||||
import { ParsedTask } from '../sync/types';
|
||||
import { ParsedTask } from '../sync/markdown-parser';
|
||||
import { LocalUserCfg } from '../local-config';
|
||||
|
||||
/**
|
||||
|
|
@ -167,7 +167,7 @@ export const createMockConfig = (overrides?: Partial<LocalUserCfg>): LocalUserCf
|
|||
...overrides,
|
||||
});
|
||||
|
||||
export const createMockPluginAPI = () => {
|
||||
export const createMockPluginAPI = (): any => {
|
||||
const mockAPI = {
|
||||
registerHook: jest.fn(),
|
||||
onWindowFocusChange: jest.fn(),
|
||||
|
|
@ -176,6 +176,19 @@ export const createMockPluginAPI = () => {
|
|||
.fn()
|
||||
.mockResolvedValue([{ id: 'test-project', title: 'Test Project' }]),
|
||||
batchUpdateForProject: jest.fn().mockResolvedValue(undefined),
|
||||
log: {
|
||||
critical: jest.fn(),
|
||||
err: jest.fn(),
|
||||
error: jest.fn(),
|
||||
log: jest.fn(),
|
||||
normal: jest.fn(),
|
||||
info: jest.fn(),
|
||||
verbose: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
},
|
||||
persistDataSynced: jest.fn(),
|
||||
loadSyncedData: jest.fn(),
|
||||
};
|
||||
|
||||
(global as any).PluginAPI = mockAPI;
|
||||
|
|
@ -242,7 +255,8 @@ export const generateLargeMarkdown = (taskCount: number): string => {
|
|||
return builder.build();
|
||||
};
|
||||
|
||||
export const waitForAsync = () => new Promise((resolve) => setImmediate(resolve));
|
||||
export const waitForAsync = (): Promise<void> =>
|
||||
new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
export const measureExecutionTime = async (
|
||||
fn: () => void | Promise<void>,
|
||||
|
|
@ -256,7 +270,7 @@ export const measureExecutionTime = async (
|
|||
* Custom Jest matchers
|
||||
*/
|
||||
export const customMatchers = {
|
||||
toBeValidTask(received: any) {
|
||||
toBeValidTask: (received: any) => {
|
||||
const pass =
|
||||
received &&
|
||||
typeof received.id === 'string' &&
|
||||
|
|
@ -273,7 +287,7 @@ export const customMatchers = {
|
|||
};
|
||||
},
|
||||
|
||||
toBeValidParsedTask(received: any) {
|
||||
toBeValidParsedTask: (received: any) => {
|
||||
const pass =
|
||||
received &&
|
||||
typeof received.line === 'number' &&
|
||||
|
|
|
|||
|
|
@ -0,0 +1,180 @@
|
|||
import { mdToSp } from './md-to-sp';
|
||||
import { spToMd } from './sp-to-md';
|
||||
import { LocalUserCfg } from '../local-config';
|
||||
import { Task } from '@super-productivity/plugin-api';
|
||||
// Import mocked modules
|
||||
import {
|
||||
ensureDirectoryExists,
|
||||
writeTasksFile,
|
||||
readTasksFile,
|
||||
} from '../helper/file-utils';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('../helper/file-utils');
|
||||
|
||||
// Mock the PluginAPI
|
||||
|
||||
describe('Header Preservation Integration', () => {
|
||||
const mockConfig: LocalUserCfg = {
|
||||
filePath: '/test/tasks.md',
|
||||
projectId: 'test-project',
|
||||
};
|
||||
|
||||
let mockTasks: Task[] = [];
|
||||
let mockProjects: any[] = [];
|
||||
let mockSyncedData: Record<string, any> = {};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockTasks = [];
|
||||
mockProjects = [
|
||||
{
|
||||
id: 'test-project',
|
||||
title: 'Test Project',
|
||||
taskIds: [],
|
||||
},
|
||||
];
|
||||
mockSyncedData = {};
|
||||
|
||||
// Setup PluginAPI mock
|
||||
(global as any).PluginAPI = {
|
||||
getTasks: jest.fn().mockResolvedValue(mockTasks),
|
||||
getAllProjects: jest.fn().mockResolvedValue(mockProjects),
|
||||
batchUpdateForProject: jest.fn().mockImplementation(({ operations }) => {
|
||||
// Simulate batch operations
|
||||
operations.forEach((op: any) => {
|
||||
if (op.type === 'ADD_TASK') {
|
||||
mockTasks.push(op.task);
|
||||
} else if (op.type === 'UPDATE_TASK') {
|
||||
const index = mockTasks.findIndex((t) => t.id === op.task.id);
|
||||
if (index >= 0) {
|
||||
mockTasks[index] = { ...mockTasks[index], ...op.task };
|
||||
}
|
||||
}
|
||||
});
|
||||
return Promise.resolve();
|
||||
}),
|
||||
persistDataSynced: jest.fn().mockImplementation((data) => {
|
||||
Object.assign(mockSyncedData, data);
|
||||
return Promise.resolve();
|
||||
}),
|
||||
loadSyncedData: jest.fn().mockResolvedValue(mockSyncedData),
|
||||
};
|
||||
});
|
||||
|
||||
it('should preserve header content through full sync cycle', async () => {
|
||||
const markdownWithHeader = `# Project Tasks
|
||||
This is my project description.
|
||||
|
||||
## Important Notes
|
||||
- Remember to check dependencies
|
||||
- Update documentation
|
||||
|
||||
- [ ] <!--task-1--> Task 1
|
||||
- [ ] <!--task-2--> Task 2
|
||||
- [ ] <!--subtask-1--> Subtask 2.1`;
|
||||
|
||||
// Step 1: Parse markdown and sync to SP
|
||||
await mdToSp(markdownWithHeader, mockConfig.projectId);
|
||||
|
||||
// Step 2: Mock the file read to return the original content with header
|
||||
(readTasksFile as jest.Mock).mockResolvedValue(markdownWithHeader);
|
||||
|
||||
// Mock file operations
|
||||
(ensureDirectoryExists as jest.Mock).mockResolvedValue(undefined);
|
||||
(writeTasksFile as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
// Setup mock tasks that would have been created from mdToSp
|
||||
mockTasks = [
|
||||
{
|
||||
id: 'task-1',
|
||||
title: 'Task 1',
|
||||
isDone: false,
|
||||
projectId: 'test-project',
|
||||
parentId: null,
|
||||
subTaskIds: [],
|
||||
},
|
||||
{
|
||||
id: 'task-2',
|
||||
title: 'Task 2',
|
||||
isDone: false,
|
||||
projectId: 'test-project',
|
||||
parentId: null,
|
||||
subTaskIds: ['subtask-1'],
|
||||
},
|
||||
{
|
||||
id: 'subtask-1',
|
||||
title: 'Subtask 2.1',
|
||||
isDone: false,
|
||||
projectId: 'test-project',
|
||||
parentId: 'task-2',
|
||||
subTaskIds: [],
|
||||
},
|
||||
] as unknown as Task[];
|
||||
|
||||
// Update the mock to return our tasks
|
||||
(global as any).PluginAPI.getTasks.mockResolvedValue(mockTasks);
|
||||
|
||||
// Update project to have the task IDs
|
||||
mockProjects[0].taskIds = ['task-1', 'task-2'];
|
||||
|
||||
// Step 3: Run spToMd and verify header is preserved
|
||||
await spToMd(mockConfig);
|
||||
|
||||
// Verify the written content includes the header
|
||||
expect(writeTasksFile).toHaveBeenCalled();
|
||||
const writtenContent = (writeTasksFile as jest.Mock).mock.calls[0][1];
|
||||
|
||||
// The content should start with the header
|
||||
expect(writtenContent).toMatch(/^# Project Tasks/);
|
||||
expect(writtenContent).toContain('This is my project description.');
|
||||
expect(writtenContent).toContain('## Important Notes');
|
||||
expect(writtenContent).toContain('- Remember to check dependencies');
|
||||
expect(writtenContent).toContain('- Update documentation');
|
||||
|
||||
// And should include the tasks
|
||||
expect(writtenContent).toContain('- [ ] <!--task-1--> Task 1');
|
||||
expect(writtenContent).toContain('- [ ] <!--task-2--> Task 2');
|
||||
expect(writtenContent).toContain(' - [ ] <!--subtask-1--> Subtask 2.1');
|
||||
});
|
||||
|
||||
it('should handle empty header', async () => {
|
||||
const markdownNoHeader = `- [ ] Task 1
|
||||
- [ ] Task 2`;
|
||||
|
||||
// Mock file operations
|
||||
(readTasksFile as jest.Mock).mockResolvedValue(markdownNoHeader);
|
||||
(ensureDirectoryExists as jest.Mock).mockResolvedValue(undefined);
|
||||
(writeTasksFile as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
// Setup mock tasks
|
||||
mockTasks = [
|
||||
{
|
||||
id: 'task-1',
|
||||
title: 'Task 1',
|
||||
isDone: false,
|
||||
projectId: 'test-project',
|
||||
parentId: null,
|
||||
subTaskIds: [],
|
||||
},
|
||||
{
|
||||
id: 'task-2',
|
||||
title: 'Task 2',
|
||||
isDone: false,
|
||||
projectId: 'test-project',
|
||||
parentId: null,
|
||||
subTaskIds: [],
|
||||
},
|
||||
] as unknown as Task[];
|
||||
|
||||
(global as any).PluginAPI.getTasks.mockResolvedValue(mockTasks);
|
||||
|
||||
// Run spToMd
|
||||
await spToMd(mockConfig);
|
||||
|
||||
// Verify no header was added
|
||||
const writtenContent = (writeTasksFile as jest.Mock).mock.calls[0][1];
|
||||
expect(writtenContent).not.toMatch(/^#/);
|
||||
expect(writtenContent).toMatch(/^- \[ \] <!--task-1--> Task 1/);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
import { parseMarkdownWithErrors as parseTasks } from './markdown-parser';
|
||||
import {
|
||||
parseMarkdownWithErrors as parseTasks,
|
||||
parseMarkdownWithHeader,
|
||||
} from './markdown-parser';
|
||||
|
||||
describe('markdown-parser', () => {
|
||||
describe('parseTasks', () => {
|
||||
|
|
@ -238,4 +241,94 @@ describe('markdown-parser', () => {
|
|||
expect(result.tasks[4].notes).toBe('Note line 1\nNote line 2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseMarkdownWithHeader', () => {
|
||||
it('should extract header content before first task', () => {
|
||||
const markdown = `# My Tasks
|
||||
This is a description of my tasks.
|
||||
|
||||
Some more text here.
|
||||
|
||||
- [ ] Task 1
|
||||
- [ ] Task 2`;
|
||||
|
||||
const result = parseMarkdownWithHeader(markdown);
|
||||
|
||||
expect(result.header).toBe(
|
||||
'# My Tasks\nThis is a description of my tasks.\n\nSome more text here.\n',
|
||||
);
|
||||
expect(result.tasks.length).toBe(2);
|
||||
expect(result.tasks[0].title).toBe('Task 1');
|
||||
expect(result.tasks[1].title).toBe('Task 2');
|
||||
});
|
||||
|
||||
it('should handle markdown with no header', () => {
|
||||
const markdown = `- [ ] Task 1
|
||||
- [ ] Task 2`;
|
||||
|
||||
const result = parseMarkdownWithHeader(markdown);
|
||||
|
||||
expect(result.header).toBeUndefined();
|
||||
expect(result.tasks.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should handle markdown with only header and no tasks', () => {
|
||||
const markdown = `# My Header
|
||||
This is just a header with no tasks.`;
|
||||
|
||||
const result = parseMarkdownWithHeader(markdown);
|
||||
|
||||
expect(result.header).toBe('# My Header\nThis is just a header with no tasks.');
|
||||
expect(result.tasks.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should preserve complex header with metadata', () => {
|
||||
const markdown = `---
|
||||
title: My Project Tasks
|
||||
date: 2024-01-01
|
||||
tags: [project, tasks]
|
||||
---
|
||||
|
||||
# Project Overview
|
||||
|
||||
This document contains all the tasks for my project.
|
||||
|
||||
## Important Notes
|
||||
- Remember to check dependencies
|
||||
- Update documentation
|
||||
|
||||
- [ ] Task 1
|
||||
- [ ] Task 2`;
|
||||
|
||||
const result = parseMarkdownWithHeader(markdown);
|
||||
|
||||
expect(result.header).toBe(`---
|
||||
title: My Project Tasks
|
||||
date: 2024-01-01
|
||||
tags: [project, tasks]
|
||||
---
|
||||
|
||||
# Project Overview
|
||||
|
||||
This document contains all the tasks for my project.
|
||||
|
||||
## Important Notes
|
||||
- Remember to check dependencies
|
||||
- Update documentation
|
||||
`);
|
||||
expect(result.tasks.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should handle empty lines before first task', () => {
|
||||
const markdown = `# Header
|
||||
|
||||
|
||||
- [ ] Task 1`;
|
||||
|
||||
const result = parseMarkdownWithHeader(markdown);
|
||||
|
||||
expect(result.header).toBe('# Header\n\n');
|
||||
expect(result.tasks.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export interface ParsedTask {
|
|||
export interface TaskParseResult {
|
||||
tasks: ParsedTask[];
|
||||
errors: string[];
|
||||
header?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -61,6 +62,14 @@ export const parseMarkdown = (content: string): ParsedTask[] => {
|
|||
return result.tasks;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse markdown content and return header content
|
||||
* Returns everything before the first task
|
||||
*/
|
||||
export const parseMarkdownWithHeader = (content: string): TaskParseResult => {
|
||||
return parseMarkdownWithErrors(content);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse markdown content into task objects with error collection
|
||||
* Handles tasks and subtasks (first two levels) and converts deeper levels to notes
|
||||
|
|
@ -80,6 +89,8 @@ export const parseMarkdownWithErrors = (content: string): TaskParseResult => {
|
|||
console.log(`[sync-md] Detected indent size: ${detectedIndentSize} spaces`);
|
||||
|
||||
let currentTaskIndex = -1;
|
||||
let firstTaskLineIndex = -1;
|
||||
let header: string | undefined;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
|
@ -88,6 +99,15 @@ export const parseMarkdownWithErrors = (content: string): TaskParseResult => {
|
|||
const taskMatch = line.match(/^(\s*)- \[([ x])\]\s*(?:<!--([^>]+)-->\s*)?(.*)$/);
|
||||
|
||||
if (taskMatch) {
|
||||
// Mark the first task line if not already marked
|
||||
if (firstTaskLineIndex === -1) {
|
||||
firstTaskLineIndex = i;
|
||||
// Extract header content (everything before the first task)
|
||||
if (i > 0) {
|
||||
header = lines.slice(0, i).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
const [, indent, completed, id, title] = taskMatch;
|
||||
const indentLevel = indent.length;
|
||||
const depth = indentLevel === 0 ? 0 : Math.floor(indentLevel / detectedIndentSize);
|
||||
|
|
@ -200,7 +220,12 @@ export const parseMarkdownWithErrors = (content: string): TaskParseResult => {
|
|||
}
|
||||
}
|
||||
|
||||
return { tasks, errors };
|
||||
// If no tasks were found, the entire content is considered header
|
||||
if (firstTaskLineIndex === -1 && lines.length > 0) {
|
||||
header = content;
|
||||
}
|
||||
|
||||
return { tasks, errors, header };
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { parseMarkdown } from './markdown-parser';
|
||||
import { parseMarkdownWithHeader } from './markdown-parser';
|
||||
import { generateTaskOperations } from './generate-task-operations';
|
||||
// import { Task } from '@super-productivity/plugin-api';
|
||||
|
||||
|
|
@ -10,7 +10,9 @@ export const mdToSp = async (
|
|||
markdownContent: string,
|
||||
projectId: string,
|
||||
): Promise<void> => {
|
||||
const parsedTasks = parseMarkdown(markdownContent);
|
||||
// Use parseMarkdownWithHeader to get header, but handle backward compatibility
|
||||
const parseResult = parseMarkdownWithHeader(markdownContent);
|
||||
const parsedTasks = parseResult.tasks;
|
||||
|
||||
// Get current state
|
||||
const currentTasks = await PluginAPI.getTasks();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import { Task } from '@super-productivity/plugin-api';
|
||||
import { writeTasksFile, ensureDirectoryExists } from '../helper/file-utils';
|
||||
import {
|
||||
writeTasksFile,
|
||||
ensureDirectoryExists,
|
||||
readTasksFile,
|
||||
} from '../helper/file-utils';
|
||||
import { LocalUserCfg } from '../local-config';
|
||||
import { parseMarkdownWithHeader } from './markdown-parser';
|
||||
|
||||
/**
|
||||
* Replicate Super Productivity tasks to markdown file
|
||||
|
|
@ -44,8 +49,27 @@ export const spToMd = async (config: LocalUserCfg): Promise<void> => {
|
|||
// Convert project tasks to markdown
|
||||
const markdown = convertTasksToMarkdown(orderedTasks);
|
||||
|
||||
// Get existing header from file
|
||||
let header = '';
|
||||
try {
|
||||
const existingContent = await readTasksFile(config.filePath);
|
||||
if (existingContent) {
|
||||
const parsed = parseMarkdownWithHeader(existingContent);
|
||||
header = parsed.header || '';
|
||||
}
|
||||
} catch (error) {
|
||||
// File doesn't exist yet, no header to preserve
|
||||
}
|
||||
|
||||
// Combine header and tasks
|
||||
let finalContent = markdown;
|
||||
if (header) {
|
||||
// Add newline between header and tasks if both exist
|
||||
finalContent = header + (markdown ? '\n' + markdown : '');
|
||||
}
|
||||
|
||||
// Write to file
|
||||
await writeTasksFile(config.filePath, markdown);
|
||||
await writeTasksFile(config.filePath, finalContent);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,11 +2,7 @@ import { startFileWatcher, stopFileWatcher } from './file-watcher';
|
|||
import { spToMd } from './sp-to-md';
|
||||
import { mdToSp } from './md-to-sp';
|
||||
import { getFileStats, readTasksFile } from '../helper/file-utils';
|
||||
import {
|
||||
SYNC_DEBOUNCE_MS,
|
||||
SYNC_DEBOUNCE_MS_UNFOCUSED,
|
||||
SYNC_DEBOUNCE_MS_MD_TO_SP,
|
||||
} from '../config.const';
|
||||
import { SYNC_DEBOUNCE_MS, SYNC_DEBOUNCE_MS_MD_TO_SP } from '../config.const';
|
||||
import { PluginHooks } from '@super-productivity/plugin-api';
|
||||
import { LocalUserCfg } from '../local-config';
|
||||
import { logSyncVerification, verifySyncState } from './verify-sync';
|
||||
|
|
@ -26,7 +22,9 @@ export const initSyncManager = (config: LocalUserCfg): void => {
|
|||
setupWindowFocusTracking();
|
||||
|
||||
// Perform initial sync
|
||||
performInitialSync(config).then((r) => log.log('SyncMD initial sync', r));
|
||||
performInitialSync(config)
|
||||
.then(() => log.debug('SyncMD initial sync completed'))
|
||||
.catch((error) => log.error('SyncMD initial sync failed', error));
|
||||
|
||||
// Set up file watcher for ongoing sync
|
||||
startFileWatcher(config.filePath, () => {
|
||||
|
|
|
|||
|
|
@ -134,7 +134,6 @@ export const verifySyncState = async (
|
|||
}
|
||||
|
||||
// Check task order
|
||||
const spParentTasks = spTasks.filter((t) => !t.parentId);
|
||||
const mdParentTasks = mdTasks.filter((t) => !t.parentId);
|
||||
|
||||
// If project has taskIds, use them for ordering
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue