todoosy 0.3.3 → 0.3.5

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.
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Todoosy Query Engine
3
+ */
4
+ import type { UpcomingResult, MiscResult, Scheme } from './types.js';
5
+ export declare function queryUpcoming(text: string, scheme?: Scheme): UpcomingResult;
6
+ export declare function queryMisc(text: string, scheme?: Scheme): MiscResult;
7
+ export interface HashtagItem {
8
+ id: string;
9
+ title_text: string;
10
+ path: string;
11
+ item_span: [number, number];
12
+ hashtags: string[];
13
+ effective_hashtags: string[];
14
+ }
15
+ export interface HashtagResult {
16
+ items: HashtagItem[];
17
+ }
18
+ export interface HashtagListResult {
19
+ hashtags: string[];
20
+ }
21
+ export declare function queryByHashtag(text: string, hashtag: string): HashtagResult;
22
+ export declare function listHashtags(text: string): HashtagListResult;
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ /**
3
+ * Todoosy Query Engine
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.queryUpcoming = queryUpcoming;
7
+ exports.queryMisc = queryMisc;
8
+ exports.queryByHashtag = queryByHashtag;
9
+ exports.listHashtags = listHashtags;
10
+ const parser_js_1 = require("./parser.js");
11
+ function buildPath(itemId, ast) {
12
+ const itemMap = new Map();
13
+ const parentMap = new Map();
14
+ for (const item of ast.items) {
15
+ itemMap.set(item.id, item);
16
+ for (const childId of item.children) {
17
+ parentMap.set(childId, item.id);
18
+ }
19
+ }
20
+ const parts = [];
21
+ let currentId = itemId;
22
+ while (currentId !== undefined) {
23
+ const item = itemMap.get(currentId);
24
+ if (item) {
25
+ parts.unshift(item.title_text);
26
+ }
27
+ currentId = parentMap.get(currentId);
28
+ }
29
+ return parts.join(' > ');
30
+ }
31
+ function queryUpcoming(text, scheme) {
32
+ const { ast } = (0, parser_js_1.parse)(text);
33
+ const items = [];
34
+ // Find all items with due dates
35
+ for (const item of ast.items) {
36
+ if (item.metadata.due) {
37
+ const upcomingItem = {
38
+ id: item.id,
39
+ due: item.metadata.due,
40
+ priority: item.metadata.priority,
41
+ path: buildPath(item.id, ast),
42
+ item_span: item.item_span,
43
+ };
44
+ // Add priority label if scheme is provided
45
+ if (scheme && item.metadata.priority !== null) {
46
+ const label = scheme.priorities[String(item.metadata.priority)];
47
+ if (label) {
48
+ upcomingItem.priority_label = label;
49
+ }
50
+ }
51
+ items.push(upcomingItem);
52
+ }
53
+ }
54
+ // Sort by:
55
+ // 1. Due date ascending
56
+ // 2. Priority ascending (lower is higher priority, nulls last)
57
+ // 3. Document order (by item_span start)
58
+ items.sort((a, b) => {
59
+ // Due date comparison
60
+ const dateCompare = a.due.localeCompare(b.due);
61
+ if (dateCompare !== 0)
62
+ return dateCompare;
63
+ // Priority comparison (null treated as infinity)
64
+ const aPri = a.priority ?? Infinity;
65
+ const bPri = b.priority ?? Infinity;
66
+ if (aPri !== bPri)
67
+ return aPri - bPri;
68
+ // Document order
69
+ return a.item_span[0] - b.item_span[0];
70
+ });
71
+ return { items };
72
+ }
73
+ function parseMiscLocation(misc) {
74
+ const slashIndex = misc.indexOf('/');
75
+ if (slashIndex === -1) {
76
+ return { filename: misc, heading: 'Misc' };
77
+ }
78
+ return {
79
+ filename: misc.substring(0, slashIndex),
80
+ heading: misc.substring(slashIndex + 1),
81
+ };
82
+ }
83
+ function queryMisc(text, scheme) {
84
+ const { ast } = (0, parser_js_1.parse)(text);
85
+ const items = [];
86
+ // Determine the heading name from scheme or use default
87
+ const miscLocation = parseMiscLocation(scheme?.misc ?? 'todoosy.md/Misc');
88
+ // Find the Misc section
89
+ let miscSectionId = null;
90
+ for (const item of ast.items) {
91
+ if (item.type === 'heading' && item.title_text === miscLocation.heading && item.level === 1) {
92
+ miscSectionId = item.id;
93
+ break;
94
+ }
95
+ }
96
+ if (miscSectionId === null) {
97
+ return { items };
98
+ }
99
+ // Get all direct children of the Misc section
100
+ const miscSection = ast.items.find(i => i.id === miscSectionId);
101
+ if (!miscSection) {
102
+ return { items };
103
+ }
104
+ const itemMap = new Map();
105
+ for (const item of ast.items) {
106
+ itemMap.set(item.id, item);
107
+ }
108
+ for (const childId of miscSection.children) {
109
+ const child = itemMap.get(childId);
110
+ if (child) {
111
+ items.push({
112
+ id: child.id,
113
+ title_text: child.title_text,
114
+ item_span: child.item_span,
115
+ });
116
+ }
117
+ }
118
+ return { items };
119
+ }
120
+ function queryByHashtag(text, hashtag) {
121
+ const { ast } = (0, parser_js_1.parse)(text);
122
+ const items = [];
123
+ // Normalize hashtag (lowercase, remove # if present)
124
+ const normalizedHashtag = hashtag.replace(/^#/, '').toLowerCase();
125
+ // Find all items with the hashtag (using effective_hashtags for inheritance)
126
+ for (const item of ast.items) {
127
+ if (item.metadata.effective_hashtags.includes(normalizedHashtag)) {
128
+ items.push({
129
+ id: item.id,
130
+ title_text: item.title_text,
131
+ path: buildPath(item.id, ast),
132
+ item_span: item.item_span,
133
+ hashtags: item.metadata.hashtags,
134
+ effective_hashtags: item.metadata.effective_hashtags,
135
+ });
136
+ }
137
+ }
138
+ // Sort by document order
139
+ items.sort((a, b) => a.item_span[0] - b.item_span[0]);
140
+ return { items };
141
+ }
142
+ function listHashtags(text) {
143
+ const { ast } = (0, parser_js_1.parse)(text);
144
+ const hashtagSet = new Set();
145
+ // Collect all unique hashtags from all items
146
+ for (const item of ast.items) {
147
+ for (const tag of item.metadata.hashtags) {
148
+ hashtagSet.add(tag);
149
+ }
150
+ }
151
+ // Return sorted list
152
+ return { hashtags: [...hashtagSet].sort() };
153
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Todoosy Scheme Parser
3
+ *
4
+ * @deprecated This module is deprecated. Use settings.ts instead.
5
+ * This file is maintained for backwards compatibility.
6
+ */
7
+ export { parseScheme, parseSettings, VALID_CALENDAR_FORMATS, VALID_FORMATTING_STYLES, } from './settings.js';
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ /**
3
+ * Todoosy Scheme Parser
4
+ *
5
+ * @deprecated This module is deprecated. Use settings.ts instead.
6
+ * This file is maintained for backwards compatibility.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.VALID_FORMATTING_STYLES = exports.VALID_CALENDAR_FORMATS = exports.parseSettings = exports.parseScheme = void 0;
10
+ var settings_js_1 = require("./settings.js");
11
+ Object.defineProperty(exports, "parseScheme", { enumerable: true, get: function () { return settings_js_1.parseScheme; } });
12
+ Object.defineProperty(exports, "parseSettings", { enumerable: true, get: function () { return settings_js_1.parseSettings; } });
13
+ Object.defineProperty(exports, "VALID_CALENDAR_FORMATS", { enumerable: true, get: function () { return settings_js_1.VALID_CALENDAR_FORMATS; } });
14
+ Object.defineProperty(exports, "VALID_FORMATTING_STYLES", { enumerable: true, get: function () { return settings_js_1.VALID_FORMATTING_STYLES; } });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Todoosy Sequence Utilities
3
+ *
4
+ * Functions for working with sequenced (numbered) task lists.
5
+ */
6
+ import type { AST, ItemNode } from './types.js';
7
+ export interface SequenceInfo {
8
+ parentId: string;
9
+ hasSequence: boolean;
10
+ numberedChildren: {
11
+ id: string;
12
+ sequenceNumber: number;
13
+ }[];
14
+ gaps: {
15
+ position: number;
16
+ expected: number;
17
+ actual: number;
18
+ }[];
19
+ duplicates: {
20
+ number: number;
21
+ ids: string[];
22
+ }[];
23
+ }
24
+ /**
25
+ * Analyze the sequence of numbered children under a parent item.
26
+ * Returns information about gaps and duplicates in the sequence.
27
+ */
28
+ export declare function analyzeSequence(ast: AST, parentId: string): SequenceInfo;
29
+ /**
30
+ * Renumber children of a parent to have consecutive sequence numbers starting from 1.
31
+ * Returns a new AST with the renumbered items.
32
+ */
33
+ export declare function renumberChildren(ast: AST, parentId: string): AST;
34
+ /**
35
+ * Insert a new item into a sequenced list at a specific position and renumber.
36
+ * Position is 0-indexed. Returns a new AST with the inserted and renumbered items.
37
+ */
38
+ export declare function insertSequencedItem(ast: AST, parentId: string, position: number, newItem: Omit<ItemNode, 'id' | 'sequence_number'>): AST;
39
+ /**
40
+ * Remove an item from a sequenced list and renumber siblings.
41
+ * Returns a new AST with the item removed and siblings renumbered.
42
+ */
43
+ export declare function removeSequencedItem(ast: AST, itemId: string): AST;
44
+ /**
45
+ * Convert bullet children of a parent to numbered items.
46
+ * Returns a new AST with bullet items converted to numbered.
47
+ */
48
+ export declare function convertToSequence(ast: AST, parentId: string): AST;
49
+ /**
50
+ * Convert numbered children of a parent to bullet items.
51
+ * Returns a new AST with numbered items converted to bullets.
52
+ */
53
+ export declare function convertToBullets(ast: AST, parentId: string): AST;
@@ -0,0 +1,233 @@
1
+ "use strict";
2
+ /**
3
+ * Todoosy Sequence Utilities
4
+ *
5
+ * Functions for working with sequenced (numbered) task lists.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.analyzeSequence = analyzeSequence;
9
+ exports.renumberChildren = renumberChildren;
10
+ exports.insertSequencedItem = insertSequencedItem;
11
+ exports.removeSequencedItem = removeSequencedItem;
12
+ exports.convertToSequence = convertToSequence;
13
+ exports.convertToBullets = convertToBullets;
14
+ /**
15
+ * Analyze the sequence of numbered children under a parent item.
16
+ * Returns information about gaps and duplicates in the sequence.
17
+ */
18
+ function analyzeSequence(ast, parentId) {
19
+ const itemMap = new Map();
20
+ for (const item of ast.items) {
21
+ itemMap.set(item.id, item);
22
+ }
23
+ const parent = itemMap.get(parentId);
24
+ if (!parent) {
25
+ return {
26
+ parentId,
27
+ hasSequence: false,
28
+ numberedChildren: [],
29
+ gaps: [],
30
+ duplicates: [],
31
+ };
32
+ }
33
+ // Collect numbered children
34
+ const numberedChildren = [];
35
+ for (const childId of parent.children) {
36
+ const child = itemMap.get(childId);
37
+ if (child && child.marker_type === 'numbered' && child.sequence_number !== undefined) {
38
+ numberedChildren.push({ id: childId, sequenceNumber: child.sequence_number });
39
+ }
40
+ }
41
+ if (numberedChildren.length === 0) {
42
+ return {
43
+ parentId,
44
+ hasSequence: false,
45
+ numberedChildren: [],
46
+ gaps: [],
47
+ duplicates: [],
48
+ };
49
+ }
50
+ // Find gaps - numbers should be consecutive starting from 1
51
+ const gaps = [];
52
+ for (let i = 0; i < numberedChildren.length; i++) {
53
+ const expected = i + 1;
54
+ const actual = numberedChildren[i].sequenceNumber;
55
+ if (actual !== expected) {
56
+ gaps.push({ position: i, expected, actual });
57
+ }
58
+ }
59
+ // Find duplicates
60
+ const numberCounts = new Map();
61
+ for (const child of numberedChildren) {
62
+ const ids = numberCounts.get(child.sequenceNumber) || [];
63
+ ids.push(child.id);
64
+ numberCounts.set(child.sequenceNumber, ids);
65
+ }
66
+ const duplicates = [];
67
+ for (const [num, ids] of numberCounts) {
68
+ if (ids.length > 1) {
69
+ duplicates.push({ number: num, ids });
70
+ }
71
+ }
72
+ return {
73
+ parentId,
74
+ hasSequence: true,
75
+ numberedChildren,
76
+ gaps,
77
+ duplicates,
78
+ };
79
+ }
80
+ /**
81
+ * Renumber children of a parent to have consecutive sequence numbers starting from 1.
82
+ * Returns a new AST with the renumbered items.
83
+ */
84
+ function renumberChildren(ast, parentId) {
85
+ const itemMap = new Map();
86
+ for (const item of ast.items) {
87
+ itemMap.set(item.id, { ...item });
88
+ }
89
+ const parent = itemMap.get(parentId);
90
+ if (!parent) {
91
+ return ast;
92
+ }
93
+ // Renumber numbered children
94
+ let nextNum = 1;
95
+ for (const childId of parent.children) {
96
+ const child = itemMap.get(childId);
97
+ if (child && child.marker_type === 'numbered') {
98
+ child.sequence_number = nextNum++;
99
+ }
100
+ }
101
+ return {
102
+ items: Array.from(itemMap.values()),
103
+ root_ids: [...ast.root_ids],
104
+ };
105
+ }
106
+ /**
107
+ * Insert a new item into a sequenced list at a specific position and renumber.
108
+ * Position is 0-indexed. Returns a new AST with the inserted and renumbered items.
109
+ */
110
+ function insertSequencedItem(ast, parentId, position, newItem) {
111
+ const itemMap = new Map();
112
+ for (const item of ast.items) {
113
+ itemMap.set(item.id, { ...item });
114
+ }
115
+ const parent = itemMap.get(parentId);
116
+ if (!parent) {
117
+ return ast;
118
+ }
119
+ // Generate new ID
120
+ const maxId = Math.max(...ast.items.map(item => parseInt(item.id, 10)));
121
+ const newId = String(maxId + 1);
122
+ // Create new item
123
+ const item = {
124
+ ...newItem,
125
+ id: newId,
126
+ sequence_number: position + 1,
127
+ children: newItem.children || [],
128
+ };
129
+ // Insert into parent's children
130
+ const newChildren = [...parent.children];
131
+ newChildren.splice(position, 0, newId);
132
+ parent.children = newChildren;
133
+ // Add to items
134
+ itemMap.set(newId, item);
135
+ // Renumber all numbered children
136
+ let nextNum = 1;
137
+ for (const childId of parent.children) {
138
+ const child = itemMap.get(childId);
139
+ if (child && child.marker_type === 'numbered') {
140
+ child.sequence_number = nextNum++;
141
+ }
142
+ }
143
+ return {
144
+ items: Array.from(itemMap.values()),
145
+ root_ids: [...ast.root_ids],
146
+ };
147
+ }
148
+ /**
149
+ * Remove an item from a sequenced list and renumber siblings.
150
+ * Returns a new AST with the item removed and siblings renumbered.
151
+ */
152
+ function removeSequencedItem(ast, itemId) {
153
+ const itemMap = new Map();
154
+ for (const item of ast.items) {
155
+ if (item.id !== itemId) {
156
+ itemMap.set(item.id, { ...item });
157
+ }
158
+ }
159
+ // Find and update parent
160
+ for (const [id, item] of itemMap) {
161
+ if (item.children.includes(itemId)) {
162
+ item.children = item.children.filter(c => c !== itemId);
163
+ // Renumber numbered children
164
+ let nextNum = 1;
165
+ for (const childId of item.children) {
166
+ const child = itemMap.get(childId);
167
+ if (child && child.marker_type === 'numbered') {
168
+ child.sequence_number = nextNum++;
169
+ }
170
+ }
171
+ break;
172
+ }
173
+ }
174
+ // Update root_ids if necessary
175
+ const newRootIds = ast.root_ids.filter(id => id !== itemId);
176
+ return {
177
+ items: Array.from(itemMap.values()),
178
+ root_ids: newRootIds,
179
+ };
180
+ }
181
+ /**
182
+ * Convert bullet children of a parent to numbered items.
183
+ * Returns a new AST with bullet items converted to numbered.
184
+ */
185
+ function convertToSequence(ast, parentId) {
186
+ const itemMap = new Map();
187
+ for (const item of ast.items) {
188
+ itemMap.set(item.id, { ...item });
189
+ }
190
+ const parent = itemMap.get(parentId);
191
+ if (!parent) {
192
+ return ast;
193
+ }
194
+ // Convert bullet children to numbered
195
+ let nextNum = 1;
196
+ for (const childId of parent.children) {
197
+ const child = itemMap.get(childId);
198
+ if (child && child.type === 'list') {
199
+ child.marker_type = 'numbered';
200
+ child.sequence_number = nextNum++;
201
+ }
202
+ }
203
+ return {
204
+ items: Array.from(itemMap.values()),
205
+ root_ids: [...ast.root_ids],
206
+ };
207
+ }
208
+ /**
209
+ * Convert numbered children of a parent to bullet items.
210
+ * Returns a new AST with numbered items converted to bullets.
211
+ */
212
+ function convertToBullets(ast, parentId) {
213
+ const itemMap = new Map();
214
+ for (const item of ast.items) {
215
+ itemMap.set(item.id, { ...item });
216
+ }
217
+ const parent = itemMap.get(parentId);
218
+ if (!parent) {
219
+ return ast;
220
+ }
221
+ // Convert numbered children to bullets
222
+ for (const childId of parent.children) {
223
+ const child = itemMap.get(childId);
224
+ if (child && child.marker_type === 'numbered') {
225
+ child.marker_type = 'bullet';
226
+ child.sequence_number = undefined;
227
+ }
228
+ }
229
+ return {
230
+ items: Array.from(itemMap.values()),
231
+ root_ids: [...ast.root_ids],
232
+ };
233
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Todoosy Settings Parser
3
+ *
4
+ * Parses todoosy.settings.md files using a canonical Markdown-compatible format.
5
+ *
6
+ * ## Canonical Format
7
+ *
8
+ * Settings files use standard Markdown with level-1 headings as setting names:
9
+ *
10
+ * ```markdown
11
+ * # Setting Name
12
+ *
13
+ * value
14
+ * ```
15
+ *
16
+ * ## Value Types
17
+ *
18
+ * 1. **Single Value** - First non-empty line after heading
19
+ * ```markdown
20
+ * # Timezone
21
+ *
22
+ * America/Denver
23
+ * ```
24
+ *
25
+ * 2. **List Value** - Bulleted list items (- or *)
26
+ * ```markdown
27
+ * # Tags
28
+ *
29
+ * - work
30
+ * - personal
31
+ * ```
32
+ *
33
+ * 3. **Key-Value Map** - Lines with `key - value` format
34
+ * ```markdown
35
+ * # Priorities
36
+ *
37
+ * P0 - Critical
38
+ * P1 - High
39
+ * ```
40
+ *
41
+ * ## Known Settings
42
+ *
43
+ * - Timezone: Single value (IANA timezone identifier)
44
+ * - Priorities: Key-value map (P0 - Label)
45
+ * - Misc: Single value (filename/headingname)
46
+ * - Calendar Format: Single value (yyyy-mm-dd, mm/dd/yyyy, etc.)
47
+ * - Formatting Style: Single value (roomy, balanced, tight)
48
+ *
49
+ * ## Extended Settings
50
+ *
51
+ * Any heading not matching known settings is captured as an extended setting,
52
+ * with automatic value type inference.
53
+ */
54
+ import type { Settings, Scheme } from './types.js';
55
+ export declare const VALID_CALENDAR_FORMATS: Set<string>;
56
+ export declare const VALID_FORMATTING_STYLES: Set<string>;
57
+ /**
58
+ * Parse a settings file and return structured settings
59
+ */
60
+ export declare function parseSettings(text: string): Settings;
61
+ /**
62
+ * Parse a settings file and return legacy Scheme format
63
+ * @deprecated Use parseSettings instead
64
+ */
65
+ export declare function parseScheme(text: string): Scheme;