diff --git a/src/Query/Group.ts b/src/Query/Group.ts index a62668b941..82c5e3781a 100644 --- a/src/Query/Group.ts +++ b/src/Query/Group.ts @@ -1,13 +1,13 @@ import type { Task } from '../Task'; import { Priority } from '../Task'; -import type { Grouping, GroupingProperty } from './Query'; +import type { Grouping, GroupingArg, GroupingProperty } from './Query'; import { TaskGroups } from './TaskGroups'; import { HappensDateField } from './Filter/HappensDateField'; /** * A naming function, that takes a Task object and returns the corresponding group property name */ -type Grouper = (task: Task) => string[]; +type Grouper = (task: Task, arg?: GroupingArg) => string[]; /** * Implementation of the 'group by' instruction. @@ -32,9 +32,9 @@ export class Group { * @param property * @param task */ - public static getGroupNamesForTask(property: GroupingProperty, task: Task): string[] { - const grouper = Group.groupers[property]; - return grouper(task); + public static getGroupNamesForTask(grouping: Grouping, task: Task): string[] { + const grouper = Group.groupers[grouping.property]; + return grouper(task, grouping.arg); } private static groupers: Record = { @@ -45,6 +45,7 @@ export class Group { folder: Group.groupByFolder, happens: Group.groupByHappensDate, heading: Group.groupByHeading, + fn: Group.groupByFn, path: Group.groupByPath, priority: Group.groupByPriority, recurrence: Group.groupByRecurrence, @@ -56,6 +57,16 @@ export class Group { tags: Group.groupByTags, }; + private static root(task: Task) { + const path = task.path.replace(/\\/g, '/'); + const separatorIndex = path.indexOf('/'); + if (separatorIndex == -1) { + return '/'; + } else { + return path.substring(0, separatorIndex + 1); + } + } + private static escapeMarkdownCharacters(filename: string) { // https://wilsonmar.github.io/markdown-text-for-github-from-html/#special-characters return filename.replace(/\\/g, '\\\\').replace(/_/g, '\\_'); @@ -151,13 +162,44 @@ export class Group { return [Group.escapeMarkdownCharacters(filename)]; } - private static groupByRoot(task: Task): string[] { - const path = task.path.replace(/\\/g, '/'); - const separatorIndex = path.indexOf('/'); - if (separatorIndex == -1) { - return ['/']; + private static groupByFn(task: Task, arg?: GroupingArg): string[] { + const paramsArgs: [string, any][] = [ + ['description', task.description], + ['done', task.doneDate], + ['due', task.dueDate], + ['filename', task.filename], + ['happens', new HappensDateField().earliestDate(task)], + ['header', task.precedingHeader], + ['markdown', task.originalMarkdown], + ['path', task.path.replace('.md', '')], + ['priority', task.priority], + ['recurrence', task.recurrence], + ['root', Group.root(task)], + ['scheduled', task.scheduledDate], + ['start', task.startDate], + ['status', task.status], + ['t', task], + ['tags', task.tags], + ['task', task], + ['urgency', task.urgency], + ]; + + const params = paramsArgs.map(([p]) => p); + const groupBy = arg && new Function(...params, `return ${arg}`); + + if (groupBy instanceof Function) { + const args = paramsArgs.map(([_, a]) => a); + const result = groupBy(...args); + const group = typeof result === 'string' ? result : 'Error with group result'; + + return [Group.escapeMarkdownCharacters(group)]; + } else { + return ['Error parsing group function']; } - return [Group.escapeMarkdownCharacters(path.substring(0, separatorIndex + 1))]; + } + + private static groupByRoot(task: Task): string[] { + return [Group.escapeMarkdownCharacters(Group.root(task))]; } private static groupByBacklink(task: Task): string[] { diff --git a/src/Query/IntermediateTaskGroups.ts b/src/Query/IntermediateTaskGroups.ts index 78b8a7ab36..dc60023460 100644 --- a/src/Query/IntermediateTaskGroups.ts +++ b/src/Query/IntermediateTaskGroups.ts @@ -78,7 +78,7 @@ export class IntermediateTaskGroups { const nextTreeLevel = []; for (const currentTreeNode of currentTreeLevel) { for (const task of currentTreeNode.values) { - const groupNames = Group.getGroupNamesForTask(grouping.property, task); + const groupNames = Group.getGroupNamesForTask(grouping, task); for (const groupName of groupNames) { let child = currentTreeNode.children.get(groupName); if (child === undefined) { diff --git a/src/Query/Query.ts b/src/Query/Query.ts index 136b814530..4a12102649 100644 --- a/src/Query/Query.ts +++ b/src/Query/Query.ts @@ -17,6 +17,7 @@ export type GroupingProperty = | 'folder' | 'happens' | 'heading' + | 'fn' | 'path' | 'priority' | 'recurrence' @@ -26,7 +27,13 @@ export type GroupingProperty = | 'start' | 'status' | 'tags'; -export type Grouping = { property: GroupingProperty }; + +export type GroupingArg = string | null; + +export type Grouping = { + property: GroupingProperty; + arg?: GroupingArg; +}; export class Query implements IQuery { public source: string; @@ -39,7 +46,7 @@ export class Query implements IQuery { private _grouping: Grouping[] = []; private readonly groupByRegexp = - /^group by (backlink|done|due|filename|folder|happens|heading|path|priority|recurrence|recurring|root|scheduled|start|status|tags)/; + /^group by (backlink|done|due|filename|fn|folder|happens|heading|path|priority|recurrence|recurring|root|scheduled|start|status|tags)[\s]*(.*)/; private readonly hideOptionsRegexp = /^(hide|show) (task count|backlink|priority|start date|scheduled date|done date|due date|recurrence rule|edit button|urgency)/; @@ -227,10 +234,22 @@ export class Query implements IQuery { private parseGroupBy({ line }: { line: string }): void { const fieldMatch = line.match(this.groupByRegexp); + if (fieldMatch !== null) { - this._grouping.push({ - property: fieldMatch[1] as GroupingProperty, - }); + const property = fieldMatch[1] as GroupingProperty; + + if (property !== 'fn') { + this._grouping.push({ + property: property, + }); + } else if (fieldMatch[2] !== null) { + this._grouping.push({ + property: property, + arg: fieldMatch[2] as GroupingArg, + }); + } else { + this._error = 'do not understand fn query grouping'; + } } else { this._error = 'do not understand query grouping'; } diff --git a/tests/Group.test.ts b/tests/Group.test.ts index 37ccc76a87..1a1f3ed136 100644 --- a/tests/Group.test.ts +++ b/tests/Group.test.ts @@ -10,7 +10,8 @@ import { fromLine } from './TestHelpers'; window.moment = moment; function checkGroupNamesOfTask(task: Task, property: GroupingProperty, expectedGroupNames: string[]) { - const group = Group.getGroupNamesForTask(property, task); + const grouping: Grouping = { property }; + const group = Group.getGroupNamesForTask(grouping, task); expect(group).toEqual(expectedGroupNames); }