@md2do/core 0.3.0 → 0.5.1

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.
Files changed (62) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +6 -6
  3. package/dist/index.d.mts +40 -10
  4. package/dist/index.d.ts +40 -10
  5. package/dist/index.js +59 -11
  6. package/dist/index.mjs +61 -12
  7. package/package.json +1 -1
  8. package/src/parser/index.ts +28 -5
  9. package/src/parser/patterns.ts +8 -5
  10. package/src/scanner/index.ts +17 -0
  11. package/src/types/index.ts +4 -0
  12. package/src/utils/dates.ts +58 -10
  13. package/coverage/base.css +0 -224
  14. package/coverage/block-navigation.js +0 -87
  15. package/coverage/coverage-final.json +0 -10
  16. package/coverage/favicon.png +0 -0
  17. package/coverage/index.html +0 -206
  18. package/coverage/lcov-report/base.css +0 -224
  19. package/coverage/lcov-report/block-navigation.js +0 -87
  20. package/coverage/lcov-report/favicon.png +0 -0
  21. package/coverage/lcov-report/index.html +0 -206
  22. package/coverage/lcov-report/prettify.css +0 -1
  23. package/coverage/lcov-report/prettify.js +0 -2
  24. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  25. package/coverage/lcov-report/sorter.js +0 -210
  26. package/coverage/lcov-report/src/filters/index.html +0 -116
  27. package/coverage/lcov-report/src/filters/index.ts.html +0 -1156
  28. package/coverage/lcov-report/src/index.html +0 -116
  29. package/coverage/lcov-report/src/index.ts.html +0 -157
  30. package/coverage/lcov-report/src/parser/index.html +0 -131
  31. package/coverage/lcov-report/src/parser/index.ts.html +0 -1102
  32. package/coverage/lcov-report/src/parser/patterns.ts.html +0 -661
  33. package/coverage/lcov-report/src/scanner/index.html +0 -116
  34. package/coverage/lcov-report/src/scanner/index.ts.html +0 -658
  35. package/coverage/lcov-report/src/sorting/index.html +0 -116
  36. package/coverage/lcov-report/src/sorting/index.ts.html +0 -736
  37. package/coverage/lcov-report/src/utils/dates.ts.html +0 -499
  38. package/coverage/lcov-report/src/utils/id.ts.html +0 -169
  39. package/coverage/lcov-report/src/utils/index.html +0 -131
  40. package/coverage/lcov-report/src/writer/index.html +0 -116
  41. package/coverage/lcov-report/src/writer/index.ts.html +0 -1093
  42. package/coverage/lcov.info +0 -2294
  43. package/coverage/prettify.css +0 -1
  44. package/coverage/prettify.js +0 -2
  45. package/coverage/sort-arrow-sprite.png +0 -0
  46. package/coverage/sorter.js +0 -210
  47. package/coverage/src/filters/index.html +0 -116
  48. package/coverage/src/filters/index.ts.html +0 -1156
  49. package/coverage/src/index.html +0 -116
  50. package/coverage/src/index.ts.html +0 -157
  51. package/coverage/src/parser/index.html +0 -131
  52. package/coverage/src/parser/index.ts.html +0 -1102
  53. package/coverage/src/parser/patterns.ts.html +0 -661
  54. package/coverage/src/scanner/index.html +0 -116
  55. package/coverage/src/scanner/index.ts.html +0 -658
  56. package/coverage/src/sorting/index.html +0 -116
  57. package/coverage/src/sorting/index.ts.html +0 -736
  58. package/coverage/src/utils/dates.ts.html +0 -499
  59. package/coverage/src/utils/id.ts.html +0 -169
  60. package/coverage/src/utils/index.html +0 -131
  61. package/coverage/src/writer/index.html +0 -116
  62. package/coverage/src/writer/index.ts.html +0 -1093
package/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # @md2do/core
2
2
 
3
+ ## 0.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#47](https://github.com/TeamNickHart/md2do/pull/47) [`ce65eaf`](https://github.com/TeamNickHart/md2do/commit/ce65eaf588c702254fb564f748a5841241cc5c97) Thanks [@nickhart](https://github.com/nickhart)! - Documentation improvements and standardization
8
+ - Standardized bracket syntax across all docs (use space after colon: `[due: ...]`)
9
+ - Marked semantic/relative dates as experimental with clear warnings
10
+ - Clarified Todoist sync is one-way (pull only), not bidirectional
11
+ - Documented context extraction limitation (must run from repo root)
12
+ - Updated code examples in READMEs to match best practices
13
+ - Removed unhelpful parentheses syntax warning
14
+
15
+ ## 0.4.0
16
+
17
+ ### Minor Changes
18
+
19
+ - 8e0f80a: Add time support to due dates with workday configuration
20
+
21
+ **New Features:**
22
+ - Support optional time component in due dates: `[due: 2026-02-06 17:00]`
23
+ - Parse both 24-hour format times (H:MM and HH:MM)
24
+ - Added `parseTime()` utility for time validation
25
+ - New workday config schema with `startTime`, `endTime`, and `defaultDueTime`
26
+ - When no time specified in due date, applies default from config (defaults to 17:00 end of workday)
27
+ - Prevents "due 8 hours ago" issues for dates without explicit times
28
+
29
+ **Configuration:**
30
+
31
+ ```json
32
+ {
33
+ "workday": {
34
+ "startTime": "08:00",
35
+ "endTime": "17:00",
36
+ "defaultDueTime": "end"
37
+ }
38
+ }
39
+ ```
40
+
3
41
  ## 0.3.0
4
42
 
5
43
  ### Minor Changes
package/README.md CHANGED
@@ -66,15 +66,15 @@ Parse markdown content and extract all tasks.
66
66
  import { parseMarkdown } from '@md2do/core';
67
67
 
68
68
  const content = `
69
- - [ ] Fix bug @nick !!! #backend (2026-01-25)
70
- - [x] Write docs @jane !! #docs
69
+ - [ ] Fix bug @nick !!! #backend [due: 2026-01-25]
70
+ - [x] Write docs @jane !! #docs [completed: 2026-01-20]
71
71
  `;
72
72
 
73
73
  const tasks = parseMarkdown(content, 'tasks.md');
74
74
  // [
75
75
  // {
76
76
  // id: 'abc123',
77
- // text: 'Fix bug @nick !!! #backend (2026-01-25)',
77
+ // text: 'Fix bug',
78
78
  // completed: false,
79
79
  // file: 'tasks.md',
80
80
  // line: 2,
@@ -96,7 +96,7 @@ Parse a single markdown task line.
96
96
  import { parseTaskLine } from '@md2do/core';
97
97
 
98
98
  const task = parseTaskLine(
99
- '- [ ] Fix bug @nick !!! #backend (2026-01-25)',
99
+ '- [ ] Fix bug @nick !!! #backend [due: 2026-01-25]',
100
100
  5,
101
101
  'tasks.md',
102
102
  );
@@ -262,7 +262,7 @@ await updateTask({
262
262
  line: 5,
263
263
  updates: {
264
264
  completed: true,
265
- text: 'Fix bug @nick !!! #backend (2026-01-25) [todoist:123456]',
265
+ text: 'Fix bug @nick !!! #backend [due: 2026-01-25] [todoist: 123456] [completed: 2026-01-26]',
266
266
  },
267
267
  });
268
268
  ```
@@ -501,7 +501,7 @@ pnpm test:coverage
501
501
 
502
502
  - **[@md2do/cli](../cli)** - Command-line interface (uses this package)
503
503
  - **[@md2do/config](../config)** - Configuration management
504
- - **[@md2do/todoist](../todoist)** - Todoist API integration (uses this package)
504
+ - **[@md2do/todoist](../todoist)** - [Todoist](https://www.todoist.com) API integration (uses this package)
505
505
  - **[@md2do/mcp](../mcp)** - MCP server for AI integration (uses this package)
506
506
 
507
507
  ## Documentation
package/dist/index.d.mts CHANGED
@@ -21,6 +21,9 @@ interface ParsingContext {
21
21
  person?: string;
22
22
  currentDate?: Date;
23
23
  currentHeading?: string;
24
+ workdayStartTime?: string;
25
+ workdayEndTime?: string;
26
+ defaultDueTime?: 'start' | 'end';
24
27
  }
25
28
  interface TaskFilterCriteria {
26
29
  assignee?: string | string[];
@@ -122,10 +125,10 @@ declare function extractCompletedDate(text: string): Date | undefined;
122
125
  * Extract due date from task text with context awareness
123
126
  *
124
127
  * Handles both absolute dates ([due: 2026-01-25]) and relative dates
125
- * ([due: tomorrow]) which require context.
128
+ * ([due: tomorrow]) which require context. Supports optional time ([due: 2026-01-25 17:00]).
126
129
  *
127
130
  * @param text - Task text
128
- * @param context - Parsing context (for relative dates)
131
+ * @param context - Parsing context (for relative dates and workday config)
129
132
  * @returns Object with parsed date and optional warning
130
133
  */
131
134
  declare function extractDueDate(text: string, context: ParsingContext): {
@@ -232,15 +235,17 @@ declare const PRIORITY_HIGH: RegExp;
232
235
  */
233
236
  declare const PRIORITY_NORMAL: RegExp;
234
237
  /**
235
- * Matches absolute due date in ISO format [due: YYYY-MM-DD]
238
+ * Matches absolute due date in ISO format [due: YYYY-MM-DD] with optional time [due: YYYY-MM-DD HH:MM]
236
239
  *
237
240
  * Examples:
238
- * "[due: 2026-01-25]" → "2026-01-25"
239
- * "[due:2026-01-25]" → "2026-01-25" (spaces optional)
240
- * "[due: 2026-01-25 ]" → "2026-01-25" (whitespace allowed)
241
+ * "[due: 2026-01-25]" → "2026-01-25", undefined
242
+ * "[due: 2026-01-25 17:00]" → "2026-01-25", "17:00"
243
+ * "[due: 2026-01-25 9:00]" → "2026-01-25", "9:00"
244
+ * "[due:2026-01-25]" → "2026-01-25", undefined (spaces optional)
241
245
  *
242
246
  * Groups:
243
247
  * [1] - Date string in YYYY-MM-DD format
248
+ * [2] - Optional time string in H:MM or HH:MM format (24-hour)
244
249
  */
245
250
  declare const DUE_DATE_ABSOLUTE: RegExp;
246
251
  /**
@@ -400,9 +405,14 @@ declare class MarkdownScanner {
400
405
  *
401
406
  * @param filePath - Relative file path (for context extraction)
402
407
  * @param content - File content as string
408
+ * @param options - Optional scanner options including workday config
403
409
  * @returns Object containing tasks and warnings
404
410
  */
405
- scanFile(filePath: string, content: string): {
411
+ scanFile(filePath: string, content: string, options?: {
412
+ workdayStartTime?: string;
413
+ workdayEndTime?: string;
414
+ defaultDueTime?: 'start' | 'end';
415
+ }): {
406
416
  tasks: Task[];
407
417
  warnings: Warning[];
408
418
  };
@@ -874,7 +884,22 @@ declare function groupWarningsBySeverity(warnings: Warning[]): {
874
884
  };
875
885
 
876
886
  /**
877
- * Parse an absolute date string in various formats
887
+ * Parse a time string in HH:MM or H:MM format
888
+ *
889
+ * @param timeStr - Time string to parse (e.g., "17:00", "9:00")
890
+ * @returns Object with hours and minutes, or null if invalid
891
+ *
892
+ * @example
893
+ * parseTime("17:00") // => { hours: 17, minutes: 0 }
894
+ * parseTime("9:30") // => { hours: 9, minutes: 30 }
895
+ * parseTime("25:00") // => null (invalid hour)
896
+ */
897
+ declare function parseTime(timeStr: string): {
898
+ hours: number;
899
+ minutes: number;
900
+ } | null;
901
+ /**
902
+ * Parse an absolute date string in various formats, with optional time
878
903
  *
879
904
  * Supported formats:
880
905
  * - ISO: 2026-01-25
@@ -882,9 +907,14 @@ declare function groupWarningsBySeverity(warnings: Warning[]): {
882
907
  * - US full: 1/25/2026
883
908
  *
884
909
  * @param dateStr - Date string to parse
910
+ * @param timeStr - Optional time string in HH:MM or H:MM format
885
911
  * @returns Parsed Date object or null if invalid
912
+ *
913
+ * @example
914
+ * parseAbsoluteDate("2026-01-25") // => Date at midnight
915
+ * parseAbsoluteDate("2026-01-25", "17:00") // => Date at 5 PM
886
916
  */
887
- declare function parseAbsoluteDate(dateStr: string): Date | null;
917
+ declare function parseAbsoluteDate(dateStr: string, timeStr?: string): Date | null;
888
918
  /**
889
919
  * Resolve a relative date keyword against a base date
890
920
  *
@@ -937,4 +967,4 @@ declare function extractDateFromHeading(line: string): Date | null;
937
967
  */
938
968
  declare function generateTaskId(file: string, line: number, text: string): string;
939
969
 
940
- export { ASSIGNEE, COMPLETED_DATE, DUE_DATE_ABSOLUTE, DUE_DATE_RELATIVE, DUE_DATE_SHORT, HEADING_DATE_ISO, HEADING_DATE_NATURAL, HEADING_DATE_SLASH, MarkdownScanner, PATTERNS, PRIORITY_HIGH, PRIORITY_NORMAL, PRIORITY_URGENT, type ParsingContext, type Priority, type ScanResult, TAG, TASK_CHECKBOX, TODOIST_ID, type Task, type TaskFilterCriteria, type UpdateTaskOptions, type Warning, type WarningCode, type WarningFilterConfig, type WarningSeverity, type WriteTaskResult, addTask, cleanTaskText, extractAssignee, extractCompletedDate, extractDateFromHeading, extractDueDate, extractPersonFromFilename, extractPriority, extractProjectFromPath, extractTags, extractTodoistId, filterWarnings, index$1 as filters, generateTaskId, groupWarningsBySeverity, parseAbsoluteDate, parseTask, resolveRelativeDate, index as sorting, updateTask, updateTasks };
970
+ export { ASSIGNEE, COMPLETED_DATE, DUE_DATE_ABSOLUTE, DUE_DATE_RELATIVE, DUE_DATE_SHORT, HEADING_DATE_ISO, HEADING_DATE_NATURAL, HEADING_DATE_SLASH, MarkdownScanner, PATTERNS, PRIORITY_HIGH, PRIORITY_NORMAL, PRIORITY_URGENT, type ParsingContext, type Priority, type ScanResult, TAG, TASK_CHECKBOX, TODOIST_ID, type Task, type TaskFilterCriteria, type UpdateTaskOptions, type Warning, type WarningCode, type WarningFilterConfig, type WarningSeverity, type WriteTaskResult, addTask, cleanTaskText, extractAssignee, extractCompletedDate, extractDateFromHeading, extractDueDate, extractPersonFromFilename, extractPriority, extractProjectFromPath, extractTags, extractTodoistId, filterWarnings, index$1 as filters, generateTaskId, groupWarningsBySeverity, parseAbsoluteDate, parseTask, parseTime, resolveRelativeDate, index as sorting, updateTask, updateTasks };
package/dist/index.d.ts CHANGED
@@ -21,6 +21,9 @@ interface ParsingContext {
21
21
  person?: string;
22
22
  currentDate?: Date;
23
23
  currentHeading?: string;
24
+ workdayStartTime?: string;
25
+ workdayEndTime?: string;
26
+ defaultDueTime?: 'start' | 'end';
24
27
  }
25
28
  interface TaskFilterCriteria {
26
29
  assignee?: string | string[];
@@ -122,10 +125,10 @@ declare function extractCompletedDate(text: string): Date | undefined;
122
125
  * Extract due date from task text with context awareness
123
126
  *
124
127
  * Handles both absolute dates ([due: 2026-01-25]) and relative dates
125
- * ([due: tomorrow]) which require context.
128
+ * ([due: tomorrow]) which require context. Supports optional time ([due: 2026-01-25 17:00]).
126
129
  *
127
130
  * @param text - Task text
128
- * @param context - Parsing context (for relative dates)
131
+ * @param context - Parsing context (for relative dates and workday config)
129
132
  * @returns Object with parsed date and optional warning
130
133
  */
131
134
  declare function extractDueDate(text: string, context: ParsingContext): {
@@ -232,15 +235,17 @@ declare const PRIORITY_HIGH: RegExp;
232
235
  */
233
236
  declare const PRIORITY_NORMAL: RegExp;
234
237
  /**
235
- * Matches absolute due date in ISO format [due: YYYY-MM-DD]
238
+ * Matches absolute due date in ISO format [due: YYYY-MM-DD] with optional time [due: YYYY-MM-DD HH:MM]
236
239
  *
237
240
  * Examples:
238
- * "[due: 2026-01-25]" → "2026-01-25"
239
- * "[due:2026-01-25]" → "2026-01-25" (spaces optional)
240
- * "[due: 2026-01-25 ]" → "2026-01-25" (whitespace allowed)
241
+ * "[due: 2026-01-25]" → "2026-01-25", undefined
242
+ * "[due: 2026-01-25 17:00]" → "2026-01-25", "17:00"
243
+ * "[due: 2026-01-25 9:00]" → "2026-01-25", "9:00"
244
+ * "[due:2026-01-25]" → "2026-01-25", undefined (spaces optional)
241
245
  *
242
246
  * Groups:
243
247
  * [1] - Date string in YYYY-MM-DD format
248
+ * [2] - Optional time string in H:MM or HH:MM format (24-hour)
244
249
  */
245
250
  declare const DUE_DATE_ABSOLUTE: RegExp;
246
251
  /**
@@ -400,9 +405,14 @@ declare class MarkdownScanner {
400
405
  *
401
406
  * @param filePath - Relative file path (for context extraction)
402
407
  * @param content - File content as string
408
+ * @param options - Optional scanner options including workday config
403
409
  * @returns Object containing tasks and warnings
404
410
  */
405
- scanFile(filePath: string, content: string): {
411
+ scanFile(filePath: string, content: string, options?: {
412
+ workdayStartTime?: string;
413
+ workdayEndTime?: string;
414
+ defaultDueTime?: 'start' | 'end';
415
+ }): {
406
416
  tasks: Task[];
407
417
  warnings: Warning[];
408
418
  };
@@ -874,7 +884,22 @@ declare function groupWarningsBySeverity(warnings: Warning[]): {
874
884
  };
875
885
 
876
886
  /**
877
- * Parse an absolute date string in various formats
887
+ * Parse a time string in HH:MM or H:MM format
888
+ *
889
+ * @param timeStr - Time string to parse (e.g., "17:00", "9:00")
890
+ * @returns Object with hours and minutes, or null if invalid
891
+ *
892
+ * @example
893
+ * parseTime("17:00") // => { hours: 17, minutes: 0 }
894
+ * parseTime("9:30") // => { hours: 9, minutes: 30 }
895
+ * parseTime("25:00") // => null (invalid hour)
896
+ */
897
+ declare function parseTime(timeStr: string): {
898
+ hours: number;
899
+ minutes: number;
900
+ } | null;
901
+ /**
902
+ * Parse an absolute date string in various formats, with optional time
878
903
  *
879
904
  * Supported formats:
880
905
  * - ISO: 2026-01-25
@@ -882,9 +907,14 @@ declare function groupWarningsBySeverity(warnings: Warning[]): {
882
907
  * - US full: 1/25/2026
883
908
  *
884
909
  * @param dateStr - Date string to parse
910
+ * @param timeStr - Optional time string in HH:MM or H:MM format
885
911
  * @returns Parsed Date object or null if invalid
912
+ *
913
+ * @example
914
+ * parseAbsoluteDate("2026-01-25") // => Date at midnight
915
+ * parseAbsoluteDate("2026-01-25", "17:00") // => Date at 5 PM
886
916
  */
887
- declare function parseAbsoluteDate(dateStr: string): Date | null;
917
+ declare function parseAbsoluteDate(dateStr: string, timeStr?: string): Date | null;
888
918
  /**
889
919
  * Resolve a relative date keyword against a base date
890
920
  *
@@ -937,4 +967,4 @@ declare function extractDateFromHeading(line: string): Date | null;
937
967
  */
938
968
  declare function generateTaskId(file: string, line: number, text: string): string;
939
969
 
940
- export { ASSIGNEE, COMPLETED_DATE, DUE_DATE_ABSOLUTE, DUE_DATE_RELATIVE, DUE_DATE_SHORT, HEADING_DATE_ISO, HEADING_DATE_NATURAL, HEADING_DATE_SLASH, MarkdownScanner, PATTERNS, PRIORITY_HIGH, PRIORITY_NORMAL, PRIORITY_URGENT, type ParsingContext, type Priority, type ScanResult, TAG, TASK_CHECKBOX, TODOIST_ID, type Task, type TaskFilterCriteria, type UpdateTaskOptions, type Warning, type WarningCode, type WarningFilterConfig, type WarningSeverity, type WriteTaskResult, addTask, cleanTaskText, extractAssignee, extractCompletedDate, extractDateFromHeading, extractDueDate, extractPersonFromFilename, extractPriority, extractProjectFromPath, extractTags, extractTodoistId, filterWarnings, index$1 as filters, generateTaskId, groupWarningsBySeverity, parseAbsoluteDate, parseTask, resolveRelativeDate, index as sorting, updateTask, updateTasks };
970
+ export { ASSIGNEE, COMPLETED_DATE, DUE_DATE_ABSOLUTE, DUE_DATE_RELATIVE, DUE_DATE_SHORT, HEADING_DATE_ISO, HEADING_DATE_NATURAL, HEADING_DATE_SLASH, MarkdownScanner, PATTERNS, PRIORITY_HIGH, PRIORITY_NORMAL, PRIORITY_URGENT, type ParsingContext, type Priority, type ScanResult, TAG, TASK_CHECKBOX, TODOIST_ID, type Task, type TaskFilterCriteria, type UpdateTaskOptions, type Warning, type WarningCode, type WarningFilterConfig, type WarningSeverity, type WriteTaskResult, addTask, cleanTaskText, extractAssignee, extractCompletedDate, extractDateFromHeading, extractDueDate, extractPersonFromFilename, extractPriority, extractProjectFromPath, extractTags, extractTodoistId, filterWarnings, index$1 as filters, generateTaskId, groupWarningsBySeverity, parseAbsoluteDate, parseTask, parseTime, resolveRelativeDate, index as sorting, updateTask, updateTasks };
package/dist/index.js CHANGED
@@ -63,6 +63,7 @@ __export(index_exports, {
63
63
  groupWarningsBySeverity: () => groupWarningsBySeverity,
64
64
  parseAbsoluteDate: () => parseAbsoluteDate,
65
65
  parseTask: () => parseTask,
66
+ parseTime: () => parseTime,
66
67
  resolveRelativeDate: () => resolveRelativeDate,
67
68
  sorting: () => sorting_exports,
68
69
  updateTask: () => updateTask,
@@ -76,7 +77,7 @@ var ASSIGNEE = /@([\w-]+)/;
76
77
  var PRIORITY_URGENT = /!!!/;
77
78
  var PRIORITY_HIGH = /!!/;
78
79
  var PRIORITY_NORMAL = /(?<!!)!(?!!)/;
79
- var DUE_DATE_ABSOLUTE = /\[due:\s*(\d{4}-\d{2}-\d{2})\s*\]/i;
80
+ var DUE_DATE_ABSOLUTE = /\[due:\s*(\d{4}-\d{2}-\d{2})(?:\s+(\d{1,2}:\d{2}))?\s*\]/i;
80
81
  var DUE_DATE_RELATIVE = /\[due:\s*(tomorrow|today|next\s+week|next\s+month)\]/i;
81
82
  var DUE_DATE_SHORT = /\[due:\s*(\d{1,2}\/\d{1,2}(?:\/\d{2,4})?)\]/i;
82
83
  var TAG = /#([\w-]+)/g;
@@ -104,15 +105,34 @@ var PATTERNS = {
104
105
 
105
106
  // src/utils/dates.ts
106
107
  var import_date_fns = require("date-fns");
107
- function parseAbsoluteDate(dateStr) {
108
+ function parseTime(timeStr) {
109
+ const match = timeStr.match(/^(\d{1,2}):(\d{2})$/);
110
+ if (!match || !match[1] || !match[2]) return null;
111
+ const hours = parseInt(match[1], 10);
112
+ const minutes = parseInt(match[2], 10);
113
+ if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
114
+ return null;
115
+ }
116
+ return { hours, minutes };
117
+ }
118
+ function parseAbsoluteDate(dateStr, timeStr) {
108
119
  const referenceDate = /* @__PURE__ */ new Date();
109
120
  let date = (0, import_date_fns.parse)(dateStr, "yyyy-MM-dd", referenceDate);
110
- if ((0, import_date_fns.isValid)(date)) return date;
111
- date = (0, import_date_fns.parse)(dateStr, "M/d/yy", referenceDate);
112
- if ((0, import_date_fns.isValid)(date)) return date;
113
- date = (0, import_date_fns.parse)(dateStr, "M/d/yyyy", referenceDate);
114
- if ((0, import_date_fns.isValid)(date)) return date;
115
- return null;
121
+ if (!(0, import_date_fns.isValid)(date)) {
122
+ date = (0, import_date_fns.parse)(dateStr, "M/d/yy", referenceDate);
123
+ }
124
+ if (!(0, import_date_fns.isValid)(date)) {
125
+ date = (0, import_date_fns.parse)(dateStr, "M/d/yyyy", referenceDate);
126
+ }
127
+ if (!(0, import_date_fns.isValid)(date)) return null;
128
+ if (timeStr) {
129
+ const time = parseTime(timeStr);
130
+ if (time) {
131
+ date = (0, import_date_fns.setHours)(date, time.hours);
132
+ date = (0, import_date_fns.setMinutes)(date, time.minutes);
133
+ }
134
+ }
135
+ return date;
116
136
  }
117
137
  function resolveRelativeDate(relative, baseDate) {
118
138
  const normalized = relative.toLowerCase().trim();
@@ -204,12 +224,29 @@ function extractCompletedDate(text) {
204
224
  function extractDueDate(text, context) {
205
225
  const absoluteMatch = text.match(PATTERNS.DUE_DATE_ABSOLUTE);
206
226
  if (absoluteMatch?.[1]) {
207
- const date = parseAbsoluteDate(absoluteMatch[1]);
227
+ const dateStr = absoluteMatch[1];
228
+ let timeStr = absoluteMatch[2];
229
+ if (!timeStr && context.defaultDueTime) {
230
+ if (context.defaultDueTime === "start" && context.workdayStartTime) {
231
+ timeStr = context.workdayStartTime;
232
+ } else if (context.defaultDueTime === "end" && context.workdayEndTime) {
233
+ timeStr = context.workdayEndTime;
234
+ }
235
+ }
236
+ const date = parseAbsoluteDate(dateStr, timeStr);
208
237
  return { date: date ?? void 0 };
209
238
  }
210
239
  const shortMatch = text.match(PATTERNS.DUE_DATE_SHORT);
211
240
  if (shortMatch?.[1]) {
212
- const date = parseAbsoluteDate(shortMatch[1]);
241
+ let timeStr;
242
+ if (context.defaultDueTime) {
243
+ if (context.defaultDueTime === "start" && context.workdayStartTime) {
244
+ timeStr = context.workdayStartTime;
245
+ } else if (context.defaultDueTime === "end" && context.workdayEndTime) {
246
+ timeStr = context.workdayEndTime;
247
+ }
248
+ }
249
+ const date = parseAbsoluteDate(shortMatch[1], timeStr);
213
250
  return { date: date ?? void 0 };
214
251
  }
215
252
  const relativeMatch = text.match(PATTERNS.DUE_DATE_RELATIVE);
@@ -376,9 +413,10 @@ var MarkdownScanner = class {
376
413
  *
377
414
  * @param filePath - Relative file path (for context extraction)
378
415
  * @param content - File content as string
416
+ * @param options - Optional scanner options including workday config
379
417
  * @returns Object containing tasks and warnings
380
418
  */
381
- scanFile(filePath, content) {
419
+ scanFile(filePath, content, options) {
382
420
  const tasks = [];
383
421
  const warnings = [];
384
422
  const todoistIds = /* @__PURE__ */ new Map();
@@ -387,6 +425,15 @@ var MarkdownScanner = class {
387
425
  if (project !== void 0) context.project = project;
388
426
  const person = extractPersonFromFilename(filePath);
389
427
  if (person !== void 0) context.person = person;
428
+ if (options?.workdayStartTime) {
429
+ context.workdayStartTime = options.workdayStartTime;
430
+ }
431
+ if (options?.workdayEndTime) {
432
+ context.workdayEndTime = options.workdayEndTime;
433
+ }
434
+ if (options?.defaultDueTime) {
435
+ context.defaultDueTime = options.defaultDueTime;
436
+ }
390
437
  const lines = content.split("\n");
391
438
  for (let i = 0; i < lines.length; i++) {
392
439
  const line = lines[i];
@@ -966,6 +1013,7 @@ function groupWarningsBySeverity(warnings) {
966
1013
  groupWarningsBySeverity,
967
1014
  parseAbsoluteDate,
968
1015
  parseTask,
1016
+ parseTime,
969
1017
  resolveRelativeDate,
970
1018
  sorting,
971
1019
  updateTask,
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 parseAbsoluteDate(dateStr) {
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)) return date;
52
- date = parse(dateStr, "M/d/yy", referenceDate);
53
- if (isValid(date)) return date;
54
- date = parse(dateStr, "M/d/yyyy", referenceDate);
55
- if (isValid(date)) return date;
56
- return null;
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 date = parseAbsoluteDate(absoluteMatch[1]);
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
- const date = parseAbsoluteDate(shortMatch[1]);
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);
@@ -317,9 +355,10 @@ var MarkdownScanner = class {
317
355
  *
318
356
  * @param filePath - Relative file path (for context extraction)
319
357
  * @param content - File content as string
358
+ * @param options - Optional scanner options including workday config
320
359
  * @returns Object containing tasks and warnings
321
360
  */
322
- scanFile(filePath, content) {
361
+ scanFile(filePath, content, options) {
323
362
  const tasks = [];
324
363
  const warnings = [];
325
364
  const todoistIds = /* @__PURE__ */ new Map();
@@ -328,6 +367,15 @@ var MarkdownScanner = class {
328
367
  if (project !== void 0) context.project = project;
329
368
  const person = extractPersonFromFilename(filePath);
330
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
+ }
331
379
  const lines = content.split("\n");
332
380
  for (let i = 0; i < lines.length; i++) {
333
381
  const line = lines[i];
@@ -906,6 +954,7 @@ export {
906
954
  groupWarningsBySeverity,
907
955
  parseAbsoluteDate,
908
956
  parseTask,
957
+ parseTime,
909
958
  resolveRelativeDate,
910
959
  sorting_exports as sorting,
911
960
  updateTask,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@md2do/core",
3
- "version": "0.3.0",
3
+ "version": "0.5.1",
4
4
  "description": "Core parsing, filtering, scanning, and file writing for md2do",
5
5
  "keywords": [
6
6
  "markdown",
@@ -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 date = parseAbsoluteDate(absoluteMatch[1]);
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
- const date = parseAbsoluteDate(shortMatch[1]);
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
 
@@ -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" (spaces optional)
71
- * "[due: 2026-01-25 ]" → "2026-01-25" (whitespace allowed)
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 = /\[due:\s*(\d{4}-\d{2}-\d{2})\s*\]/i;
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