{ function makeCommand(type, filters, 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); // Store original parts for reconstruction const originalParts = validParts.filter(part => part !== null); let validFilters; if (!!filters) { validFilters = filters.flat(); } else { validFilters = []; } return { type: type, description: description || [], attributes: attributes || [], filters: validFilters, project: project, tags: JSON.stringify(tags), 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 / ImplicitFilterCommand // ADD COMMAND AddCommand = "add" _ parts:(Part / _)+ EOF { return makeCommand('add', null, parts.filter(p => p !== null)); } // DONE COMMAND DoneCommand = filters:Filters _ "done" EOF { return makeCommand('done', filters, null); } ExplicitFilterCommand = filters:Filters _ "filter" _* moreFilters:Filters* EOF { return makeCommand('filter', [...filters, ...moreFilters], null); } ImplicitFilterCommand = filters:Filters+ EOF { return makeCommand('filter', filters, null); } Filters = first:Filter rest:(_ Filter)* { 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]; } Filter = IdFilter / Tag / Project / Priority / Due 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 = "@" value:TimeValue { 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:" value:[HML] { return { type: "priority", value: value, 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); } TimeValue = chars:[0-9:]+ ("am" / "pm")? { return chars.join('') + (text() || ''); } Word = chars:[a-zA-Z0-9_-]+ { return chars.join(''); } _ = [ \t]+ { return null; } EOF = !.