@noteplanco/noteplan-mcp 1.1.6 → 1.1.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 (73) hide show
  1. package/dist/noteplan/file-writer.d.ts.map +1 -1
  2. package/dist/noteplan/file-writer.js +73 -16
  3. package/dist/noteplan/file-writer.js.map +1 -1
  4. package/dist/noteplan/file-writer.test.d.ts +2 -0
  5. package/dist/noteplan/file-writer.test.d.ts.map +1 -0
  6. package/dist/noteplan/file-writer.test.js +892 -0
  7. package/dist/noteplan/file-writer.test.js.map +1 -0
  8. package/dist/noteplan/filter-store.d.ts.map +1 -1
  9. package/dist/noteplan/filter-store.js +13 -1
  10. package/dist/noteplan/filter-store.js.map +1 -1
  11. package/dist/noteplan/frontmatter-parser.d.ts +10 -1
  12. package/dist/noteplan/frontmatter-parser.d.ts.map +1 -1
  13. package/dist/noteplan/frontmatter-parser.js +66 -10
  14. package/dist/noteplan/frontmatter-parser.js.map +1 -1
  15. package/dist/noteplan/frontmatter-parser.test.js +484 -1
  16. package/dist/noteplan/frontmatter-parser.test.js.map +1 -1
  17. package/dist/noteplan/markdown-parser.d.ts +6 -1
  18. package/dist/noteplan/markdown-parser.d.ts.map +1 -1
  19. package/dist/noteplan/markdown-parser.js +21 -44
  20. package/dist/noteplan/markdown-parser.js.map +1 -1
  21. package/dist/noteplan/markdown-parser.test.d.ts +2 -0
  22. package/dist/noteplan/markdown-parser.test.d.ts.map +1 -0
  23. package/dist/noteplan/markdown-parser.test.js +653 -0
  24. package/dist/noteplan/markdown-parser.test.js.map +1 -0
  25. package/dist/server.d.ts.map +1 -1
  26. package/dist/server.js +405 -205
  27. package/dist/server.js.map +1 -1
  28. package/dist/tools/attachments.d.ts +151 -0
  29. package/dist/tools/attachments.d.ts.map +1 -0
  30. package/dist/tools/attachments.js +421 -0
  31. package/dist/tools/attachments.js.map +1 -0
  32. package/dist/tools/attachments.test.d.ts +2 -0
  33. package/dist/tools/attachments.test.d.ts.map +1 -0
  34. package/dist/tools/attachments.test.js +561 -0
  35. package/dist/tools/attachments.test.js.map +1 -0
  36. package/dist/tools/calendar.d.ts +5 -5
  37. package/dist/tools/notes.d.ts +67 -26
  38. package/dist/tools/notes.d.ts.map +1 -1
  39. package/dist/tools/notes.js +124 -33
  40. package/dist/tools/notes.js.map +1 -1
  41. package/dist/tools/notes.test.d.ts +2 -0
  42. package/dist/tools/notes.test.d.ts.map +1 -0
  43. package/dist/tools/notes.test.js +598 -0
  44. package/dist/tools/notes.test.js.map +1 -0
  45. package/dist/tools/reminders.d.ts +4 -4
  46. package/dist/tools/tasks.d.ts +10 -10
  47. package/dist/tools/tasks.d.ts.map +1 -1
  48. package/dist/tools/tasks.js +14 -27
  49. package/dist/tools/tasks.js.map +1 -1
  50. package/dist/tools/templates.d.ts +69 -0
  51. package/dist/tools/templates.d.ts.map +1 -0
  52. package/dist/tools/templates.js +145 -0
  53. package/dist/tools/templates.js.map +1 -0
  54. package/dist/tools/templates.test.d.ts +2 -0
  55. package/dist/tools/templates.test.d.ts.map +1 -0
  56. package/dist/tools/templates.test.js +48 -0
  57. package/dist/tools/templates.test.js.map +1 -0
  58. package/dist/tools/ui.d.ts +2 -0
  59. package/dist/tools/ui.d.ts.map +1 -1
  60. package/dist/tools/ui.js +24 -0
  61. package/dist/tools/ui.js.map +1 -1
  62. package/dist/utils/applescript.d.ts.map +1 -1
  63. package/dist/utils/applescript.js +21 -0
  64. package/dist/utils/applescript.js.map +1 -1
  65. package/dist/utils/confirmation-tokens.test.d.ts +2 -0
  66. package/dist/utils/confirmation-tokens.test.d.ts.map +1 -0
  67. package/dist/utils/confirmation-tokens.test.js +159 -0
  68. package/dist/utils/confirmation-tokens.test.js.map +1 -0
  69. package/dist/utils/version.d.ts +2 -0
  70. package/dist/utils/version.d.ts.map +1 -1
  71. package/dist/utils/version.js +4 -0
  72. package/dist/utils/version.js.map +1 -1
  73. package/package.json +1 -1
@@ -0,0 +1,653 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ vi.mock('./preferences.js', () => ({
3
+ getTaskMarkerConfigCached: vi.fn(() => ({
4
+ isAsteriskTodo: true,
5
+ isDashTodo: false,
6
+ defaultTodoCharacter: '*',
7
+ useCheckbox: true,
8
+ })),
9
+ getTaskPrefix: vi.fn(() => '* [ ] '),
10
+ }));
11
+ import { parseTasks, parseTaskLine, extractTags, extractMentions, extractTagsFromContent, extractScheduledDate, extractPriority, extractTitle, updateTaskStatus, updateTaskContent, addTask, extractHeadings, parseParagraphLine, buildParagraphLine, stripRawMarkers, filterTasksByStatus, } from './markdown-parser.js';
12
+ import { getTaskMarkerConfigCached } from './preferences.js';
13
+ // ---------------------------------------------------------------------------
14
+ // extractTags
15
+ // ---------------------------------------------------------------------------
16
+ describe('extractTags', () => {
17
+ it('extracts a simple #tag', () => {
18
+ expect(extractTags('hello #tag world')).toEqual(['#tag']);
19
+ });
20
+ it('extracts hierarchical #parent/child with expansion', () => {
21
+ const result = extractTags('hello #parent/child');
22
+ expect(result).toContain('#parent');
23
+ expect(result).toContain('#parent/child');
24
+ });
25
+ it('extracts multiple tags', () => {
26
+ const result = extractTags('#one #two #three');
27
+ expect(result).toContain('#one');
28
+ expect(result).toContain('#two');
29
+ expect(result).toContain('#three');
30
+ expect(result).toHaveLength(3);
31
+ });
32
+ it('ignores tags inside inline code', () => {
33
+ expect(extractTags('some `#notag` text')).toEqual([]);
34
+ });
35
+ it('ignores tags in markdown link URLs', () => {
36
+ const result = extractTags('[text](#anchor)');
37
+ expect(result).not.toContain('#anchor');
38
+ });
39
+ it('does NOT extract purely numeric #123', () => {
40
+ expect(extractTags('issue #123 here')).toEqual([]);
41
+ });
42
+ it('strips tag attributes #tag(value)', () => {
43
+ const result = extractTags('hello #tag(value) world');
44
+ expect(result).toEqual(['#tag']);
45
+ });
46
+ it('handles emoji/unicode in tags', () => {
47
+ const result = extractTags('hello #caf\u00e9 world');
48
+ expect(result).toEqual(['#caf\u00e9']);
49
+ });
50
+ it('extracts tags after allowed boundary chars', () => {
51
+ // space, (, [, *
52
+ expect(extractTags('(#inparens)')).toContain('#inparens');
53
+ expect(extractTags('[#inbracket]')).toContain('#inbracket');
54
+ expect(extractTags('*#afterstar')).toContain('#afterstar');
55
+ });
56
+ it('returns empty for no tags', () => {
57
+ expect(extractTags('no tags here')).toEqual([]);
58
+ });
59
+ });
60
+ // ---------------------------------------------------------------------------
61
+ // extractMentions
62
+ // ---------------------------------------------------------------------------
63
+ describe('extractMentions', () => {
64
+ it('extracts @person', () => {
65
+ expect(extractMentions('hello @person')).toEqual(['@person']);
66
+ });
67
+ it('extracts hierarchical @team/member with expansion', () => {
68
+ const result = extractMentions('hello @team/member');
69
+ expect(result).toContain('@team');
70
+ expect(result).toContain('@team/member');
71
+ });
72
+ it('strips attributes @repeat(daily)', () => {
73
+ const result = extractMentions('task @repeat(daily)');
74
+ expect(result).toEqual(['@repeat']);
75
+ });
76
+ it('extracts multiple mentions', () => {
77
+ const result = extractMentions('@alice and @bob');
78
+ expect(result).toContain('@alice');
79
+ expect(result).toContain('@bob');
80
+ expect(result).toHaveLength(2);
81
+ });
82
+ it('ignores mentions in inline code', () => {
83
+ expect(extractMentions('use `@Injectable` here')).toEqual([]);
84
+ });
85
+ it('returns empty for no mentions', () => {
86
+ expect(extractMentions('no mentions here')).toEqual([]);
87
+ });
88
+ });
89
+ // ---------------------------------------------------------------------------
90
+ // extractTagsFromContent
91
+ // ---------------------------------------------------------------------------
92
+ describe('extractTagsFromContent', () => {
93
+ it('strips code fences before extracting', () => {
94
+ const content = 'hello #visible\n```\n#hidden\n```\nworld';
95
+ const result = extractTagsFromContent(content);
96
+ expect(result).toContain('#visible');
97
+ expect(result).not.toContain('#hidden');
98
+ });
99
+ it('excludes @done, @repeat, @final-repeat', () => {
100
+ const content = '#keep @done(2024-01-01) @repeat(daily) @final-repeat';
101
+ const result = extractTagsFromContent(content);
102
+ expect(result).toContain('#keep');
103
+ expect(result).not.toContain('@done');
104
+ expect(result).not.toContain('@repeat');
105
+ expect(result).not.toContain('@final-repeat');
106
+ });
107
+ it('includes both #tags and @mentions except excluded ones', () => {
108
+ const content = '#project @person';
109
+ const result = extractTagsFromContent(content);
110
+ expect(result).toContain('#project');
111
+ expect(result).toContain('@person');
112
+ });
113
+ it('handles multi-line content', () => {
114
+ const content = 'line1 #tag1\nline2 @mention1\nline3 #tag2';
115
+ const result = extractTagsFromContent(content);
116
+ expect(result).toContain('#tag1');
117
+ expect(result).toContain('#tag2');
118
+ expect(result).toContain('@mention1');
119
+ });
120
+ it('expands hierarchies', () => {
121
+ const content = '#a/b/c';
122
+ const result = extractTagsFromContent(content);
123
+ expect(result).toContain('#a');
124
+ expect(result).toContain('#a/b');
125
+ expect(result).toContain('#a/b/c');
126
+ });
127
+ });
128
+ // ---------------------------------------------------------------------------
129
+ // extractScheduledDate
130
+ // ---------------------------------------------------------------------------
131
+ describe('extractScheduledDate', () => {
132
+ it('extracts >2024-01-15', () => {
133
+ expect(extractScheduledDate('task >2024-01-15')).toBe('2024-01-15');
134
+ });
135
+ it('returns undefined when no date', () => {
136
+ expect(extractScheduledDate('no date here')).toBeUndefined();
137
+ });
138
+ it('extracts first match', () => {
139
+ expect(extractScheduledDate('>2024-01-01 >2024-12-31')).toBe('2024-01-01');
140
+ });
141
+ it('works with surrounding text', () => {
142
+ expect(extractScheduledDate('buy milk >2024-06-15 #shopping')).toBe('2024-06-15');
143
+ });
144
+ });
145
+ // ---------------------------------------------------------------------------
146
+ // extractPriority
147
+ // ---------------------------------------------------------------------------
148
+ describe('extractPriority', () => {
149
+ it('extracts ! as 1', () => {
150
+ expect(extractPriority('task !')).toBe(1);
151
+ });
152
+ it('extracts !! as 2', () => {
153
+ expect(extractPriority('task !!')).toBe(2);
154
+ });
155
+ it('extracts !!! as 3', () => {
156
+ expect(extractPriority('task !!!')).toBe(3);
157
+ });
158
+ it('returns undefined when no priority', () => {
159
+ expect(extractPriority('no priority')).toBeUndefined();
160
+ });
161
+ it('does not match ! followed by word char', () => {
162
+ expect(extractPriority('!important')).toBeUndefined();
163
+ });
164
+ it('works with surrounding text', () => {
165
+ expect(extractPriority('buy milk !! #shopping')).toBe(2);
166
+ });
167
+ });
168
+ // ---------------------------------------------------------------------------
169
+ // extractTitle
170
+ // ---------------------------------------------------------------------------
171
+ describe('extractTitle', () => {
172
+ it('extracts # My Title', () => {
173
+ expect(extractTitle('# My Title')).toBe('My Title');
174
+ });
175
+ it('extracts ## Sub heading', () => {
176
+ expect(extractTitle('## Sub heading')).toBe('Sub heading');
177
+ });
178
+ it('uses plain text first line', () => {
179
+ expect(extractTitle('Plain title\nBody text')).toBe('Plain title');
180
+ });
181
+ it('returns Untitled for empty content', () => {
182
+ expect(extractTitle('')).toBe('Untitled');
183
+ });
184
+ });
185
+ // ---------------------------------------------------------------------------
186
+ // parseTaskLine
187
+ // ---------------------------------------------------------------------------
188
+ describe('parseTaskLine', () => {
189
+ beforeEach(() => {
190
+ vi.mocked(getTaskMarkerConfigCached).mockReturnValue({
191
+ isAsteriskTodo: true,
192
+ isDashTodo: false,
193
+ defaultTodoCharacter: '*',
194
+ useCheckbox: true,
195
+ taskPrefix: '* [ ] ',
196
+ });
197
+ });
198
+ it('parses checkbox open task', () => {
199
+ const task = parseTaskLine('* [ ] Buy milk', 0);
200
+ expect(task).not.toBeNull();
201
+ expect(task.status).toBe('open');
202
+ expect(task.content).toBe('Buy milk');
203
+ expect(task.marker).toBe('*');
204
+ expect(task.hasCheckbox).toBe(true);
205
+ });
206
+ it('parses done task', () => {
207
+ const task = parseTaskLine('* [x] Done item', 1);
208
+ expect(task).not.toBeNull();
209
+ expect(task.status).toBe('done');
210
+ expect(task.content).toBe('Done item');
211
+ });
212
+ it('parses cancelled task', () => {
213
+ const task = parseTaskLine('- [-] Cancelled', 2);
214
+ expect(task).not.toBeNull();
215
+ expect(task.status).toBe('cancelled');
216
+ });
217
+ it('parses scheduled task', () => {
218
+ const task = parseTaskLine('* [>] Scheduled', 3);
219
+ expect(task).not.toBeNull();
220
+ expect(task.status).toBe('scheduled');
221
+ });
222
+ it('parses plain marker task when isAsteriskTodo is true', () => {
223
+ const task = parseTaskLine('* Do something', 0);
224
+ expect(task).not.toBeNull();
225
+ expect(task.status).toBe('open');
226
+ expect(task.hasCheckbox).toBe(false);
227
+ expect(task.content).toBe('Do something');
228
+ });
229
+ it('returns null for dash list item when isDashTodo is false', () => {
230
+ const task = parseTaskLine('- list item', 0);
231
+ expect(task).toBeNull();
232
+ });
233
+ it('returns null for plain text', () => {
234
+ expect(parseTaskLine('just some text', 0)).toBeNull();
235
+ });
236
+ it('extracts tags, mentions, scheduledDate, priority from task content', () => {
237
+ const task = parseTaskLine('* [ ] Buy milk #shopping @store >2024-01-15 !!', 0);
238
+ expect(task).not.toBeNull();
239
+ expect(task.tags).toContain('#shopping');
240
+ expect(task.mentions).toContain('@store');
241
+ expect(task.scheduledDate).toBe('2024-01-15');
242
+ expect(task.priority).toBe(2);
243
+ });
244
+ });
245
+ // ---------------------------------------------------------------------------
246
+ // parseParagraphLine
247
+ // ---------------------------------------------------------------------------
248
+ describe('parseParagraphLine', () => {
249
+ beforeEach(() => {
250
+ vi.mocked(getTaskMarkerConfigCached).mockReturnValue({
251
+ isAsteriskTodo: true,
252
+ isDashTodo: false,
253
+ defaultTodoCharacter: '*',
254
+ useCheckbox: true,
255
+ taskPrefix: '* [ ] ',
256
+ });
257
+ });
258
+ it('empty line -> type empty', () => {
259
+ const result = parseParagraphLine('', 0, false);
260
+ expect(result.type).toBe('empty');
261
+ expect(result.indentLevel).toBe(0);
262
+ });
263
+ it('separator --- -> type separator', () => {
264
+ expect(parseParagraphLine('---', 0, false).type).toBe('separator');
265
+ });
266
+ it('separator *** -> type separator', () => {
267
+ expect(parseParagraphLine('***', 0, false).type).toBe('separator');
268
+ });
269
+ it('# Title on first line -> type title', () => {
270
+ const result = parseParagraphLine('# Title', 0, true);
271
+ expect(result.type).toBe('title');
272
+ expect(result.headingLevel).toBe(1);
273
+ });
274
+ it('## Section on non-first line -> type heading', () => {
275
+ const result = parseParagraphLine('## Section', 1, false);
276
+ expect(result.type).toBe('heading');
277
+ expect(result.headingLevel).toBe(2);
278
+ });
279
+ it('plain first line -> type title', () => {
280
+ const result = parseParagraphLine('My Note Title', 0, true);
281
+ expect(result.type).toBe('title');
282
+ expect(result.headingLevel).toBe(1);
283
+ });
284
+ it('> text -> type quote', () => {
285
+ const result = parseParagraphLine('> some quote', 1, false);
286
+ expect(result.type).toBe('quote');
287
+ });
288
+ it('* [ ] task -> type task with checkbox', () => {
289
+ const result = parseParagraphLine('* [ ] task content', 1, false);
290
+ expect(result.type).toBe('task');
291
+ expect(result.hasCheckbox).toBe(true);
292
+ expect(result.taskStatus).toBe('open');
293
+ expect(result.marker).toBe('*');
294
+ });
295
+ it('+ [ ] item -> type checklist', () => {
296
+ const result = parseParagraphLine('+ [ ] checklist item', 1, false);
297
+ expect(result.type).toBe('checklist');
298
+ expect(result.hasCheckbox).toBe(true);
299
+ expect(result.marker).toBe('+');
300
+ });
301
+ it('plain * item -> type task when isAsteriskTodo', () => {
302
+ const result = parseParagraphLine('* item', 1, false);
303
+ expect(result.type).toBe('task');
304
+ expect(result.hasCheckbox).toBe(false);
305
+ expect(result.taskStatus).toBe('open');
306
+ });
307
+ it('plain + item -> type checklist', () => {
308
+ const result = parseParagraphLine('+ item', 1, false);
309
+ expect(result.type).toBe('checklist');
310
+ expect(result.hasCheckbox).toBe(false);
311
+ });
312
+ it('- item -> type bullet when isDashTodo is false', () => {
313
+ const result = parseParagraphLine('- item', 1, false);
314
+ expect(result.type).toBe('bullet');
315
+ expect(result.marker).toBe('-');
316
+ });
317
+ it('plain text -> type text', () => {
318
+ const result = parseParagraphLine('just some text', 1, false);
319
+ expect(result.type).toBe('text');
320
+ });
321
+ it('correct indentLevel for tab-indented items', () => {
322
+ const result = parseParagraphLine('\t\t* [ ] nested', 1, false);
323
+ expect(result.indentLevel).toBe(2);
324
+ });
325
+ it('correct indentLevel for space-indented items (2 spaces = 1 level)', () => {
326
+ const result = parseParagraphLine(' * [ ] nested', 1, false);
327
+ expect(result.indentLevel).toBe(2);
328
+ });
329
+ });
330
+ // ---------------------------------------------------------------------------
331
+ // stripRawMarkers
332
+ // ---------------------------------------------------------------------------
333
+ describe('stripRawMarkers', () => {
334
+ it('strips "- [ ] " prefix', () => {
335
+ expect(stripRawMarkers('- [ ] Buy groceries')).toBe('Buy groceries');
336
+ });
337
+ it('strips "* [ ] " prefix', () => {
338
+ expect(stripRawMarkers('* [ ] Buy groceries')).toBe('Buy groceries');
339
+ });
340
+ it('strips "* [x] " prefix', () => {
341
+ expect(stripRawMarkers('* [x] Done thing')).toBe('Done thing');
342
+ });
343
+ it('strips "- [>] " prefix (scheduled)', () => {
344
+ expect(stripRawMarkers('- [>] Scheduled task')).toBe('Scheduled task');
345
+ });
346
+ it('strips "- [-] " prefix (cancelled)', () => {
347
+ expect(stripRawMarkers('- [-] Cancelled task')).toBe('Cancelled task');
348
+ });
349
+ it('strips plain "* " marker without checkbox', () => {
350
+ expect(stripRawMarkers('* Plain task')).toBe('Plain task');
351
+ });
352
+ it('strips plain "- " marker without checkbox', () => {
353
+ expect(stripRawMarkers('- Bullet item')).toBe('Bullet item');
354
+ });
355
+ it('strips "+ [ ] " checklist prefix', () => {
356
+ expect(stripRawMarkers('+ [ ] Checklist item')).toBe('Checklist item');
357
+ });
358
+ it('leaves plain text unchanged', () => {
359
+ expect(stripRawMarkers('Buy groceries')).toBe('Buy groceries');
360
+ });
361
+ it('leaves text with dashes in the middle unchanged', () => {
362
+ expect(stripRawMarkers('Buy - groceries')).toBe('Buy - groceries');
363
+ });
364
+ });
365
+ // ---------------------------------------------------------------------------
366
+ // buildParagraphLine — strips raw markers from LLM content
367
+ // ---------------------------------------------------------------------------
368
+ describe('buildParagraphLine strips raw markers', () => {
369
+ beforeEach(() => {
370
+ vi.mocked(getTaskMarkerConfigCached).mockReturnValue({
371
+ isAsteriskTodo: true,
372
+ isDashTodo: false,
373
+ defaultTodoCharacter: '*',
374
+ useCheckbox: true,
375
+ taskPrefix: '* [ ] ',
376
+ });
377
+ });
378
+ it('strips "- [ ] " when type=task', () => {
379
+ expect(buildParagraphLine('- [ ] Buy groceries', 'task', { taskStatus: 'open' })).toBe('* [ ] Buy groceries');
380
+ });
381
+ it('strips "* [x] " when type=task done', () => {
382
+ expect(buildParagraphLine('* [x] Done thing', 'task', { taskStatus: 'done' })).toBe('* [x] Done thing');
383
+ });
384
+ it('strips "- [ ] " when type=bullet', () => {
385
+ expect(buildParagraphLine('- [ ] Some item', 'bullet')).toBe('- Some item');
386
+ });
387
+ it('strips "* " when type=checklist', () => {
388
+ expect(buildParagraphLine('* Already marked', 'checklist', { taskStatus: 'open' })).toBe('+ [ ] Already marked');
389
+ });
390
+ });
391
+ // ---------------------------------------------------------------------------
392
+ // buildParagraphLine
393
+ // ---------------------------------------------------------------------------
394
+ describe('buildParagraphLine', () => {
395
+ beforeEach(() => {
396
+ vi.mocked(getTaskMarkerConfigCached).mockReturnValue({
397
+ isAsteriskTodo: true,
398
+ isDashTodo: false,
399
+ defaultTodoCharacter: '*',
400
+ useCheckbox: true,
401
+ taskPrefix: '* [ ] ',
402
+ });
403
+ });
404
+ it('title -> # content', () => {
405
+ expect(buildParagraphLine('My Title', 'title')).toBe('# My Title');
406
+ });
407
+ it('heading level 3 -> ### content', () => {
408
+ expect(buildParagraphLine('Section', 'heading', { headingLevel: 3 })).toBe('### Section');
409
+ });
410
+ it('task open with checkbox -> * [ ] content', () => {
411
+ expect(buildParagraphLine('task', 'task', { taskStatus: 'open' })).toBe('* [ ] task');
412
+ });
413
+ it('task done -> * [x] content', () => {
414
+ expect(buildParagraphLine('task', 'task', { taskStatus: 'done' })).toBe('* [x] task');
415
+ });
416
+ it('task without checkbox, open -> * content', () => {
417
+ expect(buildParagraphLine('task', 'task', { hasCheckbox: false, taskStatus: 'open' })).toBe('* task');
418
+ });
419
+ it('checklist -> + [ ] content', () => {
420
+ expect(buildParagraphLine('item', 'checklist', { taskStatus: 'open' })).toBe('+ [ ] item');
421
+ });
422
+ it('bullet -> - content', () => {
423
+ expect(buildParagraphLine('item', 'bullet')).toBe('- item');
424
+ });
425
+ it('quote -> > content', () => {
426
+ expect(buildParagraphLine('quoted', 'quote')).toBe('> quoted');
427
+ });
428
+ it('separator -> ---', () => {
429
+ expect(buildParagraphLine('anything', 'separator')).toBe('---');
430
+ });
431
+ it('empty -> empty string', () => {
432
+ expect(buildParagraphLine('anything', 'empty')).toBe('');
433
+ });
434
+ it('text -> raw content', () => {
435
+ expect(buildParagraphLine('hello world', 'text')).toBe('hello world');
436
+ });
437
+ it('with indentLevel=2 -> tabs prefixed', () => {
438
+ expect(buildParagraphLine('task', 'task', { taskStatus: 'open', indentLevel: 2 })).toBe('\t\t* [ ] task');
439
+ });
440
+ it('with priority=3 -> appends !!!', () => {
441
+ expect(buildParagraphLine('task', 'task', { taskStatus: 'open', priority: 3 })).toBe('* [ ] task !!!');
442
+ });
443
+ });
444
+ // ---------------------------------------------------------------------------
445
+ // updateTaskStatus
446
+ // ---------------------------------------------------------------------------
447
+ describe('updateTaskStatus', () => {
448
+ it('changes [ ] to [x] for done', () => {
449
+ const content = '# Title\n* [ ] Buy milk';
450
+ const result = updateTaskStatus(content, 1, 'done');
451
+ expect(result).toBe('# Title\n* [x] Buy milk');
452
+ });
453
+ it('changes [x] to [ ] for open', () => {
454
+ const content = '* [x] Done task';
455
+ const result = updateTaskStatus(content, 0, 'open');
456
+ expect(result).toBe('* [ ] Done task');
457
+ });
458
+ it('adds checkbox to plain marker task', () => {
459
+ const content = '* plain task';
460
+ const result = updateTaskStatus(content, 0, 'done');
461
+ expect(result).toBe('* [x] plain task');
462
+ });
463
+ it('throws for invalid lineIndex', () => {
464
+ expect(() => updateTaskStatus('line', 5, 'done')).toThrow('Invalid line index');
465
+ });
466
+ it('throws for non-task line', () => {
467
+ expect(() => updateTaskStatus('just text', 0, 'done')).toThrow('not a task');
468
+ });
469
+ });
470
+ // ---------------------------------------------------------------------------
471
+ // updateTaskContent
472
+ // ---------------------------------------------------------------------------
473
+ describe('updateTaskContent', () => {
474
+ it('updates content of checkbox task', () => {
475
+ const content = '* [ ] Old content';
476
+ const result = updateTaskContent(content, 0, 'New content');
477
+ expect(result).toBe('* [ ] New content');
478
+ });
479
+ it('updates content of plain marker task', () => {
480
+ const content = '* Old content';
481
+ const result = updateTaskContent(content, 0, 'New content');
482
+ expect(result).toBe('* New content');
483
+ });
484
+ it('throws for invalid lineIndex', () => {
485
+ expect(() => updateTaskContent('line', 5, 'new')).toThrow('Invalid line index');
486
+ });
487
+ it('throws for non-task line', () => {
488
+ expect(() => updateTaskContent('just text', 0, 'new')).toThrow('not a task');
489
+ });
490
+ });
491
+ // ---------------------------------------------------------------------------
492
+ // addTask
493
+ // ---------------------------------------------------------------------------
494
+ describe('addTask', () => {
495
+ it('adds task at end by default', () => {
496
+ const content = '# Title\nSome text';
497
+ const result = addTask(content, 'New task');
498
+ expect(result).toBe('# Title\nSome text\n* [ ] New task');
499
+ });
500
+ it('adds task at start after frontmatter', () => {
501
+ const content = '---\ntitle: note\n---\n# Title';
502
+ const result = addTask(content, 'New task', 'start');
503
+ expect(result).toBe('---\ntitle: note\n---\n* [ ] New task\n# Title');
504
+ });
505
+ it('adds task after heading', () => {
506
+ const content = '# Title\n## Tasks\nExisting text';
507
+ const result = addTask(content, 'New task', 'after-heading', 'Tasks');
508
+ expect(result).toBe('# Title\n## Tasks\n* [ ] New task\nExisting text');
509
+ });
510
+ it('heading not found -> throws with available headings', () => {
511
+ const content = '# Title\nSome text';
512
+ expect(() => addTask(content, 'New task', 'after-heading', 'NonExistent')).toThrow(/not found/);
513
+ expect(() => addTask(content, 'New task', 'after-heading', 'NonExistent')).toThrow(/Available headings/);
514
+ });
515
+ it('with status and priority options', () => {
516
+ const content = '# Title';
517
+ const result = addTask(content, 'Important', 'end', undefined, { status: 'open', priority: 3 });
518
+ expect(result).toBe('# Title\n* [ ] Important !!!');
519
+ });
520
+ it('inserts after heading when position=start + heading are both provided', () => {
521
+ const content = [
522
+ '---',
523
+ 'title: note',
524
+ '---',
525
+ '# Daily Note',
526
+ '',
527
+ '## Tasks',
528
+ '* [ ] Existing task',
529
+ '',
530
+ '## NotePlan',
531
+ '* [ ] Existing item',
532
+ ].join('\n');
533
+ const result = addTask(content, 'New item', 'start', 'NotePlan');
534
+ const resultLines = result.split('\n');
535
+ const headingIdx = resultLines.indexOf('## NotePlan');
536
+ expect(headingIdx).toBeGreaterThan(-1);
537
+ expect(resultLines[headingIdx + 1]).toBe('* [ ] New item');
538
+ });
539
+ it('inserts at end of section when position=end + heading are both provided', () => {
540
+ const content = [
541
+ '# Daily Note',
542
+ '',
543
+ '## Tasks',
544
+ '* [ ] Existing task',
545
+ '',
546
+ '## NotePlan',
547
+ '* [ ] Existing item',
548
+ '',
549
+ '## Other',
550
+ '* [ ] Other item',
551
+ ].join('\n');
552
+ const result = addTask(content, 'New item', 'end', 'NotePlan');
553
+ const resultLines = result.split('\n');
554
+ const notePlanIdx = resultLines.indexOf('## NotePlan');
555
+ const otherIdx = resultLines.indexOf('## Other');
556
+ const newItemIdx = resultLines.indexOf('* [ ] New item');
557
+ expect(newItemIdx).toBeGreaterThan(notePlanIdx);
558
+ expect(newItemIdx).toBeLessThan(otherIdx);
559
+ });
560
+ it('does not treat a thematic break as a frontmatter closer', () => {
561
+ const content = [
562
+ '---',
563
+ 'bg-color: amber-50',
564
+ // Missing closing ---
565
+ '',
566
+ '## Goals',
567
+ '* [ ] Goal 1',
568
+ '',
569
+ '---', // thematic break, NOT frontmatter
570
+ '',
571
+ '## Other',
572
+ ].join('\n');
573
+ // position=start should insert at top (frontmatter is broken/unclosed)
574
+ const result = addTask(content, 'Top task', 'start');
575
+ const resultLines = result.split('\n');
576
+ const insertedIdx = resultLines.indexOf('* [ ] Top task');
577
+ const thematicIdx = resultLines.indexOf('---', 1);
578
+ expect(insertedIdx).toBeLessThan(thematicIdx);
579
+ });
580
+ });
581
+ // ---------------------------------------------------------------------------
582
+ // extractHeadings
583
+ // ---------------------------------------------------------------------------
584
+ describe('extractHeadings', () => {
585
+ it('extracts ATX headings with levels', () => {
586
+ const content = '# Title\nSome text\n## Section\n### Subsection';
587
+ const headings = extractHeadings(content);
588
+ expect(headings).toHaveLength(3);
589
+ expect(headings[0]).toEqual({ level: 1, text: 'Title', lineIndex: 0 });
590
+ expect(headings[1]).toEqual({ level: 2, text: 'Section', lineIndex: 2 });
591
+ expect(headings[2]).toEqual({ level: 3, text: 'Subsection', lineIndex: 3 });
592
+ });
593
+ it('extracts multiple headings', () => {
594
+ const content = '## A\n## B\n## C';
595
+ const headings = extractHeadings(content);
596
+ expect(headings).toHaveLength(3);
597
+ });
598
+ it('returns empty array when no headings', () => {
599
+ expect(extractHeadings('no headings here\njust text')).toEqual([]);
600
+ });
601
+ });
602
+ // ---------------------------------------------------------------------------
603
+ // filterTasksByStatus
604
+ // ---------------------------------------------------------------------------
605
+ describe('filterTasksByStatus', () => {
606
+ const tasks = [
607
+ { lineIndex: 0, content: 'open', rawLine: '* [ ] open', status: 'open', indentLevel: 0, tags: [], mentions: [] },
608
+ { lineIndex: 1, content: 'done', rawLine: '* [x] done', status: 'done', indentLevel: 0, tags: [], mentions: [] },
609
+ { lineIndex: 2, content: 'cancelled', rawLine: '* [-] cancelled', status: 'cancelled', indentLevel: 0, tags: [], mentions: [] },
610
+ ];
611
+ it('filters by single status', () => {
612
+ const result = filterTasksByStatus(tasks, 'open');
613
+ expect(result).toHaveLength(1);
614
+ expect(result[0].status).toBe('open');
615
+ });
616
+ it('filters by array of statuses', () => {
617
+ const result = filterTasksByStatus(tasks, ['open', 'done']);
618
+ expect(result).toHaveLength(2);
619
+ });
620
+ it('no filter returns all', () => {
621
+ const result = filterTasksByStatus(tasks);
622
+ expect(result).toHaveLength(3);
623
+ });
624
+ });
625
+ // ---------------------------------------------------------------------------
626
+ // parseTasks (integration)
627
+ // ---------------------------------------------------------------------------
628
+ describe('parseTasks', () => {
629
+ beforeEach(() => {
630
+ vi.mocked(getTaskMarkerConfigCached).mockReturnValue({
631
+ isAsteriskTodo: true,
632
+ isDashTodo: false,
633
+ defaultTodoCharacter: '*',
634
+ useCheckbox: true,
635
+ taskPrefix: '* [ ] ',
636
+ });
637
+ });
638
+ it('parses multiple tasks from content', () => {
639
+ const content = '# Title\n* [ ] Task one\nSome text\n* [x] Task two\n- list item';
640
+ const tasks = parseTasks(content);
641
+ expect(tasks).toHaveLength(2);
642
+ expect(tasks[0].content).toBe('Task one');
643
+ expect(tasks[0].status).toBe('open');
644
+ expect(tasks[0].lineIndex).toBe(1);
645
+ expect(tasks[1].content).toBe('Task two');
646
+ expect(tasks[1].status).toBe('done');
647
+ expect(tasks[1].lineIndex).toBe(3);
648
+ });
649
+ it('returns empty array for no tasks', () => {
650
+ expect(parseTasks('# Title\nJust text\n- bullet')).toEqual([]);
651
+ });
652
+ });
653
+ //# sourceMappingURL=markdown-parser.test.js.map