1{
2 function makeCommand(action, selection, parts) {
3
4 const validParts = parts || [];
5 // Extract pure description (without attributes)
6 const description = validParts
7 .filter(part => part.type === 'text')
8 .map(part => part.value)
9 .join(' ')
10 .trim();
11
12 // Collect all attributes
13 const attributes = validParts
14 .filter(part => part.type !== 'text')
15 .filter(part => part !== null);
16
17 const project = validParts
18 .filter(part => part.type === 'project')
19 .map(part => part.value)
20 .join('')
21 .trim();
22
23 const tags = validParts
24 .filter(part => part.type === 'tag')
25 .map(part => part.value);
26
27 // Extract priority flag
28 const hasPriority = validParts.some(part => part.type === 'priority');
29
30 // Extract due date
31 const dueDate = validParts
32 .filter(part => part.type === 'due')
33 .map(part => part.value)[0] || null;
34
35 return {
36 action: action,
37 selection: selection || [],
38 description: description || [],
39 attributes: attributes || [],
40 project: project,
41 tags: tags,
42 priority: hasPriority,
43 due: dueDate,
44 parts: parts || [],
45 reconstruct: function() {
46 const filterStr = this.filters.map(f => f.reconstruct()).join(',');
47 const partsStr = this.parts.map(p => p.reconstruct()).join(' ');
48 return [filterStr, this.type, partsStr].filter(Boolean).join(' ');
49 }
50 };
51 }
52}
53
54Start = AddCommand / DoneCommand / ExplicitFilterCommand / ModifyCommand
55
56// ADD COMMAND
57AddCommand = selection:Selections? _? "add" _ parts:(Part / _)+ EOF {
58 return makeCommand('add', selection, parts.filter(p => p !== null));
59}
60
61// DONE COMMAND
62DoneCommand = selection:Selections? _? "done" _? parts:(Part / _)* EOF {
63 return makeCommand('done', selection, parts.filter(p => p !== null));
64}
65
66ExplicitFilterCommand = selection:Selections? _? "filter" _* moreFilters:Filters EOF {
67 return makeCommand('filter', selection, moreFilters);
68}
69
70ModifyCommand = selection:Selections? _? "modify" _* moreFilters:Filters EOF {
71 return makeCommand('modify', selection, moreFilters);
72}
73
74Filters = first:Part rest:(_ Part)* {
75 return [first, ...rest.map(r => r[1])];
76}
77
78
79IdRange = start:Integer "-" end:Integer {
80 const ids = [];
81 for (let i = start; i <= end; i++) {
82 ids.push(i);
83 }
84 return ids;
85}
86
87SingleId = id:Integer {
88 return [id];
89}
90
91Selections = first:Selection rest:(_ Selection)* {
92 return [first, ...rest.map(r => r[1])];
93}
94
95Selection = IdFilter / Attribute
96
97IdFilter = first:(IdRange / SingleId) rest:("," (IdRange / SingleId))* trailing:"," ? {
98 const ids = [first, ...rest.map(r => r[1])].flat();
99 return {
100 type: 'id',
101 ids: ids,
102 reconstruct: function() { return this.ids.join(','); }
103 };
104}
105
106Part = Attribute / TextPart
107
108TextPart = chars:Word {
109 return {
110 type: "text",
111 value: chars,
112 reconstruct: function() { return this.value; }
113 };
114}
115
116Attribute = Due / Tag / Project / Priority
117
118Due = ("due:" / "@") value:DateValue {
119 return {
120 type: "due",
121 value: value,
122 reconstruct: function() { return `@${this.value}`; }
123 };
124}
125
126Project = ("pro:" / "project:" / "+") value:Word {
127 return {
128 type: "project",
129 value: value,
130 reconstruct: function() { return `+${this.value}`; }
131 };
132}
133
134Priority = ("priority" / "!!") {
135 return {
136 type: "priority",
137 value: true,
138 reconstruct: function() { return `priority:${this.value}`; }
139 };
140}
141
142Tag = "#" value:Word {
143 return {
144 type: "tag",
145 value: value,
146 reconstruct: function() { return `#${this.value}`; }
147 };
148}
149
150Integer = digits:[0-9]+ {
151 return parseInt(digits.join(''), 10);
152}
153
154DateValue = chars:[0-9/-]+ {
155 return chars.join('');
156}
157
158Word = chars:[a-zA-Z0-9_-]+ {
159 return chars.join('');
160}
161
162_ = [ \t]+ {
163 return null;
164}
165
166EOF = !.
167
168