@md2do/core 0.2.3 → 0.4.0
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 +36 -0
- package/coverage/coverage-final.json +6 -5
- package/coverage/index.html +52 -37
- package/coverage/lcov-report/index.html +52 -37
- 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 +5 -5
- package/coverage/lcov-report/src/index.ts.html +13 -4
- package/coverage/lcov-report/src/parser/index.html +17 -17
- package/coverage/lcov-report/src/parser/index.ts.html +181 -13
- package/coverage/lcov-report/src/parser/patterns.ts.html +18 -9
- package/coverage/lcov-report/src/scanner/index.html +15 -15
- package/coverage/lcov-report/src/scanner/index.ts.html +83 -8
- 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 +169 -25
- package/coverage/lcov-report/src/utils/id.ts.html +1 -1
- package/coverage/lcov-report/src/utils/index.html +19 -19
- package/coverage/lcov-report/src/warnings/filter.ts.html +364 -0
- package/coverage/lcov-report/src/warnings/index.html +116 -0
- 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 +760 -512
- package/coverage/src/filters/index.html +1 -1
- package/coverage/src/filters/index.ts.html +1 -1
- package/coverage/src/index.html +5 -5
- package/coverage/src/index.ts.html +13 -4
- package/coverage/src/parser/index.html +17 -17
- package/coverage/src/parser/index.ts.html +181 -13
- package/coverage/src/parser/patterns.ts.html +18 -9
- package/coverage/src/scanner/index.html +15 -15
- package/coverage/src/scanner/index.ts.html +83 -8
- package/coverage/src/sorting/index.html +1 -1
- package/coverage/src/sorting/index.ts.html +1 -1
- package/coverage/src/utils/dates.ts.html +169 -25
- package/coverage/src/utils/id.ts.html +1 -1
- package/coverage/src/utils/index.html +19 -19
- package/coverage/src/warnings/filter.ts.html +364 -0
- package/coverage/src/warnings/index.html +116 -0
- package/coverage/src/writer/index.html +1 -1
- package/coverage/src/writer/index.ts.html +1 -1
- package/dist/index.d.mts +103 -12
- package/dist/index.d.ts +103 -12
- package/dist/index.js +125 -11
- package/dist/index.mjs +125 -12
- package/package.json +1 -1
- package/src/index.ts +3 -0
- package/src/parser/index.ts +61 -5
- package/src/parser/patterns.ts +8 -5
- package/src/scanner/index.ts +25 -0
- package/src/types/index.ts +35 -2
- package/src/utils/dates.ts +58 -10
- package/src/warnings/filter.ts +93 -0
package/dist/index.mjs
CHANGED
|
@@ -10,7 +10,7 @@ var ASSIGNEE = /@([\w-]+)/;
|
|
|
10
10
|
var PRIORITY_URGENT = /!!!/;
|
|
11
11
|
var PRIORITY_HIGH = /!!/;
|
|
12
12
|
var PRIORITY_NORMAL = /(?<!!)!(?!!)/;
|
|
13
|
-
var DUE_DATE_ABSOLUTE = /\[due:\s*(\d{4}-\d{2}-\d{2})\s*\]/i;
|
|
13
|
+
var DUE_DATE_ABSOLUTE = /\[due:\s*(\d{4}-\d{2}-\d{2})(?:\s+(\d{1,2}:\d{2}))?\s*\]/i;
|
|
14
14
|
var DUE_DATE_RELATIVE = /\[due:\s*(tomorrow|today|next\s+week|next\s+month)\]/i;
|
|
15
15
|
var DUE_DATE_SHORT = /\[due:\s*(\d{1,2}\/\d{1,2}(?:\/\d{2,4})?)\]/i;
|
|
16
16
|
var TAG = /#([\w-]+)/g;
|
|
@@ -43,17 +43,38 @@ import {
|
|
|
43
43
|
addDays,
|
|
44
44
|
addWeeks,
|
|
45
45
|
addMonths,
|
|
46
|
-
startOfWeek
|
|
46
|
+
startOfWeek,
|
|
47
|
+
setHours,
|
|
48
|
+
setMinutes
|
|
47
49
|
} from "date-fns";
|
|
48
|
-
function
|
|
50
|
+
function parseTime(timeStr) {
|
|
51
|
+
const match = timeStr.match(/^(\d{1,2}):(\d{2})$/);
|
|
52
|
+
if (!match || !match[1] || !match[2]) return null;
|
|
53
|
+
const hours = parseInt(match[1], 10);
|
|
54
|
+
const minutes = parseInt(match[2], 10);
|
|
55
|
+
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return { hours, minutes };
|
|
59
|
+
}
|
|
60
|
+
function parseAbsoluteDate(dateStr, timeStr) {
|
|
49
61
|
const referenceDate = /* @__PURE__ */ new Date();
|
|
50
62
|
let date = parse(dateStr, "yyyy-MM-dd", referenceDate);
|
|
51
|
-
if (isValid(date))
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
if (!isValid(date)) {
|
|
64
|
+
date = parse(dateStr, "M/d/yy", referenceDate);
|
|
65
|
+
}
|
|
66
|
+
if (!isValid(date)) {
|
|
67
|
+
date = parse(dateStr, "M/d/yyyy", referenceDate);
|
|
68
|
+
}
|
|
69
|
+
if (!isValid(date)) return null;
|
|
70
|
+
if (timeStr) {
|
|
71
|
+
const time = parseTime(timeStr);
|
|
72
|
+
if (time) {
|
|
73
|
+
date = setHours(date, time.hours);
|
|
74
|
+
date = setMinutes(date, time.minutes);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return date;
|
|
57
78
|
}
|
|
58
79
|
function resolveRelativeDate(relative, baseDate) {
|
|
59
80
|
const normalized = relative.toLowerCase().trim();
|
|
@@ -145,12 +166,29 @@ function extractCompletedDate(text) {
|
|
|
145
166
|
function extractDueDate(text, context) {
|
|
146
167
|
const absoluteMatch = text.match(PATTERNS.DUE_DATE_ABSOLUTE);
|
|
147
168
|
if (absoluteMatch?.[1]) {
|
|
148
|
-
const
|
|
169
|
+
const dateStr = absoluteMatch[1];
|
|
170
|
+
let timeStr = absoluteMatch[2];
|
|
171
|
+
if (!timeStr && context.defaultDueTime) {
|
|
172
|
+
if (context.defaultDueTime === "start" && context.workdayStartTime) {
|
|
173
|
+
timeStr = context.workdayStartTime;
|
|
174
|
+
} else if (context.defaultDueTime === "end" && context.workdayEndTime) {
|
|
175
|
+
timeStr = context.workdayEndTime;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const date = parseAbsoluteDate(dateStr, timeStr);
|
|
149
179
|
return { date: date ?? void 0 };
|
|
150
180
|
}
|
|
151
181
|
const shortMatch = text.match(PATTERNS.DUE_DATE_SHORT);
|
|
152
182
|
if (shortMatch?.[1]) {
|
|
153
|
-
|
|
183
|
+
let timeStr;
|
|
184
|
+
if (context.defaultDueTime) {
|
|
185
|
+
if (context.defaultDueTime === "start" && context.workdayStartTime) {
|
|
186
|
+
timeStr = context.workdayStartTime;
|
|
187
|
+
} else if (context.defaultDueTime === "end" && context.workdayEndTime) {
|
|
188
|
+
timeStr = context.workdayEndTime;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const date = parseAbsoluteDate(shortMatch[1], timeStr);
|
|
154
192
|
return { date: date ?? void 0 };
|
|
155
193
|
}
|
|
156
194
|
const relativeMatch = text.match(PATTERNS.DUE_DATE_RELATIVE);
|
|
@@ -159,11 +197,15 @@ function extractDueDate(text, context) {
|
|
|
159
197
|
return {
|
|
160
198
|
date: void 0,
|
|
161
199
|
warning: {
|
|
200
|
+
severity: "warning",
|
|
201
|
+
source: "md2do",
|
|
202
|
+
ruleId: "relative-date-no-context",
|
|
162
203
|
file: "",
|
|
163
204
|
// Will be filled in by caller
|
|
164
205
|
line: 0,
|
|
165
206
|
// Will be filled in by caller
|
|
166
207
|
text: text.trim(),
|
|
208
|
+
message: "Relative due date without context date from heading. Add a heading with a date above this task.",
|
|
167
209
|
reason: "Relative due date without context date from heading. Add a heading with a date above this task."
|
|
168
210
|
}
|
|
169
211
|
};
|
|
@@ -180,36 +222,52 @@ function parseTask(line, lineNumber, file, context) {
|
|
|
180
222
|
const warnings = [];
|
|
181
223
|
if (/^\s*[*+]\s+\[[ xX]\]/.test(line)) {
|
|
182
224
|
warnings.push({
|
|
225
|
+
severity: "warning",
|
|
226
|
+
source: "md2do",
|
|
227
|
+
ruleId: "unsupported-bullet",
|
|
183
228
|
file,
|
|
184
229
|
line: lineNumber,
|
|
185
230
|
text: line.trim(),
|
|
231
|
+
message: "Unsupported bullet marker (* or +). Use dash (-) for task lists.",
|
|
186
232
|
reason: "Unsupported bullet marker (* or +). Use dash (-) for task lists."
|
|
187
233
|
});
|
|
188
234
|
return { task: null, warnings };
|
|
189
235
|
}
|
|
190
236
|
if (/^\s*-\s+\[[xX]\s+\]/.test(line) || /^\s*-\s+\[\s+[xX]\]/.test(line)) {
|
|
191
237
|
warnings.push({
|
|
238
|
+
severity: "warning",
|
|
239
|
+
source: "md2do",
|
|
240
|
+
ruleId: "malformed-checkbox",
|
|
192
241
|
file,
|
|
193
242
|
line: lineNumber,
|
|
194
243
|
text: line.trim(),
|
|
244
|
+
message: "Malformed checkbox with extra spaces. Use [x] or [ ] without extra spaces.",
|
|
195
245
|
reason: "Malformed checkbox with extra spaces. Use [x] or [ ] without extra spaces."
|
|
196
246
|
});
|
|
197
247
|
return { task: null, warnings };
|
|
198
248
|
}
|
|
199
249
|
if (/^\s*-\s+\[[ xX]\][^\s]/.test(line)) {
|
|
200
250
|
warnings.push({
|
|
251
|
+
severity: "warning",
|
|
252
|
+
source: "md2do",
|
|
253
|
+
ruleId: "missing-space-after",
|
|
201
254
|
file,
|
|
202
255
|
line: lineNumber,
|
|
203
256
|
text: line.trim(),
|
|
257
|
+
message: 'Missing space after checkbox. Use "- [x] Task" format.',
|
|
204
258
|
reason: 'Missing space after checkbox. Use "- [x] Task" format.'
|
|
205
259
|
});
|
|
206
260
|
return { task: null, warnings };
|
|
207
261
|
}
|
|
208
262
|
if (/^\s*-\[[ xX]\]/.test(line)) {
|
|
209
263
|
warnings.push({
|
|
264
|
+
severity: "warning",
|
|
265
|
+
source: "md2do",
|
|
266
|
+
ruleId: "missing-space-before",
|
|
210
267
|
file,
|
|
211
268
|
line: lineNumber,
|
|
212
269
|
text: line.trim(),
|
|
270
|
+
message: 'Missing space before checkbox. Use "- [x] Task" format.',
|
|
213
271
|
reason: 'Missing space before checkbox. Use "- [x] Task" format.'
|
|
214
272
|
});
|
|
215
273
|
return { task: null, warnings };
|
|
@@ -235,17 +293,25 @@ function parseTask(line, lineNumber, file, context) {
|
|
|
235
293
|
}
|
|
236
294
|
if (!completed && !dueDateResult.date && !context.currentDate) {
|
|
237
295
|
warnings.push({
|
|
296
|
+
severity: "info",
|
|
297
|
+
source: "md2do",
|
|
298
|
+
ruleId: "missing-due-date",
|
|
238
299
|
file,
|
|
239
300
|
line: lineNumber,
|
|
240
301
|
text: fullText.trim(),
|
|
302
|
+
message: "Task has no due date. Add [due: YYYY-MM-DD] or place under a heading with a date.",
|
|
241
303
|
reason: "Task has no due date. Add [due: YYYY-MM-DD] or place under a heading with a date."
|
|
242
304
|
});
|
|
243
305
|
}
|
|
244
306
|
if (completed && !completedDate) {
|
|
245
307
|
warnings.push({
|
|
308
|
+
severity: "info",
|
|
309
|
+
source: "md2do",
|
|
310
|
+
ruleId: "missing-completed-date",
|
|
246
311
|
file,
|
|
247
312
|
line: lineNumber,
|
|
248
313
|
text: fullText.trim(),
|
|
314
|
+
message: "Completed task missing completion date. Add [completed: YYYY-MM-DD].",
|
|
249
315
|
reason: "Completed task missing completion date. Add [completed: YYYY-MM-DD]."
|
|
250
316
|
});
|
|
251
317
|
}
|
|
@@ -289,9 +355,10 @@ var MarkdownScanner = class {
|
|
|
289
355
|
*
|
|
290
356
|
* @param filePath - Relative file path (for context extraction)
|
|
291
357
|
* @param content - File content as string
|
|
358
|
+
* @param options - Optional scanner options including workday config
|
|
292
359
|
* @returns Object containing tasks and warnings
|
|
293
360
|
*/
|
|
294
|
-
scanFile(filePath, content) {
|
|
361
|
+
scanFile(filePath, content, options) {
|
|
295
362
|
const tasks = [];
|
|
296
363
|
const warnings = [];
|
|
297
364
|
const todoistIds = /* @__PURE__ */ new Map();
|
|
@@ -300,6 +367,15 @@ var MarkdownScanner = class {
|
|
|
300
367
|
if (project !== void 0) context.project = project;
|
|
301
368
|
const person = extractPersonFromFilename(filePath);
|
|
302
369
|
if (person !== void 0) context.person = person;
|
|
370
|
+
if (options?.workdayStartTime) {
|
|
371
|
+
context.workdayStartTime = options.workdayStartTime;
|
|
372
|
+
}
|
|
373
|
+
if (options?.workdayEndTime) {
|
|
374
|
+
context.workdayEndTime = options.workdayEndTime;
|
|
375
|
+
}
|
|
376
|
+
if (options?.defaultDueTime) {
|
|
377
|
+
context.defaultDueTime = options.defaultDueTime;
|
|
378
|
+
}
|
|
303
379
|
const lines = content.split("\n");
|
|
304
380
|
for (let i = 0; i < lines.length; i++) {
|
|
305
381
|
const line = lines[i];
|
|
@@ -318,9 +394,13 @@ var MarkdownScanner = class {
|
|
|
318
394
|
const existing = todoistIds.get(result.task.todoistId);
|
|
319
395
|
if (existing) {
|
|
320
396
|
warnings.push({
|
|
397
|
+
severity: "error",
|
|
398
|
+
source: "md2do",
|
|
399
|
+
ruleId: "duplicate-todoist-id",
|
|
321
400
|
file: filePath,
|
|
322
401
|
line: lineNumber,
|
|
323
402
|
text: result.task.text,
|
|
403
|
+
message: `Duplicate Todoist ID [todoist:${result.task.todoistId}]. Also found at ${existing.file}:${existing.line}.`,
|
|
324
404
|
reason: `Duplicate Todoist ID [todoist:${result.task.todoistId}]. Also found at ${existing.file}:${existing.line}.`
|
|
325
405
|
});
|
|
326
406
|
} else {
|
|
@@ -359,9 +439,13 @@ var MarkdownScanner = class {
|
|
|
359
439
|
const existing = todoistIds.get(task.todoistId);
|
|
360
440
|
if (existing && existing.file !== task.file) {
|
|
361
441
|
allWarnings.push({
|
|
442
|
+
severity: "error",
|
|
443
|
+
source: "md2do",
|
|
444
|
+
ruleId: "duplicate-todoist-id",
|
|
362
445
|
file: task.file,
|
|
363
446
|
line: task.line,
|
|
364
447
|
text: task.text,
|
|
448
|
+
message: `Duplicate Todoist ID [todoist:${task.todoistId}] across files. Also found at ${existing.file}:${existing.line}.`,
|
|
365
449
|
reason: `Duplicate Todoist ID [todoist:${task.todoistId}] across files. Also found at ${existing.file}:${existing.line}.`
|
|
366
450
|
});
|
|
367
451
|
} else if (!existing) {
|
|
@@ -810,6 +894,32 @@ function combineComparators(comparators) {
|
|
|
810
894
|
function reverse(comparator) {
|
|
811
895
|
return (a, b) => -comparator(a, b);
|
|
812
896
|
}
|
|
897
|
+
|
|
898
|
+
// src/warnings/filter.ts
|
|
899
|
+
function filterWarnings(warnings, config = {}) {
|
|
900
|
+
if (config.enabled === false) {
|
|
901
|
+
return [];
|
|
902
|
+
}
|
|
903
|
+
if (!config.rules) {
|
|
904
|
+
return warnings;
|
|
905
|
+
}
|
|
906
|
+
return warnings.filter((warning) => {
|
|
907
|
+
const level = config.rules?.[warning.ruleId];
|
|
908
|
+
if (level === void 0) {
|
|
909
|
+
return true;
|
|
910
|
+
}
|
|
911
|
+
return level !== "off";
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
function groupWarningsBySeverity(warnings) {
|
|
915
|
+
return warnings.reduce(
|
|
916
|
+
(acc, warning) => {
|
|
917
|
+
acc[warning.severity].push(warning);
|
|
918
|
+
return acc;
|
|
919
|
+
},
|
|
920
|
+
{ error: [], warning: [], info: [] }
|
|
921
|
+
);
|
|
922
|
+
}
|
|
813
923
|
export {
|
|
814
924
|
ASSIGNEE,
|
|
815
925
|
COMPLETED_DATE,
|
|
@@ -838,10 +948,13 @@ export {
|
|
|
838
948
|
extractProjectFromPath,
|
|
839
949
|
extractTags,
|
|
840
950
|
extractTodoistId,
|
|
951
|
+
filterWarnings,
|
|
841
952
|
filters_exports as filters,
|
|
842
953
|
generateTaskId,
|
|
954
|
+
groupWarningsBySeverity,
|
|
843
955
|
parseAbsoluteDate,
|
|
844
956
|
parseTask,
|
|
957
|
+
parseTime,
|
|
845
958
|
resolveRelativeDate,
|
|
846
959
|
sorting_exports as sorting,
|
|
847
960
|
updateTask,
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -19,6 +19,9 @@ export * as filters from './filters/index.js';
|
|
|
19
19
|
// Sorting (export as namespace to avoid conflicts)
|
|
20
20
|
export * as sorting from './sorting/index.js';
|
|
21
21
|
|
|
22
|
+
// Warnings
|
|
23
|
+
export * from './warnings/filter.js';
|
|
24
|
+
|
|
22
25
|
// Utilities
|
|
23
26
|
export * from './utils/dates.js';
|
|
24
27
|
export * from './utils/id.js';
|
package/src/parser/index.ts
CHANGED
|
@@ -94,27 +94,50 @@ export function extractCompletedDate(text: string): Date | undefined {
|
|
|
94
94
|
* Extract due date from task text with context awareness
|
|
95
95
|
*
|
|
96
96
|
* Handles both absolute dates ([due: 2026-01-25]) and relative dates
|
|
97
|
-
* ([due: tomorrow]) which require context.
|
|
97
|
+
* ([due: tomorrow]) which require context. Supports optional time ([due: 2026-01-25 17:00]).
|
|
98
98
|
*
|
|
99
99
|
* @param text - Task text
|
|
100
|
-
* @param context - Parsing context (for relative dates)
|
|
100
|
+
* @param context - Parsing context (for relative dates and workday config)
|
|
101
101
|
* @returns Object with parsed date and optional warning
|
|
102
102
|
*/
|
|
103
103
|
export function extractDueDate(
|
|
104
104
|
text: string,
|
|
105
105
|
context: ParsingContext,
|
|
106
106
|
): { date: Date | undefined; warning?: Warning } {
|
|
107
|
-
// Try absolute date first
|
|
107
|
+
// Try absolute date first (with optional time)
|
|
108
108
|
const absoluteMatch = text.match(PATTERNS.DUE_DATE_ABSOLUTE);
|
|
109
109
|
if (absoluteMatch?.[1]) {
|
|
110
|
-
const
|
|
110
|
+
const dateStr = absoluteMatch[1];
|
|
111
|
+
let timeStr = absoluteMatch[2]; // May be undefined
|
|
112
|
+
|
|
113
|
+
// If no time specified, apply default from workday config
|
|
114
|
+
if (!timeStr && context.defaultDueTime) {
|
|
115
|
+
if (context.defaultDueTime === 'start' && context.workdayStartTime) {
|
|
116
|
+
timeStr = context.workdayStartTime;
|
|
117
|
+
} else if (context.defaultDueTime === 'end' && context.workdayEndTime) {
|
|
118
|
+
timeStr = context.workdayEndTime;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const date = parseAbsoluteDate(dateStr, timeStr);
|
|
111
123
|
return { date: date ?? undefined };
|
|
112
124
|
}
|
|
113
125
|
|
|
114
126
|
// Try short format (M/D or M/D/YY)
|
|
115
127
|
const shortMatch = text.match(PATTERNS.DUE_DATE_SHORT);
|
|
116
128
|
if (shortMatch?.[1]) {
|
|
117
|
-
|
|
129
|
+
let timeStr: string | undefined;
|
|
130
|
+
|
|
131
|
+
// Apply default time from workday config
|
|
132
|
+
if (context.defaultDueTime) {
|
|
133
|
+
if (context.defaultDueTime === 'start' && context.workdayStartTime) {
|
|
134
|
+
timeStr = context.workdayStartTime;
|
|
135
|
+
} else if (context.defaultDueTime === 'end' && context.workdayEndTime) {
|
|
136
|
+
timeStr = context.workdayEndTime;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const date = parseAbsoluteDate(shortMatch[1], timeStr);
|
|
118
141
|
return { date: date ?? undefined };
|
|
119
142
|
}
|
|
120
143
|
|
|
@@ -126,9 +149,14 @@ export function extractDueDate(
|
|
|
126
149
|
return {
|
|
127
150
|
date: undefined,
|
|
128
151
|
warning: {
|
|
152
|
+
severity: 'warning',
|
|
153
|
+
source: 'md2do',
|
|
154
|
+
ruleId: 'relative-date-no-context',
|
|
129
155
|
file: '', // Will be filled in by caller
|
|
130
156
|
line: 0, // Will be filled in by caller
|
|
131
157
|
text: text.trim(),
|
|
158
|
+
message:
|
|
159
|
+
'Relative due date without context date from heading. Add a heading with a date above this task.',
|
|
132
160
|
reason:
|
|
133
161
|
'Relative due date without context date from heading. Add a heading with a date above this task.',
|
|
134
162
|
},
|
|
@@ -212,9 +240,14 @@ export function parseTask(
|
|
|
212
240
|
// Check for asterisk/plus bullet markers (not supported)
|
|
213
241
|
if (/^\s*[*+]\s+\[[ xX]\]/.test(line)) {
|
|
214
242
|
warnings.push({
|
|
243
|
+
severity: 'warning',
|
|
244
|
+
source: 'md2do',
|
|
245
|
+
ruleId: 'unsupported-bullet',
|
|
215
246
|
file,
|
|
216
247
|
line: lineNumber,
|
|
217
248
|
text: line.trim(),
|
|
249
|
+
message:
|
|
250
|
+
'Unsupported bullet marker (* or +). Use dash (-) for task lists.',
|
|
218
251
|
reason:
|
|
219
252
|
'Unsupported bullet marker (* or +). Use dash (-) for task lists.',
|
|
220
253
|
});
|
|
@@ -224,9 +257,14 @@ export function parseTask(
|
|
|
224
257
|
// Check for extra spaces inside checkbox: [x ] or [ x]
|
|
225
258
|
if (/^\s*-\s+\[[xX]\s+\]/.test(line) || /^\s*-\s+\[\s+[xX]\]/.test(line)) {
|
|
226
259
|
warnings.push({
|
|
260
|
+
severity: 'warning',
|
|
261
|
+
source: 'md2do',
|
|
262
|
+
ruleId: 'malformed-checkbox',
|
|
227
263
|
file,
|
|
228
264
|
line: lineNumber,
|
|
229
265
|
text: line.trim(),
|
|
266
|
+
message:
|
|
267
|
+
'Malformed checkbox with extra spaces. Use [x] or [ ] without extra spaces.',
|
|
230
268
|
reason:
|
|
231
269
|
'Malformed checkbox with extra spaces. Use [x] or [ ] without extra spaces.',
|
|
232
270
|
});
|
|
@@ -236,9 +274,13 @@ export function parseTask(
|
|
|
236
274
|
// Check for missing space after checkbox: [x]Task
|
|
237
275
|
if (/^\s*-\s+\[[ xX]\][^\s]/.test(line)) {
|
|
238
276
|
warnings.push({
|
|
277
|
+
severity: 'warning',
|
|
278
|
+
source: 'md2do',
|
|
279
|
+
ruleId: 'missing-space-after',
|
|
239
280
|
file,
|
|
240
281
|
line: lineNumber,
|
|
241
282
|
text: line.trim(),
|
|
283
|
+
message: 'Missing space after checkbox. Use "- [x] Task" format.',
|
|
242
284
|
reason: 'Missing space after checkbox. Use "- [x] Task" format.',
|
|
243
285
|
});
|
|
244
286
|
return { task: null, warnings };
|
|
@@ -247,9 +289,13 @@ export function parseTask(
|
|
|
247
289
|
// Check for missing space before checkbox: -[x]
|
|
248
290
|
if (/^\s*-\[[ xX]\]/.test(line)) {
|
|
249
291
|
warnings.push({
|
|
292
|
+
severity: 'warning',
|
|
293
|
+
source: 'md2do',
|
|
294
|
+
ruleId: 'missing-space-before',
|
|
250
295
|
file,
|
|
251
296
|
line: lineNumber,
|
|
252
297
|
text: line.trim(),
|
|
298
|
+
message: 'Missing space before checkbox. Use "- [x] Task" format.',
|
|
253
299
|
reason: 'Missing space before checkbox. Use "- [x] Task" format.',
|
|
254
300
|
});
|
|
255
301
|
return { task: null, warnings };
|
|
@@ -288,9 +334,14 @@ export function parseTask(
|
|
|
288
334
|
// Incomplete tasks without any date (no explicit due date and no context date)
|
|
289
335
|
if (!completed && !dueDateResult.date && !context.currentDate) {
|
|
290
336
|
warnings.push({
|
|
337
|
+
severity: 'info',
|
|
338
|
+
source: 'md2do',
|
|
339
|
+
ruleId: 'missing-due-date',
|
|
291
340
|
file,
|
|
292
341
|
line: lineNumber,
|
|
293
342
|
text: fullText.trim(),
|
|
343
|
+
message:
|
|
344
|
+
'Task has no due date. Add [due: YYYY-MM-DD] or place under a heading with a date.',
|
|
294
345
|
reason:
|
|
295
346
|
'Task has no due date. Add [due: YYYY-MM-DD] or place under a heading with a date.',
|
|
296
347
|
});
|
|
@@ -299,9 +350,14 @@ export function parseTask(
|
|
|
299
350
|
// Completed tasks should have completion dates
|
|
300
351
|
if (completed && !completedDate) {
|
|
301
352
|
warnings.push({
|
|
353
|
+
severity: 'info',
|
|
354
|
+
source: 'md2do',
|
|
355
|
+
ruleId: 'missing-completed-date',
|
|
302
356
|
file,
|
|
303
357
|
line: lineNumber,
|
|
304
358
|
text: fullText.trim(),
|
|
359
|
+
message:
|
|
360
|
+
'Completed task missing completion date. Add [completed: YYYY-MM-DD].',
|
|
305
361
|
reason:
|
|
306
362
|
'Completed task missing completion date. Add [completed: YYYY-MM-DD].',
|
|
307
363
|
});
|
package/src/parser/patterns.ts
CHANGED
|
@@ -63,17 +63,20 @@ export const PRIORITY_HIGH = /!!/;
|
|
|
63
63
|
export const PRIORITY_NORMAL = /(?<!!)!(?!!)/;
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
|
-
* Matches absolute due date in ISO format [due: YYYY-MM-DD]
|
|
66
|
+
* Matches absolute due date in ISO format [due: YYYY-MM-DD] with optional time [due: YYYY-MM-DD HH:MM]
|
|
67
67
|
*
|
|
68
68
|
* Examples:
|
|
69
|
-
* "[due: 2026-01-25]" → "2026-01-25"
|
|
70
|
-
* "[due:2026-01-25]" → "2026-01-25"
|
|
71
|
-
* "[due:
|
|
69
|
+
* "[due: 2026-01-25]" → "2026-01-25", undefined
|
|
70
|
+
* "[due: 2026-01-25 17:00]" → "2026-01-25", "17:00"
|
|
71
|
+
* "[due: 2026-01-25 9:00]" → "2026-01-25", "9:00"
|
|
72
|
+
* "[due:2026-01-25]" → "2026-01-25", undefined (spaces optional)
|
|
72
73
|
*
|
|
73
74
|
* Groups:
|
|
74
75
|
* [1] - Date string in YYYY-MM-DD format
|
|
76
|
+
* [2] - Optional time string in H:MM or HH:MM format (24-hour)
|
|
75
77
|
*/
|
|
76
|
-
export const DUE_DATE_ABSOLUTE =
|
|
78
|
+
export const DUE_DATE_ABSOLUTE =
|
|
79
|
+
/\[due:\s*(\d{4}-\d{2}-\d{2})(?:\s+(\d{1,2}:\d{2}))?\s*\]/i;
|
|
77
80
|
|
|
78
81
|
/**
|
|
79
82
|
* Matches relative due date keywords
|
package/src/scanner/index.ts
CHANGED
|
@@ -62,11 +62,17 @@ export class MarkdownScanner {
|
|
|
62
62
|
*
|
|
63
63
|
* @param filePath - Relative file path (for context extraction)
|
|
64
64
|
* @param content - File content as string
|
|
65
|
+
* @param options - Optional scanner options including workday config
|
|
65
66
|
* @returns Object containing tasks and warnings
|
|
66
67
|
*/
|
|
67
68
|
scanFile(
|
|
68
69
|
filePath: string,
|
|
69
70
|
content: string,
|
|
71
|
+
options?: {
|
|
72
|
+
workdayStartTime?: string;
|
|
73
|
+
workdayEndTime?: string;
|
|
74
|
+
defaultDueTime?: 'start' | 'end';
|
|
75
|
+
},
|
|
70
76
|
): {
|
|
71
77
|
tasks: Task[];
|
|
72
78
|
warnings: Warning[];
|
|
@@ -86,6 +92,17 @@ export class MarkdownScanner {
|
|
|
86
92
|
const person = extractPersonFromFilename(filePath);
|
|
87
93
|
if (person !== undefined) context.person = person;
|
|
88
94
|
|
|
95
|
+
// Add workday config to context if provided
|
|
96
|
+
if (options?.workdayStartTime) {
|
|
97
|
+
context.workdayStartTime = options.workdayStartTime;
|
|
98
|
+
}
|
|
99
|
+
if (options?.workdayEndTime) {
|
|
100
|
+
context.workdayEndTime = options.workdayEndTime;
|
|
101
|
+
}
|
|
102
|
+
if (options?.defaultDueTime) {
|
|
103
|
+
context.defaultDueTime = options.defaultDueTime;
|
|
104
|
+
}
|
|
105
|
+
|
|
89
106
|
// Process file line by line
|
|
90
107
|
const lines = content.split('\n');
|
|
91
108
|
|
|
@@ -114,9 +131,13 @@ export class MarkdownScanner {
|
|
|
114
131
|
const existing = todoistIds.get(result.task.todoistId);
|
|
115
132
|
if (existing) {
|
|
116
133
|
warnings.push({
|
|
134
|
+
severity: 'error',
|
|
135
|
+
source: 'md2do',
|
|
136
|
+
ruleId: 'duplicate-todoist-id',
|
|
117
137
|
file: filePath,
|
|
118
138
|
line: lineNumber,
|
|
119
139
|
text: result.task.text,
|
|
140
|
+
message: `Duplicate Todoist ID [todoist:${result.task.todoistId}]. Also found at ${existing.file}:${existing.line}.`,
|
|
120
141
|
reason: `Duplicate Todoist ID [todoist:${result.task.todoistId}]. Also found at ${existing.file}:${existing.line}.`,
|
|
121
142
|
});
|
|
122
143
|
} else {
|
|
@@ -168,9 +189,13 @@ export class MarkdownScanner {
|
|
|
168
189
|
// Only add warning if duplicate is in a different file
|
|
169
190
|
// (same-file duplicates are already caught by scanFile)
|
|
170
191
|
allWarnings.push({
|
|
192
|
+
severity: 'error',
|
|
193
|
+
source: 'md2do',
|
|
194
|
+
ruleId: 'duplicate-todoist-id',
|
|
171
195
|
file: task.file,
|
|
172
196
|
line: task.line,
|
|
173
197
|
text: task.text,
|
|
198
|
+
message: `Duplicate Todoist ID [todoist:${task.todoistId}] across files. Also found at ${existing.file}:${existing.line}.`,
|
|
174
199
|
reason: `Duplicate Todoist ID [todoist:${task.todoistId}] across files. Also found at ${existing.file}:${existing.line}.`,
|
|
175
200
|
});
|
|
176
201
|
} else if (!existing) {
|
package/src/types/index.ts
CHANGED
|
@@ -36,6 +36,10 @@ export interface ParsingContext {
|
|
|
36
36
|
person?: string;
|
|
37
37
|
currentDate?: Date;
|
|
38
38
|
currentHeading?: string;
|
|
39
|
+
// Workday configuration for time handling
|
|
40
|
+
workdayStartTime?: string; // e.g., "08:00"
|
|
41
|
+
workdayEndTime?: string; // e.g., "17:00"
|
|
42
|
+
defaultDueTime?: 'start' | 'end'; // Which time to use when no time specified
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
export interface TaskFilterCriteria {
|
|
@@ -65,9 +69,38 @@ export interface ScanResult {
|
|
|
65
69
|
};
|
|
66
70
|
}
|
|
67
71
|
|
|
72
|
+
export type WarningSeverity = 'info' | 'warning' | 'error';
|
|
73
|
+
|
|
74
|
+
export type WarningCode =
|
|
75
|
+
| 'unsupported-bullet' // * or + instead of -
|
|
76
|
+
| 'malformed-checkbox' // [x ] or [ x]
|
|
77
|
+
| 'missing-space-after' // -[x]Task
|
|
78
|
+
| 'missing-space-before' // -[x] Task
|
|
79
|
+
| 'relative-date-no-context' // [due: tomorrow] without heading date
|
|
80
|
+
| 'missing-due-date' // Incomplete task with no due date
|
|
81
|
+
| 'missing-completed-date' // [x] without [completed: date]
|
|
82
|
+
| 'duplicate-todoist-id' // Same Todoist ID in multiple tasks
|
|
83
|
+
| 'file-read-error'; // Failed to read file
|
|
84
|
+
|
|
68
85
|
export interface Warning {
|
|
86
|
+
// Position
|
|
69
87
|
file: string;
|
|
70
88
|
line: number;
|
|
71
|
-
|
|
72
|
-
|
|
89
|
+
column?: number;
|
|
90
|
+
|
|
91
|
+
// Classification
|
|
92
|
+
severity: WarningSeverity;
|
|
93
|
+
source: 'md2do';
|
|
94
|
+
ruleId: WarningCode;
|
|
95
|
+
|
|
96
|
+
// Content
|
|
97
|
+
message: string; // User-facing message
|
|
98
|
+
text?: string; // The actual text that triggered it
|
|
99
|
+
|
|
100
|
+
// Documentation (optional - for future use)
|
|
101
|
+
url?: string;
|
|
102
|
+
|
|
103
|
+
// Legacy field for backward compatibility (deprecated)
|
|
104
|
+
/** @deprecated Use message instead */
|
|
105
|
+
reason?: string;
|
|
73
106
|
}
|