diff --git a/README.md b/README.md index 260544894b..5bab06f391 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,9 @@ The following filters exist: - `description (includes|does not include) ` - `exclude sub-items` - When this is set, the result list will only include tasks that are not indented in their file. It will only show tasks that are top level list items in their list. +- `limit to tasks` + - Only lists the first `` tasks of the result. + - Shorthand is `limit `. All filters of a query have to match in order for a task to be listed. This means you cannot show tasks that have "GitHub in the path and have no due date or are due after 2021-04-04". @@ -232,6 +235,15 @@ Show all tasks that were done before the 1st of December 2020: done before 2020-12-01 ``` +Show one task that is due on the 5th of May and includes `#prio1` in its description: + + ```tasks + not done + due on 2021-05-05 + description includes #prio1 + limit to 1 tasks + ```` + ### Tips #### Daily Agenda diff --git a/src/Query.ts b/src/Query.ts index e9b4a836ec..3707510c60 100644 --- a/src/Query.ts +++ b/src/Query.ts @@ -3,7 +3,8 @@ import moment from 'moment'; import { Status, Task } from './Task'; export class Query { - public readonly filters: ((task: Task) => boolean)[]; + private _limit: number | undefined = undefined; + private _filters: ((task: Task) => boolean)[] = []; private readonly noDueString = 'no due date'; private readonly dueRegexp = /due (before|after|on) (\d{4}-\d{2}-\d{2})/; private readonly doneString = 'done'; @@ -11,68 +12,61 @@ export class Query { private readonly doneRegexp = /done (before|after|on) (\d{4}-\d{2}-\d{2})/; private readonly pathRegexp = /path (includes|does not include) (.*)/; private readonly descriptionRegexp = /description (includes|does not include) (.*)/; + private readonly limitRegexp = /limit (to )?(\d+)( tasks?)?/; private readonly excludeSubItemsString = 'exclude sub-items'; constructor({ source }: { source: string }) { - this.filters = this.parseFilters({ source }); + source + .split('\n') + .map((line: string) => line.trim()) + .forEach((line: string) => { + switch (true) { + case line === '': + break; + case line === this.doneString: + this._filters.push( + (task) => task.status === Status.Done, + ); + break; + case line === this.notDoneString: + this._filters.push( + (task) => task.status !== Status.Done, + ); + break; + case line === this.excludeSubItemsString: + this._filters.push((task) => task.indentation === ''); + break; + case line === this.noDueString: + this._filters.push((task) => task.dueDate === null); + break; + case this.dueRegexp.test(line): + this.parseDueFilter({ line }); + break; + case this.doneRegexp.test(line): + this.parseDoneFilter({ line }); + break; + case this.pathRegexp.test(line): + this.parsePathFilter({ line }); + break; + case this.descriptionRegexp.test(line): + this.parseDescriptionFilter({ line }); + break; + case this.limitRegexp.test(line): + this.parseLimit({ line }); + break; + } + }); } - private parseFilters({ - source, - }: { - source: string; - }): ((task: Task) => boolean)[] { - const filters: ((task: Task) => boolean)[] = []; - const sourceLines = source.split('\n').map((line) => line.trim()); - - for (const sourceLine of sourceLines) { - if (sourceLine === '') { - continue; - } - - if (sourceLine === this.doneString) { - filters.push((task) => task.status === Status.Done); - } else if (sourceLine === this.notDoneString) { - filters.push((task) => task.status !== Status.Done); - } else if (sourceLine === this.noDueString) { - filters.push((task) => task.dueDate === null); - } else if (this.dueRegexp.test(sourceLine)) { - const filter = this.parseDueFilter({ line: sourceLine }); - if (filter !== null) { - filters.push(filter); - } - } else if (this.doneRegexp.test(sourceLine)) { - const filter = this.parseDoneFilter({ line: sourceLine }); - if (filter !== null) { - filters.push(filter); - } - } else if (this.pathRegexp.test(sourceLine)) { - const filter = this.parsePathFilter({ line: sourceLine }); - if (filter !== null) { - filters.push(filter); - } - } else if (this.descriptionRegexp.test(sourceLine)) { - const filter = this.parseDescriptionFilter({ - line: sourceLine, - }); - if (filter !== null) { - filters.push(filter); - } - } else if (sourceLine === this.excludeSubItemsString) { - filters.push((task) => task.indentation === ''); - } else { - console.error('Tasks: unknown query filter:', sourceLine); - } - } + public get limit(): number | undefined { + return this._limit; + } - return filters; + public get filters(): ((task: Task) => boolean)[] { + return this._filters; } - private parseDueFilter({ - line, - }: { - line: string; - }): ((task: Task) => boolean) | null { + private parseDueFilter({ line }: { line: string }): void { const dueMatch = line.match(this.dueRegexp); if (dueMatch !== null) { let filter; @@ -88,18 +82,13 @@ export class Query { task.dueDate ? task.dueDate.isSame(filterDate) : false; } - return filter; + this._filters.push(filter); + } else { + console.error('Tasks: unknown query filter (due date):', line); } - - console.error('Tasks: unknown query filter (due date):', line); - return null; } - private parseDoneFilter({ - line, - }: { - line: string; - }): ((task: Task) => boolean) | null { + private parseDoneFilter({ line }: { line: string }): void { const doneMatch = line.match(this.doneRegexp); if (doneMatch !== null) { let filter; @@ -115,65 +104,62 @@ export class Query { task.doneDate ? task.doneDate.isSame(filterDate) : false; } - return filter; + this._filters.push(filter); } - - console.error('Tasks: unknown query filter (done date):', line); - return null; } - private parsePathFilter({ - line, - }: { - line: string; - }): ((task: Task) => boolean) | null { + private parsePathFilter({ line }: { line: string }): void { const pathMatch = line.match(this.pathRegexp); if (pathMatch !== null) { - let filter; const filterMethod = pathMatch[1]; if (filterMethod === 'includes') { - filter = (task: Task) => task.path.includes(pathMatch[2]); + this._filters.push((task: Task) => + task.path.includes(pathMatch[2]), + ); } else if (pathMatch[1] === 'does not include') { - filter = (task: Task) => !task.path.includes(pathMatch[2]); + this._filters.push( + (task: Task) => !task.path.includes(pathMatch[2]), + ); } else { console.error('Tasks: unknown query filter (path):', line); - return null; } - - return filter; + } else { + console.error('Tasks: unknown query filter (path):', line); } - - console.error('Tasks: unknown query filter (path):', line); - return null; } - private parseDescriptionFilter({ - line, - }: { - line: string; - }): ((task: Task) => boolean) | null { + private parseDescriptionFilter({ line }: { line: string }): void { const descriptionMatch = line.match(this.descriptionRegexp); if (descriptionMatch !== null) { - let filter; const filterMethod = descriptionMatch[1]; if (filterMethod === 'includes') { - filter = (task: Task) => - task.description.includes(descriptionMatch[2]); + this._filters.push((task: Task) => + task.description.includes(descriptionMatch[2]), + ); } else if (descriptionMatch[1] === 'does not include') { - filter = (task: Task) => - !task.description.includes(descriptionMatch[2]); + this._filters.push( + (task: Task) => + !task.description.includes(descriptionMatch[2]), + ); } else { console.error( 'Tasks: unknown query filter (description):', line, ); - return null; } - - return filter; + } else { + console.error('Tasks: unknown query filter (description):', line); } + } - console.error('Tasks: unknown query filter (description):', line); - return null; + private parseLimit({ line }: { line: string }): void { + const limitMatch = line.match(this.limitRegexp); + if (limitMatch !== null) { + // limitMatch[2] is per regex always digits and therefore parsable. + const limit = Number.parseInt(limitMatch[2], 10); + this._limit = limit; + } else { + console.error('Tasks: unknown query limit:', line); + } } } diff --git a/src/QueryRenderer.ts b/src/QueryRenderer.ts index c08fa2dcbf..0d19a6ed74 100644 --- a/src/QueryRenderer.ts +++ b/src/QueryRenderer.ts @@ -6,7 +6,6 @@ import { import { Cache, State } from './Cache'; import { Sort } from './Sort'; -import type { Task } from './Task'; import { Query } from './Query'; export class QueryRenderer { @@ -32,7 +31,7 @@ export class QueryRenderer { new QueryRenderChild({ cache: this.cache, container: element, - filters: query.filters, + query, }), ); } @@ -40,23 +39,23 @@ export class QueryRenderer { class QueryRenderChild extends MarkdownRenderChild { private readonly cache: Cache; - private readonly filters: ((task: Task) => boolean)[]; + private readonly query: Query; private cacheCallbackId: number | undefined; constructor({ cache, container, - filters, + query, }: { cache: Cache; container: HTMLElement; - filters: ((task: Task) => boolean)[]; + query: Query; }) { super(container); this.cache = cache; - this.filters = filters; + this.query = query; } onload() { @@ -92,17 +91,20 @@ class QueryRenderChild extends MarkdownRenderChild { content: HTMLDivElement, ): Promise<{ taskList: HTMLUListElement; tasksCount: number }> { let tasks = this.cache.getTasks(); - for (const filter of this.filters) { + this.query.filters.forEach((filter) => { tasks = tasks.filter(filter); - } + }); - const tasksSorted = Sort.byDateThenPath(tasks); - const tasksCount = tasksSorted.length; + const tasksSortedLimited = Sort.byDateThenPath(tasks).slice( + 0, + this.query.limit, + ); + const tasksCount = tasksSortedLimited.length; const taskList = content.createEl('ul'); taskList.addClass('contains-task-list'); for (let i = 0; i < tasksCount; i++) { - const task = tasksSorted[i]; + const task = tasksSortedLimited[i]; let fileName: string | undefined; const fileNameMatch = task.path.match(/([^/]+)\.md$/);