@noteplanco/noteplan-mcp 1.1.15 → 1.1.18
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/dist/noteplan/markdown-parser.d.ts +17 -1
- package/dist/noteplan/markdown-parser.d.ts.map +1 -1
- package/dist/noteplan/markdown-parser.js +101 -7
- package/dist/noteplan/markdown-parser.js.map +1 -1
- package/dist/noteplan/markdown-parser.test.js +47 -0
- package/dist/noteplan/markdown-parser.test.js.map +1 -1
- package/dist/noteplan/types.d.ts +1 -1
- package/dist/noteplan/types.d.ts.map +1 -1
- package/dist/noteplan/types.js +1 -0
- package/dist/noteplan/types.js.map +1 -1
- package/dist/server.js +2 -2
- package/dist/server.js.map +1 -1
- package/dist/tools/date-formatting.test.d.ts +2 -0
- package/dist/tools/date-formatting.test.d.ts.map +1 -0
- package/dist/tools/date-formatting.test.js +89 -0
- package/dist/tools/date-formatting.test.js.map +1 -0
- package/dist/tools/events.d.ts +23 -0
- package/dist/tools/events.d.ts.map +1 -1
- package/dist/tools/events.js +62 -9
- package/dist/tools/events.js.map +1 -1
- package/dist/tools/filters.d.ts +1 -1
- package/dist/tools/notes.d.ts +5 -0
- package/dist/tools/notes.d.ts.map +1 -1
- package/dist/tools/notes.js +85 -15
- package/dist/tools/notes.js.map +1 -1
- package/dist/tools/notes.test.js +608 -1
- package/dist/tools/notes.test.js.map +1 -1
- package/dist/tools/reminders.d.ts +21 -0
- package/dist/tools/reminders.d.ts.map +1 -1
- package/dist/tools/reminders.js +60 -6
- package/dist/tools/reminders.js.map +1 -1
- package/dist/tools/spaces.d.ts +4 -4
- package/package.json +1 -1
package/dist/tools/notes.test.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { parseParagraphLine, parseAllParagraphLines, isCodeFenceLine, isTableSeparator, isTableRow } from '../noteplan/markdown-parser.js';
|
|
2
3
|
// ---------------------------------------------------------------------------
|
|
3
4
|
// Copies of private helper functions from notes.ts
|
|
4
5
|
// These are verbatim copies so we can unit-test them without exporting.
|
|
@@ -1552,5 +1553,611 @@ describe('searchParagraphsGlobal – frontmatter text matching (issue #3 verific
|
|
|
1552
1553
|
//
|
|
1553
1554
|
// CLEANUP:
|
|
1554
1555
|
// noteplan_manage_note(action="delete", filename="...")
|
|
1555
|
-
//
|
|
1556
|
+
// ---------------------------------------------------------------------------
|
|
1557
|
+
// matchesTypeFilter – copy of private function from notes.ts
|
|
1558
|
+
// ---------------------------------------------------------------------------
|
|
1559
|
+
function matchesTypeFilter(meta, filters) {
|
|
1560
|
+
if (filters.has(meta.type))
|
|
1561
|
+
return true;
|
|
1562
|
+
if (meta.taskStatus && filters.has(`${meta.taskStatus}-${meta.type}`))
|
|
1563
|
+
return true;
|
|
1564
|
+
return false;
|
|
1565
|
+
}
|
|
1566
|
+
describe('matchesTypeFilter', () => {
|
|
1567
|
+
it('matches plain paragraph types (heading, text, bullet, etc.)', () => {
|
|
1568
|
+
expect(matchesTypeFilter({ type: 'heading' }, new Set(['heading']))).toBe(true);
|
|
1569
|
+
expect(matchesTypeFilter({ type: 'text' }, new Set(['text']))).toBe(true);
|
|
1570
|
+
expect(matchesTypeFilter({ type: 'bullet' }, new Set(['bullet']))).toBe(true);
|
|
1571
|
+
expect(matchesTypeFilter({ type: 'quote' }, new Set(['quote']))).toBe(true);
|
|
1572
|
+
expect(matchesTypeFilter({ type: 'separator' }, new Set(['separator']))).toBe(true);
|
|
1573
|
+
expect(matchesTypeFilter({ type: 'empty' }, new Set(['empty']))).toBe(true);
|
|
1574
|
+
});
|
|
1575
|
+
it('does not match plain types against wrong filter', () => {
|
|
1576
|
+
expect(matchesTypeFilter({ type: 'heading' }, new Set(['text']))).toBe(false);
|
|
1577
|
+
expect(matchesTypeFilter({ type: 'bullet' }, new Set(['task']))).toBe(false);
|
|
1578
|
+
});
|
|
1579
|
+
it('"task" filter matches tasks of any status', () => {
|
|
1580
|
+
expect(matchesTypeFilter({ type: 'task', taskStatus: 'open' }, new Set(['task']))).toBe(true);
|
|
1581
|
+
expect(matchesTypeFilter({ type: 'task', taskStatus: 'done' }, new Set(['task']))).toBe(true);
|
|
1582
|
+
expect(matchesTypeFilter({ type: 'task', taskStatus: 'cancelled' }, new Set(['task']))).toBe(true);
|
|
1583
|
+
expect(matchesTypeFilter({ type: 'task', taskStatus: 'scheduled' }, new Set(['task']))).toBe(true);
|
|
1584
|
+
});
|
|
1585
|
+
it('"checklist" filter matches checklists of any status', () => {
|
|
1586
|
+
expect(matchesTypeFilter({ type: 'checklist', taskStatus: 'open' }, new Set(['checklist']))).toBe(true);
|
|
1587
|
+
expect(matchesTypeFilter({ type: 'checklist', taskStatus: 'done' }, new Set(['checklist']))).toBe(true);
|
|
1588
|
+
expect(matchesTypeFilter({ type: 'checklist', taskStatus: 'cancelled' }, new Set(['checklist']))).toBe(true);
|
|
1589
|
+
});
|
|
1590
|
+
it('status-qualified filter matches only the specific status', () => {
|
|
1591
|
+
expect(matchesTypeFilter({ type: 'task', taskStatus: 'open' }, new Set(['open-task']))).toBe(true);
|
|
1592
|
+
expect(matchesTypeFilter({ type: 'task', taskStatus: 'done' }, new Set(['open-task']))).toBe(false);
|
|
1593
|
+
expect(matchesTypeFilter({ type: 'task', taskStatus: 'done' }, new Set(['done-task']))).toBe(true);
|
|
1594
|
+
expect(matchesTypeFilter({ type: 'task', taskStatus: 'cancelled' }, new Set(['cancelled-task']))).toBe(true);
|
|
1595
|
+
expect(matchesTypeFilter({ type: 'task', taskStatus: 'scheduled' }, new Set(['scheduled-task']))).toBe(true);
|
|
1596
|
+
});
|
|
1597
|
+
it('status-qualified filter for checklists', () => {
|
|
1598
|
+
expect(matchesTypeFilter({ type: 'checklist', taskStatus: 'open' }, new Set(['open-checklist']))).toBe(true);
|
|
1599
|
+
expect(matchesTypeFilter({ type: 'checklist', taskStatus: 'done' }, new Set(['done-checklist']))).toBe(true);
|
|
1600
|
+
expect(matchesTypeFilter({ type: 'checklist', taskStatus: 'cancelled' }, new Set(['cancelled-checklist']))).toBe(true);
|
|
1601
|
+
expect(matchesTypeFilter({ type: 'checklist', taskStatus: 'scheduled' }, new Set(['scheduled-checklist']))).toBe(true);
|
|
1602
|
+
expect(matchesTypeFilter({ type: 'checklist', taskStatus: 'open' }, new Set(['done-checklist']))).toBe(false);
|
|
1603
|
+
});
|
|
1604
|
+
it('does not cross-match task vs checklist', () => {
|
|
1605
|
+
expect(matchesTypeFilter({ type: 'task', taskStatus: 'open' }, new Set(['open-checklist']))).toBe(false);
|
|
1606
|
+
expect(matchesTypeFilter({ type: 'checklist', taskStatus: 'open' }, new Set(['open-task']))).toBe(false);
|
|
1607
|
+
expect(matchesTypeFilter({ type: 'task', taskStatus: 'done' }, new Set(['checklist']))).toBe(false);
|
|
1608
|
+
expect(matchesTypeFilter({ type: 'checklist', taskStatus: 'done' }, new Set(['task']))).toBe(false);
|
|
1609
|
+
});
|
|
1610
|
+
it('matches with multiple filters (OR logic)', () => {
|
|
1611
|
+
const filters = new Set(['open-task', 'open-checklist', 'heading']);
|
|
1612
|
+
expect(matchesTypeFilter({ type: 'task', taskStatus: 'open' }, filters)).toBe(true);
|
|
1613
|
+
expect(matchesTypeFilter({ type: 'checklist', taskStatus: 'open' }, filters)).toBe(true);
|
|
1614
|
+
expect(matchesTypeFilter({ type: 'heading' }, filters)).toBe(true);
|
|
1615
|
+
expect(matchesTypeFilter({ type: 'task', taskStatus: 'done' }, filters)).toBe(false);
|
|
1616
|
+
expect(matchesTypeFilter({ type: 'text' }, filters)).toBe(false);
|
|
1617
|
+
});
|
|
1618
|
+
it('does not match when filters set is empty', () => {
|
|
1619
|
+
expect(matchesTypeFilter({ type: 'task', taskStatus: 'open' }, new Set())).toBe(false);
|
|
1620
|
+
expect(matchesTypeFilter({ type: 'heading' }, new Set())).toBe(false);
|
|
1621
|
+
});
|
|
1622
|
+
});
|
|
1623
|
+
// ---------------------------------------------------------------------------
|
|
1624
|
+
// getParagraphs type filtering – integration tests using parseParagraphLine
|
|
1625
|
+
// ---------------------------------------------------------------------------
|
|
1626
|
+
/**
|
|
1627
|
+
* Simulate the filtered path of getParagraphs: parse lines, filter, paginate.
|
|
1628
|
+
* This mirrors the actual implementation without needing a mocked note store.
|
|
1629
|
+
*/
|
|
1630
|
+
function simulateFilteredGetParagraphs(noteContent, types, options) {
|
|
1631
|
+
const allLines = noteContent.split('\n');
|
|
1632
|
+
const totalLineCount = allLines.length;
|
|
1633
|
+
const startLine = Math.max(1, Math.min(options?.startLine ?? 1, totalLineCount));
|
|
1634
|
+
const endLine = Math.max(startLine, Math.min(options?.endLine ?? totalLineCount, totalLineCount));
|
|
1635
|
+
const rangeStartIndex = startLine - 1;
|
|
1636
|
+
const typeFilters = new Set(types);
|
|
1637
|
+
const filtered = [];
|
|
1638
|
+
// Use stateful parser (matches actual getParagraphs implementation)
|
|
1639
|
+
const allMeta = parseAllParagraphLines(allLines);
|
|
1640
|
+
for (let i = rangeStartIndex; i < endLine; i++) {
|
|
1641
|
+
const meta = allMeta[i];
|
|
1642
|
+
if (matchesTypeFilter(meta, typeFilters)) {
|
|
1643
|
+
filtered.push({
|
|
1644
|
+
line: i + 1,
|
|
1645
|
+
lineIndex: i,
|
|
1646
|
+
content: allLines[i],
|
|
1647
|
+
type: meta.type,
|
|
1648
|
+
...(meta.taskStatus && { taskStatus: meta.taskStatus }),
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
const offset = options?.offset ?? 0;
|
|
1653
|
+
const limit = options?.limit ?? 200;
|
|
1654
|
+
const page = filtered.slice(offset, offset + limit);
|
|
1655
|
+
const hasMore = offset + page.length < filtered.length;
|
|
1656
|
+
return {
|
|
1657
|
+
filteredCount: filtered.length,
|
|
1658
|
+
returnedLineCount: page.length,
|
|
1659
|
+
hasMore,
|
|
1660
|
+
offset,
|
|
1661
|
+
limit,
|
|
1662
|
+
lines: page,
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
describe('getParagraphs type filtering (integration)', () => {
|
|
1666
|
+
const sampleNote = [
|
|
1667
|
+
'# My Note', // line 1 - title
|
|
1668
|
+
'', // line 2 - empty
|
|
1669
|
+
'## Tasks Section', // line 3 - heading
|
|
1670
|
+
'* [ ] Buy groceries', // line 4 - open task
|
|
1671
|
+
'* [x] Call dentist', // line 5 - done task
|
|
1672
|
+
'* [-] Old cancelled thing', // line 6 - cancelled task
|
|
1673
|
+
'* [>] Deferred meeting', // line 7 - scheduled task
|
|
1674
|
+
'', // line 8 - empty
|
|
1675
|
+
'## Checklist', // line 9 - heading
|
|
1676
|
+
'+ [ ] Pack bags', // line 10 - open checklist
|
|
1677
|
+
'+ [x] Book hotel', // line 11 - done checklist
|
|
1678
|
+
'+ [-] Cancelled flight', // line 12 - cancelled checklist
|
|
1679
|
+
'+ [>] Scheduled pickup', // line 13 - scheduled checklist
|
|
1680
|
+
'', // line 14 - empty
|
|
1681
|
+
'Some regular text', // line 15 - text
|
|
1682
|
+
'- A bullet point', // line 16 - bullet
|
|
1683
|
+
'> A quote', // line 17 - quote
|
|
1684
|
+
].join('\n');
|
|
1685
|
+
it('filters open tasks only', () => {
|
|
1686
|
+
const result = simulateFilteredGetParagraphs(sampleNote, ['open-task']);
|
|
1687
|
+
expect(result.filteredCount).toBe(1);
|
|
1688
|
+
expect(result.lines[0].content).toBe('* [ ] Buy groceries');
|
|
1689
|
+
expect(result.lines[0].taskStatus).toBe('open');
|
|
1690
|
+
expect(result.lines[0].type).toBe('task');
|
|
1691
|
+
});
|
|
1692
|
+
it('filters done tasks only', () => {
|
|
1693
|
+
const result = simulateFilteredGetParagraphs(sampleNote, ['done-task']);
|
|
1694
|
+
expect(result.filteredCount).toBe(1);
|
|
1695
|
+
expect(result.lines[0].content).toBe('* [x] Call dentist');
|
|
1696
|
+
});
|
|
1697
|
+
it('filters cancelled tasks only', () => {
|
|
1698
|
+
const result = simulateFilteredGetParagraphs(sampleNote, ['cancelled-task']);
|
|
1699
|
+
expect(result.filteredCount).toBe(1);
|
|
1700
|
+
expect(result.lines[0].content).toBe('* [-] Old cancelled thing');
|
|
1701
|
+
});
|
|
1702
|
+
it('filters scheduled tasks only', () => {
|
|
1703
|
+
const result = simulateFilteredGetParagraphs(sampleNote, ['scheduled-task']);
|
|
1704
|
+
expect(result.filteredCount).toBe(1);
|
|
1705
|
+
expect(result.lines[0].content).toBe('* [>] Deferred meeting');
|
|
1706
|
+
});
|
|
1707
|
+
it('"task" matches all task statuses', () => {
|
|
1708
|
+
const result = simulateFilteredGetParagraphs(sampleNote, ['task']);
|
|
1709
|
+
expect(result.filteredCount).toBe(4);
|
|
1710
|
+
expect(result.lines.map(l => l.taskStatus)).toEqual(['open', 'done', 'cancelled', 'scheduled']);
|
|
1711
|
+
});
|
|
1712
|
+
it('filters open checklists only', () => {
|
|
1713
|
+
const result = simulateFilteredGetParagraphs(sampleNote, ['open-checklist']);
|
|
1714
|
+
expect(result.filteredCount).toBe(1);
|
|
1715
|
+
expect(result.lines[0].content).toBe('+ [ ] Pack bags');
|
|
1716
|
+
expect(result.lines[0].type).toBe('checklist');
|
|
1717
|
+
});
|
|
1718
|
+
it('filters done checklists only', () => {
|
|
1719
|
+
const result = simulateFilteredGetParagraphs(sampleNote, ['done-checklist']);
|
|
1720
|
+
expect(result.filteredCount).toBe(1);
|
|
1721
|
+
expect(result.lines[0].content).toBe('+ [x] Book hotel');
|
|
1722
|
+
});
|
|
1723
|
+
it('filters cancelled checklists only', () => {
|
|
1724
|
+
const result = simulateFilteredGetParagraphs(sampleNote, ['cancelled-checklist']);
|
|
1725
|
+
expect(result.filteredCount).toBe(1);
|
|
1726
|
+
expect(result.lines[0].content).toBe('+ [-] Cancelled flight');
|
|
1727
|
+
});
|
|
1728
|
+
it('"checklist" matches all checklist statuses', () => {
|
|
1729
|
+
const result = simulateFilteredGetParagraphs(sampleNote, ['checklist']);
|
|
1730
|
+
expect(result.filteredCount).toBe(4);
|
|
1731
|
+
expect(result.lines.every(l => l.type === 'checklist')).toBe(true);
|
|
1732
|
+
});
|
|
1733
|
+
it('combines open tasks and open checklists', () => {
|
|
1734
|
+
const result = simulateFilteredGetParagraphs(sampleNote, ['open-task', 'open-checklist']);
|
|
1735
|
+
expect(result.filteredCount).toBe(2);
|
|
1736
|
+
expect(result.lines.map(l => l.content)).toEqual([
|
|
1737
|
+
'* [ ] Buy groceries',
|
|
1738
|
+
'+ [ ] Pack bags',
|
|
1739
|
+
]);
|
|
1740
|
+
});
|
|
1741
|
+
it('filters headings only', () => {
|
|
1742
|
+
const result = simulateFilteredGetParagraphs(sampleNote, ['heading']);
|
|
1743
|
+
expect(result.filteredCount).toBe(2);
|
|
1744
|
+
expect(result.lines.map(l => l.content)).toEqual([
|
|
1745
|
+
'## Tasks Section',
|
|
1746
|
+
'## Checklist',
|
|
1747
|
+
]);
|
|
1748
|
+
});
|
|
1749
|
+
it('filters text paragraphs', () => {
|
|
1750
|
+
const result = simulateFilteredGetParagraphs(sampleNote, ['text']);
|
|
1751
|
+
expect(result.filteredCount).toBe(1);
|
|
1752
|
+
expect(result.lines[0].content).toBe('Some regular text');
|
|
1753
|
+
});
|
|
1754
|
+
it('filters bullets', () => {
|
|
1755
|
+
const result = simulateFilteredGetParagraphs(sampleNote, ['bullet']);
|
|
1756
|
+
expect(result.filteredCount).toBe(1);
|
|
1757
|
+
expect(result.lines[0].content).toBe('- A bullet point');
|
|
1758
|
+
});
|
|
1759
|
+
it('filters quotes', () => {
|
|
1760
|
+
const result = simulateFilteredGetParagraphs(sampleNote, ['quote']);
|
|
1761
|
+
expect(result.filteredCount).toBe(1);
|
|
1762
|
+
expect(result.lines[0].content).toBe('> A quote');
|
|
1763
|
+
});
|
|
1764
|
+
it('filters empty lines', () => {
|
|
1765
|
+
const result = simulateFilteredGetParagraphs(sampleNote, ['empty']);
|
|
1766
|
+
expect(result.filteredCount).toBe(3); // lines 2, 8, 14
|
|
1767
|
+
expect(result.lines.every(l => l.content === '')).toBe(true);
|
|
1768
|
+
});
|
|
1769
|
+
it('filters title', () => {
|
|
1770
|
+
const result = simulateFilteredGetParagraphs(sampleNote, ['title']);
|
|
1771
|
+
expect(result.filteredCount).toBe(1);
|
|
1772
|
+
expect(result.lines[0].content).toBe('# My Note');
|
|
1773
|
+
expect(result.lines[0].line).toBe(1);
|
|
1774
|
+
});
|
|
1775
|
+
it('combines multiple different types', () => {
|
|
1776
|
+
const result = simulateFilteredGetParagraphs(sampleNote, ['heading', 'quote', 'bullet']);
|
|
1777
|
+
expect(result.filteredCount).toBe(4); // 2 headings + 1 bullet + 1 quote
|
|
1778
|
+
});
|
|
1779
|
+
it('preserves original line numbers in filtered results', () => {
|
|
1780
|
+
const result = simulateFilteredGetParagraphs(sampleNote, ['open-task', 'open-checklist']);
|
|
1781
|
+
expect(result.lines[0].line).toBe(4); // * [ ] Buy groceries
|
|
1782
|
+
expect(result.lines[0].lineIndex).toBe(3);
|
|
1783
|
+
expect(result.lines[1].line).toBe(10); // + [ ] Pack bags
|
|
1784
|
+
expect(result.lines[1].lineIndex).toBe(9);
|
|
1785
|
+
});
|
|
1786
|
+
it('pagination works on filtered results', () => {
|
|
1787
|
+
const result = simulateFilteredGetParagraphs(sampleNote, ['task'], { limit: 2, offset: 0 });
|
|
1788
|
+
expect(result.filteredCount).toBe(4);
|
|
1789
|
+
expect(result.returnedLineCount).toBe(2);
|
|
1790
|
+
expect(result.hasMore).toBe(true);
|
|
1791
|
+
expect(result.lines.map(l => l.taskStatus)).toEqual(['open', 'done']);
|
|
1792
|
+
const page2 = simulateFilteredGetParagraphs(sampleNote, ['task'], { limit: 2, offset: 2 });
|
|
1793
|
+
expect(page2.returnedLineCount).toBe(2);
|
|
1794
|
+
expect(page2.hasMore).toBe(false);
|
|
1795
|
+
expect(page2.lines.map(l => l.taskStatus)).toEqual(['cancelled', 'scheduled']);
|
|
1796
|
+
});
|
|
1797
|
+
it('startLine/endLine restricts range before filtering', () => {
|
|
1798
|
+
// Only look at lines 4-7 (the tasks section)
|
|
1799
|
+
const result = simulateFilteredGetParagraphs(sampleNote, ['checklist'], { startLine: 4, endLine: 7 });
|
|
1800
|
+
expect(result.filteredCount).toBe(0); // no checklists in lines 4-7
|
|
1801
|
+
const result2 = simulateFilteredGetParagraphs(sampleNote, ['task'], { startLine: 4, endLine: 7 });
|
|
1802
|
+
expect(result2.filteredCount).toBe(4);
|
|
1803
|
+
});
|
|
1804
|
+
it('returns empty when no lines match the filter', () => {
|
|
1805
|
+
const result = simulateFilteredGetParagraphs(sampleNote, ['separator']);
|
|
1806
|
+
expect(result.filteredCount).toBe(0);
|
|
1807
|
+
expect(result.lines).toEqual([]);
|
|
1808
|
+
expect(result.hasMore).toBe(false);
|
|
1809
|
+
});
|
|
1810
|
+
it('all done items across tasks and checklists', () => {
|
|
1811
|
+
const result = simulateFilteredGetParagraphs(sampleNote, ['done-task', 'done-checklist']);
|
|
1812
|
+
expect(result.filteredCount).toBe(2);
|
|
1813
|
+
expect(result.lines.map(l => l.content)).toEqual([
|
|
1814
|
+
'* [x] Call dentist',
|
|
1815
|
+
'+ [x] Book hotel',
|
|
1816
|
+
]);
|
|
1817
|
+
});
|
|
1818
|
+
it('all cancelled items across tasks and checklists', () => {
|
|
1819
|
+
const result = simulateFilteredGetParagraphs(sampleNote, ['cancelled-task', 'cancelled-checklist']);
|
|
1820
|
+
expect(result.filteredCount).toBe(2);
|
|
1821
|
+
expect(result.lines.map(l => l.content)).toEqual([
|
|
1822
|
+
'* [-] Old cancelled thing',
|
|
1823
|
+
'+ [-] Cancelled flight',
|
|
1824
|
+
]);
|
|
1825
|
+
});
|
|
1826
|
+
});
|
|
1827
|
+
// ---------------------------------------------------------------------------
|
|
1828
|
+
// Swift parity edge cases — ensuring MCP matches Swift app behavior
|
|
1829
|
+
// ---------------------------------------------------------------------------
|
|
1830
|
+
describe('Swift parity: checkbox parsing edge cases', () => {
|
|
1831
|
+
it('uppercase [X] is treated as done (case-insensitive, matching Swift)', () => {
|
|
1832
|
+
const meta = parseParagraphLine('* [X] Completed task', 1, false);
|
|
1833
|
+
expect(meta.type).toBe('task');
|
|
1834
|
+
expect(meta.taskStatus).toBe('done');
|
|
1835
|
+
});
|
|
1836
|
+
it('uppercase [X] checklist is treated as done', () => {
|
|
1837
|
+
const meta = parseParagraphLine('+ [X] Completed checklist', 1, false);
|
|
1838
|
+
expect(meta.type).toBe('checklist');
|
|
1839
|
+
expect(meta.taskStatus).toBe('done');
|
|
1840
|
+
});
|
|
1841
|
+
it('[x] lowercase still works as done', () => {
|
|
1842
|
+
const meta = parseParagraphLine('* [x] Done task', 1, false);
|
|
1843
|
+
expect(meta.type).toBe('task');
|
|
1844
|
+
expect(meta.taskStatus).toBe('done');
|
|
1845
|
+
});
|
|
1846
|
+
it('[ ] is open', () => {
|
|
1847
|
+
const meta = parseParagraphLine('* [ ] Open task', 1, false);
|
|
1848
|
+
expect(meta.taskStatus).toBe('open');
|
|
1849
|
+
});
|
|
1850
|
+
it('[-] is cancelled', () => {
|
|
1851
|
+
const meta = parseParagraphLine('* [-] Cancelled', 1, false);
|
|
1852
|
+
expect(meta.taskStatus).toBe('cancelled');
|
|
1853
|
+
});
|
|
1854
|
+
it('[>] is scheduled', () => {
|
|
1855
|
+
const meta = parseParagraphLine('* [>] Scheduled', 1, false);
|
|
1856
|
+
expect(meta.taskStatus).toBe('scheduled');
|
|
1857
|
+
});
|
|
1858
|
+
it('unrecognized checkbox char gets no taskStatus (matches Swift returning nil)', () => {
|
|
1859
|
+
const meta = parseParagraphLine('* [~] Weird marker', 1, false);
|
|
1860
|
+
expect(meta.type).toBe('task');
|
|
1861
|
+
expect(meta.taskStatus).toBeUndefined();
|
|
1862
|
+
});
|
|
1863
|
+
});
|
|
1864
|
+
describe('Swift parity: uppercase [X] in type filtering', () => {
|
|
1865
|
+
const noteWithUppercaseX = [
|
|
1866
|
+
'# Test Note',
|
|
1867
|
+
'* [X] Done with uppercase',
|
|
1868
|
+
'* [x] Done with lowercase',
|
|
1869
|
+
'* [ ] Still open',
|
|
1870
|
+
'+ [X] Checklist done uppercase',
|
|
1871
|
+
].join('\n');
|
|
1872
|
+
it('done-task filter catches both [x] and [X]', () => {
|
|
1873
|
+
const result = simulateFilteredGetParagraphs(noteWithUppercaseX, ['done-task']);
|
|
1874
|
+
expect(result.filteredCount).toBe(2);
|
|
1875
|
+
expect(result.lines.map(l => l.content)).toEqual([
|
|
1876
|
+
'* [X] Done with uppercase',
|
|
1877
|
+
'* [x] Done with lowercase',
|
|
1878
|
+
]);
|
|
1879
|
+
});
|
|
1880
|
+
it('done-checklist filter catches [X] checklist', () => {
|
|
1881
|
+
const result = simulateFilteredGetParagraphs(noteWithUppercaseX, ['done-checklist']);
|
|
1882
|
+
expect(result.filteredCount).toBe(1);
|
|
1883
|
+
expect(result.lines[0].content).toBe('+ [X] Checklist done uppercase');
|
|
1884
|
+
});
|
|
1885
|
+
it('"task" filter includes uppercase [X] tasks', () => {
|
|
1886
|
+
const result = simulateFilteredGetParagraphs(noteWithUppercaseX, ['task']);
|
|
1887
|
+
expect(result.filteredCount).toBe(3);
|
|
1888
|
+
});
|
|
1889
|
+
});
|
|
1890
|
+
// ---------------------------------------------------------------------------
|
|
1891
|
+
// Code fence detection
|
|
1892
|
+
// ---------------------------------------------------------------------------
|
|
1893
|
+
describe('isCodeFenceLine', () => {
|
|
1894
|
+
it('detects triple backticks', () => {
|
|
1895
|
+
expect(isCodeFenceLine('```')).toBe(true);
|
|
1896
|
+
});
|
|
1897
|
+
it('detects backticks with language tag', () => {
|
|
1898
|
+
expect(isCodeFenceLine('```javascript')).toBe(true);
|
|
1899
|
+
expect(isCodeFenceLine('```swift')).toBe(true);
|
|
1900
|
+
});
|
|
1901
|
+
it('detects backticks with leading whitespace', () => {
|
|
1902
|
+
expect(isCodeFenceLine(' ```')).toBe(true);
|
|
1903
|
+
expect(isCodeFenceLine('\t```')).toBe(true);
|
|
1904
|
+
});
|
|
1905
|
+
it('detects 4+ backticks', () => {
|
|
1906
|
+
expect(isCodeFenceLine('````')).toBe(true);
|
|
1907
|
+
});
|
|
1908
|
+
it('does not match inline backticks', () => {
|
|
1909
|
+
expect(isCodeFenceLine('some `code` here')).toBe(false);
|
|
1910
|
+
expect(isCodeFenceLine('``not a fence')).toBe(false);
|
|
1911
|
+
});
|
|
1912
|
+
it('does not match plain text', () => {
|
|
1913
|
+
expect(isCodeFenceLine('hello world')).toBe(false);
|
|
1914
|
+
});
|
|
1915
|
+
});
|
|
1916
|
+
describe('parseAllParagraphLines: code fence tracking', () => {
|
|
1917
|
+
it('classifies lines inside code fences as "code"', () => {
|
|
1918
|
+
const lines = [
|
|
1919
|
+
'# Title',
|
|
1920
|
+
'```',
|
|
1921
|
+
'const x = 1;',
|
|
1922
|
+
'const y = 2;',
|
|
1923
|
+
'```',
|
|
1924
|
+
'Normal text',
|
|
1925
|
+
];
|
|
1926
|
+
const result = parseAllParagraphLines(lines);
|
|
1927
|
+
expect(result[0].type).toBe('title');
|
|
1928
|
+
expect(result[1].type).toBe('code'); // opening fence
|
|
1929
|
+
expect(result[2].type).toBe('code'); // inside
|
|
1930
|
+
expect(result[3].type).toBe('code'); // inside
|
|
1931
|
+
expect(result[4].type).toBe('code'); // closing fence
|
|
1932
|
+
expect(result[5].type).toBe('text'); // after fence
|
|
1933
|
+
});
|
|
1934
|
+
it('does not classify tasks inside code fences as tasks', () => {
|
|
1935
|
+
const lines = [
|
|
1936
|
+
'Note title',
|
|
1937
|
+
'```',
|
|
1938
|
+
'* [ ] This is not a task',
|
|
1939
|
+
'+ [x] This is not a checklist',
|
|
1940
|
+
'```',
|
|
1941
|
+
'* [ ] This IS a task',
|
|
1942
|
+
];
|
|
1943
|
+
const result = parseAllParagraphLines(lines);
|
|
1944
|
+
expect(result[2].type).toBe('code');
|
|
1945
|
+
expect(result[3].type).toBe('code');
|
|
1946
|
+
expect(result[5].type).toBe('task');
|
|
1947
|
+
expect(result[5].taskStatus).toBe('open');
|
|
1948
|
+
});
|
|
1949
|
+
it('handles code fences with language tags', () => {
|
|
1950
|
+
const lines = [
|
|
1951
|
+
'Title',
|
|
1952
|
+
'```python',
|
|
1953
|
+
'print("hello")',
|
|
1954
|
+
'```',
|
|
1955
|
+
];
|
|
1956
|
+
const result = parseAllParagraphLines(lines);
|
|
1957
|
+
expect(result[1].type).toBe('code');
|
|
1958
|
+
expect(result[2].type).toBe('code');
|
|
1959
|
+
expect(result[3].type).toBe('code');
|
|
1960
|
+
});
|
|
1961
|
+
it('handles multiple code blocks', () => {
|
|
1962
|
+
const lines = [
|
|
1963
|
+
'Title',
|
|
1964
|
+
'```',
|
|
1965
|
+
'first block',
|
|
1966
|
+
'```',
|
|
1967
|
+
'between blocks',
|
|
1968
|
+
'```',
|
|
1969
|
+
'second block',
|
|
1970
|
+
'```',
|
|
1971
|
+
];
|
|
1972
|
+
const result = parseAllParagraphLines(lines);
|
|
1973
|
+
expect(result[1].type).toBe('code');
|
|
1974
|
+
expect(result[2].type).toBe('code');
|
|
1975
|
+
expect(result[3].type).toBe('code');
|
|
1976
|
+
expect(result[4].type).toBe('text');
|
|
1977
|
+
expect(result[5].type).toBe('code');
|
|
1978
|
+
expect(result[6].type).toBe('code');
|
|
1979
|
+
expect(result[7].type).toBe('code');
|
|
1980
|
+
});
|
|
1981
|
+
it('unclosed code fence marks remaining lines as code', () => {
|
|
1982
|
+
const lines = [
|
|
1983
|
+
'Title',
|
|
1984
|
+
'```',
|
|
1985
|
+
'never closed',
|
|
1986
|
+
'* [ ] not a task',
|
|
1987
|
+
];
|
|
1988
|
+
const result = parseAllParagraphLines(lines);
|
|
1989
|
+
expect(result[2].type).toBe('code');
|
|
1990
|
+
expect(result[3].type).toBe('code');
|
|
1991
|
+
});
|
|
1992
|
+
});
|
|
1993
|
+
// ---------------------------------------------------------------------------
|
|
1994
|
+
// Table detection
|
|
1995
|
+
// ---------------------------------------------------------------------------
|
|
1996
|
+
describe('isTableSeparator', () => {
|
|
1997
|
+
it('detects standard separator', () => {
|
|
1998
|
+
expect(isTableSeparator('| --- | --- |')).toBe(true);
|
|
1999
|
+
expect(isTableSeparator('|---|---|')).toBe(true);
|
|
2000
|
+
});
|
|
2001
|
+
it('detects separator with alignment colons', () => {
|
|
2002
|
+
expect(isTableSeparator('| :--- | :---: | ---: |')).toBe(true);
|
|
2003
|
+
});
|
|
2004
|
+
it('rejects lines without pipe boundaries', () => {
|
|
2005
|
+
expect(isTableSeparator('--- | ---')).toBe(false);
|
|
2006
|
+
expect(isTableSeparator('| ---')).toBe(false);
|
|
2007
|
+
});
|
|
2008
|
+
it('rejects lines without dashes', () => {
|
|
2009
|
+
expect(isTableSeparator('| ::: |')).toBe(false);
|
|
2010
|
+
});
|
|
2011
|
+
it('rejects lines with content characters', () => {
|
|
2012
|
+
expect(isTableSeparator('| abc | def |')).toBe(false);
|
|
2013
|
+
});
|
|
2014
|
+
});
|
|
2015
|
+
describe('isTableRow', () => {
|
|
2016
|
+
it('detects standard rows', () => {
|
|
2017
|
+
expect(isTableRow('| Cell A | Cell B |')).toBe(true);
|
|
2018
|
+
});
|
|
2019
|
+
it('requires at least 2 pipes', () => {
|
|
2020
|
+
expect(isTableRow('|single|')).toBe(true); // 2 pipes
|
|
2021
|
+
expect(isTableRow('|')).toBe(false); // 1 pipe
|
|
2022
|
+
});
|
|
2023
|
+
it('rejects lines not starting/ending with pipe', () => {
|
|
2024
|
+
expect(isTableRow('Cell | Cell')).toBe(false);
|
|
2025
|
+
});
|
|
2026
|
+
});
|
|
2027
|
+
describe('parseAllParagraphLines: table tracking', () => {
|
|
2028
|
+
it('classifies table lines as "table" including header row (look-back)', () => {
|
|
2029
|
+
const lines = [
|
|
2030
|
+
'Title',
|
|
2031
|
+
'| Header 1 | Header 2 |',
|
|
2032
|
+
'| --- | --- |',
|
|
2033
|
+
'| Cell A | Cell B |',
|
|
2034
|
+
'| Cell C | Cell D |',
|
|
2035
|
+
'Normal text after',
|
|
2036
|
+
];
|
|
2037
|
+
const result = parseAllParagraphLines(lines);
|
|
2038
|
+
expect(result[0].type).toBe('title');
|
|
2039
|
+
// Line 1 (header row): reclassified as table via look-back when separator is found
|
|
2040
|
+
expect(result[1].type).toBe('table');
|
|
2041
|
+
// Line 2 (separator): triggers isInTable = true → table
|
|
2042
|
+
expect(result[2].type).toBe('table');
|
|
2043
|
+
// Lines 3-4: inside table
|
|
2044
|
+
expect(result[3].type).toBe('table');
|
|
2045
|
+
expect(result[4].type).toBe('table');
|
|
2046
|
+
// Line 5: not a table row → exits table
|
|
2047
|
+
expect(result[5].type).toBe('text');
|
|
2048
|
+
});
|
|
2049
|
+
it('does not classify tasks inside tables as tasks', () => {
|
|
2050
|
+
const lines = [
|
|
2051
|
+
'Title',
|
|
2052
|
+
'| Task | Status |',
|
|
2053
|
+
'| --- | --- |',
|
|
2054
|
+
'| * [ ] Buy milk | Open |',
|
|
2055
|
+
'| * [x] Call vet | Done |',
|
|
2056
|
+
'After table',
|
|
2057
|
+
];
|
|
2058
|
+
const result = parseAllParagraphLines(lines);
|
|
2059
|
+
expect(result[3].type).toBe('table');
|
|
2060
|
+
expect(result[4].type).toBe('table');
|
|
2061
|
+
expect(result[5].type).toBe('text');
|
|
2062
|
+
});
|
|
2063
|
+
it('handles table followed by code block', () => {
|
|
2064
|
+
const lines = [
|
|
2065
|
+
'Title',
|
|
2066
|
+
'| A | B |',
|
|
2067
|
+
'| - | - |',
|
|
2068
|
+
'| 1 | 2 |',
|
|
2069
|
+
'```',
|
|
2070
|
+
'code here',
|
|
2071
|
+
'```',
|
|
2072
|
+
];
|
|
2073
|
+
const result = parseAllParagraphLines(lines);
|
|
2074
|
+
expect(result[1].type).toBe('table'); // header (look-back)
|
|
2075
|
+
expect(result[2].type).toBe('table'); // separator
|
|
2076
|
+
expect(result[3].type).toBe('table'); // data row
|
|
2077
|
+
expect(result[4].type).toBe('code');
|
|
2078
|
+
expect(result[5].type).toBe('code');
|
|
2079
|
+
expect(result[6].type).toBe('code');
|
|
2080
|
+
});
|
|
2081
|
+
});
|
|
2082
|
+
// ---------------------------------------------------------------------------
|
|
2083
|
+
// Separator regex (Swift parity)
|
|
2084
|
+
// ---------------------------------------------------------------------------
|
|
2085
|
+
describe('Swift parity: separator patterns', () => {
|
|
2086
|
+
it('detects standard --- separator', () => {
|
|
2087
|
+
const meta = parseParagraphLine('---', 1, false);
|
|
2088
|
+
expect(meta.type).toBe('separator');
|
|
2089
|
+
});
|
|
2090
|
+
it('detects *** separator', () => {
|
|
2091
|
+
const meta = parseParagraphLine('***', 1, false);
|
|
2092
|
+
expect(meta.type).toBe('separator');
|
|
2093
|
+
});
|
|
2094
|
+
it('detects ___ separator', () => {
|
|
2095
|
+
const meta = parseParagraphLine('___', 1, false);
|
|
2096
|
+
expect(meta.type).toBe('separator');
|
|
2097
|
+
});
|
|
2098
|
+
it('detects long dashes ----', () => {
|
|
2099
|
+
const meta = parseParagraphLine('----', 1, false);
|
|
2100
|
+
expect(meta.type).toBe('separator');
|
|
2101
|
+
});
|
|
2102
|
+
it('detects spaced dashes - - -', () => {
|
|
2103
|
+
const meta = parseParagraphLine('- - -', 1, false);
|
|
2104
|
+
expect(meta.type).toBe('separator');
|
|
2105
|
+
});
|
|
2106
|
+
it('detects spaced dashes with tabs -\t-\t-', () => {
|
|
2107
|
+
const meta = parseParagraphLine('-\t-\t-', 1, false);
|
|
2108
|
+
expect(meta.type).toBe('separator');
|
|
2109
|
+
});
|
|
2110
|
+
it('detects 5+ asterisks *****', () => {
|
|
2111
|
+
const meta = parseParagraphLine('*****', 1, false);
|
|
2112
|
+
expect(meta.type).toBe('separator');
|
|
2113
|
+
});
|
|
2114
|
+
it('detects mixed dashes and underscores -_-', () => {
|
|
2115
|
+
const meta = parseParagraphLine('-_-', 1, false);
|
|
2116
|
+
expect(meta.type).toBe('separator');
|
|
2117
|
+
});
|
|
2118
|
+
it('does not match single dash', () => {
|
|
2119
|
+
const meta = parseParagraphLine('-', 1, false);
|
|
2120
|
+
expect(meta.type).not.toBe('separator');
|
|
2121
|
+
});
|
|
2122
|
+
it('does not match two dashes', () => {
|
|
2123
|
+
const meta = parseParagraphLine('--', 1, false);
|
|
2124
|
+
expect(meta.type).not.toBe('separator');
|
|
2125
|
+
});
|
|
2126
|
+
});
|
|
2127
|
+
// ---------------------------------------------------------------------------
|
|
2128
|
+
// Type filtering with code/table types
|
|
2129
|
+
// ---------------------------------------------------------------------------
|
|
2130
|
+
describe('type filtering with code and table', () => {
|
|
2131
|
+
const noteWithCodeAndTable = [
|
|
2132
|
+
'# Note with code and table', // line 1 - title
|
|
2133
|
+
'* [ ] A real task', // line 2 - open task
|
|
2134
|
+
'```javascript', // line 3 - code
|
|
2135
|
+
'const x = 1;', // line 4 - code
|
|
2136
|
+
'* [ ] fake task in code', // line 5 - code (not task!)
|
|
2137
|
+
'```', // line 6 - code
|
|
2138
|
+
'| Name | Value |', // line 7 - text (header before separator)
|
|
2139
|
+
'| --- | --- |', // line 8 - table
|
|
2140
|
+
'| foo | bar |', // line 9 - table
|
|
2141
|
+
'After table text', // line 10 - text
|
|
2142
|
+
].join('\n');
|
|
2143
|
+
it('filters code lines only', () => {
|
|
2144
|
+
const result = simulateFilteredGetParagraphs(noteWithCodeAndTable, ['code']);
|
|
2145
|
+
expect(result.filteredCount).toBe(4); // lines 3-6
|
|
2146
|
+
expect(result.lines.every(l => l.type === 'code')).toBe(true);
|
|
2147
|
+
});
|
|
2148
|
+
it('filters table lines only', () => {
|
|
2149
|
+
const result = simulateFilteredGetParagraphs(noteWithCodeAndTable, ['table']);
|
|
2150
|
+
expect(result.filteredCount).toBe(3); // lines 7-9 (header look-back + separator + data row)
|
|
2151
|
+
expect(result.lines.every(l => l.type === 'table')).toBe(true);
|
|
2152
|
+
});
|
|
2153
|
+
it('fake task inside code block is NOT returned by task filter', () => {
|
|
2154
|
+
const result = simulateFilteredGetParagraphs(noteWithCodeAndTable, ['open-task']);
|
|
2155
|
+
expect(result.filteredCount).toBe(1);
|
|
2156
|
+
expect(result.lines[0].content).toBe('* [ ] A real task');
|
|
2157
|
+
});
|
|
2158
|
+
it('combines code + task filters', () => {
|
|
2159
|
+
const result = simulateFilteredGetParagraphs(noteWithCodeAndTable, ['code', 'open-task']);
|
|
2160
|
+
expect(result.filteredCount).toBe(5); // 4 code + 1 task
|
|
2161
|
+
});
|
|
2162
|
+
});
|
|
1556
2163
|
//# sourceMappingURL=notes.test.js.map
|