@md2do/core 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/coverage/coverage-final.json +9 -9
- package/coverage/index.html +14 -14
- package/coverage/lcov-report/index.html +14 -14
- package/coverage/lcov-report/src/filters/index.html +1 -1
- package/coverage/lcov-report/src/filters/index.ts.html +1 -1
- package/coverage/lcov-report/src/index.html +1 -1
- package/coverage/lcov-report/src/index.ts.html +1 -1
- package/coverage/lcov-report/src/parser/index.html +9 -9
- package/coverage/lcov-report/src/parser/index.ts.html +334 -124
- package/coverage/lcov-report/src/parser/patterns.ts.html +1 -1
- package/coverage/lcov-report/src/scanner/index.html +7 -7
- package/coverage/lcov-report/src/scanner/index.ts.html +208 -70
- package/coverage/lcov-report/src/sorting/index.html +1 -1
- package/coverage/lcov-report/src/sorting/index.ts.html +1 -1
- package/coverage/lcov-report/src/utils/dates.ts.html +21 -21
- package/coverage/lcov-report/src/utils/id.ts.html +8 -8
- package/coverage/lcov-report/src/utils/index.html +1 -1
- package/coverage/lcov-report/src/writer/index.html +1 -1
- package/coverage/lcov-report/src/writer/index.ts.html +1 -1
- package/coverage/lcov.info +479 -342
- package/coverage/src/filters/index.html +1 -1
- package/coverage/src/filters/index.ts.html +1 -1
- package/coverage/src/index.html +1 -1
- package/coverage/src/index.ts.html +1 -1
- package/coverage/src/parser/index.html +9 -9
- package/coverage/src/parser/index.ts.html +334 -124
- package/coverage/src/parser/patterns.ts.html +1 -1
- package/coverage/src/scanner/index.html +7 -7
- package/coverage/src/scanner/index.ts.html +208 -70
- package/coverage/src/sorting/index.html +1 -1
- package/coverage/src/sorting/index.ts.html +1 -1
- package/coverage/src/utils/dates.ts.html +21 -21
- package/coverage/src/utils/id.ts.html +8 -8
- package/coverage/src/utils/index.html +1 -1
- package/coverage/src/writer/index.html +1 -1
- package/coverage/src/writer/index.ts.html +1 -1
- package/dist/index.js +88 -0
- package/dist/index.mjs +88 -0
- package/package.json +1 -1
- package/src/parser/index.ts +70 -0
- package/src/scanner/index.ts +46 -0
- package/tests/parser/index.test.ts +107 -1
- package/tests/scanner/index.test.ts +94 -4
package/dist/index.js
CHANGED
|
@@ -235,6 +235,42 @@ function cleanTaskText(text) {
|
|
|
235
235
|
}
|
|
236
236
|
function parseTask(line, lineNumber, file, context) {
|
|
237
237
|
const warnings = [];
|
|
238
|
+
if (/^\s*[*+]\s+\[[ xX]\]/.test(line)) {
|
|
239
|
+
warnings.push({
|
|
240
|
+
file,
|
|
241
|
+
line: lineNumber,
|
|
242
|
+
text: line.trim(),
|
|
243
|
+
reason: "Unsupported bullet marker (* or +). Use dash (-) for task lists."
|
|
244
|
+
});
|
|
245
|
+
return { task: null, warnings };
|
|
246
|
+
}
|
|
247
|
+
if (/^\s*-\s+\[[xX]\s+\]/.test(line) || /^\s*-\s+\[\s+[xX]\]/.test(line)) {
|
|
248
|
+
warnings.push({
|
|
249
|
+
file,
|
|
250
|
+
line: lineNumber,
|
|
251
|
+
text: line.trim(),
|
|
252
|
+
reason: "Malformed checkbox with extra spaces. Use [x] or [ ] without extra spaces."
|
|
253
|
+
});
|
|
254
|
+
return { task: null, warnings };
|
|
255
|
+
}
|
|
256
|
+
if (/^\s*-\s+\[[ xX]\][^\s]/.test(line)) {
|
|
257
|
+
warnings.push({
|
|
258
|
+
file,
|
|
259
|
+
line: lineNumber,
|
|
260
|
+
text: line.trim(),
|
|
261
|
+
reason: 'Missing space after checkbox. Use "- [x] Task" format.'
|
|
262
|
+
});
|
|
263
|
+
return { task: null, warnings };
|
|
264
|
+
}
|
|
265
|
+
if (/^\s*-\[[ xX]\]/.test(line)) {
|
|
266
|
+
warnings.push({
|
|
267
|
+
file,
|
|
268
|
+
line: lineNumber,
|
|
269
|
+
text: line.trim(),
|
|
270
|
+
reason: 'Missing space before checkbox. Use "- [x] Task" format.'
|
|
271
|
+
});
|
|
272
|
+
return { task: null, warnings };
|
|
273
|
+
}
|
|
238
274
|
const taskMatch = line.match(PATTERNS.TASK_CHECKBOX);
|
|
239
275
|
if (!taskMatch?.[0] || !taskMatch[2]) {
|
|
240
276
|
return { task: null, warnings };
|
|
@@ -254,6 +290,22 @@ function parseTask(line, lineNumber, file, context) {
|
|
|
254
290
|
line: lineNumber
|
|
255
291
|
});
|
|
256
292
|
}
|
|
293
|
+
if (!completed && !dueDateResult.date && !context.currentDate) {
|
|
294
|
+
warnings.push({
|
|
295
|
+
file,
|
|
296
|
+
line: lineNumber,
|
|
297
|
+
text: fullText.trim(),
|
|
298
|
+
reason: "Task has no due date. Add [due: YYYY-MM-DD] or place under a heading with a date."
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
if (completed && !completedDate) {
|
|
302
|
+
warnings.push({
|
|
303
|
+
file,
|
|
304
|
+
line: lineNumber,
|
|
305
|
+
text: fullText.trim(),
|
|
306
|
+
reason: "Completed task missing completion date. Add [completed: YYYY-MM-DD]."
|
|
307
|
+
});
|
|
308
|
+
}
|
|
257
309
|
const cleanText = cleanTaskText(fullText);
|
|
258
310
|
const id = generateTaskId(file, lineNumber, cleanText);
|
|
259
311
|
const task = {
|
|
@@ -299,6 +351,7 @@ var MarkdownScanner = class {
|
|
|
299
351
|
scanFile(filePath, content) {
|
|
300
352
|
const tasks = [];
|
|
301
353
|
const warnings = [];
|
|
354
|
+
const todoistIds = /* @__PURE__ */ new Map();
|
|
302
355
|
const context = {};
|
|
303
356
|
const project = extractProjectFromPath(filePath);
|
|
304
357
|
if (project !== void 0) context.project = project;
|
|
@@ -318,6 +371,22 @@ var MarkdownScanner = class {
|
|
|
318
371
|
const result = parseTask(line, lineNumber, filePath, context);
|
|
319
372
|
if (result.task) {
|
|
320
373
|
tasks.push(result.task);
|
|
374
|
+
if (result.task.todoistId) {
|
|
375
|
+
const existing = todoistIds.get(result.task.todoistId);
|
|
376
|
+
if (existing) {
|
|
377
|
+
warnings.push({
|
|
378
|
+
file: filePath,
|
|
379
|
+
line: lineNumber,
|
|
380
|
+
text: result.task.text,
|
|
381
|
+
reason: `Duplicate Todoist ID [todoist:${result.task.todoistId}]. Also found at ${existing.file}:${existing.line}.`
|
|
382
|
+
});
|
|
383
|
+
} else {
|
|
384
|
+
todoistIds.set(result.task.todoistId, {
|
|
385
|
+
file: filePath,
|
|
386
|
+
line: lineNumber
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
321
390
|
}
|
|
322
391
|
if (result.warnings.length > 0) {
|
|
323
392
|
warnings.push(...result.warnings);
|
|
@@ -337,10 +406,29 @@ var MarkdownScanner = class {
|
|
|
337
406
|
scanFiles(files) {
|
|
338
407
|
const allTasks = [];
|
|
339
408
|
const allWarnings = [];
|
|
409
|
+
const todoistIds = /* @__PURE__ */ new Map();
|
|
340
410
|
for (const file of files) {
|
|
341
411
|
const result = this.scanFile(file.path, file.content);
|
|
342
412
|
allTasks.push(...result.tasks);
|
|
343
413
|
allWarnings.push(...result.warnings);
|
|
414
|
+
for (const task of result.tasks) {
|
|
415
|
+
if (task.todoistId) {
|
|
416
|
+
const existing = todoistIds.get(task.todoistId);
|
|
417
|
+
if (existing && existing.file !== task.file) {
|
|
418
|
+
allWarnings.push({
|
|
419
|
+
file: task.file,
|
|
420
|
+
line: task.line,
|
|
421
|
+
text: task.text,
|
|
422
|
+
reason: `Duplicate Todoist ID [todoist:${task.todoistId}] across files. Also found at ${existing.file}:${existing.line}.`
|
|
423
|
+
});
|
|
424
|
+
} else if (!existing) {
|
|
425
|
+
todoistIds.set(task.todoistId, {
|
|
426
|
+
file: task.file,
|
|
427
|
+
line: task.line
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
344
432
|
}
|
|
345
433
|
return {
|
|
346
434
|
tasks: allTasks,
|
package/dist/index.mjs
CHANGED
|
@@ -178,6 +178,42 @@ function cleanTaskText(text) {
|
|
|
178
178
|
}
|
|
179
179
|
function parseTask(line, lineNumber, file, context) {
|
|
180
180
|
const warnings = [];
|
|
181
|
+
if (/^\s*[*+]\s+\[[ xX]\]/.test(line)) {
|
|
182
|
+
warnings.push({
|
|
183
|
+
file,
|
|
184
|
+
line: lineNumber,
|
|
185
|
+
text: line.trim(),
|
|
186
|
+
reason: "Unsupported bullet marker (* or +). Use dash (-) for task lists."
|
|
187
|
+
});
|
|
188
|
+
return { task: null, warnings };
|
|
189
|
+
}
|
|
190
|
+
if (/^\s*-\s+\[[xX]\s+\]/.test(line) || /^\s*-\s+\[\s+[xX]\]/.test(line)) {
|
|
191
|
+
warnings.push({
|
|
192
|
+
file,
|
|
193
|
+
line: lineNumber,
|
|
194
|
+
text: line.trim(),
|
|
195
|
+
reason: "Malformed checkbox with extra spaces. Use [x] or [ ] without extra spaces."
|
|
196
|
+
});
|
|
197
|
+
return { task: null, warnings };
|
|
198
|
+
}
|
|
199
|
+
if (/^\s*-\s+\[[ xX]\][^\s]/.test(line)) {
|
|
200
|
+
warnings.push({
|
|
201
|
+
file,
|
|
202
|
+
line: lineNumber,
|
|
203
|
+
text: line.trim(),
|
|
204
|
+
reason: 'Missing space after checkbox. Use "- [x] Task" format.'
|
|
205
|
+
});
|
|
206
|
+
return { task: null, warnings };
|
|
207
|
+
}
|
|
208
|
+
if (/^\s*-\[[ xX]\]/.test(line)) {
|
|
209
|
+
warnings.push({
|
|
210
|
+
file,
|
|
211
|
+
line: lineNumber,
|
|
212
|
+
text: line.trim(),
|
|
213
|
+
reason: 'Missing space before checkbox. Use "- [x] Task" format.'
|
|
214
|
+
});
|
|
215
|
+
return { task: null, warnings };
|
|
216
|
+
}
|
|
181
217
|
const taskMatch = line.match(PATTERNS.TASK_CHECKBOX);
|
|
182
218
|
if (!taskMatch?.[0] || !taskMatch[2]) {
|
|
183
219
|
return { task: null, warnings };
|
|
@@ -197,6 +233,22 @@ function parseTask(line, lineNumber, file, context) {
|
|
|
197
233
|
line: lineNumber
|
|
198
234
|
});
|
|
199
235
|
}
|
|
236
|
+
if (!completed && !dueDateResult.date && !context.currentDate) {
|
|
237
|
+
warnings.push({
|
|
238
|
+
file,
|
|
239
|
+
line: lineNumber,
|
|
240
|
+
text: fullText.trim(),
|
|
241
|
+
reason: "Task has no due date. Add [due: YYYY-MM-DD] or place under a heading with a date."
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
if (completed && !completedDate) {
|
|
245
|
+
warnings.push({
|
|
246
|
+
file,
|
|
247
|
+
line: lineNumber,
|
|
248
|
+
text: fullText.trim(),
|
|
249
|
+
reason: "Completed task missing completion date. Add [completed: YYYY-MM-DD]."
|
|
250
|
+
});
|
|
251
|
+
}
|
|
200
252
|
const cleanText = cleanTaskText(fullText);
|
|
201
253
|
const id = generateTaskId(file, lineNumber, cleanText);
|
|
202
254
|
const task = {
|
|
@@ -242,6 +294,7 @@ var MarkdownScanner = class {
|
|
|
242
294
|
scanFile(filePath, content) {
|
|
243
295
|
const tasks = [];
|
|
244
296
|
const warnings = [];
|
|
297
|
+
const todoistIds = /* @__PURE__ */ new Map();
|
|
245
298
|
const context = {};
|
|
246
299
|
const project = extractProjectFromPath(filePath);
|
|
247
300
|
if (project !== void 0) context.project = project;
|
|
@@ -261,6 +314,22 @@ var MarkdownScanner = class {
|
|
|
261
314
|
const result = parseTask(line, lineNumber, filePath, context);
|
|
262
315
|
if (result.task) {
|
|
263
316
|
tasks.push(result.task);
|
|
317
|
+
if (result.task.todoistId) {
|
|
318
|
+
const existing = todoistIds.get(result.task.todoistId);
|
|
319
|
+
if (existing) {
|
|
320
|
+
warnings.push({
|
|
321
|
+
file: filePath,
|
|
322
|
+
line: lineNumber,
|
|
323
|
+
text: result.task.text,
|
|
324
|
+
reason: `Duplicate Todoist ID [todoist:${result.task.todoistId}]. Also found at ${existing.file}:${existing.line}.`
|
|
325
|
+
});
|
|
326
|
+
} else {
|
|
327
|
+
todoistIds.set(result.task.todoistId, {
|
|
328
|
+
file: filePath,
|
|
329
|
+
line: lineNumber
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
264
333
|
}
|
|
265
334
|
if (result.warnings.length > 0) {
|
|
266
335
|
warnings.push(...result.warnings);
|
|
@@ -280,10 +349,29 @@ var MarkdownScanner = class {
|
|
|
280
349
|
scanFiles(files) {
|
|
281
350
|
const allTasks = [];
|
|
282
351
|
const allWarnings = [];
|
|
352
|
+
const todoistIds = /* @__PURE__ */ new Map();
|
|
283
353
|
for (const file of files) {
|
|
284
354
|
const result = this.scanFile(file.path, file.content);
|
|
285
355
|
allTasks.push(...result.tasks);
|
|
286
356
|
allWarnings.push(...result.warnings);
|
|
357
|
+
for (const task of result.tasks) {
|
|
358
|
+
if (task.todoistId) {
|
|
359
|
+
const existing = todoistIds.get(task.todoistId);
|
|
360
|
+
if (existing && existing.file !== task.file) {
|
|
361
|
+
allWarnings.push({
|
|
362
|
+
file: task.file,
|
|
363
|
+
line: task.line,
|
|
364
|
+
text: task.text,
|
|
365
|
+
reason: `Duplicate Todoist ID [todoist:${task.todoistId}] across files. Also found at ${existing.file}:${existing.line}.`
|
|
366
|
+
});
|
|
367
|
+
} else if (!existing) {
|
|
368
|
+
todoistIds.set(task.todoistId, {
|
|
369
|
+
file: task.file,
|
|
370
|
+
line: task.line
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
287
375
|
}
|
|
288
376
|
return {
|
|
289
377
|
tasks: allTasks,
|
package/package.json
CHANGED
package/src/parser/index.ts
CHANGED
|
@@ -208,6 +208,53 @@ export function parseTask(
|
|
|
208
208
|
): { task: Task | null; warnings: Warning[] } {
|
|
209
209
|
const warnings: Warning[] = [];
|
|
210
210
|
|
|
211
|
+
// Detect malformed checkboxes and provide helpful warnings
|
|
212
|
+
// Check for asterisk/plus bullet markers (not supported)
|
|
213
|
+
if (/^\s*[*+]\s+\[[ xX]\]/.test(line)) {
|
|
214
|
+
warnings.push({
|
|
215
|
+
file,
|
|
216
|
+
line: lineNumber,
|
|
217
|
+
text: line.trim(),
|
|
218
|
+
reason:
|
|
219
|
+
'Unsupported bullet marker (* or +). Use dash (-) for task lists.',
|
|
220
|
+
});
|
|
221
|
+
return { task: null, warnings };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check for extra spaces inside checkbox: [x ] or [ x]
|
|
225
|
+
if (/^\s*-\s+\[[xX]\s+\]/.test(line) || /^\s*-\s+\[\s+[xX]\]/.test(line)) {
|
|
226
|
+
warnings.push({
|
|
227
|
+
file,
|
|
228
|
+
line: lineNumber,
|
|
229
|
+
text: line.trim(),
|
|
230
|
+
reason:
|
|
231
|
+
'Malformed checkbox with extra spaces. Use [x] or [ ] without extra spaces.',
|
|
232
|
+
});
|
|
233
|
+
return { task: null, warnings };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check for missing space after checkbox: [x]Task
|
|
237
|
+
if (/^\s*-\s+\[[ xX]\][^\s]/.test(line)) {
|
|
238
|
+
warnings.push({
|
|
239
|
+
file,
|
|
240
|
+
line: lineNumber,
|
|
241
|
+
text: line.trim(),
|
|
242
|
+
reason: 'Missing space after checkbox. Use "- [x] Task" format.',
|
|
243
|
+
});
|
|
244
|
+
return { task: null, warnings };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Check for missing space before checkbox: -[x]
|
|
248
|
+
if (/^\s*-\[[ xX]\]/.test(line)) {
|
|
249
|
+
warnings.push({
|
|
250
|
+
file,
|
|
251
|
+
line: lineNumber,
|
|
252
|
+
text: line.trim(),
|
|
253
|
+
reason: 'Missing space before checkbox. Use "- [x] Task" format.',
|
|
254
|
+
});
|
|
255
|
+
return { task: null, warnings };
|
|
256
|
+
}
|
|
257
|
+
|
|
211
258
|
// Check if line is a task
|
|
212
259
|
const taskMatch = line.match(PATTERNS.TASK_CHECKBOX);
|
|
213
260
|
if (!taskMatch?.[0] || !taskMatch[2]) {
|
|
@@ -237,6 +284,29 @@ export function parseTask(
|
|
|
237
284
|
});
|
|
238
285
|
}
|
|
239
286
|
|
|
287
|
+
// Warn about missing dates
|
|
288
|
+
// Incomplete tasks without any date (no explicit due date and no context date)
|
|
289
|
+
if (!completed && !dueDateResult.date && !context.currentDate) {
|
|
290
|
+
warnings.push({
|
|
291
|
+
file,
|
|
292
|
+
line: lineNumber,
|
|
293
|
+
text: fullText.trim(),
|
|
294
|
+
reason:
|
|
295
|
+
'Task has no due date. Add [due: YYYY-MM-DD] or place under a heading with a date.',
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Completed tasks should have completion dates
|
|
300
|
+
if (completed && !completedDate) {
|
|
301
|
+
warnings.push({
|
|
302
|
+
file,
|
|
303
|
+
line: lineNumber,
|
|
304
|
+
text: fullText.trim(),
|
|
305
|
+
reason:
|
|
306
|
+
'Completed task missing completion date. Add [completed: YYYY-MM-DD].',
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
240
310
|
// Clean text (remove metadata markers)
|
|
241
311
|
const cleanText = cleanTaskText(fullText);
|
|
242
312
|
|
package/src/scanner/index.ts
CHANGED
|
@@ -74,6 +74,9 @@ export class MarkdownScanner {
|
|
|
74
74
|
const tasks: Task[] = [];
|
|
75
75
|
const warnings: Warning[] = [];
|
|
76
76
|
|
|
77
|
+
// Track Todoist IDs to detect duplicates
|
|
78
|
+
const todoistIds = new Map<string, { file: string; line: number }>();
|
|
79
|
+
|
|
77
80
|
// Initialize context from file path
|
|
78
81
|
const context: ParsingContext = {};
|
|
79
82
|
|
|
@@ -105,6 +108,24 @@ export class MarkdownScanner {
|
|
|
105
108
|
|
|
106
109
|
if (result.task) {
|
|
107
110
|
tasks.push(result.task);
|
|
111
|
+
|
|
112
|
+
// Check for duplicate Todoist IDs
|
|
113
|
+
if (result.task.todoistId) {
|
|
114
|
+
const existing = todoistIds.get(result.task.todoistId);
|
|
115
|
+
if (existing) {
|
|
116
|
+
warnings.push({
|
|
117
|
+
file: filePath,
|
|
118
|
+
line: lineNumber,
|
|
119
|
+
text: result.task.text,
|
|
120
|
+
reason: `Duplicate Todoist ID [todoist:${result.task.todoistId}]. Also found at ${existing.file}:${existing.line}.`,
|
|
121
|
+
});
|
|
122
|
+
} else {
|
|
123
|
+
todoistIds.set(result.task.todoistId, {
|
|
124
|
+
file: filePath,
|
|
125
|
+
line: lineNumber,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
108
129
|
}
|
|
109
130
|
|
|
110
131
|
if (result.warnings.length > 0) {
|
|
@@ -131,10 +152,35 @@ export class MarkdownScanner {
|
|
|
131
152
|
const allTasks: Task[] = [];
|
|
132
153
|
const allWarnings: Warning[] = [];
|
|
133
154
|
|
|
155
|
+
// Track Todoist IDs across all files
|
|
156
|
+
const todoistIds = new Map<string, { file: string; line: number }>();
|
|
157
|
+
|
|
134
158
|
for (const file of files) {
|
|
135
159
|
const result = this.scanFile(file.path, file.content);
|
|
136
160
|
allTasks.push(...result.tasks);
|
|
137
161
|
allWarnings.push(...result.warnings);
|
|
162
|
+
|
|
163
|
+
// Check for duplicate Todoist IDs across files
|
|
164
|
+
for (const task of result.tasks) {
|
|
165
|
+
if (task.todoistId) {
|
|
166
|
+
const existing = todoistIds.get(task.todoistId);
|
|
167
|
+
if (existing && existing.file !== task.file) {
|
|
168
|
+
// Only add warning if duplicate is in a different file
|
|
169
|
+
// (same-file duplicates are already caught by scanFile)
|
|
170
|
+
allWarnings.push({
|
|
171
|
+
file: task.file,
|
|
172
|
+
line: task.line,
|
|
173
|
+
text: task.text,
|
|
174
|
+
reason: `Duplicate Todoist ID [todoist:${task.todoistId}] across files. Also found at ${existing.file}:${existing.line}.`,
|
|
175
|
+
});
|
|
176
|
+
} else if (!existing) {
|
|
177
|
+
todoistIds.set(task.todoistId, {
|
|
178
|
+
file: task.file,
|
|
179
|
+
line: task.line,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
138
184
|
}
|
|
139
185
|
|
|
140
186
|
return {
|
|
@@ -419,7 +419,7 @@ describe('parseTask', () => {
|
|
|
419
419
|
const result = parseTask('- [ ] Task [due: tomorrow]', 1, file, {});
|
|
420
420
|
|
|
421
421
|
expect(result.task?.dueDate).toBeUndefined();
|
|
422
|
-
expect(result.warnings).toHaveLength(
|
|
422
|
+
expect(result.warnings).toHaveLength(2); // Relative date + missing date warnings
|
|
423
423
|
expect(result.warnings[0]?.reason).toContain(
|
|
424
424
|
'Relative due date without context',
|
|
425
425
|
);
|
|
@@ -535,4 +535,110 @@ describe('parseTask', () => {
|
|
|
535
535
|
expect(result.task?.completedDate).toBeInstanceOf(Date);
|
|
536
536
|
});
|
|
537
537
|
});
|
|
538
|
+
|
|
539
|
+
describe('Malformed checkbox warnings', () => {
|
|
540
|
+
it('should warn on asterisk bullet marker', () => {
|
|
541
|
+
const result = parseTask('* [x] Task with asterisk', 1, file, {});
|
|
542
|
+
expect(result.task).toBeNull();
|
|
543
|
+
expect(result.warnings).toHaveLength(1);
|
|
544
|
+
expect(result.warnings[0]?.reason).toContain(
|
|
545
|
+
'Unsupported bullet marker (* or +)',
|
|
546
|
+
);
|
|
547
|
+
expect(result.warnings[0]?.file).toBe(file);
|
|
548
|
+
expect(result.warnings[0]?.line).toBe(1);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('should warn on plus bullet marker', () => {
|
|
552
|
+
const result = parseTask('+ [ ] Task with plus', 1, file, {});
|
|
553
|
+
expect(result.task).toBeNull();
|
|
554
|
+
expect(result.warnings).toHaveLength(1);
|
|
555
|
+
expect(result.warnings[0]?.reason).toContain(
|
|
556
|
+
'Unsupported bullet marker (* or +)',
|
|
557
|
+
);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it('should warn on extra space after x', () => {
|
|
561
|
+
const result = parseTask('- [x ] Task with extra space', 1, file, {});
|
|
562
|
+
expect(result.task).toBeNull();
|
|
563
|
+
expect(result.warnings).toHaveLength(1);
|
|
564
|
+
expect(result.warnings[0]?.reason).toContain('Malformed checkbox');
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('should warn on extra space before x', () => {
|
|
568
|
+
const result = parseTask('- [ x] Task with extra space', 1, file, {});
|
|
569
|
+
expect(result.task).toBeNull();
|
|
570
|
+
expect(result.warnings).toHaveLength(1);
|
|
571
|
+
expect(result.warnings[0]?.reason).toContain('Malformed checkbox');
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it('should warn on missing space before checkbox', () => {
|
|
575
|
+
const result = parseTask('-[x] Task without space', 1, file, {});
|
|
576
|
+
expect(result.task).toBeNull();
|
|
577
|
+
expect(result.warnings).toHaveLength(1);
|
|
578
|
+
expect(result.warnings[0]?.reason).toContain(
|
|
579
|
+
'Missing space before checkbox',
|
|
580
|
+
);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it('should warn on missing space after checkbox', () => {
|
|
584
|
+
const result = parseTask('- [x]Task without space', 1, file, {});
|
|
585
|
+
expect(result.task).toBeNull();
|
|
586
|
+
expect(result.warnings).toHaveLength(1);
|
|
587
|
+
expect(result.warnings[0]?.reason).toContain(
|
|
588
|
+
'Missing space after checkbox',
|
|
589
|
+
);
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
describe('Missing date warnings', () => {
|
|
594
|
+
it('should warn on incomplete task without date or context', () => {
|
|
595
|
+
const result = parseTask('- [ ] Task without date', 1, file, {});
|
|
596
|
+
expect(result.task).not.toBeNull();
|
|
597
|
+
expect(result.warnings).toHaveLength(1);
|
|
598
|
+
expect(result.warnings[0]?.reason).toContain('Task has no due date');
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('should not warn on task with explicit due date', () => {
|
|
602
|
+
const result = parseTask('- [ ] Task [due: 2026-01-30]', 1, file, {});
|
|
603
|
+
expect(result.task).not.toBeNull();
|
|
604
|
+
expect(result.warnings).toHaveLength(0);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it('should not warn on task with context date', () => {
|
|
608
|
+
const context: ParsingContext = {
|
|
609
|
+
currentDate: new Date('2026-01-30'),
|
|
610
|
+
};
|
|
611
|
+
const result = parseTask('- [ ] Task with context', 1, file, context);
|
|
612
|
+
expect(result.task).not.toBeNull();
|
|
613
|
+
expect(result.warnings).toHaveLength(0);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it('should not warn on completed tasks without date', () => {
|
|
617
|
+
const result = parseTask('- [x] Completed task', 1, file, {});
|
|
618
|
+
expect(result.task).not.toBeNull();
|
|
619
|
+
// Should only warn about missing completion date, not due date
|
|
620
|
+
expect(result.warnings).toHaveLength(1);
|
|
621
|
+
expect(result.warnings[0]?.reason).toContain('completion date');
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it('should warn on completed task without completion date', () => {
|
|
625
|
+
const result = parseTask('- [x] Completed task', 1, file, {});
|
|
626
|
+
expect(result.task).not.toBeNull();
|
|
627
|
+
expect(result.warnings).toHaveLength(1);
|
|
628
|
+
expect(result.warnings[0]?.reason).toContain(
|
|
629
|
+
'Completed task missing completion date',
|
|
630
|
+
);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('should not warn on completed task with completion date', () => {
|
|
634
|
+
const result = parseTask(
|
|
635
|
+
'- [x] Task [completed: 2026-01-25]',
|
|
636
|
+
1,
|
|
637
|
+
file,
|
|
638
|
+
{},
|
|
639
|
+
);
|
|
640
|
+
expect(result.task).not.toBeNull();
|
|
641
|
+
expect(result.warnings).toHaveLength(0);
|
|
642
|
+
});
|
|
643
|
+
});
|
|
538
644
|
});
|
|
@@ -65,7 +65,7 @@ describe('MarkdownScanner', () => {
|
|
|
65
65
|
|
|
66
66
|
describe('Basic task scanning', () => {
|
|
67
67
|
it('should scan simple unchecked task', () => {
|
|
68
|
-
const content = '- [ ] Simple task';
|
|
68
|
+
const content = '- [ ] Simple task [due: 2026-01-30]';
|
|
69
69
|
const result = scanner.scanFile('test.md', content);
|
|
70
70
|
|
|
71
71
|
expect(result.tasks).toHaveLength(1);
|
|
@@ -281,7 +281,7 @@ No tasks at all.
|
|
|
281
281
|
const result = scanner.scanFile('test.md', content);
|
|
282
282
|
|
|
283
283
|
expect(result.tasks[0]?.dueDate).toBeUndefined();
|
|
284
|
-
expect(result.warnings).toHaveLength(
|
|
284
|
+
expect(result.warnings).toHaveLength(2); // Relative date + missing date warnings
|
|
285
285
|
expect(result.warnings[0]?.reason).toContain(
|
|
286
286
|
'Relative due date without context',
|
|
287
287
|
);
|
|
@@ -461,9 +461,9 @@ Line 4
|
|
|
461
461
|
|
|
462
462
|
const result = scanner.scanFiles(files);
|
|
463
463
|
|
|
464
|
-
expect(result.warnings).toHaveLength(
|
|
464
|
+
expect(result.warnings).toHaveLength(4); // 2 relative date + 2 missing date warnings
|
|
465
465
|
expect(result.warnings[0]?.file).toBe('file1.md');
|
|
466
|
-
expect(result.warnings[
|
|
466
|
+
expect(result.warnings[2]?.file).toBe('file2.md');
|
|
467
467
|
});
|
|
468
468
|
|
|
469
469
|
it('should handle empty file array', () => {
|
|
@@ -558,4 +558,94 @@ Line 4
|
|
|
558
558
|
expect(result.tasks).toHaveLength(0);
|
|
559
559
|
});
|
|
560
560
|
});
|
|
561
|
+
|
|
562
|
+
describe('Duplicate Todoist ID warnings', () => {
|
|
563
|
+
it('should warn on duplicate Todoist ID within same file', () => {
|
|
564
|
+
const content = `
|
|
565
|
+
- [ ] First task [todoist:123456] [due: 2026-01-30]
|
|
566
|
+
- [ ] Second task [todoist:123456] [due: 2026-01-31]
|
|
567
|
+
`.trim();
|
|
568
|
+
const result = scanner.scanFile('test.md', content);
|
|
569
|
+
|
|
570
|
+
expect(result.tasks).toHaveLength(2);
|
|
571
|
+
expect(result.warnings).toHaveLength(1);
|
|
572
|
+
expect(result.warnings[0]?.reason).toContain('Duplicate Todoist ID');
|
|
573
|
+
expect(result.warnings[0]?.reason).toContain('123456');
|
|
574
|
+
expect(result.warnings[0]?.line).toBe(2);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('should not warn on unique Todoist IDs', () => {
|
|
578
|
+
const content = `
|
|
579
|
+
- [ ] First task [todoist:111111] [due: 2026-01-30]
|
|
580
|
+
- [ ] Second task [todoist:222222] [due: 2026-01-31]
|
|
581
|
+
- [ ] Third task [todoist:333333] [due: 2026-02-01]
|
|
582
|
+
`.trim();
|
|
583
|
+
const result = scanner.scanFile('test.md', content);
|
|
584
|
+
|
|
585
|
+
expect(result.tasks).toHaveLength(3);
|
|
586
|
+
expect(result.warnings).toHaveLength(0);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('should warn on multiple duplicates', () => {
|
|
590
|
+
const content = `
|
|
591
|
+
- [ ] Task A [todoist:111] [due: 2026-01-30]
|
|
592
|
+
- [ ] Task B [todoist:222] [due: 2026-01-31]
|
|
593
|
+
- [ ] Task C [todoist:111] [due: 2026-02-01]
|
|
594
|
+
- [ ] Task D [todoist:222] [due: 2026-02-02]
|
|
595
|
+
- [ ] Task E [todoist:111] [due: 2026-02-03]
|
|
596
|
+
`.trim();
|
|
597
|
+
const result = scanner.scanFile('test.md', content);
|
|
598
|
+
|
|
599
|
+
expect(result.tasks).toHaveLength(5);
|
|
600
|
+
// Should have 3 warnings: C duplicates A, D duplicates B, E duplicates A
|
|
601
|
+
expect(result.warnings).toHaveLength(3);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('should track duplicates across multiple files', () => {
|
|
605
|
+
const files = [
|
|
606
|
+
{
|
|
607
|
+
path: 'file1.md',
|
|
608
|
+
content: '- [ ] Task in file 1 [todoist:123456] [due: 2026-01-30]',
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
path: 'file2.md',
|
|
612
|
+
content: '- [ ] Task in file 2 [todoist:123456] [due: 2026-01-31]',
|
|
613
|
+
},
|
|
614
|
+
];
|
|
615
|
+
|
|
616
|
+
const result = scanner.scanFiles(files);
|
|
617
|
+
|
|
618
|
+
expect(result.tasks).toHaveLength(2);
|
|
619
|
+
expect(result.warnings).toHaveLength(1);
|
|
620
|
+
expect(result.warnings[0]?.reason).toContain('Duplicate Todoist ID');
|
|
621
|
+
expect(result.warnings[0]?.reason).toContain('across files');
|
|
622
|
+
expect(result.warnings[0]?.file).toBe('file2.md');
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it('should not warn when same file scanned separately', () => {
|
|
626
|
+
const content = '- [ ] Task [todoist:123456] [due: 2026-01-30]';
|
|
627
|
+
|
|
628
|
+
// Scan same file multiple times as if they were different files
|
|
629
|
+
const result1 = scanner.scanFile('test1.md', content);
|
|
630
|
+
const result2 = scanner.scanFile('test2.md', content);
|
|
631
|
+
|
|
632
|
+
// Each scan should have no warnings (they're separate scans)
|
|
633
|
+
expect(result1.warnings).toHaveLength(0);
|
|
634
|
+
expect(result2.warnings).toHaveLength(0);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it('should include location information in warning', () => {
|
|
638
|
+
const content = `
|
|
639
|
+
- [ ] First task [todoist:999] [due: 2026-01-30]
|
|
640
|
+
- [ ] Second task [due: 2026-01-31]
|
|
641
|
+
- [ ] Third task [todoist:999] [due: 2026-02-01]
|
|
642
|
+
`.trim();
|
|
643
|
+
const result = scanner.scanFile('backlog.md', content);
|
|
644
|
+
|
|
645
|
+
expect(result.warnings).toHaveLength(1);
|
|
646
|
+
expect(result.warnings[0]?.file).toBe('backlog.md');
|
|
647
|
+
expect(result.warnings[0]?.line).toBe(3);
|
|
648
|
+
expect(result.warnings[0]?.reason).toContain('backlog.md:1');
|
|
649
|
+
});
|
|
650
|
+
});
|
|
561
651
|
});
|