todoosy 0.3.4 → 0.3.7

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 (43) hide show
  1. package/dist/cjs/formatter.d.ts +2 -1
  2. package/dist/cjs/formatter.js +11 -4
  3. package/dist/cjs/index.d.ts +3 -1
  4. package/dist/cjs/index.js +5 -1
  5. package/dist/cjs/linter.d.ts +2 -1
  6. package/dist/cjs/linter.js +140 -126
  7. package/dist/cjs/parser.d.ts +11 -4
  8. package/dist/cjs/parser.js +269 -287
  9. package/dist/cjs/query.d.ts +2 -1
  10. package/dist/cjs/query.js +12 -5
  11. package/dist/cjs/relative-date.d.ts +34 -0
  12. package/dist/cjs/relative-date.js +233 -0
  13. package/dist/cjs/settings.js +1 -1
  14. package/dist/cjs/types.d.ts +2 -0
  15. package/dist/formatter.d.ts +2 -1
  16. package/dist/formatter.d.ts.map +1 -1
  17. package/dist/formatter.js +11 -4
  18. package/dist/formatter.js.map +1 -1
  19. package/dist/index.d.ts +3 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +1 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/linter.d.ts +2 -1
  24. package/dist/linter.d.ts.map +1 -1
  25. package/dist/linter.js +140 -126
  26. package/dist/linter.js.map +1 -1
  27. package/dist/parser.d.ts +11 -4
  28. package/dist/parser.d.ts.map +1 -1
  29. package/dist/parser.js +269 -287
  30. package/dist/parser.js.map +1 -1
  31. package/dist/query.d.ts +2 -1
  32. package/dist/query.d.ts.map +1 -1
  33. package/dist/query.js +12 -5
  34. package/dist/query.js.map +1 -1
  35. package/dist/relative-date.d.ts +35 -0
  36. package/dist/relative-date.d.ts.map +1 -0
  37. package/dist/relative-date.js +229 -0
  38. package/dist/relative-date.js.map +1 -0
  39. package/dist/settings.js +1 -1
  40. package/dist/settings.js.map +1 -1
  41. package/dist/types.d.ts +2 -0
  42. package/dist/types.d.ts.map +1 -1
  43. package/package.json +5 -5
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Todoosy Formatter
3
3
  */
4
+ import { type ParseOptions } from './parser.js';
4
5
  import type { Scheme } from './types.js';
5
- export declare function format(text: string, scheme?: Scheme, filename?: string): string;
6
+ export declare function format(text: string, scheme?: Scheme, filename?: string, options?: ParseOptions): string;
@@ -18,8 +18,15 @@ function parseMiscLocation(misc) {
18
18
  function formatMetadata(metadata) {
19
19
  const parts = [];
20
20
  if (metadata.due) {
21
- const softPrefix = metadata.due_soft ? '~' : '';
22
- parts.push(`due ${softPrefix}${metadata.due}`);
21
+ if (metadata.due_start) {
22
+ // Span form: `due start~end`. The `~` between dates carries the soft semantics;
23
+ // a leading `~` is never used in conjunction with a span.
24
+ parts.push(`due ${metadata.due_start}~${metadata.due}`);
25
+ }
26
+ else {
27
+ const softPrefix = metadata.due_soft ? '~' : '';
28
+ parts.push(`due ${softPrefix}${metadata.due}`);
29
+ }
23
30
  }
24
31
  if (metadata.progress) {
25
32
  parts.push(metadata.progress);
@@ -70,8 +77,8 @@ function formatComments(comments, isListItem, indent) {
70
77
  // Heading comments are not indented
71
78
  return comments;
72
79
  }
73
- function format(text, scheme, filename) {
74
- const { ast } = (0, parser_js_1.parse)(text);
80
+ function format(text, scheme, filename, options) {
81
+ const { ast } = (0, parser_js_1.parse)(text, options);
75
82
  const lines = [];
76
83
  const itemMap = new Map();
77
84
  for (const item of ast.items) {
@@ -2,7 +2,9 @@
2
2
  * Todoosy - Markdown-based todo system
3
3
  */
4
4
  export { parse, parseTokensInParenGroup, extractParenGroups } from './parser.js';
5
- export type { ParseResult } from './parser.js';
5
+ export type { ParseResult, ParseOptions } from './parser.js';
6
+ export { resolveNow, resolveKeyword, tryParseRelative } from './relative-date.js';
7
+ export type { ResolvedNow } from './relative-date.js';
6
8
  export { format } from './formatter.js';
7
9
  export { lint } from './linter.js';
8
10
  export { queryUpcoming, queryMisc, queryByHashtag, listHashtags } from './query.js';
package/dist/cjs/index.js CHANGED
@@ -3,11 +3,15 @@
3
3
  * Todoosy - Markdown-based todo system
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.convertToBullets = exports.convertToSequence = exports.removeSequencedItem = exports.insertSequencedItem = exports.renumberChildren = exports.analyzeSequence = exports.parseSettings = exports.parseScheme = exports.listHashtags = exports.queryByHashtag = exports.queryMisc = exports.queryUpcoming = exports.lint = exports.format = exports.extractParenGroups = exports.parseTokensInParenGroup = exports.parse = void 0;
6
+ exports.convertToBullets = exports.convertToSequence = exports.removeSequencedItem = exports.insertSequencedItem = exports.renumberChildren = exports.analyzeSequence = exports.parseSettings = exports.parseScheme = exports.listHashtags = exports.queryByHashtag = exports.queryMisc = exports.queryUpcoming = exports.lint = exports.format = exports.tryParseRelative = exports.resolveKeyword = exports.resolveNow = exports.extractParenGroups = exports.parseTokensInParenGroup = exports.parse = void 0;
7
7
  var parser_js_1 = require("./parser.js");
8
8
  Object.defineProperty(exports, "parse", { enumerable: true, get: function () { return parser_js_1.parse; } });
9
9
  Object.defineProperty(exports, "parseTokensInParenGroup", { enumerable: true, get: function () { return parser_js_1.parseTokensInParenGroup; } });
10
10
  Object.defineProperty(exports, "extractParenGroups", { enumerable: true, get: function () { return parser_js_1.extractParenGroups; } });
11
+ var relative_date_js_1 = require("./relative-date.js");
12
+ Object.defineProperty(exports, "resolveNow", { enumerable: true, get: function () { return relative_date_js_1.resolveNow; } });
13
+ Object.defineProperty(exports, "resolveKeyword", { enumerable: true, get: function () { return relative_date_js_1.resolveKeyword; } });
14
+ Object.defineProperty(exports, "tryParseRelative", { enumerable: true, get: function () { return relative_date_js_1.tryParseRelative; } });
11
15
  var formatter_js_1 = require("./formatter.js");
12
16
  Object.defineProperty(exports, "format", { enumerable: true, get: function () { return formatter_js_1.format; } });
13
17
  var linter_js_1 = require("./linter.js");
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Todoosy Linter
3
3
  */
4
+ import { type ParseOptions } from './parser.js';
4
5
  import type { LintResult, Scheme } from './types.js';
5
- export declare function lint(text: string, scheme?: Scheme, filename?: string): LintResult;
6
+ export declare function lint(text: string, scheme?: Scheme, filename?: string, options?: ParseOptions): LintResult;
6
7
  export declare function lintScheme(scheme: Scheme): LintResult;
@@ -76,8 +76,8 @@ function parseMiscLocation(misc) {
76
76
  heading: misc.substring(slashIndex + 1),
77
77
  };
78
78
  }
79
- function lint(text, scheme, filename) {
80
- const { ast } = (0, parser_js_1.parse)(text);
79
+ function lint(text, scheme, filename, options) {
80
+ const { ast } = (0, parser_js_1.parse)(text, options);
81
81
  const warnings = [];
82
82
  const lines = text.split('\n');
83
83
  // Determine misc location from scheme or use default
@@ -102,6 +102,12 @@ function lint(text, scheme, filename) {
102
102
  miscSectionSpan = item.item_span;
103
103
  }
104
104
  }
105
+ // If the parser already recognized a due date for this item, skip the
106
+ // regex-based date-format validation entirely. The parser accepts a
107
+ // richer surface than these regexes know about (today/tomorrow keywords,
108
+ // relative offsets like "in 2 weeks", spans like `start~end`); trusting
109
+ // the parser is both more correct and avoids divergence between the two.
110
+ const skipDateFormatChecks = item.metadata.due !== null;
105
111
  // Check for invalid date formats in parentheses
106
112
  const parenMatches = rawLine.matchAll(/\(([^)]+)\)/g);
107
113
  for (const match of parenMatches) {
@@ -114,7 +120,7 @@ function lint(text, scheme, filename) {
114
120
  for (const textDueMatch of textDueMatches) {
115
121
  textDueIndices.add(textDueMatch.index || 0);
116
122
  const dateStr = textDueMatch[1];
117
- if (!isValidDate(dateStr)) {
123
+ if (!isValidDate(dateStr) && !skipDateFormatChecks) {
118
124
  const tokenStart = parenStart + 1 + (textDueMatch.index || 0);
119
125
  warnings.push({
120
126
  code: 'INVALID_DATE_FORMAT',
@@ -130,7 +136,7 @@ function lint(text, scheme, filename) {
130
136
  for (const textDueMatch of textDueDayFirstMatches) {
131
137
  textDueIndices.add(textDueMatch.index || 0);
132
138
  const dateStr = textDueMatch[1];
133
- if (!isValidDate(dateStr)) {
139
+ if (!isValidDate(dateStr) && !skipDateFormatChecks) {
134
140
  const tokenStart = parenStart + 1 + (textDueMatch.index || 0);
135
141
  warnings.push({
136
142
  code: 'INVALID_DATE_FORMAT',
@@ -156,7 +162,7 @@ function lint(text, scheme, filename) {
156
162
  // Skip if this looks like a day number (could be start of day-first date, with or without ~)
157
163
  if (/^~?\d{1,2}$/.test(dateStr))
158
164
  continue;
159
- if (!isValidDate(dateStr)) {
165
+ if (!isValidDate(dateStr) && !skipDateFormatChecks) {
160
166
  const tokenStart = parenStart + 1 + (dueMatch.index || 0);
161
167
  warnings.push({
162
168
  code: 'INVALID_DATE_FORMAT',
@@ -167,134 +173,138 @@ function lint(text, scheme, filename) {
167
173
  });
168
174
  }
169
175
  }
170
- // Check for multiple due dates across all paren groups
176
+ // Check for multiple due dates across all paren groups.
177
+ // Span syntax (`start~end`) legitimately contains two ISO/text dates,
178
+ // so skip duplicate detection when the parser recognized a span.
171
179
  const allDueDates = [];
172
- const allParenMatches = rawLine.matchAll(/\(([^)]+)\)/g);
173
- for (const pMatch of allParenMatches) {
174
- const pContent = pMatch[1];
175
- const pStart = item.item_span[0] + (pMatch.index || 0);
176
- const matchedPositions = new Set();
177
- // Check text dates first (month-first, year can be 2 or 4 digits) with "due" prefix
178
- // Allow optional ~ prefix for soft dates
179
- const textDueDatesInGroup = pContent.matchAll(/\bdue\s+(~?(?:january|jan|february|feb|march|mar|april|apr|may|june|jun|july|jul|august|aug|september|sep|october|oct|november|nov|december|dec)\s+\d{1,2}(?:\s+\d{2,4})?)/gi);
180
- for (const tdm of textDueDatesInGroup) {
181
- const ds = tdm[1];
182
- if (isValidDate(ds)) {
183
- matchedPositions.add(tdm.index || 0);
184
- const dStart = pStart + 1 + (tdm.index || 0);
185
- allDueDates.push({
186
- dateStr: ds,
187
- span: [dStart, dStart + tdm[0].length],
188
- });
180
+ if (item.metadata.due_start === null) {
181
+ const allParenMatches = rawLine.matchAll(/\(([^)]+)\)/g);
182
+ for (const pMatch of allParenMatches) {
183
+ const pContent = pMatch[1];
184
+ const pStart = item.item_span[0] + (pMatch.index || 0);
185
+ const matchedPositions = new Set();
186
+ // Check text dates first (month-first, year can be 2 or 4 digits) with "due" prefix
187
+ // Allow optional ~ prefix for soft dates
188
+ const textDueDatesInGroup = pContent.matchAll(/\bdue\s+(~?(?:january|jan|february|feb|march|mar|april|apr|may|june|jun|july|jul|august|aug|september|sep|october|oct|november|nov|december|dec)\s+\d{1,2}(?:\s+\d{2,4})?)/gi);
189
+ for (const tdm of textDueDatesInGroup) {
190
+ const ds = tdm[1];
191
+ if (isValidDate(ds)) {
192
+ matchedPositions.add(tdm.index || 0);
193
+ const dStart = pStart + 1 + (tdm.index || 0);
194
+ allDueDates.push({
195
+ dateStr: ds,
196
+ span: [dStart, dStart + tdm[0].length],
197
+ });
198
+ }
189
199
  }
190
- }
191
- // Check day-first text dates (year can be 2 or 4 digits) with "due" prefix
192
- // Allow optional ~ prefix for soft dates
193
- const textDueDayFirstInGroup = pContent.matchAll(/\bdue\s+(~?\d{1,2}\s+(?:january|jan|february|feb|march|mar|april|apr|may|june|jun|july|jul|august|aug|september|sep|october|oct|november|nov|december|dec)(?:\s+\d{2,4})?)/gi);
194
- for (const tdm of textDueDayFirstInGroup) {
195
- const ds = tdm[1];
196
- if (isValidDate(ds)) {
197
- matchedPositions.add(tdm.index || 0);
198
- const dStart = pStart + 1 + (tdm.index || 0);
199
- allDueDates.push({
200
- dateStr: ds,
201
- span: [dStart, dStart + tdm[0].length],
202
- });
200
+ // Check day-first text dates (year can be 2 or 4 digits) with "due" prefix
201
+ // Allow optional ~ prefix for soft dates
202
+ const textDueDayFirstInGroup = pContent.matchAll(/\bdue\s+(~?\d{1,2}\s+(?:january|jan|february|feb|march|mar|april|apr|may|june|jun|july|jul|august|aug|september|sep|october|oct|november|nov|december|dec)(?:\s+\d{2,4})?)/gi);
203
+ for (const tdm of textDueDayFirstInGroup) {
204
+ const ds = tdm[1];
205
+ if (isValidDate(ds)) {
206
+ matchedPositions.add(tdm.index || 0);
207
+ const dStart = pStart + 1 + (tdm.index || 0);
208
+ allDueDates.push({
209
+ dateStr: ds,
210
+ span: [dStart, dStart + tdm[0].length],
211
+ });
212
+ }
203
213
  }
204
- }
205
- // Check standard dates with "due" prefix
206
- // Allow optional ~ prefix for soft dates
207
- const dueDatesInGroup = pContent.matchAll(/\bdue\s+(~?[^\s,)]+)/gi);
208
- for (const dm of dueDatesInGroup) {
209
- // Skip if already matched as text date
210
- if (matchedPositions.has(dm.index || 0))
211
- continue;
212
- const ds = dm[1];
213
- // Skip if it's a month name (part of text date, with or without ~)
214
- const strippedDs = ds.startsWith('~') ? ds.slice(1) : ds;
215
- if (MONTH_NAMES.has(strippedDs.toLowerCase()))
216
- continue;
217
- // Skip if it's a day number (part of day-first text date, with or without ~)
218
- if (/^~?\d{1,2}$/.test(ds))
219
- continue;
220
- if (isValidDate(ds)) {
221
- matchedPositions.add(dm.index || 0);
222
- const dStart = pStart + 1 + (dm.index || 0);
223
- allDueDates.push({
224
- dateStr: ds,
225
- span: [dStart, dStart + dm[0].length],
226
- });
214
+ // Check standard dates with "due" prefix
215
+ // Allow optional ~ prefix for soft dates
216
+ const dueDatesInGroup = pContent.matchAll(/\bdue\s+(~?[^\s,)]+)/gi);
217
+ for (const dm of dueDatesInGroup) {
218
+ // Skip if already matched as text date
219
+ if (matchedPositions.has(dm.index || 0))
220
+ continue;
221
+ const ds = dm[1];
222
+ // Skip if it's a month name (part of text date, with or without ~)
223
+ const strippedDs = ds.startsWith('~') ? ds.slice(1) : ds;
224
+ if (MONTH_NAMES.has(strippedDs.toLowerCase()))
225
+ continue;
226
+ // Skip if it's a day number (part of day-first text date, with or without ~)
227
+ if (/^~?\d{1,2}$/.test(ds))
228
+ continue;
229
+ if (isValidDate(ds)) {
230
+ matchedPositions.add(dm.index || 0);
231
+ const dStart = pStart + 1 + (dm.index || 0);
232
+ allDueDates.push({
233
+ dateStr: ds,
234
+ span: [dStart, dStart + dm[0].length],
235
+ });
236
+ }
227
237
  }
228
- }
229
- // Check standalone text dates (without "due" prefix, not preceded by digit+space which indicates day-first)
230
- // Allow optional ~ prefix for soft dates
231
- // Exclude if preceded by "due " or "due ~"
232
- const standaloneTextDatesInGroup = pContent.matchAll(/(?<!\bdue\s)(?<!\bdue\s~)(?<!\d\s)(~?(?:january|jan|february|feb|march|mar|april|apr|may|june|jun|july|jul|august|aug|september|sep|october|oct|november|nov|december|dec)\s+\d{1,2}(?:\s+\d{2,4})?)/gi);
233
- for (const stm of standaloneTextDatesInGroup) {
234
- // Skip if already matched
235
- if (matchedPositions.has(stm.index || 0))
236
- continue;
237
- const ds = stm[1];
238
- if (isValidDate(ds)) {
239
- matchedPositions.add(stm.index || 0);
240
- const dStart = pStart + 1 + (stm.index || 0);
241
- allDueDates.push({
242
- dateStr: ds,
243
- span: [dStart, dStart + stm[0].length],
244
- });
238
+ // Check standalone text dates (without "due" prefix, not preceded by digit+space which indicates day-first)
239
+ // Allow optional ~ prefix for soft dates
240
+ // Exclude if preceded by "due " or "due ~"
241
+ const standaloneTextDatesInGroup = pContent.matchAll(/(?<!\bdue\s)(?<!\bdue\s~)(?<!\d\s)(~?(?:january|jan|february|feb|march|mar|april|apr|may|june|jun|july|jul|august|aug|september|sep|october|oct|november|nov|december|dec)\s+\d{1,2}(?:\s+\d{2,4})?)/gi);
242
+ for (const stm of standaloneTextDatesInGroup) {
243
+ // Skip if already matched
244
+ if (matchedPositions.has(stm.index || 0))
245
+ continue;
246
+ const ds = stm[1];
247
+ if (isValidDate(ds)) {
248
+ matchedPositions.add(stm.index || 0);
249
+ const dStart = pStart + 1 + (stm.index || 0);
250
+ allDueDates.push({
251
+ dateStr: ds,
252
+ span: [dStart, dStart + stm[0].length],
253
+ });
254
+ }
245
255
  }
246
- }
247
- // Check standalone day-first text dates (without "due" prefix, not preceded by digit)
248
- // Note: Day-first soft dates would be like ~15 Feb, but this is unusual; keep pattern simple
249
- // Exclude if preceded by "due " or "due ~"
250
- const standaloneDayFirstInGroup = pContent.matchAll(/(?<!\bdue\s)(?<!\bdue\s~)(?<!\d)(~?\d{1,2}\s+(?:january|jan|february|feb|march|mar|april|apr|may|june|jun|july|jul|august|aug|september|sep|october|oct|november|nov|december|dec)(?:\s+\d{2,4})?)/gi);
251
- for (const stm of standaloneDayFirstInGroup) {
252
- // Skip if already matched
253
- if (matchedPositions.has(stm.index || 0))
254
- continue;
255
- const ds = stm[1];
256
- if (isValidDate(ds)) {
257
- matchedPositions.add(stm.index || 0);
258
- const dStart = pStart + 1 + (stm.index || 0);
259
- allDueDates.push({
260
- dateStr: ds,
261
- span: [dStart, dStart + stm[0].length],
262
- });
256
+ // Check standalone day-first text dates (without "due" prefix, not preceded by digit)
257
+ // Note: Day-first soft dates would be like ~15 Feb, but this is unusual; keep pattern simple
258
+ // Exclude if preceded by "due " or "due ~"
259
+ const standaloneDayFirstInGroup = pContent.matchAll(/(?<!\bdue\s)(?<!\bdue\s~)(?<!\d)(~?\d{1,2}\s+(?:january|jan|february|feb|march|mar|april|apr|may|june|jun|july|jul|august|aug|september|sep|october|oct|november|nov|december|dec)(?:\s+\d{2,4})?)/gi);
260
+ for (const stm of standaloneDayFirstInGroup) {
261
+ // Skip if already matched
262
+ if (matchedPositions.has(stm.index || 0))
263
+ continue;
264
+ const ds = stm[1];
265
+ if (isValidDate(ds)) {
266
+ matchedPositions.add(stm.index || 0);
267
+ const dStart = pStart + 1 + (stm.index || 0);
268
+ allDueDates.push({
269
+ dateStr: ds,
270
+ span: [dStart, dStart + stm[0].length],
271
+ });
272
+ }
273
+ }
274
+ // Check standalone ISO/slash dates (without "due" prefix)
275
+ // Use word boundary to avoid matching partial dates
276
+ // Allow optional ~ prefix for soft dates
277
+ // Exclude if preceded by "due " or "due ~"
278
+ const standaloneNumericDates = pContent.matchAll(/(?<!\bdue\s)(?<!\bdue\s~)(?<!\d)(~?\d{2,4}[-\/]\d{1,2}[-\/]\d{1,4})(?!\d)/gi);
279
+ for (const snm of standaloneNumericDates) {
280
+ // Skip if already matched
281
+ if (matchedPositions.has(snm.index || 0))
282
+ continue;
283
+ const ds = snm[1];
284
+ if (isValidDate(ds)) {
285
+ matchedPositions.add(snm.index || 0);
286
+ const dStart = pStart + 1 + (snm.index || 0);
287
+ allDueDates.push({
288
+ dateStr: ds,
289
+ span: [dStart, dStart + snm[0].length],
290
+ });
291
+ }
263
292
  }
264
293
  }
265
- // Check standalone ISO/slash dates (without "due" prefix)
266
- // Use word boundary to avoid matching partial dates
267
- // Allow optional ~ prefix for soft dates
268
- // Exclude if preceded by "due " or "due ~"
269
- const standaloneNumericDates = pContent.matchAll(/(?<!\bdue\s)(?<!\bdue\s~)(?<!\d)(~?\d{2,4}[-\/]\d{1,2}[-\/]\d{1,4})(?!\d)/gi);
270
- for (const snm of standaloneNumericDates) {
271
- // Skip if already matched
272
- if (matchedPositions.has(snm.index || 0))
273
- continue;
274
- const ds = snm[1];
275
- if (isValidDate(ds)) {
276
- matchedPositions.add(snm.index || 0);
277
- const dStart = pStart + 1 + (snm.index || 0);
278
- allDueDates.push({
279
- dateStr: ds,
280
- span: [dStart, dStart + snm[0].length],
294
+ if (allDueDates.length > 1) {
295
+ // Warn for the second and subsequent ones
296
+ for (let i = 1; i < allDueDates.length; i++) {
297
+ warnings.push({
298
+ code: 'DUPLICATE_DUE_DATE',
299
+ message: 'Multiple due dates found, using last value',
300
+ line: item.line,
301
+ column: allDueDates[i].span[0] - item.item_span[0] + 1,
302
+ span: allDueDates[i].span,
281
303
  });
282
304
  }
305
+ break; // Only warn once per item
283
306
  }
284
- }
285
- if (allDueDates.length > 1) {
286
- // Warn for the second and subsequent ones
287
- for (let i = 1; i < allDueDates.length; i++) {
288
- warnings.push({
289
- code: 'DUPLICATE_DUE_DATE',
290
- message: 'Multiple due dates found, using last value',
291
- line: item.line,
292
- column: allDueDates[i].span[0] - item.item_span[0] + 1,
293
- span: allDueDates[i].span,
294
- });
295
- }
296
- break; // Only warn once per item
297
- }
307
+ } // end if (item.metadata.due_start === null)
298
308
  // Check for standalone dates (without "due" prefix)
299
309
  // Standalone text dates: Month Day [Year] (not preceded by "due" or digit+space)
300
310
  // Allow optional ~ prefix for soft dates
@@ -302,7 +312,7 @@ function lint(text, scheme, filename) {
302
312
  const standaloneTextDates = content.matchAll(/(?<!\bdue\s)(?<!\bdue\s~)(?<!\d\s)(~?(?:january|jan|february|feb|march|mar|april|apr|may|june|jun|july|jul|august|aug|september|sep|october|oct|november|nov|december|dec)\s+\d{1,2}(?:\s+\d{2,4})?)/gi);
303
313
  for (const stdMatch of standaloneTextDates) {
304
314
  const dateStr = stdMatch[1];
305
- if (!isValidDate(dateStr)) {
315
+ if (!isValidDate(dateStr) && !skipDateFormatChecks) {
306
316
  const tokenStart = parenStart + 1 + (stdMatch.index || 0);
307
317
  warnings.push({
308
318
  code: 'INVALID_DATE_FORMAT',
@@ -319,7 +329,7 @@ function lint(text, scheme, filename) {
319
329
  const standaloneDayFirstDates = content.matchAll(/(?<!\bdue\s)(?<!\bdue\s~)(?<!\d)(~?\d{1,2}\s+(?:january|jan|february|feb|march|mar|april|apr|may|june|jun|july|jul|august|aug|september|sep|october|oct|november|nov|december|dec)(?:\s+\d{2,4})?)/gi);
320
330
  for (const stdMatch of standaloneDayFirstDates) {
321
331
  const dateStr = stdMatch[1];
322
- if (!isValidDate(dateStr)) {
332
+ if (!isValidDate(dateStr) && !skipDateFormatChecks) {
323
333
  const tokenStart = parenStart + 1 + (stdMatch.index || 0);
324
334
  warnings.push({
325
335
  code: 'INVALID_DATE_FORMAT',
@@ -350,7 +360,11 @@ function lint(text, scheme, filename) {
350
360
  const invalidEstimates = content.matchAll(/\b(\d+)([a-zA-Z])(?![mhdMHD])\b/gi);
351
361
  for (const ieMatch of invalidEstimates) {
352
362
  const unit = ieMatch[2].toLowerCase();
353
- if (unit !== 'm' && unit !== 'h' && unit !== 'd') {
363
+ // Allow relative-date single-letter units `w` (week) and `y` (year)
364
+ // alongside the existing estimate units m/h/d. Multi-letter forms
365
+ // (`2wk`, `2week`, `2years`, `2 days`, `in 2 weeks`) don't match this
366
+ // regex anyway because the trailing word boundary fails on extra letters.
367
+ if (unit !== 'm' && unit !== 'h' && unit !== 'd' && unit !== 'w' && unit !== 'y') {
354
368
  const tokenStart = parenStart + 1 + (ieMatch.index || 0);
355
369
  warnings.push({
356
370
  code: 'INVALID_TOKEN',
@@ -1,11 +1,18 @@
1
1
  /**
2
2
  * Todoosy Parser
3
3
  */
4
- import type { AST, ParenGroup, Warning } from './types.js';
4
+ import type { AST, ParenGroup, Warning, Settings } from './types.js';
5
+ import { type ResolvedNow } from './relative-date.js';
6
+ export interface ParseOptions {
7
+ /** Override "now" for deterministic tests. */
8
+ now?: Date;
9
+ /** Settings (timezone) used when resolving relative dates. */
10
+ settings?: Settings;
11
+ }
5
12
  export interface ParseResult {
6
13
  ast: AST;
7
14
  warnings: Warning[];
8
15
  }
9
- export declare function parseTokensInParenGroup(content: string, groupStart: number): ParenGroup;
10
- export declare function extractParenGroups(line: string, lineStart: number): ParenGroup[];
11
- export declare function parse(text: string): ParseResult;
16
+ export declare function parseTokensInParenGroup(content: string, groupStart: number, now?: ResolvedNow): ParenGroup;
17
+ export declare function extractParenGroups(line: string, _lineStart: number, now?: ResolvedNow): ParenGroup[];
18
+ export declare function parse(text: string, options?: ParseOptions): ParseResult;