{ function makeCommand(action, selection, parts) { const validParts = parts || []; // Extract pure description (without attributes) const description = validParts .filter(part => part.type === 'text') .map(part => part.value) .join(' ') .trim(); // Collect all attributes const attributes = validParts .filter(part => part.type !== 'text') .filter(part => part !== null); const project = validParts .filter(part => part.type === 'project') .map(part => part.value) .join('') .trim(); const tags = validParts .filter(part => part.type === 'tag') .map(part => part.value); // Extract priority flag const hasPriority = validParts.some(part => part.type === 'priority'); // Extract due date const dueDate = validParts .filter(part => part.type === 'due') .map(part => part.value)[0] || null; return { action: action, selection: selection || [], description: description || [], attributes: attributes || [], project: project, tags: tags, priority: hasPriority, due: dueDate, parts: parts || [], reconstruct: function() { const filterStr = this.filters.map(f => f.reconstruct()).join(','); const partsStr = this.parts.map(p => p.reconstruct()).join(' '); return [filterStr, this.type, partsStr].filter(Boolean).join(' '); } }; } } Start = AddCommand / DoneCommand / ExplicitFilterCommand / ModifyCommand // ADD COMMAND AddCommand = selection:Selections? _? "add" _ parts:(Part / _)+ EOF { return makeCommand('add', selection, parts.filter(p => p !== null)); } // DONE COMMAND DoneCommand = selection:Selections? _? "done" _? parts:(Part / _)* EOF { return makeCommand('done', selection, parts.filter(p => p !== null)); } ExplicitFilterCommand = selection:Selections? _? "filter" _* moreFilters:Filters EOF { return makeCommand('filter', selection, moreFilters); } ModifyCommand = selection:Selections? _? "modify" _* moreFilters:Filters EOF { return makeCommand('modify', selection, moreFilters); } Filters = first:Part rest:(_ Part)* { return [first, ...rest.map(r => r[1])]; } IdRange = start:Integer "-" end:Integer { const ids = []; for (let i = start; i <= end; i++) { ids.push(i); } return ids; } SingleId = id:Integer { return [id]; } Selections = first:Selection rest:(_ Selection)* { return [first, ...rest.map(r => r[1])]; } Selection = IdFilter / Attribute IdFilter = first:(IdRange / SingleId) rest:("," (IdRange / SingleId))* trailing:"," ? { const ids = [first, ...rest.map(r => r[1])].flat(); return { type: 'id', ids: ids, reconstruct: function() { return this.ids.join(','); } }; } Part = Attribute / TextPart TextPart = chars:Word { return { type: "text", value: chars, reconstruct: function() { return this.value; } }; } Attribute = Due / Tag / Project / Priority Due = ("due:" / "@") value:DateValue { return { type: "due", value: value, reconstruct: function() { return `@${this.value}`; } }; } Project = ("pro:" / "project:" / "+") value:Word { return { type: "project", value: value, reconstruct: function() { return `+${this.value}`; } }; } Priority = ("priority" / "!!") { return { type: "priority", value: true, reconstruct: function() { return `priority:${this.value}`; } }; } Tag = "#" value:Word { return { type: "tag", value: value, reconstruct: function() { return `#${this.value}`; } }; } Integer = digits:[0-9]+ { return parseInt(digits.join(''), 10); } DateValue = chars:[0-9/-]+ { return chars.join(''); } Word = chars:[a-zA-Z0-9_-]+ { return chars.join(''); } _ = [ \t]+ { return null; } EOF = !.