From 53d2d8e776c9a7d144bcde59b53141916627d732 Mon Sep 17 00:00:00 2001 From: sytone Date: Mon, 2 May 2022 23:36:05 -0700 Subject: [PATCH 01/23] Markdown lint fixes Add dev setup to contributing Add tags as a property of task Add query by tag in query language Add sort by tag Added unit tests for coverage --- .editorconfig | 31 ++++++++++++ CONTRIBUTING.md | 31 ++++++++---- src/Commands/CreateOrEdit.ts | 2 + src/Query.ts | 34 +++++++++++++- src/Sort.ts | 12 +++++ src/Task.ts | 82 ++++++++++++++++++++++++++------ tests/Query.test.ts | 91 ++++++++++++++++++++++++++++++++++++ tests/Sort.test.ts | 35 ++++++++++++++ tests/Task.test.ts | 82 ++++++++++++++++++++++++++++++++ 9 files changed, 377 insertions(+), 23 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..18e612703f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,31 @@ +# See http://EditorConfig.org for more information about .editorconfig files. +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.xml] +indent_size = 2 + +[*.json] +indent_size = 2 + +[*.{yml,yaml}] +indent_size = 2 + +[*.{md,mdx}] +trim_trailing_whitespace = true + +[*.{htm,html,js,jsm,ts,tsx,mjs}] +indent_size = 4 + +[*.{cmd,bat,ps1}] +end_of_line = crlf + +[*.sh] +end_of_line = lf + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b2a0ec8c90..2cfc23a4bc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,13 +6,13 @@ Every contribution is much appreciated! ## Updating documentation The documentation resides under the `./docs` directory. -It consists of markdown files, which [Jekyll](https://jekyllrb.com/) will transform into web pages that you can view at https://schemar.github.io/obsidian-tasks/ . +It consists of markdown files, which [Jekyll](https://jekyllrb.com/) will transform into web pages that you can view at . In the simplest case, you can update the existing markdown file and create a pull request (PR) with your changes. We use [GitHub pages](https://pages.github.com/) for our documentation. You can read more about it at their [official documentation](https://docs.github.com/en/pages). -For documentation changes to show up at https://schemar.github.io/obsidian-tasks/ , they must be in the `gh-pages` branch. +For documentation changes to show up at , they must be in the `gh-pages` branch. If you want to see your changes available immediately and not only after the next release, you should make your changes on the `gh-pages` branch. When you create a PR, it should merge into the `gh-pages` branch as well. If you document an unreleased feature, you should update the documentation on `main` instead. Ideally together with the related code changes. @@ -29,6 +29,21 @@ Discussion will take place inside the PR. If you can, please add/update tests and documentation where appropriate. +## Setting up build environment + +This project uses Node 14.x, if you need to use a different version, look at using `nvm` to manage your Node versions. If you are using `nvm`, you can install the 14.x version of Node with `nvm install 14.19.1; nvm use 14.19.1`. + +To setup the local environment after cloning the repository, run the following commands: + +``` shell +yarn +yarn build +yarn test +yarn lint +``` + +Make sure you build, test and lint before pushing to the repository. + ## FAQs ### How does Tasks handle status changes? @@ -42,15 +57,15 @@ You can toggle a task‘s status by: The code is located as follows: -- For 1.: ``./src/Commands/ToggleDone.ts` -- 2. and 4. use a checkbox created by `Task.toLi()`. There, the checkbox gets a click event handler. -- For 3.: `./src/LivePreviewExtension.ts` +- For 1.: ``./src/Commands/ToggleDone.ts` +- 2. and 4. use a checkbox created by `Task.toLi()`. There, the checkbox gets a click event handler. +- For 3.: `./src/LivePreviewExtension.ts` Toggle behavior: -- 1. toggles the line directly where the cursor is. In the file inside Obsidian‘s vault. -- The click event listener of 2. and 4. uses File::replaceTaskWithTasks(). That, in turn, updates the file in Obsidian‘s Vault (like 1, but it needs to find the correct line). -- 3. toggles the line directly where the checkbox is. On the „document“ of CodeMirror (the library that Obsidian uses to show text on screen). That, in turn, updates the file in Obsidian‘s Vault, somehow. +- 1. toggles the line directly where the cursor is. In the file inside Obsidian‘s vault. +- The click event listener of 2. and 4. uses File::replaceTaskWithTasks(). That, in turn, updates the file in Obsidian‘s Vault (like 1, but it needs to find the correct line). +- 3. toggles the line directly where the checkbox is. On the „document“ of CodeMirror (the library that Obsidian uses to show text on screen). That, in turn, updates the file in Obsidian‘s Vault, somehow. Obsidian writes the changes to disk at its own pace. diff --git a/src/Commands/CreateOrEdit.ts b/src/Commands/CreateOrEdit.ts index 04318d0605..cfc970d025 100644 --- a/src/Commands/CreateOrEdit.ts +++ b/src/Commands/CreateOrEdit.ts @@ -80,6 +80,7 @@ const taskFromLine = ({ line, path }: { line: string; path: string }): Task => { sectionIndex: 0, precedingHeader: null, blockLink: '', + tags: [], }); } @@ -112,5 +113,6 @@ const taskFromLine = ({ line, path }: { line: string; path: string }): Task => { sectionStart: 0, sectionIndex: 0, precedingHeader: null, + tags: [], }); }; diff --git a/src/Query.ts b/src/Query.ts index ea8da68e5b..30a1ea2012 100644 --- a/src/Query.ts +++ b/src/Query.ts @@ -13,7 +13,8 @@ export type SortingProperty = | 'due' | 'done' | 'path' - | 'description'; + | 'description' + | 'tag'; type Sorting = { property: SortingProperty; reverse: boolean }; export class Query { @@ -44,8 +45,11 @@ export class Query { private readonly pathRegexp = /^path (includes|does not include) (.*)/; private readonly descriptionRegexp = /^description (includes|does not include) (.*)/; + + private readonly tagRegexp = /^tag (includes|does not include) (.*)/; + private readonly sortByRegexp = - /^sort by (urgency|status|priority|start|scheduled|due|done|path|description)( reverse)?/; + /^sort by (urgency|status|priority|start|scheduled|due|done|path|description|tag)( reverse)?/; private readonly headingRegexp = /^heading (includes|does not include) (.*)/; @@ -127,6 +131,9 @@ export class Query { case this.descriptionRegexp.test(line): this.parseDescriptionFilter({ line }); break; + case this.tagRegexp.test(line): + this.parseTagFilter({ line }); + break; case this.headingRegexp.test(line): this.parseHeadingFilter({ line }); break; @@ -432,6 +439,29 @@ export class Query { } } + private parseTagFilter({ line }: { line: string }): void { + const tagMatch = line.match(this.tagRegexp); + if (tagMatch !== null) { + const filterMethod = tagMatch[1]; + + if (filterMethod === 'includes') { + this._filters.push( + (task: Task) => + task.tags.find((el) => el === tagMatch[2]) != undefined, + ); + } else if (tagMatch[1] === 'does not include') { + this._filters.push( + (task: Task) => + task.tags.find((el) => el === tagMatch[2]) == undefined, + ); + } else { + this._error = 'do not understand query filter (tag)'; + } + } else { + this._error = 'do not understand query filter (tag)'; + } + } + private parseDescriptionFilter({ line }: { line: string }): void { const descriptionMatch = line.match(this.descriptionRegexp); if (descriptionMatch !== null) { diff --git a/src/Sort.ts b/src/Sort.ts index 2d525a8570..10dcbfb8cc 100644 --- a/src/Sort.ts +++ b/src/Sort.ts @@ -43,6 +43,7 @@ export class Sort { done: Sort.compareByDoneDate, path: Sort.compareByPath, status: Sort.compareByStatus, + tag: Sort.compareByTag, }; private static makeReversedComparator(comparator: Comparator): Comparator { @@ -98,6 +99,17 @@ export class Sort { return Sort.compareByDate(a.doneDate, b.doneDate); } + // Currently sorts by first tag found only. + private static compareByTag(a: Task, b: Task): -1 | 0 | 1 { + if (a.tags[0] < b.tags[0]) { + return -1; + } else if (a.tags[0] > b.tags[0]) { + return 1; + } else { + return 0; + } + } + private static compareByDate( a: moment.Moment | null, b: moment.Moment | null, diff --git a/src/Task.ts b/src/Task.ts index 16617d6bf3..114f2bdefa 100644 --- a/src/Task.ts +++ b/src/Task.ts @@ -35,6 +35,8 @@ export class Task { public readonly originalStatusCharacter: string; public readonly precedingHeader: string | null; + public readonly tags: string[]; + public readonly priority: Priority; public readonly startDate: Moment | null; @@ -47,8 +49,17 @@ export class Task { public readonly blockLink: string; public static readonly dateFormat = 'YYYY-MM-DD'; + + // Main regex for parsing a line. It matches the following: + // - Indentation + // - Status character + // - Rest of task after checkbox markdown public static readonly taskRegex = /^([\s\t]*)[-*] +\[(.)\] *(.*)/u; - // The following regexes end with `$` because they will be matched and + + // Match on block link at end. + public static readonly blockLinkRegex = / \^[a-zA-Z0-9-]+$/u; + + // The following regex's end with `$` because they will be matched and // removed from the end until none are left. public static readonly priorityRegex = /([⏫🔼🔽])$/u; public static readonly startDateRegex = /🛫 ?(\d{4}-\d{2}-\d{2})$/u; @@ -56,7 +67,8 @@ export class Task { public static readonly dueDateRegex = /[📅📆🗓] ?(\d{4}-\d{2}-\d{2})$/u; public static readonly doneDateRegex = /✅ ?(\d{4}-\d{2}-\d{2})$/u; public static readonly recurrenceRegex = /🔁 ?([a-zA-Z0-9, !]+)$/iu; - public static readonly blockLinkRegex = / \^[a-zA-Z0-9-]+$/u; + + public static readonly hashTags = /#[^ !@#$%^&*(),.?":{}|<>]*/g; private _urgency: number | null = null; @@ -76,6 +88,7 @@ export class Task { doneDate, recurrence, blockLink, + tags, }: { status: Status; description: string; @@ -92,6 +105,7 @@ export class Task { doneDate: moment.Moment | null; recurrence: Recurrence | null; blockLink: string; + tags: string[]; }) { this.status = status; this.description = description; @@ -102,6 +116,8 @@ export class Task { this.originalStatusCharacter = originalStatusCharacter; this.precedingHeader = precedingHeader; + this.tags = tags; + this.priority = priority; this.startDate = startDate; @@ -113,6 +129,18 @@ export class Task { this.blockLink = blockLink; } + /** + * Takes the given line from a obsidian note and returns a Task object. + * + * @static + * @param {string} line - The full line in the note to parse. + * @param {string} path - Path to the note in obsidian. + * @param {number} sectionStart - Line number where the section starts that contains this task. + * @param {number} sectionIndex - The index of the nth task in its section. + * @param {(string | null)} precedingHeader - The header before this task. + * @return {*} {(Task | null)} + * @memberof Task + */ public static fromLine({ line, path, @@ -126,14 +154,27 @@ export class Task { sectionIndex: number; precedingHeader: string | null; }): Task | null { + // Check the line to see if it is a markdown task. const regexMatch = line.match(Task.taskRegex); if (regexMatch === null) { return null; } + // match[3] includes the whole body of the task after the brackets. + const body = regexMatch[3].trim(); + + // return if task does not have the global filter. Do this before processing + // rest of match to improve performance. + const { globalFilter } = getSettings(); + if (!body.includes(globalFilter)) { + return null; + } + + let description = body; const indentation = regexMatch[1]; - const statusString = regexMatch[2].toLowerCase(); + // Get the status of the task, only todo and done supported. + const statusString = regexMatch[2].toLowerCase(); let status: Status; switch (statusString) { case ' ': @@ -143,16 +184,8 @@ export class Task { status = Status.Done; } - // match[3] includes the whole body of the task after the brackets. - const body = regexMatch[3].trim(); - - const { globalFilter } = getSettings(); - if (!body.includes(globalFilter)) { - return null; - } - - let description = body; - + // Match for block link and remove if found. Always expected to be + // at the end of the line. const blockLinkMatch = description.match(this.blockLinkRegex); const blockLink = blockLinkMatch !== null ? blockLinkMatch[0] : ''; @@ -170,6 +203,7 @@ export class Task { let dueDate: Moment | null = null; let doneDate: Moment | null = null; let recurrence: Recurrence | null = null; + let tags: any = []; // Add a "max runs" failsafe to never end in an endless loop: const maxRuns = 7; let runs = 0; @@ -252,6 +286,14 @@ export class Task { runs++; } while (matched && runs <= maxRuns); + // Tags are found in the string and pulled out but not removed, + // so when returning the entire task it will match what the user + // entered. + const hashTagMatch = description.match(this.hashTags); + if (hashTagMatch !== null) { + tags = hashTagMatch; + } + const task = new Task({ status, description, @@ -268,6 +310,7 @@ export class Task { doneDate, recurrence, blockLink, + tags, }); return task; @@ -367,6 +410,13 @@ export class Task { return li; } + /** + * + * + * @param {LayoutOptions} [layoutOptions] + * @return {*} {string} + * @memberof Task + */ public toString(layoutOptions?: LayoutOptions): string { layoutOptions = layoutOptions ?? new LayoutOptions(); let taskString = this.description; @@ -426,6 +476,12 @@ export class Task { return taskString; } + /** + * Returns the Task as a list item with a checkbox. + * + * @return {*} {string} + * @memberof Task + */ public toFileLineString(): string { return `${this.indentation}- [${ this.originalStatusCharacter diff --git a/tests/Query.test.ts b/tests/Query.test.ts index 2a9b59bf3c..fb16c3e5ed 100644 --- a/tests/Query.test.ts +++ b/tests/Query.test.ts @@ -29,6 +29,7 @@ describe('Query', () => { doneDate: null, recurrence: null, blockLink: '', + tags: [], }), new Task({ status: Status.Todo, @@ -46,6 +47,7 @@ describe('Query', () => { doneDate: null, recurrence: null, blockLink: '', + tags: [], }), ]; const input = 'path includes ab/c d'; @@ -145,6 +147,95 @@ describe('Query', () => { }); }); + it('filters based off a single tag', () => { + // Arrange + const originalSettings = getSettings(); + updateSettings({ globalFilter: '#task' }); + const tasks: Task[] = [ + Task.fromLine({ + line: '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', + sectionStart: 0, + sectionIndex: 0, + path: '', + precedingHeader: '', + }), + Task.fromLine({ + line: '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + sectionStart: 0, + sectionIndex: 0, + path: '', + precedingHeader: '', + }), + Task.fromLine({ + line: '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + sectionStart: 0, + sectionIndex: 0, + path: '', + precedingHeader: '', + }), + ] as Task[]; + const input = 'tag includes #home'; + const query = new Query({ source: input }); + + // Act + let filteredTasks = [...tasks]; + query.filters.forEach((filter) => { + filteredTasks = filteredTasks.filter(filter); + }); + + // Assert + expect(filteredTasks.length).toEqual(1); + expect(filteredTasks[0]).toEqual(tasks[1]); + + // Cleanup + updateSettings(originalSettings); + }); + + it('filters based off a tag not being present', () => { + // Arrange + const originalSettings = getSettings(); + updateSettings({ globalFilter: '#task' }); + const tasks: Task[] = [ + Task.fromLine({ + line: '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', + sectionStart: 0, + sectionIndex: 0, + path: '', + precedingHeader: '', + }), + Task.fromLine({ + line: '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + sectionStart: 0, + sectionIndex: 0, + path: '', + precedingHeader: '', + }), + Task.fromLine({ + line: '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + sectionStart: 0, + sectionIndex: 0, + path: '', + precedingHeader: '', + }), + ] as Task[]; + const input = 'tag does not include #home'; + const query = new Query({ source: input }); + + // Act + let filteredTasks = [...tasks]; + query.filters.forEach((filter) => { + filteredTasks = filteredTasks.filter(filter); + }); + + // Assert + expect(filteredTasks.length).toEqual(2); + expect(filteredTasks[0]).toEqual(tasks[0]); + expect(filteredTasks[1]).toEqual(tasks[2]); + + // Cleanup + updateSettings(originalSettings); + }); + describe('filtering with "happens"', () => { type HappensCase = { description: string; diff --git a/tests/Sort.test.ts b/tests/Sort.test.ts index 6c9a88616a..616248eb67 100644 --- a/tests/Sort.test.ts +++ b/tests/Sort.test.ts @@ -228,4 +228,39 @@ describe('Sort', () => { ), ).toEqual(expectedOrder); }); + + it('sorts correctly by tag, done', () => { + const one = fromLine({ + line: '- [ ] a #someday #home 📅 1970-01-01 ✅ 1971-01-01', + path: '', + }); + const two = fromLine({ + line: '- [ ] #task b #someday #home 📅 1970-01-02 ✅ 1971-01-02', + path: '', + }); + const three = fromLine({ + line: '- [ ] c #next #home 📅 1970-01-02 ✅ 1971-01-01', + path: '', + }); + const four = fromLine({ + line: '- [ ] d #urgent #home 📅 1970-01-02 ✅ 1971-01-03', + path: '', + }); + const five = fromLine({ + line: '- [ ] e 📅 1970-01-02 ✅ 1971-01-03', + path: '', + }); + const expectedOrder = [three, one, two, four, five]; + expect( + Sort.by( + { + sorting: [ + { property: 'tag', reverse: false }, + { property: 'done', reverse: false }, + ], + }, + [one, two, three, four, five], + ), + ).toEqual(expectedOrder); + }); }); diff --git a/tests/Task.test.ts b/tests/Task.test.ts index 5f7649aef8..94fe8d0f30 100644 --- a/tests/Task.test.ts +++ b/tests/Task.test.ts @@ -3,6 +3,7 @@ */ import moment from 'moment'; import { Status, Task } from '../src/Task'; +import { getSettings, updateSettings } from '../src/Settings'; jest.mock('obsidian'); window.moment = moment; @@ -39,6 +40,69 @@ describe('parsing', () => { ).toStrictEqual(true); }); + it('parses a task from a line and extract tags', () => { + // Arrange + const line = + '- [x] this is a done task #tagone #journal/daily 🗓 2021-09-12 ✅ 2021-06-20'; + const path = 'this/is a path/to a/file.md'; + const sectionStart = 1337; + const sectionIndex = 1209; + const precedingHeader = 'Eloquent Section'; + + // Act + const task = Task.fromLine({ + line, + path, + sectionStart, + sectionIndex, + precedingHeader, + }); + + // Assert + expect(task).not.toBeNull(); + expect(task!.description).toEqual( + 'this is a done task #tagone #journal/daily', + ); + expect(task!.status).toStrictEqual(Status.Done); + expect(task!.dueDate).not.toBeNull(); + expect( + task!.dueDate!.isSame(moment('2021-09-12', 'YYYY-MM-DD')), + ).toStrictEqual(true); + expect(task!.doneDate).not.toBeNull(); + expect( + task!.doneDate!.isSame(moment('2021-06-20', 'YYYY-MM-DD')), + ).toStrictEqual(true); + expect(task!.tags.length).toEqual(2); + expect(task!.tags[0]).toEqual('#tagone'); + expect(task!.tags[1]).toEqual('#journal/daily'); + }); + + it('returns null when task does not have global filter', () => { + // Arrange + const originalSettings = getSettings(); + updateSettings({ globalFilter: '#task' }); + const line = '- [x] this is a done task 🗓 2021-09-12 ✅ 2021-06-20'; + const path = 'this/is a path/to a/file.md'; + const sectionStart = 1337; + const sectionIndex = 1209; + const precedingHeader = 'Eloquent Section'; + + // Act + const task = Task.fromLine({ + line, + path, + sectionStart, + sectionIndex, + precedingHeader, + }); + + // Assert + expect(task).toBeNull(); + + // Cleanup + updateSettings(originalSettings); + }); + it('allows signifier emojis as part of the description', () => { // Arrange const line = '- [x] this is a ✅ done task 🗓 2021-09-12 ✅ 2021-06-20'; @@ -121,6 +185,24 @@ describe('to string', () => { // Assert expect(task.toFileLineString()).toStrictEqual(line); }); + + it('retains the tags', () => { + // Arrange + const line = + '- [x] this is a done task #tagone #journal/daily 📅 2021-09-12 ✅ 2021-06-20'; + + // Act + const task: Task = Task.fromLine({ + line, + path: '', + sectionStart: 0, + sectionIndex: 0, + precedingHeader: '', + }) as Task; + + // Assert + expect(task.toFileLineString()).toStrictEqual(line); + }); }); describe('toggle done', () => { From f2104c98faed4cfa9a559a303a7d21efb085e0e2 Mon Sep 17 00:00:00 2001 From: sytone Date: Tue, 3 May 2022 20:43:47 -0700 Subject: [PATCH 02/23] feat: disregard the global tag Implement feedback on filter and tag sorting, ready for next release #632 --- src/Task.ts | 4 +++- tests/Task.test.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/Task.ts b/src/Task.ts index 114f2bdefa..a293152fb5 100644 --- a/src/Task.ts +++ b/src/Task.ts @@ -68,6 +68,7 @@ export class Task { public static readonly doneDateRegex = /✅ ?(\d{4}-\d{2}-\d{2})$/u; public static readonly recurrenceRegex = /🔁 ?([a-zA-Z0-9, !]+)$/iu; + // Regex to match all hash tags, basically hash followed by anything but the characters in the negation. public static readonly hashTags = /#[^ !@#$%^&*(),.?":{}|<>]*/g; private _urgency: number | null = null; @@ -289,9 +290,10 @@ export class Task { // Tags are found in the string and pulled out but not removed, // so when returning the entire task it will match what the user // entered. + // The global filter will be removed from the collection. const hashTagMatch = description.match(this.hashTags); if (hashTagMatch !== null) { - tags = hashTagMatch; + tags = hashTagMatch.filter((tag) => tag !== globalFilter); } const task = new Task({ diff --git a/tests/Task.test.ts b/tests/Task.test.ts index 94fe8d0f30..f9e95093dc 100644 --- a/tests/Task.test.ts +++ b/tests/Task.test.ts @@ -77,6 +77,48 @@ describe('parsing', () => { expect(task!.tags[1]).toEqual('#journal/daily'); }); + it('parses a task from a line and extract tags and not global filter', () => { + // Arrange + const originalSettings = getSettings(); + updateSettings({ globalFilter: '#task' }); + const line = + '- [x] #task this is a done task #tagone #journal/daily 🗓 2021-09-12 ✅ 2021-06-20'; + const path = 'this/is a path/to a/file.md'; + const sectionStart = 1337; + const sectionIndex = 1209; + const precedingHeader = 'Eloquent Section'; + + // Act + const task = Task.fromLine({ + line, + path, + sectionStart, + sectionIndex, + precedingHeader, + }); + + // Assert + expect(task).not.toBeNull(); + expect(task!.description).toEqual( + '#task this is a done task #tagone #journal/daily', + ); + expect(task!.status).toStrictEqual(Status.Done); + expect(task!.dueDate).not.toBeNull(); + expect( + task!.dueDate!.isSame(moment('2021-09-12', 'YYYY-MM-DD')), + ).toStrictEqual(true); + expect(task!.doneDate).not.toBeNull(); + expect( + task!.doneDate!.isSame(moment('2021-06-20', 'YYYY-MM-DD')), + ).toStrictEqual(true); + expect(task!.tags.length).toEqual(2); + expect(task!.tags[0]).toEqual('#tagone'); + expect(task!.tags[1]).toEqual('#journal/daily'); + + // Cleanup + updateSettings(originalSettings); + }); + it('returns null when task does not have global filter', () => { // Arrange const originalSettings = getSettings(); From 1bb8f9fe27ec67812b3e4cc3e2f6c1d7f5f9b293 Mon Sep 17 00:00:00 2001 From: sytone Date: Wed, 4 May 2022 21:52:57 -0700 Subject: [PATCH 03/23] feat: allow tag query to be hashless --- src/Query.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Query.ts b/src/Query.ts index fe3091a3f4..00b9d87089 100644 --- a/src/Query.ts +++ b/src/Query.ts @@ -453,15 +453,28 @@ export class Query { } } + /** + * When a tag based filter is used this is the process to apply it. + * - Tags can be searched for with and without the hash tag at the start. + * + * @private + * @param {{ line: string }} { line } + * @memberof Query + */ private parseTagFilter({ line }: { line: string }): void { const tagMatch = line.match(this.tagRegexp); if (tagMatch !== null) { const filterMethod = tagMatch[1]; + // Search is done sans the hash. If it is provided then strip it off. + const search = tagMatch[2].replace(/^#/, ''); + if (filterMethod === 'includes') { this._filters.push( (task: Task) => - task.tags.find((el) => el === tagMatch[2]) != undefined, + task.tags.find((tag) => + tag.toLowerCase().includes(search.toLowerCase()), + ) !== undefined, ); } else if (tagMatch[1] === 'does not include') { this._filters.push( From 75eba71a4c01a941b07cc79198f7090613a5664e Mon Sep 17 00:00:00 2001 From: sytone Date: Wed, 4 May 2022 22:40:09 -0700 Subject: [PATCH 04/23] test: refactor tag tests out and make them data driven --- tests/Task.test.ts | 206 ++++++++++++++++++++++++++++----------------- 1 file changed, 127 insertions(+), 79 deletions(-) diff --git a/tests/Task.test.ts b/tests/Task.test.ts index f9e95093dc..94dae11d54 100644 --- a/tests/Task.test.ts +++ b/tests/Task.test.ts @@ -40,85 +40,6 @@ describe('parsing', () => { ).toStrictEqual(true); }); - it('parses a task from a line and extract tags', () => { - // Arrange - const line = - '- [x] this is a done task #tagone #journal/daily 🗓 2021-09-12 ✅ 2021-06-20'; - const path = 'this/is a path/to a/file.md'; - const sectionStart = 1337; - const sectionIndex = 1209; - const precedingHeader = 'Eloquent Section'; - - // Act - const task = Task.fromLine({ - line, - path, - sectionStart, - sectionIndex, - precedingHeader, - }); - - // Assert - expect(task).not.toBeNull(); - expect(task!.description).toEqual( - 'this is a done task #tagone #journal/daily', - ); - expect(task!.status).toStrictEqual(Status.Done); - expect(task!.dueDate).not.toBeNull(); - expect( - task!.dueDate!.isSame(moment('2021-09-12', 'YYYY-MM-DD')), - ).toStrictEqual(true); - expect(task!.doneDate).not.toBeNull(); - expect( - task!.doneDate!.isSame(moment('2021-06-20', 'YYYY-MM-DD')), - ).toStrictEqual(true); - expect(task!.tags.length).toEqual(2); - expect(task!.tags[0]).toEqual('#tagone'); - expect(task!.tags[1]).toEqual('#journal/daily'); - }); - - it('parses a task from a line and extract tags and not global filter', () => { - // Arrange - const originalSettings = getSettings(); - updateSettings({ globalFilter: '#task' }); - const line = - '- [x] #task this is a done task #tagone #journal/daily 🗓 2021-09-12 ✅ 2021-06-20'; - const path = 'this/is a path/to a/file.md'; - const sectionStart = 1337; - const sectionIndex = 1209; - const precedingHeader = 'Eloquent Section'; - - // Act - const task = Task.fromLine({ - line, - path, - sectionStart, - sectionIndex, - precedingHeader, - }); - - // Assert - expect(task).not.toBeNull(); - expect(task!.description).toEqual( - '#task this is a done task #tagone #journal/daily', - ); - expect(task!.status).toStrictEqual(Status.Done); - expect(task!.dueDate).not.toBeNull(); - expect( - task!.dueDate!.isSame(moment('2021-09-12', 'YYYY-MM-DD')), - ).toStrictEqual(true); - expect(task!.doneDate).not.toBeNull(); - expect( - task!.doneDate!.isSame(moment('2021-06-20', 'YYYY-MM-DD')), - ).toStrictEqual(true); - expect(task!.tags.length).toEqual(2); - expect(task!.tags[0]).toEqual('#tagone'); - expect(task!.tags[1]).toEqual('#journal/daily'); - - // Cleanup - updateSettings(originalSettings); - }); - it('returns null when task does not have global filter', () => { // Arrange const originalSettings = getSettings(); @@ -210,6 +131,133 @@ describe('parsing', () => { }); }); +type TagParsingExpectations = { + markdownTask: string; + expectedDescription: string; + extractedTags: string[]; + globalFilter: string; +}; + +function constructTaskFromLine(line: string) { + const task = Task.fromLine({ + line, + path: 'file.md', + sectionStart: 0, + sectionIndex: 0, + precedingHeader: '', + }); + return task; +} + +describe('parsing tags', () => { + test.each([ + { + markdownTask: + '- [x] this is a done task #tagone 🗓 2021-09-12 ✅ 2021-06-20', + expectedDescription: 'this is a done task #tagone', + extractedTags: ['#tagone'], + globalFilter: '', + }, + { + markdownTask: + '- [x] this is a done task #tagone #tagtwo 🗓 2021-09-12 ✅ 2021-06-20', + expectedDescription: 'this is a done task #tagone #tagtwo', + extractedTags: ['#tagone', '#tagtwo'], + globalFilter: '', + }, + { + markdownTask: + '- [ ] this is a normal task #tagone 🗓 2021-09-12 ✅ 2021-06-20', + expectedDescription: 'this is a normal task #tagone', + extractedTags: ['#tagone'], + globalFilter: '', + }, + { + markdownTask: + '- [ ] this is a normal task #tagone #tagtwo 🗓 2021-09-12 ✅ 2021-06-20', + expectedDescription: 'this is a normal task #tagone #tagtwo', + extractedTags: ['#tagone', '#tagtwo'], + globalFilter: '', + }, + { + markdownTask: + '- [ ] this is a normal task #tagone #tag/with/depth #tagtwo 🗓 2021-09-12 ✅ 2021-06-20', + expectedDescription: + 'this is a normal task #tagone #tag/with/depth #tagtwo', + extractedTags: ['#tagone', '#tag/with/depth', '#tagtwo'], + globalFilter: '', + }, + + { + markdownTask: + '- [x] #someglobaltasktag this is a done task #tagone 🗓 2021-09-12 ✅ 2021-06-20', + expectedDescription: + '#someglobaltasktag this is a done task #tagone', + extractedTags: ['#tagone'], + globalFilter: '#someglobaltasktag', + }, + { + markdownTask: + '- [x] #someglobaltasktag this is a done task #tagone #tagtwo 🗓 2021-09-12 ✅ 2021-06-20', + expectedDescription: + '#someglobaltasktag this is a done task #tagone #tagtwo', + extractedTags: ['#tagone', '#tagtwo'], + globalFilter: '#someglobaltasktag', + }, + { + markdownTask: + '- [ ] #someglobaltasktag this is a normal task #tagone 🗓 2021-09-12 ✅ 2021-06-20', + expectedDescription: + '#someglobaltasktag this is a normal task #tagone', + extractedTags: ['#tagone'], + globalFilter: '#someglobaltasktag', + }, + { + markdownTask: + '- [ ] #someglobaltasktag this is a normal task #tagone #tagtwo 🗓 2021-09-12 ✅ 2021-06-20', + expectedDescription: + '#someglobaltasktag this is a normal task #tagone #tagtwo', + extractedTags: ['#tagone', '#tagtwo'], + globalFilter: '#someglobaltasktag', + }, + { + markdownTask: + '- [ ] #someglobaltasktag this is a normal task #tagone #tag/with/depth #tagtwo 🗓 2021-09-12 ✅ 2021-06-20', + expectedDescription: + '#someglobaltasktag this is a normal task #tagone #tag/with/depth #tagtwo', + extractedTags: ['#tagone', '#tag/with/depth', '#tagtwo'], + globalFilter: '#someglobaltasktag', + }, + ])( + 'should parse $markdownTask and extract $extractedTags', + ({ + markdownTask, + expectedDescription, + extractedTags, + globalFilter, + }) => { + // Arrange + const originalSettings = getSettings(); + if (globalFilter != '') { + updateSettings({ globalFilter: globalFilter }); + } + + // Act + const task = constructTaskFromLine(markdownTask); + + // Assert + expect(task).not.toBeNull(); + expect(task!.description).toEqual(expectedDescription); + expect(task!.tags).toStrictEqual(extractedTags); + + // Cleanup + if (globalFilter != '') { + updateSettings(originalSettings); + } + }, + ); +}); + describe('to string', () => { it('retains the block link', () => { // Arrange From d8a95b0b7c45fb44d1bb9bb0b98816abd91a7e1a Mon Sep 17 00:00:00 2001 From: sytone Date: Wed, 4 May 2022 22:54:40 -0700 Subject: [PATCH 05/23] feat: update query language, add tests, make hash optional and query case insensative --- src/Query.ts | 10 +++-- tests/Query.test.ts | 96 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 98 insertions(+), 8 deletions(-) diff --git a/src/Query.ts b/src/Query.ts index a57bd0f7b9..1760a933e7 100644 --- a/src/Query.ts +++ b/src/Query.ts @@ -49,7 +49,7 @@ export class Query { private readonly descriptionRegexp = /^description (includes|does not include) (.*)/; - private readonly tagRegexp = /^tag (includes|does not include) (.*)/; + private readonly tagRegexp = /^tags (include|do not include) (.*)/; private readonly sortByRegexp = /^sort by (urgency|status|priority|start|scheduled|due|done|path|description|tag)( reverse)?/; @@ -472,17 +472,19 @@ export class Query { // Search is done sans the hash. If it is provided then strip it off. const search = tagMatch[2].replace(/^#/, ''); - if (filterMethod === 'includes') { + if (filterMethod === 'include') { this._filters.push( (task: Task) => task.tags.find((tag) => tag.toLowerCase().includes(search.toLowerCase()), ) !== undefined, ); - } else if (tagMatch[1] === 'does not include') { + } else if (tagMatch[1] === 'do not include') { this._filters.push( (task: Task) => - task.tags.find((el) => el === tagMatch[2]) == undefined, + task.tags.find((tag) => + tag.toLowerCase().includes(search.toLowerCase()), + ) == undefined, ); } else { this._error = 'do not understand query filter (tag)'; diff --git a/tests/Query.test.ts b/tests/Query.test.ts index f79a2c5b49..c5be84e832 100644 --- a/tests/Query.test.ts +++ b/tests/Query.test.ts @@ -281,7 +281,7 @@ describe('Query', () => { [ 'by tag presence', { - filters: ['tag includes #home'], + filters: ['tags include #home'], tasks: [ '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', @@ -295,7 +295,95 @@ describe('Query', () => { [ 'by tag absence', { - filters: ['tag does not include #home'], + filters: ['tags do not include #home'], + tasks: [ + '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + expectedResult: [ + '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + }, + ], + [ + 'by tag presence without hash', + { + filters: ['tags include home'], + tasks: [ + '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + expectedResult: [ + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + ], + }, + ], + [ + 'by tag absence without hash', + { + filters: ['tags do not include home'], + tasks: [ + '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + expectedResult: [ + '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + }, + ], + + [ + 'by tag presence case insensitive', + { + filters: ['tags include #HoMe'], + tasks: [ + '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + expectedResult: [ + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + ], + }, + ], + [ + 'by tag absence case insensitive', + { + filters: ['tags do not include #HoMe'], + tasks: [ + '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + expectedResult: [ + '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + }, + ], + [ + 'by tag presence without hash case insensitive', + { + filters: ['tags include HoMe'], + tasks: [ + '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + expectedResult: [ + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + ], + }, + ], + [ + 'by tag absence without hash case insensitive', + { + filters: ['tags do not include HoMe'], tasks: [ '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', @@ -349,7 +437,7 @@ describe('Query', () => { precedingHeader: '', }), ] as Task[]; - const input = 'tag includes #home'; + const input = 'tags include #home'; const query = new Query({ source: input }); // Act @@ -393,7 +481,7 @@ describe('Query', () => { precedingHeader: '', }), ] as Task[]; - const input = 'tag does not include #home'; + const input = 'tags do not include #home'; const query = new Query({ source: input }); // Act From 547f7765de41ff29bb069f3d0e2f361f524186d5 Mon Sep 17 00:00:00 2001 From: sytone Date: Wed, 4 May 2022 23:00:16 -0700 Subject: [PATCH 06/23] test: add tests to validate substring --- tests/Query.test.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/Query.test.ts b/tests/Query.test.ts index c5be84e832..46455ef862 100644 --- a/tests/Query.test.ts +++ b/tests/Query.test.ts @@ -395,6 +395,35 @@ describe('Query', () => { ], }, ], + [ + 'by tag presence without hash case insensitive and substring', + { + filters: ['tags include TopLevelItem'], + tasks: [ + '- [ ] #task something to do #later #work #TopLevelItem/sub 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + expectedResult: [ + '- [ ] #task something to do #later #work #TopLevelItem/sub 📅 2021-09-12 ✅ 2021-06-20', + ], + }, + ], + [ + 'by tag absence without hash case insensitive and substring', + { + filters: ['tags do not include TopLevelItem'], + tasks: [ + '- [ ] #task something to do #later #work #TopLevelItem/sub 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + expectedResult: [ + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + }, + ], ])( 'should support filtering of tags %s', (_, { tasks: allTaskLines, filters, expectedResult }) => { From 971600712779fdbeab2071fced17ea24de66c56a Mon Sep 17 00:00:00 2001 From: sytone Date: Wed, 4 May 2022 23:02:20 -0700 Subject: [PATCH 07/23] test: chect to see if global tag is not found in query --- tests/Query.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/Query.test.ts b/tests/Query.test.ts index 46455ef862..cfe74d0c83 100644 --- a/tests/Query.test.ts +++ b/tests/Query.test.ts @@ -424,6 +424,18 @@ describe('Query', () => { ], }, ], + [ + 'by tag presence when it is the global tag', + { + filters: ['tags include task'], + tasks: [ + '- [ ] #task something to do #later #work #TopLevelItem/sub 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + expectedResult: [], + }, + ], ])( 'should support filtering of tags %s', (_, { tasks: allTaskLines, filters, expectedResult }) => { From a3964f78846c4325adc99be3efc6c893933b47a3 Mon Sep 17 00:00:00 2001 From: sytone Date: Wed, 4 May 2022 23:11:08 -0700 Subject: [PATCH 08/23] docs: update filter documentation on the tag query --- docs/queries/filters.md | 81 ++++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 34 deletions(-) diff --git a/docs/queries/filters.md b/docs/queries/filters.md index 86c6ba5151..975cf46b0e 100644 --- a/docs/queries/filters.md +++ b/docs/queries/filters.md @@ -25,12 +25,12 @@ parent: Queries `` filters can be given in natural language or in formal notation. The following are some examples of valid `` filters as inspiration: -- `2021-05-05` -- `today` -- `tomorrow` -- `next monday` -- `last friday` -- `in two weeks` +- `2021-05-05` +- `today` +- `tomorrow` +- `next monday` +- `last friday` +- `in two weeks` Note that if it is Wednesday and you write `tuesday`, Tasks assumes you mean "yesterday", as that is the closest Tuesday. Use `next tuesday` instead if you mean "next tuesday". @@ -65,13 +65,13 @@ Instead you would have two queries, one for each condition: ### Done Date -- `done` -- `not done` -- `done (before|after|on) ` +- `done` +- `not done` +- `done (before|after|on) ` ### Priority -- `priority is (above|below)? (low|none|medium|high)` +- `priority is (above|below)? (low|none|medium|high)` #### Examples @@ -88,9 +88,9 @@ Instead you would have two queries, one for each condition: ### Start Date -- `no start date` -- `has start date` -- `starts (before|after|on) ` +- `no start date` +- `has start date` +- `starts (before|after|on) ` When filtering queries by [start date]({{ site.baseurl }}{% link getting-started/dates.md %}#-start), the result will include tasks without a start date. @@ -104,19 +104,19 @@ Such filter could be: ### Scheduled Date -- `no scheduled date` -- `has scheduled date` -- `scheduled (before|after|on) ` +- `no scheduled date` +- `has scheduled date` +- `scheduled (before|after|on) ` ### Due Date -- `no due date` -- `has due date` -- `due (before|after|on) ` +- `no due date` +- `has due date` +- `due (before|after|on) ` ### Happens -- `happens (before|after|on) ` +- `happens (before|after|on) ` `happens` returns any task for a matching start date, scheduled date, _or_ due date. For example, `happens before tomorrow` will return all tasks that are starting, scheduled, or due earlier than tomorrow. @@ -125,29 +125,42 @@ because the tasks starts before tomorrow. Only one of the dates needs to match. ### Recurrence -- `is recurring` -- `is not recurring` +- `is recurring` +- `is not recurring` ### File Path -- `path (includes|does not include) ` - - Matches case-insensitive (disregards capitalization). +- `path (includes|does not include) ` + - Matches case-insensitive (disregards capitalization). ### Description -- `description (includes|does not include) ` - - Matches case-insensitive (disregards capitalization). - - Disregards the global filter when matching. +- `description (includes|does not include) ` + - Matches case-insensitive (disregards capitalization). + - Disregards the global filter when matching. ### Heading -- `heading (includes|does not include) ` - - Whether or not the heading preceding the task includes the given string. - - Always tries to match the closest heading above the task, regardless of heading level. - - `does not include` will match a task that does not have a preceding heading in its file. - - Matches case-insensitive (disregards capitalization). +- `heading (includes|does not include) ` + - Whether or not the heading preceding the task includes the given string. + - Always tries to match the closest heading above the task, regardless of heading level. + - `does not include` will match a task that does not have a preceding heading in its file. + - Matches case-insensitive (disregards capitalization). ### Sub-Items -- `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. +- `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. + +### Tags + +- `tags (include|do not include) ` + - Matches case-insensitive (disregards capitalization). + - Disregards the global filter when matching. + - The `#` is optional. + - The match is partial so `tags include foo` will match `#foo/bar` and `#foo-bar`. + +#### Tag Query Examples + +- `tag includes #todo` +- `tag does not include #todo` From 9c645272b3f00f834b45cfade32eff1399dac893 Mon Sep 17 00:00:00 2001 From: sytone Date: Fri, 6 May 2022 21:08:57 -0700 Subject: [PATCH 09/23] feat: add ability to sort by tag instance/index docs: add documentation on tag sorting fix: handle lack of tag in the comparison for tag sorting test: add increased test coverage to tags --- docs/queries/sorting.md | 12 ++ src/Query.ts | 11 +- src/Sort.ts | 35 ++++- src/Task.ts | 2 +- tests/Query.test.ts | 34 ++++- tests/Sort.test.ts | 311 ++++++++++++++++++++++++++++++++-------- 6 files changed, 339 insertions(+), 66 deletions(-) diff --git a/docs/queries/sorting.md b/docs/queries/sorting.md index 163be8ef43..456138d808 100644 --- a/docs/queries/sorting.md +++ b/docs/queries/sorting.md @@ -6,6 +6,7 @@ parent: Queries --- # Sorting + {: .no_toc }
@@ -36,6 +37,7 @@ You can sort tasks by the following properties: 5. `done` (the date when the task was done) 6. `path` (the path to the file that contains the task) 7. `description` (the description of the task) +8. `tag` (the description of the task) You can add multiple `sort by` query options, each on an extra line. The first sort has the highest priority. @@ -70,6 +72,16 @@ If given, the sort order will be reverse for that property. Note that `reverse` will reverse the entire result set. For example, when you `sort by done reverse` and your query results contain tasks that do not have a done date, then those tasks without a done date will be listed first. +## Tag sorting + +If you want to sort by tags, by default it will sort by the first tag found in the description. If you want to sort by a tag that comes after that then you can specify the index at the end of the query. All tasks should have the same amount of tags for optimal sorting and the tags in the same order. The index starts from 0 which is also the default. + +For example this query will sort by the second tag found in the description. + + ```tasks + sort by tag 1 + ``` + --- ## Examples diff --git a/src/Query.ts b/src/Query.ts index 1760a933e7..3f616cf90f 100644 --- a/src/Query.ts +++ b/src/Query.ts @@ -15,7 +15,11 @@ export type SortingProperty = | 'path' | 'description' | 'tag'; -type Sorting = { property: SortingProperty; reverse: boolean }; +type Sorting = { + property: SortingProperty; + reverse: boolean; + propertyInstance: number; +}; export class Query { private _limit: number | undefined = undefined; @@ -51,8 +55,10 @@ export class Query { private readonly tagRegexp = /^tags (include|do not include) (.*)/; + // If a tag is specified the user can also add a number to specify + // which one to sort by if there is more than one. private readonly sortByRegexp = - /^sort by (urgency|status|priority|start|scheduled|due|done|path|description|tag)( reverse)?/; + /^sort by (urgency|status|priority|start|scheduled|due|done|path|description|tag)( reverse)?[\s]*(\d+)?/; private readonly headingRegexp = /^heading (includes|does not include) (.*)/; @@ -575,6 +581,7 @@ export class Query { this._sorting.push({ property: fieldMatch[1] as SortingProperty, reverse: !!fieldMatch[2], + propertyInstance: isNaN(+fieldMatch[3]) ? 0 : +fieldMatch[3], }); } else { this._error = 'do not understand query sorting'; diff --git a/src/Sort.ts b/src/Sort.ts index 10dcbfb8cc..a0517ec129 100644 --- a/src/Sort.ts +++ b/src/Sort.ts @@ -7,6 +7,8 @@ import { getSettings } from './Settings'; type Comparator = (a: Task, b: Task) => number; export class Sort { + static tagPropertyInstance: number = 0; + public static by(query: Pick, tasks: Task[]): Task[] { const defaultComparators: Comparator[] = [ Sort.compareByUrgency, @@ -18,11 +20,14 @@ export class Sort { const userComparators: Comparator[] = []; - for (const { property, reverse } of query.sorting) { + for (const { property, reverse, propertyInstance } of query.sorting) { const comparator = Sort.comparators[property]; userComparators.push( reverse ? Sort.makeReversedComparator(comparator) : comparator, ); + if (property === 'tag') { + Sort.tagPropertyInstance = propertyInstance; + } } return tasks.sort( @@ -101,9 +106,33 @@ export class Sort { // Currently sorts by first tag found only. private static compareByTag(a: Task, b: Task): -1 | 0 | 1 { - if (a.tags[0] < b.tags[0]) { + // If no tags then assume they are equal. + if (a.tags.length === 0 && b.tags.length === 0) { + return 0; + } else if (a.tags.length === 0) { + // a is less than b + return 1; + } else if (b.tags.length === 0) { + // b is less than a + return -1; + } + + // If the tag collection is smaller than the instance + // used to compare then they are just equal. + if ( + Sort.tagPropertyInstance >= a.tags.length || + Sort.tagPropertyInstance >= b.tags.length + ) { + return 0; + } + + if ( + a.tags[Sort.tagPropertyInstance] < b.tags[Sort.tagPropertyInstance] + ) { return -1; - } else if (a.tags[0] > b.tags[0]) { + } else if ( + a.tags[Sort.tagPropertyInstance] > b.tags[Sort.tagPropertyInstance] + ) { return 1; } else { return 0; diff --git a/src/Task.ts b/src/Task.ts index a293152fb5..c23c4446c1 100644 --- a/src/Task.ts +++ b/src/Task.ts @@ -106,7 +106,7 @@ export class Task { doneDate: moment.Moment | null; recurrence: Recurrence | null; blockLink: string; - tags: string[]; + tags: string[] | []; }) { this.status = status; this.description = description; diff --git a/tests/Query.test.ts b/tests/Query.test.ts index cfe74d0c83..47963fbe2a 100644 --- a/tests/Query.test.ts +++ b/tests/Query.test.ts @@ -689,17 +689,43 @@ describe('Query', () => { describe('sorting instructions', () => { const cases: { input: string; - output: { property: string; reverse: boolean }[]; + output: { + property: string; + reverse: boolean; + propertyInstance: number; + }[]; }[] = [ { input: 'sort by status', - output: [{ property: 'status', reverse: false }], + output: [ + { + property: 'status', + reverse: false, + propertyInstance: 0, + }, + ], }, { input: 'sort by status\nsort by due', output: [ - { property: 'status', reverse: false }, - { property: 'due', reverse: false }, + { + property: 'status', + reverse: false, + propertyInstance: 0, + }, + { property: 'due', reverse: false, propertyInstance: 0 }, + ], + }, + { + input: 'sort by tag', + output: [ + { property: 'tag', reverse: false, propertyInstance: 0 }, + ], + }, + { + input: 'sort by tag 2', + output: [ + { property: 'tag', reverse: false, propertyInstance: 2 }, ], }, ]; diff --git a/tests/Sort.test.ts b/tests/Sort.test.ts index 616248eb67..7ff0c008a4 100644 --- a/tests/Sort.test.ts +++ b/tests/Sort.test.ts @@ -7,6 +7,7 @@ window.moment = moment; import { Task } from '../src/Task'; import { Sort } from '../src/Sort'; +import { getSettings, updateSettings } from '../src/Settings'; function fromLine({ line, path = '' }: { line: string; path?: string }) { return Task.fromLine({ @@ -46,18 +47,32 @@ describe('Sort', () => { path: '', }); expect( - Sort.by({ sorting: [{ property: 'due', reverse: false }] }, [ - one, - two, - three, - ]), + Sort.by( + { + sorting: [ + { + property: 'due', + reverse: false, + propertyInstance: 0, + }, + ], + }, + [one, two, three], + ), ).toEqual([one, two, three]); expect( - Sort.by({ sorting: [{ property: 'due', reverse: false }] }, [ - two, - three, - one, - ]), + Sort.by( + { + sorting: [ + { + property: 'due', + reverse: false, + propertyInstance: 0, + }, + ], + }, + [two, three, one], + ), ).toEqual([one, two, three]); }); @@ -75,18 +90,32 @@ describe('Sort', () => { path: '', }); expect( - Sort.by({ sorting: [{ property: 'done', reverse: false }] }, [ - three, - two, - one, - ]), + Sort.by( + { + sorting: [ + { + property: 'done', + reverse: false, + propertyInstance: 0, + }, + ], + }, + [three, two, one], + ), ).toEqual([one, two, three]); expect( - Sort.by({ sorting: [{ property: 'done', reverse: false }] }, [ - two, - one, - three, - ]), + Sort.by( + { + sorting: [ + { + property: 'done', + reverse: false, + propertyInstance: 0, + }, + ], + }, + [two, one, three], + ), ).toEqual([one, two, three]); }); @@ -105,9 +134,21 @@ describe('Sort', () => { Sort.by( { sorting: [ - { property: 'due', reverse: false }, - { property: 'path', reverse: false }, - { property: 'status', reverse: false }, + { + property: 'due', + reverse: false, + propertyInstance: 0, + }, + { + property: 'path', + reverse: false, + propertyInstance: 0, + }, + { + property: 'status', + reverse: false, + propertyInstance: 0, + }, ], }, [one, four, two, three], @@ -137,8 +178,16 @@ describe('Sort', () => { Sort.by( { sorting: [ - { property: 'description', reverse: false }, - { property: 'done', reverse: false }, + { + property: 'description', + reverse: false, + propertyInstance: 0, + }, + { + property: 'done', + reverse: false, + propertyInstance: 0, + }, ], }, [three, one, two, four], @@ -168,8 +217,16 @@ describe('Sort', () => { Sort.by( { sorting: [ - { property: 'description', reverse: true }, - { property: 'done', reverse: false }, + { + property: 'description', + reverse: true, + propertyInstance: 0, + }, + { + property: 'done', + reverse: false, + propertyInstance: 0, + }, ], }, [two, four, three, one], @@ -191,9 +248,17 @@ describe('Sort', () => { Sort.by( { sorting: [ - { property: 'status', reverse: true }, - { property: 'due', reverse: true }, - { property: 'path', reverse: false }, + { + property: 'status', + reverse: true, + propertyInstance: 0, + }, + { property: 'due', reverse: true, propertyInstance: 0 }, + { + property: 'path', + reverse: false, + propertyInstance: 0, + }, ], }, [six, five, one, four, three, two], @@ -222,45 +287,179 @@ describe('Sort', () => { expect( Sort.by( { - sorting: [{ property: 'description', reverse: false }], + sorting: [ + { + property: 'description', + reverse: false, + propertyInstance: 0, + }, + ], }, [two, one, five, four, three], ), ).toEqual(expectedOrder); }); +}); - it('sorts correctly by tag, done', () => { - const one = fromLine({ - line: '- [ ] a #someday #home 📅 1970-01-01 ✅ 1971-01-01', - path: '', - }); - const two = fromLine({ - line: '- [ ] #task b #someday #home 📅 1970-01-02 ✅ 1971-01-02', - path: '', - }); - const three = fromLine({ - line: '- [ ] c #next #home 📅 1970-01-02 ✅ 1971-01-01', - path: '', - }); - const four = fromLine({ - line: '- [ ] d #urgent #home 📅 1970-01-02 ✅ 1971-01-03', - path: '', - }); - const five = fromLine({ - line: '- [ ] e 📅 1970-01-02 ✅ 1971-01-03', - path: '', - }); - const expectedOrder = [three, one, two, four, five]; +describe('Sort by tags', () => { + it('should sort correctly by tag defaulting to first with no global filter', () => { + const t1 = fromLine({ line: '- [ ] a #aaa #jjj' }); + const t2 = fromLine({ line: '- [ ] a #ggg #ccc' }); + const t3 = fromLine({ line: '- [ ] a #bbb #iii' }); + const t4 = fromLine({ line: '- [ ] a #fff #aaa' }); + const t5 = fromLine({ line: '- [ ] a #ccc #bbb' }); + const t6 = fromLine({ line: '- [ ] a #eee #fff' }); + const t7 = fromLine({ line: '- [ ] a #ddd #ggg' }); + const t8 = fromLine({ line: '- [ ] a #hhh #eee' }); + const t9 = fromLine({ line: '- [ ] a #iii #ddd' }); + const t10 = fromLine({ line: '- [ ] a #jjj #hhh' }); + const expectedOrder = [t1, t3, t5, t7, t6, t4, t2, t8, t9, t10]; + expect( + Sort.by( + { + sorting: [ + { + property: 'tag', + reverse: false, + propertyInstance: 0, + }, + ], + }, + [t1, t2, t3, t4, t5, t6, t7, t8, t9, t10], + ), + ).toEqual(expectedOrder); + }); + it('should sort correctly by second tag with no global filter', () => { + const t1 = fromLine({ line: '- [ ] a #aaa #jjj' }); + const t2 = fromLine({ line: '- [ ] a #ggg #ccc' }); + const t3 = fromLine({ line: '- [ ] a #bbb #iii' }); + const t4 = fromLine({ line: '- [ ] a #fff #aaa' }); + const t5 = fromLine({ line: '- [ ] a #ccc #bbb' }); + const t6 = fromLine({ line: '- [ ] a #eee #fff' }); + const t7 = fromLine({ line: '- [ ] a #ddd #ggg' }); + const t8 = fromLine({ line: '- [ ] a #hhh #eee' }); + const t9 = fromLine({ line: '- [ ] a #iii #ddd' }); + const t10 = fromLine({ line: '- [ ] a #jjj #hhh' }); + const expectedOrder = [t4, t5, t2, t9, t8, t6, t7, t10, t3, t1]; expect( Sort.by( { sorting: [ - { property: 'tag', reverse: false }, - { property: 'done', reverse: false }, + { + property: 'tag', + reverse: false, + propertyInstance: 1, + }, ], }, - [one, two, three, four, five], + [t1, t2, t3, t4, t5, t6, t7, t8, t9, t10], ), ).toEqual(expectedOrder); }); + it('should sort correctly by tag defaulting to first with global filter', () => { + // Arrange + const originalSettings = getSettings(); + updateSettings({ globalFilter: '#task' }); + + const t1 = fromLine({ line: '- [ ] #task a #aaa #jjj' }); + const t2 = fromLine({ line: '- [ ] #task a #ggg #ccc' }); + const t3 = fromLine({ line: '- [ ] #task a #bbb #iii' }); + const t4 = fromLine({ line: '- [ ] #task a #fff #aaa' }); + const t5 = fromLine({ line: '- [ ] #task a #ccc #bbb' }); + const t6 = fromLine({ line: '- [ ] #task a #eee #fff' }); + const t7 = fromLine({ line: '- [ ] #task a #ddd #ggg' }); + const t8 = fromLine({ line: '- [ ] #task a #hhh #eee' }); + const t9 = fromLine({ line: '- [ ] #task a #iii #ddd' }); + const t10 = fromLine({ line: '- [ ] #task a #jjj #hhh' }); + const t11 = fromLine({ line: '- [ ] #task a' }); + const t12 = fromLine({ line: '- [ ] #task a #aaaa #aaaa' }); + const t13 = fromLine({ line: '- [ ] #task a #bbbb ' }); + const expectedOrder = [ + t1, + t12, + t3, + t13, + t5, + t7, + t6, + t4, + t2, + t8, + t9, + t10, + t11, + ]; + + // Act + expect( + Sort.by( + { + sorting: [ + { + property: 'tag', + reverse: false, + propertyInstance: 0, + }, + ], + }, + [t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13], + ), + ).toEqual(expectedOrder); + + // Cleanup + updateSettings(originalSettings); + }); + it('should sort correctly by second tag with global filter', () => { + // Arrange + const originalSettings = getSettings(); + updateSettings({ globalFilter: '#task' }); + + const t1 = fromLine({ line: '- [ ] #task a #aaa #jjj' }); + const t2 = fromLine({ line: '- [ ] #task a #ggg #ccc' }); + const t3 = fromLine({ line: '- [ ] #task a #bbb #iii' }); + const t4 = fromLine({ line: '- [ ] #task a #fff #aaa' }); + const t5 = fromLine({ line: '- [ ] #task a #ccc #bbb' }); + const t6 = fromLine({ line: '- [ ] #task a #eee #fff' }); + const t7 = fromLine({ line: '- [ ] #task a #ddd #ggg' }); + const t8 = fromLine({ line: '- [ ] #task a #hhh #eee' }); + const t9 = fromLine({ line: '- [ ] #task a #iii #ddd' }); + const t10 = fromLine({ line: '- [ ] #task a #jjj #hhh' }); + const t11 = fromLine({ line: '- [ ] #task a' }); + const t12 = fromLine({ line: '- [ ] #task a #aaaa #aaaa' }); + const t13 = fromLine({ line: '- [ ] #task a #bbbb ' }); + const expectedOrder = [ + t4, + t12, + t5, + t2, + t9, + t8, + t6, + t7, + t10, + t3, + t1, + t13, + t11, + ]; + + // Act + expect( + Sort.by( + { + sorting: [ + { + property: 'tag', + reverse: false, + propertyInstance: 1, + }, + ], + }, + [t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13], + ), + ).toEqual(expectedOrder); + + // Cleanup + updateSettings(originalSettings); + }); }); From 4b88b4a605fafdca9fd4a9b478d3f88deb8ed28b Mon Sep 17 00:00:00 2001 From: sytone Date: Sun, 15 May 2022 08:25:26 -0700 Subject: [PATCH 10/23] fix: resolved issues raised in PR. --- .gitignore | 3 + .vscode/extensions.json | 5 ++ .vscode/tasks.json | 12 +++ docs/queries/filters.md | 2 +- docs/queries/sorting.md | 4 +- src/Query.ts | 21 +++-- src/Sort.ts | 18 ++--- src/Task.ts | 34 +++++++- tests/Query.test.ts | 167 +++++++++++++++++++++++++++++++++++++++- tests/Sort.test.ts | 90 ++++++++-------------- tests/Task.test.ts | 17 ++++ 11 files changed, 288 insertions(+), 85 deletions(-) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/tasks.json diff --git a/.gitignore b/.gitignore index f8217851fd..b579e77493 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ data.json # asdf .tool-versions + +# Backup files. +*.bak diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000..212e327868 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "DavidAnson.vscode-markdownlint" + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000000..21ca535d72 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,12 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "dev", + "problemMatcher": [], + "label": "npm: dev", + "detail": "node esbuild.config.mjs" + } + ] +} diff --git a/docs/queries/filters.md b/docs/queries/filters.md index 975cf46b0e..8de3421bd9 100644 --- a/docs/queries/filters.md +++ b/docs/queries/filters.md @@ -154,7 +154,7 @@ because the tasks starts before tomorrow. Only one of the dates needs to match. ### Tags -- `tags (include|do not include) ` +- `tags (include|does not include) ` - Matches case-insensitive (disregards capitalization). - Disregards the global filter when matching. - The `#` is optional. diff --git a/docs/queries/sorting.md b/docs/queries/sorting.md index 456138d808..9b0ec07549 100644 --- a/docs/queries/sorting.md +++ b/docs/queries/sorting.md @@ -74,12 +74,12 @@ For example, when you `sort by done reverse` and your query results contain task ## Tag sorting -If you want to sort by tags, by default it will sort by the first tag found in the description. If you want to sort by a tag that comes after that then you can specify the index at the end of the query. All tasks should have the same amount of tags for optimal sorting and the tags in the same order. The index starts from 0 which is also the default. +If you want to sort by tags, by default it will sort by the first tag found in the description. If you want to sort by a tag that comes after that then you can specify the index at the end of the query. All tasks should have the same amount of tags for optimal sorting and the tags in the same order. The index starts from 1 which is also the default. For example this query will sort by the second tag found in the description. ```tasks - sort by tag 1 + sort by tag 2 ``` --- diff --git a/src/Query.ts b/src/Query.ts index 35ce017e96..d739630a55 100644 --- a/src/Query.ts +++ b/src/Query.ts @@ -54,7 +54,9 @@ export class Query { private readonly descriptionRegexp = /^description (includes|does not include) (.*)/; - private readonly tagRegexp = /^tags (include|do not include) (.*)/; + // Handles both ways of referencing the tags query. + private readonly tagRegexp = + /^(tag|tags) (includes|does not include|include|do not include) (.*)/; // If a tag is specified the user can also add a number to specify // which one to sort by if there is more than one. @@ -482,19 +484,22 @@ export class Query { private parseTagFilter({ line }: { line: string }): void { const tagMatch = line.match(this.tagRegexp); if (tagMatch !== null) { - const filterMethod = tagMatch[1]; + const filterMethod = tagMatch[2]; // Search is done sans the hash. If it is provided then strip it off. - const search = tagMatch[2].replace(/^#/, ''); + const search = tagMatch[3].replace(/^#/, ''); - if (filterMethod === 'include') { + if (filterMethod === 'include' || filterMethod === 'includes') { this._filters.push( (task: Task) => task.tags.find((tag) => tag.toLowerCase().includes(search.toLowerCase()), ) !== undefined, ); - } else if (tagMatch[1] === 'do not include') { + } else if ( + tagMatch[2] === 'do not include' || + tagMatch[2] === 'does not include' + ) { this._filters.push( (task: Task) => task.tags.find((tag) => @@ -502,10 +507,10 @@ export class Query { ) == undefined, ); } else { - this._error = 'do not understand query filter (tag)'; + this._error = 'do not understand query filter (tag/tags)'; } } else { - this._error = 'do not understand query filter (tag)'; + this._error = 'do not understand query filter (tag/tags)'; } } @@ -590,7 +595,7 @@ export class Query { this._sorting.push({ property: fieldMatch[1] as SortingProperty, reverse: !!fieldMatch[2], - propertyInstance: isNaN(+fieldMatch[3]) ? 0 : +fieldMatch[3], + propertyInstance: isNaN(+fieldMatch[3]) ? 1 : +fieldMatch[3], }); } else { this._error = 'do not understand query sorting'; diff --git a/src/Sort.ts b/src/Sort.ts index a0517ec129..d454899f87 100644 --- a/src/Sort.ts +++ b/src/Sort.ts @@ -7,7 +7,7 @@ import { getSettings } from './Settings'; type Comparator = (a: Task, b: Task) => number; export class Sort { - static tagPropertyInstance: number = 0; + static tagPropertyInstance: number = 1; public static by(query: Pick, tasks: Task[]): Task[] { const defaultComparators: Comparator[] = [ @@ -104,7 +104,6 @@ export class Sort { return Sort.compareByDate(a.doneDate, b.doneDate); } - // Currently sorts by first tag found only. private static compareByTag(a: Task, b: Task): -1 | 0 | 1 { // If no tags then assume they are equal. if (a.tags.length === 0 && b.tags.length === 0) { @@ -117,22 +116,21 @@ export class Sort { return -1; } + // Arrays start at 0 but the users specify a tag starting at 1. + const tagInstanceToSortBy = Sort.tagPropertyInstance - 1; + // If the tag collection is smaller than the instance // used to compare then they are just equal. if ( - Sort.tagPropertyInstance >= a.tags.length || - Sort.tagPropertyInstance >= b.tags.length + a.tags.length < Sort.tagPropertyInstance || + b.tags.length < Sort.tagPropertyInstance ) { return 0; } - if ( - a.tags[Sort.tagPropertyInstance] < b.tags[Sort.tagPropertyInstance] - ) { + if (a.tags[tagInstanceToSortBy] < b.tags[tagInstanceToSortBy]) { return -1; - } else if ( - a.tags[Sort.tagPropertyInstance] > b.tags[Sort.tagPropertyInstance] - ) { + } else if (a.tags[tagInstanceToSortBy] > b.tags[tagInstanceToSortBy]) { return 1; } else { return 0; diff --git a/src/Task.ts b/src/Task.ts index c23c4446c1..572368f499 100644 --- a/src/Task.ts +++ b/src/Task.ts @@ -6,12 +6,24 @@ import { Recurrence } from './Recurrence'; import { getSettings } from './Settings'; import { Urgency } from './Urgency'; +/** + * Collection of status types supported by the plugin. + * TODO: Make this a class so it can support other types and easier mapping to status character. + * @export + * @enum {number} + */ export enum Status { Todo = 'Todo', Done = 'Done', } -// Sort low below none. +/** + * When sorting, make sure low always comes after none. This way any tasks with low will be below any exiting + * tasks that have no priority which would be the default. + * + * @export + * @enum {number} + */ export enum Priority { High = '1', Medium = '2', @@ -19,6 +31,14 @@ export enum Priority { Low = '4', } +/** + * Task encapsulates the properties of the MarkDown task along with + * the extensions provided by this plugin. This is used to parse and + * generate the markdown task for all updates and replacements. + * + * @export + * @class Task + */ export class Task { public readonly status: Status; public readonly description: string; @@ -69,7 +89,13 @@ export class Task { public static readonly recurrenceRegex = /🔁 ?([a-zA-Z0-9, !]+)$/iu; // Regex to match all hash tags, basically hash followed by anything but the characters in the negation. - public static readonly hashTags = /#[^ !@#$%^&*(),.?":{}|<>]*/g; + // To ensure URLs are not caught it is looking of beginning of string tag and any + // tag that has a space in front of it. Any # that has a character in front + // of it will be ignored. + // EXAMPLE: + // description: '#dog #car http://www/ddd#ere #house' + // matches: #dog, #car, #house + public static readonly hashTags = /(^|\s)#[^ !@#$%^&*(),.?":{}|<>]*/g; private _urgency: number | null = null; @@ -293,7 +319,9 @@ export class Task { // The global filter will be removed from the collection. const hashTagMatch = description.match(this.hashTags); if (hashTagMatch !== null) { - tags = hashTagMatch.filter((tag) => tag !== globalFilter); + tags = hashTagMatch + .filter((tag) => tag !== globalFilter) + .map((tag) => tag.trim()); } const task = new Task({ diff --git a/tests/Query.test.ts b/tests/Query.test.ts index 47963fbe2a..1253bfed28 100644 --- a/tests/Query.test.ts +++ b/tests/Query.test.ts @@ -436,6 +436,165 @@ describe('Query', () => { expectedResult: [], }, ], + // Alt Grammar + [ + 'by tag presence', + { + filters: ['tag includes #home'], + tasks: [ + '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + expectedResult: [ + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + ], + }, + ], + [ + 'by tag absence', + { + filters: ['tag does not include #home'], + tasks: [ + '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + expectedResult: [ + '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + }, + ], + [ + 'by tag presence without hash', + { + filters: ['tag includes home'], + tasks: [ + '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + expectedResult: [ + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + ], + }, + ], + [ + 'by tag absence without hash', + { + filters: ['tag does not include home'], + tasks: [ + '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + expectedResult: [ + '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + }, + ], + + [ + 'by tag presence case insensitive', + { + filters: ['tag includes #HoMe'], + tasks: [ + '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + expectedResult: [ + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + ], + }, + ], + [ + 'by tag absence case insensitive', + { + filters: ['tag does not include #HoMe'], + tasks: [ + '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + expectedResult: [ + '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + }, + ], + [ + 'by tag presence without hash case insensitive', + { + filters: ['tag includes HoMe'], + tasks: [ + '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + expectedResult: [ + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + ], + }, + ], + [ + 'by tag absence without hash case insensitive', + { + filters: ['tag does not include HoMe'], + tasks: [ + '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + expectedResult: [ + '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + }, + ], + [ + 'by tag presence without hash case insensitive and substring', + { + filters: ['tag includes TopLevelItem'], + tasks: [ + '- [ ] #task something to do #later #work #TopLevelItem/sub 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + expectedResult: [ + '- [ ] #task something to do #later #work #TopLevelItem/sub 📅 2021-09-12 ✅ 2021-06-20', + ], + }, + ], + [ + 'by tag absence without hash case insensitive and substring', + { + filters: ['tag does not include TopLevelItem'], + tasks: [ + '- [ ] #task something to do #later #work #TopLevelItem/sub 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + expectedResult: [ + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + }, + ], + [ + 'by tag presence when it is the global tag', + { + filters: ['tag includes task'], + tasks: [ + '- [ ] #task something to do #later #work #TopLevelItem/sub 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + ], + expectedResult: [], + }, + ], ])( 'should support filtering of tags %s', (_, { tasks: allTaskLines, filters, expectedResult }) => { @@ -701,7 +860,7 @@ describe('Query', () => { { property: 'status', reverse: false, - propertyInstance: 0, + propertyInstance: 1, }, ], }, @@ -711,15 +870,15 @@ describe('Query', () => { { property: 'status', reverse: false, - propertyInstance: 0, + propertyInstance: 1, }, - { property: 'due', reverse: false, propertyInstance: 0 }, + { property: 'due', reverse: false, propertyInstance: 1 }, ], }, { input: 'sort by tag', output: [ - { property: 'tag', reverse: false, propertyInstance: 0 }, + { property: 'tag', reverse: false, propertyInstance: 1 }, ], }, { diff --git a/tests/Sort.test.ts b/tests/Sort.test.ts index 7ff0c008a4..9ef373521e 100644 --- a/tests/Sort.test.ts +++ b/tests/Sort.test.ts @@ -53,7 +53,7 @@ describe('Sort', () => { { property: 'due', reverse: false, - propertyInstance: 0, + propertyInstance: 1, }, ], }, @@ -67,7 +67,7 @@ describe('Sort', () => { { property: 'due', reverse: false, - propertyInstance: 0, + propertyInstance: 1, }, ], }, @@ -96,7 +96,7 @@ describe('Sort', () => { { property: 'done', reverse: false, - propertyInstance: 0, + propertyInstance: 1, }, ], }, @@ -110,7 +110,7 @@ describe('Sort', () => { { property: 'done', reverse: false, - propertyInstance: 0, + propertyInstance: 1, }, ], }, @@ -137,17 +137,17 @@ describe('Sort', () => { { property: 'due', reverse: false, - propertyInstance: 0, + propertyInstance: 1, }, { property: 'path', reverse: false, - propertyInstance: 0, + propertyInstance: 1, }, { property: 'status', reverse: false, - propertyInstance: 0, + propertyInstance: 1, }, ], }, @@ -181,12 +181,12 @@ describe('Sort', () => { { property: 'description', reverse: false, - propertyInstance: 0, + propertyInstance: 1, }, { property: 'done', reverse: false, - propertyInstance: 0, + propertyInstance: 1, }, ], }, @@ -220,12 +220,12 @@ describe('Sort', () => { { property: 'description', reverse: true, - propertyInstance: 0, + propertyInstance: 1, }, { property: 'done', reverse: false, - propertyInstance: 0, + propertyInstance: 1, }, ], }, @@ -251,13 +251,13 @@ describe('Sort', () => { { property: 'status', reverse: true, - propertyInstance: 0, + propertyInstance: 1, }, - { property: 'due', reverse: true, propertyInstance: 0 }, + { property: 'due', reverse: true, propertyInstance: 1 }, { property: 'path', reverse: false, - propertyInstance: 0, + propertyInstance: 1, }, ], }, @@ -291,7 +291,7 @@ describe('Sort', () => { { property: 'description', reverse: false, - propertyInstance: 0, + propertyInstance: 1, }, ], }, @@ -321,7 +321,7 @@ describe('Sort by tags', () => { { property: 'tag', reverse: false, - propertyInstance: 0, + propertyInstance: 1, }, ], }, @@ -330,17 +330,12 @@ describe('Sort by tags', () => { ).toEqual(expectedOrder); }); it('should sort correctly by second tag with no global filter', () => { - const t1 = fromLine({ line: '- [ ] a #aaa #jjj' }); - const t2 = fromLine({ line: '- [ ] a #ggg #ccc' }); - const t3 = fromLine({ line: '- [ ] a #bbb #iii' }); const t4 = fromLine({ line: '- [ ] a #fff #aaa' }); - const t5 = fromLine({ line: '- [ ] a #ccc #bbb' }); - const t6 = fromLine({ line: '- [ ] a #eee #fff' }); - const t7 = fromLine({ line: '- [ ] a #ddd #ggg' }); - const t8 = fromLine({ line: '- [ ] a #hhh #eee' }); - const t9 = fromLine({ line: '- [ ] a #iii #ddd' }); - const t10 = fromLine({ line: '- [ ] a #jjj #hhh' }); - const expectedOrder = [t4, t5, t2, t9, t8, t6, t7, t10, t3, t1]; + const t3 = fromLine({ line: '- [ ] a #ccc #bbb' }); + const t2 = fromLine({ line: '- [ ] a #ggg #ccc' }); + const t1 = fromLine({ line: '- [ ] a #iii #ddd' }); + const t5 = fromLine({ line: '- [ ] a #hhh #eee' }); + const expectedOrder = [t4, t3, t2, t1, t5]; expect( Sort.by( { @@ -348,11 +343,11 @@ describe('Sort by tags', () => { { property: 'tag', reverse: false, - propertyInstance: 1, + propertyInstance: 2, }, ], }, - [t1, t2, t3, t4, t5, t6, t7, t8, t9, t10], + [t1, t2, t3, t4, t5], ), ).toEqual(expectedOrder); }); @@ -398,7 +393,7 @@ describe('Sort by tags', () => { { property: 'tag', reverse: false, - propertyInstance: 0, + propertyInstance: 1, }, ], }, @@ -414,34 +409,15 @@ describe('Sort by tags', () => { const originalSettings = getSettings(); updateSettings({ globalFilter: '#task' }); - const t1 = fromLine({ line: '- [ ] #task a #aaa #jjj' }); - const t2 = fromLine({ line: '- [ ] #task a #ggg #ccc' }); - const t3 = fromLine({ line: '- [ ] #task a #bbb #iii' }); const t4 = fromLine({ line: '- [ ] #task a #fff #aaa' }); const t5 = fromLine({ line: '- [ ] #task a #ccc #bbb' }); - const t6 = fromLine({ line: '- [ ] #task a #eee #fff' }); - const t7 = fromLine({ line: '- [ ] #task a #ddd #ggg' }); - const t8 = fromLine({ line: '- [ ] #task a #hhh #eee' }); - const t9 = fromLine({ line: '- [ ] #task a #iii #ddd' }); - const t10 = fromLine({ line: '- [ ] #task a #jjj #hhh' }); - const t11 = fromLine({ line: '- [ ] #task a' }); - const t12 = fromLine({ line: '- [ ] #task a #aaaa #aaaa' }); - const t13 = fromLine({ line: '- [ ] #task a #bbbb ' }); - const expectedOrder = [ - t4, - t12, - t5, - t2, - t9, - t8, - t6, - t7, - t10, - t3, - t1, - t13, - t11, - ]; + const t2 = fromLine({ line: '- [ ] #task a #ggg #ccc' }); + const t3 = fromLine({ line: '- [ ] #task a #bbb #iii' }); + const t1 = fromLine({ line: '- [ ] #task a #aaa #jjj' }); + const t6 = fromLine({ line: '- [ ] #task a' }); + const t7 = fromLine({ line: '- [ ] #task a #aaaa #aaaa' }); + const t8 = fromLine({ line: '- [ ] #task a #bbbb ' }); + const expectedOrder = [t4, t7, t5, t2, t3, t1, t8, t6]; // Act expect( @@ -451,11 +427,11 @@ describe('Sort by tags', () => { { property: 'tag', reverse: false, - propertyInstance: 1, + propertyInstance: 2, }, ], }, - [t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13], + [t1, t2, t3, t4, t5, t6, t7, t8], ), ).toEqual(expectedOrder); diff --git a/tests/Task.test.ts b/tests/Task.test.ts index 94dae11d54..bd50815521 100644 --- a/tests/Task.test.ts +++ b/tests/Task.test.ts @@ -228,6 +228,22 @@ describe('parsing tags', () => { extractedTags: ['#tagone', '#tag/with/depth', '#tagtwo'], globalFilter: '#someglobaltasktag', }, + { + markdownTask: + '- [ ] Export [Cloud Feedly feeds](http://cloud.feedly.com/#opml) #context/pc_clare 🔁 every 4 weeks on Sunday ⏳ 2022-05-15', + expectedDescription: + 'Export [Cloud Feedly feeds](http://cloud.feedly.com/#opml) #context/pc_clare', + extractedTags: ['#context/pc_clare'], + globalFilter: '', + }, + { + markdownTask: + '- [ ] Review [savings accounts and interest rates](https://www.moneysavingexpert.com/tips/2022/04/20/#hiya) #context/pc_clare ⏳ 2022-05-06', + expectedDescription: + 'Review [savings accounts and interest rates](https://www.moneysavingexpert.com/tips/2022/04/20/#hiya) #context/pc_clare', + extractedTags: ['#context/pc_clare'], + globalFilter: '', + }, ])( 'should parse $markdownTask and extract $extractedTags', ({ @@ -273,6 +289,7 @@ describe('to string', () => { }) as Task; // Assert + expect(task).not.toBeNull(); expect(task.toFileLineString()).toStrictEqual(line); }); From f02f2d210be336719a1e6b16d3024124abfdfc52 Mon Sep 17 00:00:00 2001 From: sytone Date: Sun, 15 May 2022 09:24:45 -0700 Subject: [PATCH 11/23] build: add markdown lint to list command --- .markdownlint.jsonc | 5 + .vscode/settings.json | 3 + .vscode/tasks.json | 12 --- CODE_OF_CONDUCT.md | 6 +- README.md | 6 +- docs/README.md | 13 ++- docs/advanced/daily-agenda.md | 2 +- docs/advanced/notifications.md | 2 +- docs/advanced/quickadd.md | 4 +- docs/advanced/styling.md | 5 +- docs/advanced/urgency.md | 2 +- docs/getting-started/dates.md | 10 +- docs/getting-started/global-filter.md | 4 +- docs/getting-started/index.md | 8 +- docs/getting-started/priority.md | 2 +- docs/getting-started/recurring-tasks.md | 34 +++---- docs/index.md | 1 + docs/queries/index.md | 1 + docs/queries/layout.md | 1 + docs/queries/sorting.md | 12 +-- package.json | 3 +- yarn.lock | 120 +++++++++++++++++++++++- 22 files changed, 189 insertions(+), 67 deletions(-) create mode 100644 .markdownlint.jsonc create mode 100644 .vscode/settings.json delete mode 100644 .vscode/tasks.json diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc new file mode 100644 index 0000000000..251c68aef4 --- /dev/null +++ b/.markdownlint.jsonc @@ -0,0 +1,5 @@ +{ + "MD013": false, // Ignore the line length rule. + "MD025": false, // Allow the title in the front matter and markdown heading one to match. + "MD033": false // Allow Inline HTML. +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..5623ddbe2e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "npm.packageManager": "yarn" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 21ca535d72..0000000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "type": "npm", - "script": "dev", - "problemMatcher": [], - "label": "npm: dev", - "detail": "node esbuild.config.mjs" - } - ] -} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 6215788dcd..debcd1d9d9 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -116,7 +116,7 @@ the community. This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at -https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. +. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). @@ -124,5 +124,5 @@ enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. +. Translations are available at +. diff --git a/README.md b/README.md index d0d4733ed8..e5b31af2a7 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,14 @@ _You can toggle the task status in any view or query and it will update the sour --- -For changes in each release, please check the releases page: https://github.com/schemar/obsidian-tasks/releases +For changes in each release, please check the releases page: --- ## Screenshots -- _All screenshots assume the [global filter](#filtering-checklist-items) `#task` which is not set by default (see also [installation](#installation))._ -- _The theme is [Obsidian Atom](https://github.com/kognise/obsidian-atom)._ +- _All screenshots assume the [global filter](#filtering-checklist-items) `#task` which is not set by default (see also [installation](#installation))._ +- _The theme is [Obsidian Atom](https://github.com/kognise/obsidian-atom)._ ![ACME Tasks](https://github.com/schemar/obsidian-tasks/raw/main/resources/screenshots/acme.png) The `ACME` note has some tasks. diff --git a/docs/README.md b/docs/README.md index fa3bcfad95..13f58ac726 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,11 +2,11 @@ ## Overview -- For background, including which branch to work on, see ["Updating documentation" in CONTRIBUTING](../CONTRIBUTING.md#updating-documentation) -- The documentation is written in Markdown -- It is converted to HTML via Ruby and Jekyll - - Important: Ruby 2 is required, for example, Ruby 2.7 -- The published documentation is at +- For background, including which branch to work on, see ["Updating documentation" in CONTRIBUTING](../CONTRIBUTING.md#updating-documentation) +- The documentation is written in Markdown +- It is converted to HTML via Ruby and Jekyll + - Important: Ruby 2 is required, for example, Ruby 2.7 +- The published documentation is at ## Test documentation locally with Jekyll @@ -27,6 +27,7 @@ there is a fast feedback cycle of: 1. Edit a markdown page 1. Wait a few seconds until you see console output like this: + ```text web_1 | Regenerating: 1 file(s) changed at 2022-05-07 08:03:54 web_1 | README.md @@ -34,6 +35,7 @@ there is a fast feedback cycle of: web_1 | Jekyll Feed: Generating feed for posts web_1 | ...done in 4.02288725 seconds. ``` + 1. Reload the page in your browser to see the changes ## Option 1: Running inside a Docker container @@ -84,6 +86,7 @@ You can stop the service by hitting `Ctrl+c`. 1. Install ruby 2.x. - It is important that you use a version 2 of ruby, not version 3, for example 2.7.0. 1. Run: + ```bash cd obsidian-tasks/ gem install bundler diff --git a/docs/advanced/daily-agenda.md b/docs/advanced/daily-agenda.md index 6acf5429f4..50f5394dbd 100644 --- a/docs/advanced/daily-agenda.md +++ b/docs/advanced/daily-agenda.md @@ -24,7 +24,7 @@ make sure new notes are created via one of these two plugins, and not `Daily-not | template syntax | `due on {% raw %}{{date+14d:YYYY-MM-DD}}{% endraw %}` | `due on {% raw %}{{date+14d:YYYY-MM-DD}}{% endraw %}` | `due on {% raw %}{{date+14d:YYYY-MM-DD}}{% endraw %}` | | output | `due on {% raw %}{{date+14d:YYYY-MM-DD}}{% endraw %}` | `due on 2021-08-28` | `due on 2021-08-28` | -### Example Daily Agenda **template**: +## Example Daily Agenda **template** ## Tasks ### Overdue diff --git a/docs/advanced/notifications.md b/docs/advanced/notifications.md index ba90cd3f58..d9d0d356a7 100644 --- a/docs/advanced/notifications.md +++ b/docs/advanced/notifications.md @@ -20,7 +20,7 @@ _Note that this is a screenshot of the reminder plugin's settings and not Tasks. The order is important when writing the task. You have to put the reminder date between the task name and the other fields : -``` +```markdown - [ ] #task task name ⏰ YYYY-MM-DD HH:mm ⏫ 🔁 every *** 🛫 YYYY-MM-DD ⏳ YYYY-MM-DD 📅 YYYY-MM-DD ``` diff --git a/docs/advanced/quickadd.md b/docs/advanced/quickadd.md index 4f58950e6e..6297300337 100644 --- a/docs/advanced/quickadd.md +++ b/docs/advanced/quickadd.md @@ -13,7 +13,7 @@ Additional to the official command to create a task, you can use a quickadd comm For example: -``` +```markdown {% raw %}#task {{VALUE:task name}} ⏰ {{VDATE:reminder date and time,YYYY-MM-DD HH:mm}} {{VALUE:⏫,🔼,🔽, }} 🔁 {{VALUE:recurrence}} 🛫 {{VDATE:start date,YYYY-MM-DD}} ⏳ {{VDATE:scheduled date,YYYY-MM-DD}} 📅 {{VDATE:due date,YYYY-MM-DD}}{% endraw %} ``` @@ -62,6 +62,6 @@ Then you can give them the same name, this way you'll write the date only once a Here is the format for the current example: -``` +```markdown {% raw %}#task {{VALUE:task name}} ⏰ {{VDATE:same date,YYYY-MM-DD}} {{VDATE:time,HH:mm}} 📅 {{VDATE:same date,YYYY-MM-DD}}{% endraw %} ``` diff --git a/docs/advanced/styling.md b/docs/advanced/styling.md index ef49fe778c..a0ec3879dd 100644 --- a/docs/advanced/styling.md +++ b/docs/advanced/styling.md @@ -8,8 +8,8 @@ has_toc: false # Styling Tasks -Each task entry has CSS styles that allow you to change the look and feel of how the tasks are displayed. The -following styles are available. +Each task entry has CSS styles that allow you to change the look and feel of how the tasks are displayed. The +following styles are available. | Class | Usage | | ------------------------ | -------------------------------------------------------------------------------------------------------------- | @@ -18,4 +18,3 @@ following styles are available. | tasks-backlink | This is applied to the SPAN that wraps the backlink if displayed on the task. | | tasks-edit | This is applied to the SPAN that wraps the edit button/icon shown next to the task that opens the task edit UI.| | task-list-item-checkbox | This is applied to the INPUT element for the task. | - diff --git a/docs/advanced/urgency.md b/docs/advanced/urgency.md index e2541350b7..9fb22cbdc6 100644 --- a/docs/advanced/urgency.md +++ b/docs/advanced/urgency.md @@ -110,7 +110,7 @@ The scores are as follows: ## Examples -``` +```markdown A task that is due today, has a "medium" priority, is not scheduled, and has no start date: urgency = 9.0 + 3.9 + 0.0 + 0.0 = 12.9 diff --git a/docs/getting-started/dates.md b/docs/getting-started/dates.md index 3f0649f0c9..ea340ec6ac 100644 --- a/docs/getting-started/dates.md +++ b/docs/getting-started/dates.md @@ -6,6 +6,7 @@ parent: Getting Started --- # Dates + {: .no_toc }
@@ -30,7 +31,6 @@ When you use the command, you can also set dates like "Monday", "tomorrow", or " --- - ## 📅 Due Tasks can have dates by when they must be done: due dates. @@ -75,6 +75,8 @@ This way, you can use the start date as a filter to filter out any tasks that yo Such filter could be: - ```tasks - starts before tomorrow - ``` +````markdown +```tasks +starts before tomorrow +``` +```` diff --git a/docs/getting-started/global-filter.md b/docs/getting-started/global-filter.md index f595fc8a5e..93e0a181cf 100644 --- a/docs/getting-started/global-filter.md +++ b/docs/getting-started/global-filter.md @@ -15,13 +15,13 @@ Leave it empty to regard all checklist items as tasks. Example with global filter `#task`: -``` +```markdown - [ ] #task take out the trash ``` If you don't have a global filter set, all regular checklist items will be considered a task: -``` +```markdown - [ ] take out the trash ``` diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 4d7c7d061f..728e6b5a76 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -14,9 +14,11 @@ Now Tasks tracks that you need to take out the trash! To list all open tasks in a markdown file, simply add a [query]({{ site.baseurl }}{% link queries/index.md %}) as a tasks code block like so: - ```tasks - not done - ``` +````markdown +```tasks +not done +``` +```` Now you have a list of all open tasks! This is enough to get started with tasks. You can _optionally_ start using one or more of the other features that Tasks offers. diff --git a/docs/getting-started/priority.md b/docs/getting-started/priority.md index d58eafa088..d70d4fb970 100644 --- a/docs/getting-started/priority.md +++ b/docs/getting-started/priority.md @@ -19,7 +19,7 @@ If a task has no priority at all, it is considered between low and medium priori This means that the priority of 🔽 low tasks is considered lower than the priority of tasks without any specific priority. The idea is that you can easily filter out unimportant tasks without needing to assign a priority to all relevant tasks. -``` +```markdown - [ ] take out the trash 🔼 ``` diff --git a/docs/getting-started/recurring-tasks.md b/docs/getting-started/recurring-tasks.md index 460fc8fd3f..c728833db0 100644 --- a/docs/getting-started/recurring-tasks.md +++ b/docs/getting-started/recurring-tasks.md @@ -34,13 +34,13 @@ The new task will have updated dates based off the original task. Take as an example the following task: -``` +```markdown - [ ] take out the trash 🔁 every Sunday 📅 2021-04-25 ``` If you mark the above task "done", the file will now look like this: -``` +```markdown - [ ] take out the trash 🔁 every Sunday 📅 2021-05-02 - [x] take out the trash 🔁 every Sunday 📅 2021-04-25 ✅ 2021-04-24 ``` @@ -67,7 +67,7 @@ The default behavior results in newly created tasks having dates relative to the For example, given that today is the 13. February 2022 and you just completed the lower task: -``` +```markdown - [ ] sweep the floors 🔁 every week ⏳ 2021-02-13 - [x] sweep the floors 🔁 every week ⏳ 2021-02-06 ✅ 2022-02-13 ``` @@ -76,10 +76,10 @@ Since you missed the original scheduled date, the newly created task is scheduled one week after the original scheduled date: the same day you completed the original task. If you want to have tasks be scheduled relative to the "done" date rather than the original dates, -then you will need to add ` when done` to the end of the recurrence rule. +then you will need to add `when done` to the end of the recurrence rule. Below is the same example as above, but this time the new task is scheduled based on the current date when you completed the task: -``` +```markdown - [ ] sweep the floors 🔁 every week when done ⏳ 2022-02-20 - [x] sweep the floors 🔁 every week when done ⏳ 2021-02-06 ✅ 2022-02-13 ``` @@ -127,15 +127,15 @@ The new task will have the due date advanced by two weeks and a scheduled date t Examples of possible recurrence rules (mix and match as desired; these should be considered inspirational): -- `🔁 every 3 days` -- `🔁 every 10 days when done` -- `🔁 every weekday` (meaning every Mon - Fri) -- `🔁 every week on Sunday` -- `🔁 every 2 weeks` -- `🔁 every 3 weeks on Friday` -- `🔁 every 2 months` -- `🔁 every month on the 1st` -- `🔁 every 6 months on the 2nd Wednesday` -- `🔁 every January on the 15th` -- `🔁 every April and December on the 1st and 24th` (meaning every _April 1st_ and _December 24th_) -- `🔁 every year` +- `🔁 every 3 days` +- `🔁 every 10 days when done` +- `🔁 every weekday` (meaning every Mon - Fri) +- `🔁 every week on Sunday` +- `🔁 every 2 weeks` +- `🔁 every 3 weeks on Friday` +- `🔁 every 2 months` +- `🔁 every month on the 1st` +- `🔁 every 6 months on the 2nd Wednesday` +- `🔁 every January on the 15th` +- `🔁 every April and December on the 1st and 24th` (meaning every _April 1st_ and _December 24th_) +- `🔁 every year` diff --git a/docs/index.md b/docs/index.md index 803d3e6458..521021d7fd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,6 +5,7 @@ nav_order: 1 --- # Introduction + {: .no_toc }
diff --git a/docs/queries/index.md b/docs/queries/index.md index f71ce2ab85..42bbba95ff 100644 --- a/docs/queries/index.md +++ b/docs/queries/index.md @@ -6,6 +6,7 @@ has_children: true --- # Queries + {: .no_toc } You can list tasks from your entire vault by querying them using a `tasks` code block. You can edit the tasks from the query results by clicking on the little pencil icon next to them. diff --git a/docs/queries/layout.md b/docs/queries/layout.md index d3bb5cfcd8..84bff60d09 100644 --- a/docs/queries/layout.md +++ b/docs/queries/layout.md @@ -6,6 +6,7 @@ parent: Queries --- # Layout options + {: .no_toc }
diff --git a/docs/queries/sorting.md b/docs/queries/sorting.md index 9b0ec07549..ae14f5b4de 100644 --- a/docs/queries/sorting.md +++ b/docs/queries/sorting.md @@ -32,12 +32,12 @@ You can sort tasks by the following properties: 2. `status` (done or todo) 3. `priority` (priority of the task; "low" is below "none") 4. `start` (the date when the task starts) -2. `scheduled` (the date when the task is scheduled) -2. `due` (the date when the task is due) -5. `done` (the date when the task was done) -6. `path` (the path to the file that contains the task) -7. `description` (the description of the task) -8. `tag` (the description of the task) +5. `scheduled` (the date when the task is scheduled) +6. `due` (the date when the task is due) +7. `done` (the date when the task was done) +8. `path` (the path to the file that contains the task) +9. `description` (the description of the task) +10. `tag` (the description of the task) You can add multiple `sort by` query options, each on an extra line. The first sort has the highest priority. diff --git a/package.json b/package.json index 0264022256..2ae4e2e149 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "node esbuild.config.mjs", "build": "node esbuild.config.mjs production", - "lint": "eslint ./src --fix && eslint ./tests --fix && tsc --noEmit --pretty && svelte-check", + "lint": "eslint ./src --fix && eslint ./tests --fix && tsc --noEmit --pretty && svelte-check && markdownlint-cli2-fix \"**/*.md\" \"#node_modules\"", "test": "jest --ci", "test:dev": "jest --watch" }, @@ -36,6 +36,7 @@ "eslint-plugin-import": "^2.22.1", "eslint-plugin-prettier": "^3.3.1", "jest": "^27.3.1", + "markdownlint-cli2": "^0.4.0", "moment": "^2.29.1", "obsidian": "0.13.21", "prettier": "^2.2.1", diff --git a/yarn.lock b/yarn.lock index 0cd1e0bcb4..72c86ba311 100644 --- a/yarn.lock +++ b/yarn.lock @@ -942,6 +942,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + array-includes@^3.1.4: version "3.1.4" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.4.tgz#f5b493162c760f3539631f005ba2bb46acb45ba9" @@ -958,6 +963,11 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +array-union@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-3.0.1.tgz#da52630d327f8b88cfbfb57728e2af5cd9b6b975" + integrity sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw== + array.prototype.flat@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz#07e0975d84bbc7c48cd1879d609e682598d33e13" @@ -1429,6 +1439,11 @@ enquirer@^2.3.5: dependencies: ansi-colors "^4.1.1" +entities@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" + integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== + es-abstract@^1.19.0, es-abstract@^1.19.1: version "1.19.1" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" @@ -1843,6 +1858,17 @@ fast-glob@^3.1.1: merge2 "^1.3.0" micromatch "^4.0.4" +fast-glob@^3.2.7: + version "3.2.11" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" + integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -2006,6 +2032,18 @@ globals@^13.6.0, globals@^13.9.0: dependencies: type-fest "^0.20.2" +globby@12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-12.1.0.tgz#471757d6d9d25651b655b1da3eae1e25209f86a5" + integrity sha512-YULDaNwsoUZkRy9TWSY/M7Obh0abamTKoKzTfOI3uU+hfpX2FZqOq8LFDxsjYheF1RH7ITdArgbQnsNBFgcdBA== + dependencies: + array-union "^3.0.1" + dir-glob "^3.0.1" + fast-glob "^3.2.7" + ignore "^5.1.9" + merge2 "^1.4.1" + slash "^4.0.0" + globby@^11.0.4: version "11.0.4" resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" @@ -2108,6 +2146,11 @@ ignore@^5.1.4, ignore@^5.1.8: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== +ignore@^5.1.9: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" + integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -2868,6 +2911,13 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +linkify-it@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.3.tgz#a98baf44ce45a550efb4d49c769d07524cc2fa2e" + integrity sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ== + dependencies: + uc.micro "^1.0.1" + locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -2946,17 +2996,63 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +markdown-it@12.3.2: + version "12.3.2" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90" + integrity sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg== + dependencies: + argparse "^2.0.1" + entities "~2.1.0" + linkify-it "^3.0.1" + mdurl "^1.0.1" + uc.micro "^1.0.5" + +markdownlint-cli2-formatter-default@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/markdownlint-cli2-formatter-default/-/markdownlint-cli2-formatter-default-0.0.3.tgz#5aecd6e576ad18801b76e58bbbaf0e916c583ab8" + integrity sha512-QEAJitT5eqX1SNboOD+SO/LNBpu4P4je8JlR02ug2cLQAqmIhh8IJnSK7AcaHBHhNADqdGydnPpQOpsNcEEqCw== + +markdownlint-cli2@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/markdownlint-cli2/-/markdownlint-cli2-0.4.0.tgz#9e29150278fd3f1fe31a05fc46bd95d2370e8758" + integrity sha512-EcwP5tAbyzzL3ACI0L16LqbNctmh8wNX56T+aVvIxWyTAkwbYNx2V7IheRkXS3mE7R/pnaApZ/RSXcXuzRVPjg== + dependencies: + globby "12.1.0" + markdownlint "0.25.1" + markdownlint-cli2-formatter-default "0.0.3" + markdownlint-rule-helpers "0.16.0" + micromatch "4.0.4" + strip-json-comments "4.0.0" + yaml "1.10.2" + +markdownlint-rule-helpers@0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/markdownlint-rule-helpers/-/markdownlint-rule-helpers-0.16.0.tgz#c327f72782bd2b9475127a240508231f0413a25e" + integrity sha512-oEacRUVeTJ5D5hW1UYd2qExYI0oELdYK72k1TKGvIeYJIbqQWAz476NAc7LNixSySUhcNl++d02DvX0ccDk9/w== + +markdownlint@0.25.1: + version "0.25.1" + resolved "https://registry.yarnpkg.com/markdownlint/-/markdownlint-0.25.1.tgz#df04536607ebeeda5ccd5e4f38138823ed623788" + integrity sha512-AG7UkLzNa1fxiOv5B+owPsPhtM4D6DoODhsJgiaNg1xowXovrYgOnLqAgOOFQpWOlHFVQUzjMY5ypNNTeov92g== + dependencies: + markdown-it "12.3.2" + +mdurl@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -merge2@^1.3.0: +merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -micromatch@^4.0.4: +micromatch@4.0.4, micromatch@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== @@ -3499,6 +3595,11 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slash@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" + integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== + slice-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" @@ -3620,6 +3721,11 @@ strip-indent@^3.0.0: dependencies: min-indent "^1.0.0" +strip-json-comments@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-4.0.0.tgz#6fd3a79f1b956905483769b0bf66598b8f87da50" + integrity sha512-LzWcbfMbAsEDTRmhjWIioe8GcDRl0fa35YMXFoJKDdiD/quGFmjJjdgPjFJJNwCMaLyQqFIDqCdHD2V4HfLgYA== + strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" @@ -3872,6 +3978,11 @@ typescript@^4.4.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8" integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg== +uc.micro@^1.0.1, uc.micro@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" + integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== + unbox-primitive@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" @@ -4037,6 +4148,11 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml@1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + yargs-parser@20.x, yargs-parser@^20.2.2: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" From 67e97c6df51cb0df15bc0e4091ed3eb7643cffb9 Mon Sep 17 00:00:00 2001 From: sytone Date: Sun, 15 May 2022 12:12:57 -0700 Subject: [PATCH 12/23] fix: resolve remaining comments and revert markdown lint changes --- .markdownlint.jsonc | 5 - CODE_OF_CONDUCT.md | 6 +- README.md | 6 +- docs/README.md | 13 +- docs/advanced/daily-agenda.md | 2 +- docs/advanced/notifications.md | 2 +- docs/advanced/quickadd.md | 4 +- docs/advanced/styling.md | 5 +- docs/advanced/urgency.md | 2 +- docs/getting-started/dates.md | 10 +- docs/getting-started/global-filter.md | 4 +- docs/getting-started/index.md | 8 +- docs/getting-started/priority.md | 2 +- docs/getting-started/recurring-tasks.md | 34 +- docs/index.md | 1 - docs/queries/index.md | 1 - docs/queries/layout.md | 1 - docs/queries/sorting.md | 12 +- package.json | 3 +- src/Sort.ts | 14 +- tests/Query.test.ts | 436 +++++++----------------- tests/Sort.test.ts | 245 ++++++++++--- yarn.lock | 120 +------ 23 files changed, 384 insertions(+), 552 deletions(-) delete mode 100644 .markdownlint.jsonc diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc deleted file mode 100644 index 251c68aef4..0000000000 --- a/.markdownlint.jsonc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "MD013": false, // Ignore the line length rule. - "MD025": false, // Allow the title in the front matter and markdown heading one to match. - "MD033": false // Allow Inline HTML. -} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index debcd1d9d9..6215788dcd 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -116,7 +116,7 @@ the community. This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at -. +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). @@ -124,5 +124,5 @@ enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at -. Translations are available at -. +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/README.md b/README.md index e5b31af2a7..d0d4733ed8 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,14 @@ _You can toggle the task status in any view or query and it will update the sour --- -For changes in each release, please check the releases page: +For changes in each release, please check the releases page: https://github.com/schemar/obsidian-tasks/releases --- ## Screenshots -- _All screenshots assume the [global filter](#filtering-checklist-items) `#task` which is not set by default (see also [installation](#installation))._ -- _The theme is [Obsidian Atom](https://github.com/kognise/obsidian-atom)._ +- _All screenshots assume the [global filter](#filtering-checklist-items) `#task` which is not set by default (see also [installation](#installation))._ +- _The theme is [Obsidian Atom](https://github.com/kognise/obsidian-atom)._ ![ACME Tasks](https://github.com/schemar/obsidian-tasks/raw/main/resources/screenshots/acme.png) The `ACME` note has some tasks. diff --git a/docs/README.md b/docs/README.md index 13f58ac726..fa3bcfad95 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,11 +2,11 @@ ## Overview -- For background, including which branch to work on, see ["Updating documentation" in CONTRIBUTING](../CONTRIBUTING.md#updating-documentation) -- The documentation is written in Markdown -- It is converted to HTML via Ruby and Jekyll - - Important: Ruby 2 is required, for example, Ruby 2.7 -- The published documentation is at +- For background, including which branch to work on, see ["Updating documentation" in CONTRIBUTING](../CONTRIBUTING.md#updating-documentation) +- The documentation is written in Markdown +- It is converted to HTML via Ruby and Jekyll + - Important: Ruby 2 is required, for example, Ruby 2.7 +- The published documentation is at ## Test documentation locally with Jekyll @@ -27,7 +27,6 @@ there is a fast feedback cycle of: 1. Edit a markdown page 1. Wait a few seconds until you see console output like this: - ```text web_1 | Regenerating: 1 file(s) changed at 2022-05-07 08:03:54 web_1 | README.md @@ -35,7 +34,6 @@ there is a fast feedback cycle of: web_1 | Jekyll Feed: Generating feed for posts web_1 | ...done in 4.02288725 seconds. ``` - 1. Reload the page in your browser to see the changes ## Option 1: Running inside a Docker container @@ -86,7 +84,6 @@ You can stop the service by hitting `Ctrl+c`. 1. Install ruby 2.x. - It is important that you use a version 2 of ruby, not version 3, for example 2.7.0. 1. Run: - ```bash cd obsidian-tasks/ gem install bundler diff --git a/docs/advanced/daily-agenda.md b/docs/advanced/daily-agenda.md index 50f5394dbd..6acf5429f4 100644 --- a/docs/advanced/daily-agenda.md +++ b/docs/advanced/daily-agenda.md @@ -24,7 +24,7 @@ make sure new notes are created via one of these two plugins, and not `Daily-not | template syntax | `due on {% raw %}{{date+14d:YYYY-MM-DD}}{% endraw %}` | `due on {% raw %}{{date+14d:YYYY-MM-DD}}{% endraw %}` | `due on {% raw %}{{date+14d:YYYY-MM-DD}}{% endraw %}` | | output | `due on {% raw %}{{date+14d:YYYY-MM-DD}}{% endraw %}` | `due on 2021-08-28` | `due on 2021-08-28` | -## Example Daily Agenda **template** +### Example Daily Agenda **template**: ## Tasks ### Overdue diff --git a/docs/advanced/notifications.md b/docs/advanced/notifications.md index d9d0d356a7..ba90cd3f58 100644 --- a/docs/advanced/notifications.md +++ b/docs/advanced/notifications.md @@ -20,7 +20,7 @@ _Note that this is a screenshot of the reminder plugin's settings and not Tasks. The order is important when writing the task. You have to put the reminder date between the task name and the other fields : -```markdown +``` - [ ] #task task name ⏰ YYYY-MM-DD HH:mm ⏫ 🔁 every *** 🛫 YYYY-MM-DD ⏳ YYYY-MM-DD 📅 YYYY-MM-DD ``` diff --git a/docs/advanced/quickadd.md b/docs/advanced/quickadd.md index 6297300337..4f58950e6e 100644 --- a/docs/advanced/quickadd.md +++ b/docs/advanced/quickadd.md @@ -13,7 +13,7 @@ Additional to the official command to create a task, you can use a quickadd comm For example: -```markdown +``` {% raw %}#task {{VALUE:task name}} ⏰ {{VDATE:reminder date and time,YYYY-MM-DD HH:mm}} {{VALUE:⏫,🔼,🔽, }} 🔁 {{VALUE:recurrence}} 🛫 {{VDATE:start date,YYYY-MM-DD}} ⏳ {{VDATE:scheduled date,YYYY-MM-DD}} 📅 {{VDATE:due date,YYYY-MM-DD}}{% endraw %} ``` @@ -62,6 +62,6 @@ Then you can give them the same name, this way you'll write the date only once a Here is the format for the current example: -```markdown +``` {% raw %}#task {{VALUE:task name}} ⏰ {{VDATE:same date,YYYY-MM-DD}} {{VDATE:time,HH:mm}} 📅 {{VDATE:same date,YYYY-MM-DD}}{% endraw %} ``` diff --git a/docs/advanced/styling.md b/docs/advanced/styling.md index a0ec3879dd..ef49fe778c 100644 --- a/docs/advanced/styling.md +++ b/docs/advanced/styling.md @@ -8,8 +8,8 @@ has_toc: false # Styling Tasks -Each task entry has CSS styles that allow you to change the look and feel of how the tasks are displayed. The -following styles are available. +Each task entry has CSS styles that allow you to change the look and feel of how the tasks are displayed. The +following styles are available. | Class | Usage | | ------------------------ | -------------------------------------------------------------------------------------------------------------- | @@ -18,3 +18,4 @@ following styles are available. | tasks-backlink | This is applied to the SPAN that wraps the backlink if displayed on the task. | | tasks-edit | This is applied to the SPAN that wraps the edit button/icon shown next to the task that opens the task edit UI.| | task-list-item-checkbox | This is applied to the INPUT element for the task. | + diff --git a/docs/advanced/urgency.md b/docs/advanced/urgency.md index 9fb22cbdc6..e2541350b7 100644 --- a/docs/advanced/urgency.md +++ b/docs/advanced/urgency.md @@ -110,7 +110,7 @@ The scores are as follows: ## Examples -```markdown +``` A task that is due today, has a "medium" priority, is not scheduled, and has no start date: urgency = 9.0 + 3.9 + 0.0 + 0.0 = 12.9 diff --git a/docs/getting-started/dates.md b/docs/getting-started/dates.md index ea340ec6ac..3f0649f0c9 100644 --- a/docs/getting-started/dates.md +++ b/docs/getting-started/dates.md @@ -6,7 +6,6 @@ parent: Getting Started --- # Dates - {: .no_toc }
@@ -31,6 +30,7 @@ When you use the command, you can also set dates like "Monday", "tomorrow", or " --- + ## 📅 Due Tasks can have dates by when they must be done: due dates. @@ -75,8 +75,6 @@ This way, you can use the start date as a filter to filter out any tasks that yo Such filter could be: -````markdown -```tasks -starts before tomorrow -``` -```` + ```tasks + starts before tomorrow + ``` diff --git a/docs/getting-started/global-filter.md b/docs/getting-started/global-filter.md index 93e0a181cf..f595fc8a5e 100644 --- a/docs/getting-started/global-filter.md +++ b/docs/getting-started/global-filter.md @@ -15,13 +15,13 @@ Leave it empty to regard all checklist items as tasks. Example with global filter `#task`: -```markdown +``` - [ ] #task take out the trash ``` If you don't have a global filter set, all regular checklist items will be considered a task: -```markdown +``` - [ ] take out the trash ``` diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 728e6b5a76..4d7c7d061f 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -14,11 +14,9 @@ Now Tasks tracks that you need to take out the trash! To list all open tasks in a markdown file, simply add a [query]({{ site.baseurl }}{% link queries/index.md %}) as a tasks code block like so: -````markdown -```tasks -not done -``` -```` + ```tasks + not done + ``` Now you have a list of all open tasks! This is enough to get started with tasks. You can _optionally_ start using one or more of the other features that Tasks offers. diff --git a/docs/getting-started/priority.md b/docs/getting-started/priority.md index d70d4fb970..d58eafa088 100644 --- a/docs/getting-started/priority.md +++ b/docs/getting-started/priority.md @@ -19,7 +19,7 @@ If a task has no priority at all, it is considered between low and medium priori This means that the priority of 🔽 low tasks is considered lower than the priority of tasks without any specific priority. The idea is that you can easily filter out unimportant tasks without needing to assign a priority to all relevant tasks. -```markdown +``` - [ ] take out the trash 🔼 ``` diff --git a/docs/getting-started/recurring-tasks.md b/docs/getting-started/recurring-tasks.md index c728833db0..460fc8fd3f 100644 --- a/docs/getting-started/recurring-tasks.md +++ b/docs/getting-started/recurring-tasks.md @@ -34,13 +34,13 @@ The new task will have updated dates based off the original task. Take as an example the following task: -```markdown +``` - [ ] take out the trash 🔁 every Sunday 📅 2021-04-25 ``` If you mark the above task "done", the file will now look like this: -```markdown +``` - [ ] take out the trash 🔁 every Sunday 📅 2021-05-02 - [x] take out the trash 🔁 every Sunday 📅 2021-04-25 ✅ 2021-04-24 ``` @@ -67,7 +67,7 @@ The default behavior results in newly created tasks having dates relative to the For example, given that today is the 13. February 2022 and you just completed the lower task: -```markdown +``` - [ ] sweep the floors 🔁 every week ⏳ 2021-02-13 - [x] sweep the floors 🔁 every week ⏳ 2021-02-06 ✅ 2022-02-13 ``` @@ -76,10 +76,10 @@ Since you missed the original scheduled date, the newly created task is scheduled one week after the original scheduled date: the same day you completed the original task. If you want to have tasks be scheduled relative to the "done" date rather than the original dates, -then you will need to add `when done` to the end of the recurrence rule. +then you will need to add ` when done` to the end of the recurrence rule. Below is the same example as above, but this time the new task is scheduled based on the current date when you completed the task: -```markdown +``` - [ ] sweep the floors 🔁 every week when done ⏳ 2022-02-20 - [x] sweep the floors 🔁 every week when done ⏳ 2021-02-06 ✅ 2022-02-13 ``` @@ -127,15 +127,15 @@ The new task will have the due date advanced by two weeks and a scheduled date t Examples of possible recurrence rules (mix and match as desired; these should be considered inspirational): -- `🔁 every 3 days` -- `🔁 every 10 days when done` -- `🔁 every weekday` (meaning every Mon - Fri) -- `🔁 every week on Sunday` -- `🔁 every 2 weeks` -- `🔁 every 3 weeks on Friday` -- `🔁 every 2 months` -- `🔁 every month on the 1st` -- `🔁 every 6 months on the 2nd Wednesday` -- `🔁 every January on the 15th` -- `🔁 every April and December on the 1st and 24th` (meaning every _April 1st_ and _December 24th_) -- `🔁 every year` +- `🔁 every 3 days` +- `🔁 every 10 days when done` +- `🔁 every weekday` (meaning every Mon - Fri) +- `🔁 every week on Sunday` +- `🔁 every 2 weeks` +- `🔁 every 3 weeks on Friday` +- `🔁 every 2 months` +- `🔁 every month on the 1st` +- `🔁 every 6 months on the 2nd Wednesday` +- `🔁 every January on the 15th` +- `🔁 every April and December on the 1st and 24th` (meaning every _April 1st_ and _December 24th_) +- `🔁 every year` diff --git a/docs/index.md b/docs/index.md index 521021d7fd..803d3e6458 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,7 +5,6 @@ nav_order: 1 --- # Introduction - {: .no_toc }
diff --git a/docs/queries/index.md b/docs/queries/index.md index 42bbba95ff..f71ce2ab85 100644 --- a/docs/queries/index.md +++ b/docs/queries/index.md @@ -6,7 +6,6 @@ has_children: true --- # Queries - {: .no_toc } You can list tasks from your entire vault by querying them using a `tasks` code block. You can edit the tasks from the query results by clicking on the little pencil icon next to them. diff --git a/docs/queries/layout.md b/docs/queries/layout.md index 84bff60d09..d3bb5cfcd8 100644 --- a/docs/queries/layout.md +++ b/docs/queries/layout.md @@ -6,7 +6,6 @@ parent: Queries --- # Layout options - {: .no_toc }
diff --git a/docs/queries/sorting.md b/docs/queries/sorting.md index ae14f5b4de..9b0ec07549 100644 --- a/docs/queries/sorting.md +++ b/docs/queries/sorting.md @@ -32,12 +32,12 @@ You can sort tasks by the following properties: 2. `status` (done or todo) 3. `priority` (priority of the task; "low" is below "none") 4. `start` (the date when the task starts) -5. `scheduled` (the date when the task is scheduled) -6. `due` (the date when the task is due) -7. `done` (the date when the task was done) -8. `path` (the path to the file that contains the task) -9. `description` (the description of the task) -10. `tag` (the description of the task) +2. `scheduled` (the date when the task is scheduled) +2. `due` (the date when the task is due) +5. `done` (the date when the task was done) +6. `path` (the path to the file that contains the task) +7. `description` (the description of the task) +8. `tag` (the description of the task) You can add multiple `sort by` query options, each on an extra line. The first sort has the highest priority. diff --git a/package.json b/package.json index 2ae4e2e149..0264022256 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "node esbuild.config.mjs", "build": "node esbuild.config.mjs production", - "lint": "eslint ./src --fix && eslint ./tests --fix && tsc --noEmit --pretty && svelte-check && markdownlint-cli2-fix \"**/*.md\" \"#node_modules\"", + "lint": "eslint ./src --fix && eslint ./tests --fix && tsc --noEmit --pretty && svelte-check", "test": "jest --ci", "test:dev": "jest --watch" }, @@ -36,7 +36,6 @@ "eslint-plugin-import": "^2.22.1", "eslint-plugin-prettier": "^3.3.1", "jest": "^27.3.1", - "markdownlint-cli2": "^0.4.0", "moment": "^2.29.1", "obsidian": "0.13.21", "prettier": "^2.2.1", diff --git a/src/Sort.ts b/src/Sort.ts index d454899f87..42cc7fcba2 100644 --- a/src/Sort.ts +++ b/src/Sort.ts @@ -119,10 +119,18 @@ export class Sort { // Arrays start at 0 but the users specify a tag starting at 1. const tagInstanceToSortBy = Sort.tagPropertyInstance - 1; - // If the tag collection is smaller than the instance - // used to compare then they are just equal. if ( - a.tags.length < Sort.tagPropertyInstance || + a.tags.length < Sort.tagPropertyInstance && + b.tags.length >= Sort.tagPropertyInstance + ) { + return 1; + } else if ( + b.tags.length < Sort.tagPropertyInstance && + a.tags.length >= Sort.tagPropertyInstance + ) { + return -1; + } else if ( + a.tags.length < Sort.tagPropertyInstance && b.tags.length < Sort.tagPropertyInstance ) { return 0; diff --git a/tests/Query.test.ts b/tests/Query.test.ts index 1253bfed28..dd39475fe8 100644 --- a/tests/Query.test.ts +++ b/tests/Query.test.ts @@ -276,19 +276,25 @@ describe('Query', () => { ); }); - describe('filtering with "tag" and global task filter', () => { - test.concurrent.each<[string, FilteringCase]>([ + const defaultTasksWithTags = [ + '- [ ] #task something to do #later #work', + '- [ ] #task something to do #later #work/meeting', + '- [ ] #task something to do #later #home', + '- [ ] #task something to do #later #home/kitchen', + '- [ ] #task get the milk', + '- [ ] #task something to do #later #work #TopLevelItem/sub', + ]; + + describe('filtering with "tags"', () => { + const TagFilteringCases: Array<[string, FilteringCase]> = [ [ 'by tag presence', { filters: ['tags include #home'], - tasks: [ - '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], + tasks: defaultTasksWithTags, expectedResult: [ - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #home', + '- [ ] #task something to do #later #home/kitchen', ], }, ], @@ -296,14 +302,12 @@ describe('Query', () => { 'by tag absence', { filters: ['tags do not include #home'], - tasks: [ - '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], + tasks: defaultTasksWithTags, expectedResult: [ - '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #work', + '- [ ] #task something to do #later #work/meeting', + '- [ ] #task get the milk', + '- [ ] #task something to do #later #work #TopLevelItem/sub', ], }, ], @@ -311,13 +315,10 @@ describe('Query', () => { 'by tag presence without hash', { filters: ['tags include home'], - tasks: [ - '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], + tasks: defaultTasksWithTags, expectedResult: [ - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #home', + '- [ ] #task something to do #later #home/kitchen', ], }, ], @@ -325,14 +326,12 @@ describe('Query', () => { 'by tag absence without hash', { filters: ['tags do not include home'], - tasks: [ - '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], + tasks: defaultTasksWithTags, expectedResult: [ - '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #work', + '- [ ] #task something to do #later #work/meeting', + '- [ ] #task get the milk', + '- [ ] #task something to do #later #work #TopLevelItem/sub', ], }, ], @@ -341,13 +340,10 @@ describe('Query', () => { 'by tag presence case insensitive', { filters: ['tags include #HoMe'], - tasks: [ - '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], + tasks: defaultTasksWithTags, expectedResult: [ - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #home', + '- [ ] #task something to do #later #home/kitchen', ], }, ], @@ -355,14 +351,12 @@ describe('Query', () => { 'by tag absence case insensitive', { filters: ['tags do not include #HoMe'], - tasks: [ - '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], + tasks: defaultTasksWithTags, expectedResult: [ - '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #work', + '- [ ] #task something to do #later #work/meeting', + '- [ ] #task get the milk', + '- [ ] #task something to do #later #work #TopLevelItem/sub', ], }, ], @@ -370,13 +364,10 @@ describe('Query', () => { 'by tag presence without hash case insensitive', { filters: ['tags include HoMe'], - tasks: [ - '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], + tasks: defaultTasksWithTags, expectedResult: [ - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #home', + '- [ ] #task something to do #later #home/kitchen', ], }, ], @@ -384,14 +375,12 @@ describe('Query', () => { 'by tag absence without hash case insensitive', { filters: ['tags do not include HoMe'], - tasks: [ - '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], + tasks: defaultTasksWithTags, expectedResult: [ - '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #work', + '- [ ] #task something to do #later #work/meeting', + '- [ ] #task get the milk', + '- [ ] #task something to do #later #work #TopLevelItem/sub', ], }, ], @@ -399,13 +388,9 @@ describe('Query', () => { 'by tag presence without hash case insensitive and substring', { filters: ['tags include TopLevelItem'], - tasks: [ - '- [ ] #task something to do #later #work #TopLevelItem/sub 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], + tasks: defaultTasksWithTags, expectedResult: [ - '- [ ] #task something to do #later #work #TopLevelItem/sub 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #work #TopLevelItem/sub', ], }, ], @@ -413,290 +398,99 @@ describe('Query', () => { 'by tag absence without hash case insensitive and substring', { filters: ['tags do not include TopLevelItem'], - tasks: [ - '- [ ] #task something to do #later #work #TopLevelItem/sub 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], - expectedResult: [ - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], - }, - ], - [ - 'by tag presence when it is the global tag', - { - filters: ['tags include task'], - tasks: [ - '- [ ] #task something to do #later #work #TopLevelItem/sub 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], - expectedResult: [], - }, - ], - // Alt Grammar - [ - 'by tag presence', - { - filters: ['tag includes #home'], - tasks: [ - '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], - expectedResult: [ - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - ], - }, - ], - [ - 'by tag absence', - { - filters: ['tag does not include #home'], - tasks: [ - '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], - expectedResult: [ - '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], - }, - ], - [ - 'by tag presence without hash', - { - filters: ['tag includes home'], - tasks: [ - '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], + tasks: defaultTasksWithTags, expectedResult: [ - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - ], - }, - ], - [ - 'by tag absence without hash', - { - filters: ['tag does not include home'], - tasks: [ - '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], - expectedResult: [ - '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', + '- [ ] #task something to do #later #work', + '- [ ] #task something to do #later #work/meeting', + '- [ ] #task something to do #later #home', + '- [ ] #task something to do #later #home/kitchen', + '- [ ] #task get the milk', ], }, ], + ]; - [ - 'by tag presence case insensitive', - { - filters: ['tag includes #HoMe'], - tasks: [ - '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], - expectedResult: [ - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - ], - }, - ], - [ - 'by tag absence case insensitive', - { - filters: ['tag does not include #HoMe'], - tasks: [ - '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], - expectedResult: [ - '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], - }, - ], - [ - 'by tag presence without hash case insensitive', - { - filters: ['tag includes HoMe'], - tasks: [ - '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], - expectedResult: [ - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - ], - }, - ], - [ - 'by tag absence without hash case insensitive', - { - filters: ['tag does not include HoMe'], - tasks: [ - '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], - expectedResult: [ - '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], - }, - ], - [ - 'by tag presence without hash case insensitive and substring', - { - filters: ['tag includes TopLevelItem'], - tasks: [ - '- [ ] #task something to do #later #work #TopLevelItem/sub 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], - expectedResult: [ - '- [ ] #task something to do #later #work #TopLevelItem/sub 📅 2021-09-12 ✅ 2021-06-20', - ], - }, - ], - [ - 'by tag absence without hash case insensitive and substring', - { - filters: ['tag does not include TopLevelItem'], - tasks: [ - '- [ ] #task something to do #later #work #TopLevelItem/sub 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], - expectedResult: [ - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], - }, - ], - [ - 'by tag presence when it is the global tag', - { - filters: ['tag includes task'], - tasks: [ - '- [ ] #task something to do #later #work #TopLevelItem/sub 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - ], - expectedResult: [], - }, - ], - ])( - 'should support filtering of tags %s', + test.concurrent.each<[string, FilteringCase]>(TagFilteringCases)( + 'should filter tag with globalFilter %s', (_, { tasks: allTaskLines, filters, expectedResult }) => { // Arrange const originalSettings = getSettings(); updateSettings({ globalFilter: '#task' }); + // Run on the plural version of the filter first. + shouldSupportFiltering(filters, allTaskLines, expectedResult); + + // Run a remap of filter to use alternative grammar for single and plural tag/tags. + // tags include #home vs tag includes #home. The first is preferred as it is a collection. + // tags -> tag + // include -> includes + // does not include -> do not include + filters.map((filter) => { + return filter + .replace('tags', 'tag') + .replace('include', 'includes') + .replace('does not include', 'do not include'); + }); + shouldSupportFiltering(filters, allTaskLines, expectedResult); // Cleanup updateSettings(originalSettings); }, ); - }); - it('filters based off a single tag', () => { - // Arrange - const originalSettings = getSettings(); - updateSettings({ globalFilter: '#task' }); - const tasks: Task[] = [ - Task.fromLine({ - line: '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - sectionStart: 0, - sectionIndex: 0, - path: '', - precedingHeader: '', - }), - Task.fromLine({ - line: '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - sectionStart: 0, - sectionIndex: 0, - path: '', - precedingHeader: '', - }), - Task.fromLine({ - line: '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - sectionStart: 0, - sectionIndex: 0, - path: '', - precedingHeader: '', - }), - ] as Task[]; - const input = 'tags include #home'; - const query = new Query({ source: input }); - - // Act - let filteredTasks = [...tasks]; - query.filters.forEach((filter) => { - filteredTasks = filteredTasks.filter(filter); - }); + test.concurrent.each<[string, FilteringCase]>(TagFilteringCases)( + 'should filter tags without globalFilter %s', + (_, { tasks: allTaskLines, filters, expectedResult }) => { + // Arrange - // Assert - expect(filteredTasks.length).toEqual(1); - expect(filteredTasks[0]).toEqual(tasks[1]); + // Run on the plural version of the filter first. + shouldSupportFiltering(filters, allTaskLines, expectedResult); - // Cleanup - updateSettings(originalSettings); - }); + // Run a remap of filter to use alternative grammar for single and plural tag/tags. + // tags include #home vs tag includes #home. The first is preferred as it is a collection. + // tags -> tag + // include -> includes + // does not include -> do not include + filters.map((filter) => { + return filter + .replace('tags', 'tag') + .replace('include', 'includes') + .replace('does not include', 'do not include'); + }); - it('filters based off a tag not being present', () => { - // Arrange - const originalSettings = getSettings(); - updateSettings({ globalFilter: '#task' }); - const tasks: Task[] = [ - Task.fromLine({ - line: '- [ ] #task something to do #later #work 📅 2021-09-12 ✅ 2021-06-20', - sectionStart: 0, - sectionIndex: 0, - path: '', - precedingHeader: '', - }), - Task.fromLine({ - line: '- [ ] #task something to do #later #home 📅 2021-09-12 ✅ 2021-06-20', - sectionStart: 0, - sectionIndex: 0, - path: '', - precedingHeader: '', - }), - Task.fromLine({ - line: '- [ ] #task get the milk 📅 2021-09-12 ✅ 2021-06-20', - sectionStart: 0, - sectionIndex: 0, - path: '', - precedingHeader: '', - }), - ] as Task[]; - const input = 'tags do not include #home'; - const query = new Query({ source: input }); - - // Act - let filteredTasks = [...tasks]; - query.filters.forEach((filter) => { - filteredTasks = filteredTasks.filter(filter); + shouldSupportFiltering(filters, allTaskLines, expectedResult); + }, + ); + + it('should filter tags without globalFilter by tag presence when it is the global tag', () => { + // Arrange + const originalSettings = getSettings(); + updateSettings({ globalFilter: '' }); + + // Act, Assert + shouldSupportFiltering( + ['tags include task'], + defaultTasksWithTags, + defaultTasksWithTags, + ); + + // Cleanup + updateSettings(originalSettings); }); - // Assert - expect(filteredTasks.length).toEqual(2); - expect(filteredTasks[0]).toEqual(tasks[0]); - expect(filteredTasks[1]).toEqual(tasks[2]); + it('should filter tag with globalFilter by tag presence when it is the global tag', () => { + // Arrange + const originalSettings = getSettings(); + updateSettings({ globalFilter: '#task' }); + const filters: Array = ['tags include task']; - // Cleanup - updateSettings(originalSettings); + // Act, Assert + shouldSupportFiltering(filters, defaultTasksWithTags, []); + + // Cleanup + updateSettings(originalSettings); + }); }); describe('filtering with "happens"', () => { diff --git a/tests/Sort.test.ts b/tests/Sort.test.ts index 9ef373521e..b48118d63d 100644 --- a/tests/Sort.test.ts +++ b/tests/Sort.test.ts @@ -301,19 +301,30 @@ describe('Sort', () => { }); }); +/* + * All the test cases below have tasks with 0 or more tags against them. This is to + * ensure that the sorting can handle the ordering correctly when there are no tags or + * if one of th tasks has less tags than the other. + * + * There is also a task with additional characters in the name to ensure it is seen + * as bigger that one with the same initial characters. + */ describe('Sort by tags', () => { it('should sort correctly by tag defaulting to first with no global filter', () => { + // Arrange const t1 = fromLine({ line: '- [ ] a #aaa #jjj' }); - const t2 = fromLine({ line: '- [ ] a #ggg #ccc' }); - const t3 = fromLine({ line: '- [ ] a #bbb #iii' }); - const t4 = fromLine({ line: '- [ ] a #fff #aaa' }); - const t5 = fromLine({ line: '- [ ] a #ccc #bbb' }); - const t6 = fromLine({ line: '- [ ] a #eee #fff' }); - const t7 = fromLine({ line: '- [ ] a #ddd #ggg' }); + const t2 = fromLine({ line: '- [ ] a #bbb #iii' }); + const t3 = fromLine({ line: '- [ ] a #ccc #bbb' }); + const t4 = fromLine({ line: '- [ ] a #ddd #ggg' }); + const t5 = fromLine({ line: '- [ ] a #eee #fff' }); + const t6 = fromLine({ line: '- [ ] a #fff #aaa' }); + const t7 = fromLine({ line: '- [ ] a #ggg #ccc' }); const t8 = fromLine({ line: '- [ ] a #hhh #eee' }); const t9 = fromLine({ line: '- [ ] a #iii #ddd' }); const t10 = fromLine({ line: '- [ ] a #jjj #hhh' }); - const expectedOrder = [t1, t3, t5, t7, t6, t4, t2, t8, t9, t10]; + const expectedOrder = [t1, t2, t3, t4, t5, t6, t7, t8, t9, t10]; + + // Act / Assert expect( Sort.by( { @@ -325,17 +336,49 @@ describe('Sort by tags', () => { }, ], }, - [t1, t2, t3, t4, t5, t6, t7, t8, t9, t10], + [t1, t3, t5, t7, t6, t4, t2, t8, t9, t10], ), ).toEqual(expectedOrder); }); - it('should sort correctly by second tag with no global filter', () => { - const t4 = fromLine({ line: '- [ ] a #fff #aaa' }); + + it('should sort correctly reversed by tag defaulting to first with no global filter', () => { + // Arrange + const t1 = fromLine({ line: '- [ ] a #aaa #jjj' }); + const t2 = fromLine({ line: '- [ ] a #bbb #iii' }); const t3 = fromLine({ line: '- [ ] a #ccc #bbb' }); - const t2 = fromLine({ line: '- [ ] a #ggg #ccc' }); - const t1 = fromLine({ line: '- [ ] a #iii #ddd' }); + const t4 = fromLine({ line: '- [ ] a #ddd #ggg' }); + const t5 = fromLine({ line: '- [ ] a #eee #fff' }); + const t6 = fromLine({ line: '- [ ] a #fff #aaa' }); + const t7 = fromLine({ line: '- [ ] a #ggg #ccc' }); + const t8 = fromLine({ line: '- [ ] a #hhh #eee' }); + const t9 = fromLine({ line: '- [ ] a #iii #ddd' }); + const t10 = fromLine({ line: '- [ ] a #jjj #hhh' }); + const expectedOrder = [t10, t9, t8, t7, t6, t5, t4, t3, t2, t1]; + + // Act / Assert + expect( + Sort.by( + { + sorting: [ + { + property: 'tag', + reverse: true, + propertyInstance: 1, + }, + ], + }, + [t1, t3, t5, t7, t6, t4, t2, t8, t9, t10], + ), + ).toEqual(expectedOrder); + }); + + it('should sort correctly by second tag with no global filter', () => { + const t1 = fromLine({ line: '- [ ] a #fff #aaa' }); + const t2 = fromLine({ line: '- [ ] a #ccc #bbb' }); + const t3 = fromLine({ line: '- [ ] a #ggg #ccc' }); + const t4 = fromLine({ line: '- [ ] a #iii #ddd' }); const t5 = fromLine({ line: '- [ ] a #hhh #eee' }); - const expectedOrder = [t4, t3, t2, t1, t5]; + const expectedOrder = [t1, t2, t3, t4, t5]; expect( Sort.by( { @@ -347,42 +390,67 @@ describe('Sort by tags', () => { }, ], }, - [t1, t2, t3, t4, t5], + [t4, t3, t2, t1, t5], ), ).toEqual(expectedOrder); }); + + it('should sort correctly reversed by second tag with no global filter', () => { + const t1 = fromLine({ line: '- [ ] a #fff #aaa' }); + const t2 = fromLine({ line: '- [ ] a #ccc #bbb' }); + const t3 = fromLine({ line: '- [ ] a #ggg #ccc' }); + const t4 = fromLine({ line: '- [ ] a #iii #ddd' }); + const t5 = fromLine({ line: '- [ ] a #hhh #eee' }); + const expectedOrder = [t5, t4, t3, t2, t1]; + expect( + Sort.by( + { + sorting: [ + { + property: 'tag', + reverse: true, + propertyInstance: 2, + }, + ], + }, + [t4, t3, t2, t1, t5], + ), + ).toEqual(expectedOrder); + }); + it('should sort correctly by tag defaulting to first with global filter', () => { // Arrange const originalSettings = getSettings(); updateSettings({ globalFilter: '#task' }); const t1 = fromLine({ line: '- [ ] #task a #aaa #jjj' }); - const t2 = fromLine({ line: '- [ ] #task a #ggg #ccc' }); + const t2 = fromLine({ line: '- [ ] #task a #aaaa #aaaa' }); const t3 = fromLine({ line: '- [ ] #task a #bbb #iii' }); - const t4 = fromLine({ line: '- [ ] #task a #fff #aaa' }); + const t4 = fromLine({ line: '- [ ] #task a #bbbb ' }); const t5 = fromLine({ line: '- [ ] #task a #ccc #bbb' }); - const t6 = fromLine({ line: '- [ ] #task a #eee #fff' }); - const t7 = fromLine({ line: '- [ ] #task a #ddd #ggg' }); - const t8 = fromLine({ line: '- [ ] #task a #hhh #eee' }); - const t9 = fromLine({ line: '- [ ] #task a #iii #ddd' }); - const t10 = fromLine({ line: '- [ ] #task a #jjj #hhh' }); - const t11 = fromLine({ line: '- [ ] #task a' }); - const t12 = fromLine({ line: '- [ ] #task a #aaaa #aaaa' }); - const t13 = fromLine({ line: '- [ ] #task a #bbbb ' }); + const t6 = fromLine({ line: '- [ ] #task a #ddd #ggg' }); + const t7 = fromLine({ line: '- [ ] #task a #eee #fff' }); + const t8 = fromLine({ line: '- [ ] #task a #fff #aaa' }); + const t9 = fromLine({ line: '- [ ] #task a #ggg #ccc' }); + const t10 = fromLine({ line: '- [ ] #task a #hhh #eee' }); + const t11 = fromLine({ line: '- [ ] #task a #iii #ddd' }); + const t12 = fromLine({ line: '- [ ] #task a #jjj #hhh' }); + const t13 = fromLine({ line: '- [ ] #task a' }); + const expectedOrder = [ t1, - t12, + t2, t3, - t13, + t4, t5, - t7, t6, - t4, - t2, + t7, t8, t9, t10, t11, + t12, + t13, ]; // Act @@ -397,27 +465,48 @@ describe('Sort by tags', () => { }, ], }, - [t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13], + [t1, t12, t3, t13, t5, t7, t6, t4, t2, t8, t9, t10, t11], ), ).toEqual(expectedOrder); // Cleanup updateSettings(originalSettings); }); - it('should sort correctly by second tag with global filter', () => { + + it('should sort correctly reversed by tag defaulting to first with global filter', () => { // Arrange const originalSettings = getSettings(); updateSettings({ globalFilter: '#task' }); - const t4 = fromLine({ line: '- [ ] #task a #fff #aaa' }); - const t5 = fromLine({ line: '- [ ] #task a #ccc #bbb' }); - const t2 = fromLine({ line: '- [ ] #task a #ggg #ccc' }); - const t3 = fromLine({ line: '- [ ] #task a #bbb #iii' }); const t1 = fromLine({ line: '- [ ] #task a #aaa #jjj' }); - const t6 = fromLine({ line: '- [ ] #task a' }); - const t7 = fromLine({ line: '- [ ] #task a #aaaa #aaaa' }); - const t8 = fromLine({ line: '- [ ] #task a #bbbb ' }); - const expectedOrder = [t4, t7, t5, t2, t3, t1, t8, t6]; + const t2 = fromLine({ line: '- [ ] #task a #aaaa #aaaa' }); + const t3 = fromLine({ line: '- [ ] #task a #bbb #iii' }); + const t4 = fromLine({ line: '- [ ] #task a #bbbb ' }); + const t5 = fromLine({ line: '- [ ] #task a #ccc #bbb' }); + const t6 = fromLine({ line: '- [ ] #task a #ddd #ggg' }); + const t7 = fromLine({ line: '- [ ] #task a #eee #fff' }); + const t8 = fromLine({ line: '- [ ] #task a #fff #aaa' }); + const t9 = fromLine({ line: '- [ ] #task a #ggg #ccc' }); + const t10 = fromLine({ line: '- [ ] #task a #hhh #eee' }); + const t11 = fromLine({ line: '- [ ] #task a #iii #ddd' }); + const t12 = fromLine({ line: '- [ ] #task a #jjj #hhh' }); + const t13 = fromLine({ line: '- [ ] #task a' }); + + const expectedOrder = [ + t13, + t12, + t11, + t10, + t9, + t8, + t7, + t6, + t5, + t4, + t3, + t2, + t1, + ]; // Act expect( @@ -426,16 +515,88 @@ describe('Sort by tags', () => { sorting: [ { property: 'tag', - reverse: false, - propertyInstance: 2, + reverse: true, + propertyInstance: 1, }, ], }, - [t1, t2, t3, t4, t5, t6, t7, t8], + [t1, t12, t3, t13, t5, t7, t6, t4, t2, t8, t9, t10, t11], ), ).toEqual(expectedOrder); // Cleanup updateSettings(originalSettings); }); + + it('should sort correctly by second tag with global filter', () => { + // Arrange + const originalSettings = getSettings(); + updateSettings({ globalFilter: '#task' }); + + const t1 = fromLine({ line: '- [ ] #task a #fff #aaa' }); + const t2 = fromLine({ line: '- [ ] #task a #aaaa #aaaa' }); + const t3 = fromLine({ line: '- [ ] #task a #ccc #bbb' }); + const t4 = fromLine({ line: '- [ ] #task a #ggg #ccc' }); + const t5 = fromLine({ line: '- [ ] #task a #bbb #iii' }); + const t6 = fromLine({ line: '- [ ] #task a #aaa #jjj' }); + const t7 = fromLine({ line: '- [ ] #task a #bbbb' }); + const t8 = fromLine({ line: '- [ ] #task a' }); + const expectedOrder = [t1, t2, t3, t4, t5, t6, t7, t8]; + + // Act + const result = Sort.by( + { + sorting: [ + { + property: 'tag', + reverse: false, + propertyInstance: 2, + }, + ], + }, + [t4, t7, t5, t2, t3, t1, t8, t6], + ); + + // Assert + expect(result).toEqual(expectedOrder); + + // Cleanup + updateSettings(originalSettings); + }); + + it('should sort correctly reversed by second tag with global filter', () => { + // Arrange + const originalSettings = getSettings(); + updateSettings({ globalFilter: '#task' }); + + const t1 = fromLine({ line: '- [ ] #task a #fff #aaa' }); + const t2 = fromLine({ line: '- [ ] #task a #aaaa #aaaa' }); + const t3 = fromLine({ line: '- [ ] #task a #ccc #bbb' }); + const t4 = fromLine({ line: '- [ ] #task a #ggg #ccc' }); + const t5 = fromLine({ line: '- [ ] #task a #bbb #iii' }); + const t6 = fromLine({ line: '- [ ] #task a #aaa #jjj' }); + const t7 = fromLine({ line: '- [ ] #task a #bbbb' }); + const t8 = fromLine({ line: '- [ ] #task a' }); + const expectedOrder = [t8, t7, t6, t5, t4, t3, t2, t1]; + + // Act + const result = Sort.by( + { + sorting: [ + { + property: 'tag', + reverse: true, + propertyInstance: 2, + }, + ], + }, + [t4, t7, t5, t2, t3, t1, t8, t6], + ); + + // Assert + expect(result).toEqual(expectedOrder); + + // Cleanup + updateSettings(originalSettings); + }); }); diff --git a/yarn.lock b/yarn.lock index 72c86ba311..0cd1e0bcb4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -942,11 +942,6 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - array-includes@^3.1.4: version "3.1.4" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.4.tgz#f5b493162c760f3539631f005ba2bb46acb45ba9" @@ -963,11 +958,6 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array-union@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-3.0.1.tgz#da52630d327f8b88cfbfb57728e2af5cd9b6b975" - integrity sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw== - array.prototype.flat@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz#07e0975d84bbc7c48cd1879d609e682598d33e13" @@ -1439,11 +1429,6 @@ enquirer@^2.3.5: dependencies: ansi-colors "^4.1.1" -entities@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" - integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== - es-abstract@^1.19.0, es-abstract@^1.19.1: version "1.19.1" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" @@ -1858,17 +1843,6 @@ fast-glob@^3.1.1: merge2 "^1.3.0" micromatch "^4.0.4" -fast-glob@^3.2.7: - version "3.2.11" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" - integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -2032,18 +2006,6 @@ globals@^13.6.0, globals@^13.9.0: dependencies: type-fest "^0.20.2" -globby@12.1.0: - version "12.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-12.1.0.tgz#471757d6d9d25651b655b1da3eae1e25209f86a5" - integrity sha512-YULDaNwsoUZkRy9TWSY/M7Obh0abamTKoKzTfOI3uU+hfpX2FZqOq8LFDxsjYheF1RH7ITdArgbQnsNBFgcdBA== - dependencies: - array-union "^3.0.1" - dir-glob "^3.0.1" - fast-glob "^3.2.7" - ignore "^5.1.9" - merge2 "^1.4.1" - slash "^4.0.0" - globby@^11.0.4: version "11.0.4" resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" @@ -2146,11 +2108,6 @@ ignore@^5.1.4, ignore@^5.1.8: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== -ignore@^5.1.9: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" - integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== - import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -2911,13 +2868,6 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -linkify-it@^3.0.1: - version "3.0.3" - resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.3.tgz#a98baf44ce45a550efb4d49c769d07524cc2fa2e" - integrity sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ== - dependencies: - uc.micro "^1.0.1" - locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -2996,63 +2946,17 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" -markdown-it@12.3.2: - version "12.3.2" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90" - integrity sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg== - dependencies: - argparse "^2.0.1" - entities "~2.1.0" - linkify-it "^3.0.1" - mdurl "^1.0.1" - uc.micro "^1.0.5" - -markdownlint-cli2-formatter-default@0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/markdownlint-cli2-formatter-default/-/markdownlint-cli2-formatter-default-0.0.3.tgz#5aecd6e576ad18801b76e58bbbaf0e916c583ab8" - integrity sha512-QEAJitT5eqX1SNboOD+SO/LNBpu4P4je8JlR02ug2cLQAqmIhh8IJnSK7AcaHBHhNADqdGydnPpQOpsNcEEqCw== - -markdownlint-cli2@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/markdownlint-cli2/-/markdownlint-cli2-0.4.0.tgz#9e29150278fd3f1fe31a05fc46bd95d2370e8758" - integrity sha512-EcwP5tAbyzzL3ACI0L16LqbNctmh8wNX56T+aVvIxWyTAkwbYNx2V7IheRkXS3mE7R/pnaApZ/RSXcXuzRVPjg== - dependencies: - globby "12.1.0" - markdownlint "0.25.1" - markdownlint-cli2-formatter-default "0.0.3" - markdownlint-rule-helpers "0.16.0" - micromatch "4.0.4" - strip-json-comments "4.0.0" - yaml "1.10.2" - -markdownlint-rule-helpers@0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/markdownlint-rule-helpers/-/markdownlint-rule-helpers-0.16.0.tgz#c327f72782bd2b9475127a240508231f0413a25e" - integrity sha512-oEacRUVeTJ5D5hW1UYd2qExYI0oELdYK72k1TKGvIeYJIbqQWAz476NAc7LNixSySUhcNl++d02DvX0ccDk9/w== - -markdownlint@0.25.1: - version "0.25.1" - resolved "https://registry.yarnpkg.com/markdownlint/-/markdownlint-0.25.1.tgz#df04536607ebeeda5ccd5e4f38138823ed623788" - integrity sha512-AG7UkLzNa1fxiOv5B+owPsPhtM4D6DoODhsJgiaNg1xowXovrYgOnLqAgOOFQpWOlHFVQUzjMY5ypNNTeov92g== - dependencies: - markdown-it "12.3.2" - -mdurl@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" - integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= - merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -merge2@^1.3.0, merge2@^1.4.1: +merge2@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -micromatch@4.0.4, micromatch@^4.0.4: +micromatch@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== @@ -3595,11 +3499,6 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -slash@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" - integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== - slice-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" @@ -3721,11 +3620,6 @@ strip-indent@^3.0.0: dependencies: min-indent "^1.0.0" -strip-json-comments@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-4.0.0.tgz#6fd3a79f1b956905483769b0bf66598b8f87da50" - integrity sha512-LzWcbfMbAsEDTRmhjWIioe8GcDRl0fa35YMXFoJKDdiD/quGFmjJjdgPjFJJNwCMaLyQqFIDqCdHD2V4HfLgYA== - strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" @@ -3978,11 +3872,6 @@ typescript@^4.4.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8" integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg== -uc.micro@^1.0.1, uc.micro@^1.0.5: - version "1.0.6" - resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" - integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== - unbox-primitive@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" @@ -4148,11 +4037,6 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@1.10.2: - version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - yargs-parser@20.x, yargs-parser@^20.2.2: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" From b2ace95dec86d5de8c1c0e440e3dd7b5eb314ace Mon Sep 17 00:00:00 2001 From: sytone Date: Sun, 15 May 2022 16:00:18 -0700 Subject: [PATCH 13/23] docs: update grammar based issue and sorting description for tags --- docs/queries/filters.md | 8 ++++---- docs/queries/sorting.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/queries/filters.md b/docs/queries/filters.md index 8de3421bd9..39122c7105 100644 --- a/docs/queries/filters.md +++ b/docs/queries/filters.md @@ -154,13 +154,13 @@ because the tasks starts before tomorrow. Only one of the dates needs to match. ### Tags -- `tags (include|does not include) ` +- `tags (include|do not include) ` (Alternative grammar `tag (includes|does not include) ` matching description syntax) - Matches case-insensitive (disregards capitalization). - Disregards the global filter when matching. - - The `#` is optional. + - The `#` is optional on the tag so `#home` and `home` will work to match `#home`. - The match is partial so `tags include foo` will match `#foo/bar` and `#foo-bar`. #### Tag Query Examples -- `tag includes #todo` -- `tag does not include #todo` +- `tags include #todo` +- `tags do not include #todo` diff --git a/docs/queries/sorting.md b/docs/queries/sorting.md index 9b0ec07549..06e43d3c4a 100644 --- a/docs/queries/sorting.md +++ b/docs/queries/sorting.md @@ -37,7 +37,7 @@ You can sort tasks by the following properties: 5. `done` (the date when the task was done) 6. `path` (the path to the file that contains the task) 7. `description` (the description of the task) -8. `tag` (the description of the task) +8. `tags` (the tags associated with the task) You can add multiple `sort by` query options, each on an extra line. The first sort has the highest priority. From 788e25e93117805a6967ca6c864163b1e88be968 Mon Sep 17 00:00:00 2001 From: sytone Date: Thu, 19 May 2022 09:33:31 -0700 Subject: [PATCH 14/23] feat: Allow additional tasks states and filtering by states Fixes #666 --- esbuild.config.mjs | 100 +++++++++++---------- package.json | 1 + src/Commands/CreateOrEdit.ts | 7 +- src/Events.ts | 4 +- src/Query.ts | 7 +- src/Settings.ts | 2 + src/SettingsTab.ts | 96 ++++++++++++++++++++ src/Sort.ts | 4 +- src/Status.ts | 99 ++++++++++++++++++++ src/StatusRegistry.ts | 170 +++++++++++++++++++++++++++++++++++ src/Task.ts | 36 +++----- src/main.ts | 29 +++++- src/ui/EditTask.svelte | 7 +- tests/Query.test.ts | 7 +- tests/Status.test.ts | 44 +++++++++ tests/StatusRegistry.test.ts | 122 +++++++++++++++++++++++++ tests/Task.test.ts | 11 +-- 17 files changed, 649 insertions(+), 97 deletions(-) create mode 100644 src/Status.ts create mode 100644 src/StatusRegistry.ts create mode 100644 tests/Status.test.ts create mode 100644 tests/StatusRegistry.test.ts diff --git a/esbuild.config.mjs b/esbuild.config.mjs index d77842aa68..d763590ef2 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -1,11 +1,10 @@ -import esbuild from "esbuild"; -import process from "process"; -import builtins from 'builtin-modules' -import esbuildSvelte from "esbuild-svelte"; -import sveltePreprocess from "svelte-preprocess"; - -const banner = -`/* +import process from 'process'; +import esbuild from 'esbuild'; +import builtins from 'builtin-modules'; +import esbuildSvelte from 'esbuild-svelte'; +import sveltePreprocess from 'svelte-preprocess'; + +const banner = `/* THIS IS A GENERATED/BUNDLED FILE BY ESBUILD if you want to view the source visit the plugins github repository */ @@ -130,44 +129,47 @@ THE SOFTWARE. */ `; -const prod = (process.argv[2] === 'production'); - -esbuild.build({ - banner: { - js: banner, - }, - bundle: true, - entryPoints: ['main.ts'], - external: [ - 'obsidian', - 'electron', - 'codemirror', - '@codemirror/autocomplete', - '@codemirror/closebrackets', - '@codemirror/commands', - '@codemirror/fold', - '@codemirror/gutter', - '@codemirror/history', - '@codemirror/language', - '@codemirror/rangeset', - '@codemirror/rectangular-selection', - '@codemirror/search', - '@codemirror/state', - '@codemirror/stream-parser', - '@codemirror/text', - '@codemirror/view', - ...builtins - ], - format: 'cjs', - logLevel: "info", - outfile: 'main.js', - plugins: [ - esbuildSvelte({ - preprocess: sveltePreprocess() - }), - ], - sourcemap: prod ? false : 'inline', - target: 'es2016', - treeShaking: true, - watch: !prod, -}).catch(() => process.exit(1)); +const prod = process.argv[2] === 'production'; +const dev = process.argv[2] === 'development'; + +esbuild + .build({ + banner: { + js: banner, + }, + bundle: true, + entryPoints: ['main.ts'], + external: [ + 'obsidian', + 'electron', + 'codemirror', + '@codemirror/autocomplete', + '@codemirror/closebrackets', + '@codemirror/commands', + '@codemirror/fold', + '@codemirror/gutter', + '@codemirror/history', + '@codemirror/language', + '@codemirror/rangeset', + '@codemirror/rectangular-selection', + '@codemirror/search', + '@codemirror/state', + '@codemirror/stream-parser', + '@codemirror/text', + '@codemirror/view', + ...builtins, + ], + format: 'cjs', + logLevel: 'info', + outfile: 'main.js', + plugins: [ + esbuildSvelte({ + preprocess: sveltePreprocess(), + }), + ], + sourcemap: prod ? false : 'inline', + target: 'es2016', + treeShaking: true, + watch: !prod && !dev, + }) + .catch(() => process.exit(1)); diff --git a/package.json b/package.json index 0264022256..400975331c 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "node esbuild.config.mjs", "build": "node esbuild.config.mjs production", + "build-dev": "node esbuild.config.mjs development", "lint": "eslint ./src --fix && eslint ./tests --fix && tsc --noEmit --pretty && svelte-check", "test": "jest --ci", "test:dev": "jest --watch" diff --git a/src/Commands/CreateOrEdit.ts b/src/Commands/CreateOrEdit.ts index cfc970d025..5a7850e824 100644 --- a/src/Commands/CreateOrEdit.ts +++ b/src/Commands/CreateOrEdit.ts @@ -1,6 +1,7 @@ import { App, Editor, MarkdownView, View } from 'obsidian'; import { TaskModal } from '../TaskModal'; -import { Priority, Status, Task } from '../Task'; +import { Status } from '../Status'; +import { Priority, Task } from '../Task'; export const createOrEdit = ( checking: boolean, @@ -64,7 +65,7 @@ const taskFromLine = ({ line, path }: { line: string; path: string }): Task => { // Should never happen; everything in the regex is optional. console.error('Tasks: Cannot create task on line:', line); return new Task({ - status: Status.Todo, + status: Status.TODO, description: '', path, indentation: '', @@ -86,7 +87,7 @@ const taskFromLine = ({ line, path }: { line: string; path: string }): Task => { const indentation: string = nonTaskMatch[1]; const statusString: string = nonTaskMatch[3] ?? ' '; - const status = statusString === ' ' ? Status.Todo : Status.Done; + const status = statusString === ' ' ? Status.TODO : Status.DONE; let description: string = nonTaskMatch[4]; const blockLinkMatch = line.match(Task.blockLinkRegex); diff --git a/src/Events.ts b/src/Events.ts index ae80a0ef12..bf87d834ad 100644 --- a/src/Events.ts +++ b/src/Events.ts @@ -16,8 +16,8 @@ interface CacheUpdateData { export class Events { private obsidianEvents: ObsidianEvents; - constructor({ obsidianEents }: { obsidianEents: ObsidianEvents }) { - this.obsidianEvents = obsidianEents; + constructor({ obsidianEvents }: { obsidianEvents: ObsidianEvents }) { + this.obsidianEvents = obsidianEvents; } public onCacheUpdate( diff --git a/src/Query.ts b/src/Query.ts index d739630a55..a7085e99a4 100644 --- a/src/Query.ts +++ b/src/Query.ts @@ -3,7 +3,8 @@ import * as chrono from 'chrono-node'; import { getSettings } from './Settings'; import { LayoutOptions } from './LayoutOptions'; import { Sort } from './Sort'; -import { Priority, Status, Task } from './Task'; +import { Status } from './Status'; +import { Priority, Task } from './Task'; export type SortingProperty = | 'urgency' @@ -88,12 +89,12 @@ export class Query { break; case line === this.doneString: this._filters.push( - (task) => task.status === Status.Done, + (task) => task.status === Status.DONE, ); break; case line === this.notDoneString: this._filters.push( - (task) => task.status !== Status.Done, + (task) => task.status !== Status.DONE, ); break; case line === this.recurringString: diff --git a/src/Settings.ts b/src/Settings.ts index e28923657a..20aa7f2da8 100644 --- a/src/Settings.ts +++ b/src/Settings.ts @@ -2,12 +2,14 @@ export interface Settings { globalFilter: string; removeGlobalFilter: boolean; setDoneDate: boolean; + status_types: Array<[string, string, string]>; } const defaultSettings: Settings = { globalFilter: '', removeGlobalFilter: false, setDoneDate: true, + status_types: [['', '', '']], }; let settings: Settings = { ...defaultSettings }; diff --git a/src/SettingsTab.ts b/src/SettingsTab.ts index 79ee75935f..afc5bf7693 100644 --- a/src/SettingsTab.ts +++ b/src/SettingsTab.ts @@ -78,5 +78,101 @@ export class SettingsTab extends PluginSettingTab { await this.plugin.saveSettings(); }); }); + + containerEl.createEl('h3', { text: 'Tasks Status Types' }); + containerEl.createEl('p', { + text: 'If you want to have the tasks support additional statuses outside of todo and done add them here with the status indicator.', + }); + + const { status_types } = getSettings(); + + status_types.forEach((status_type) => { + new Setting(this.containerEl) + .addExtraButton((extra) => { + extra + .setIcon('cross') + .setTooltip('Delete') + .onClick(async () => { + const index = status_types.indexOf(status_type); + if (index > -1) { + status_types.splice(index, 1); + updateSettings({ + status_types: status_types, + }); + await this.plugin.saveSettings(); + // Force refresh + this.display(); + } + }); + }) + .addText((text) => { + const t = text + .setPlaceholder('Status symbol') + .setValue(status_type[0]) + .onChange(async (new_symbol) => { + const index = status_types.indexOf(status_type); + if (index > -1) { + status_types[index][0] = new_symbol; + updateSettings({ + status_types: status_types, + }); + await this.plugin.saveSettings(); + } + }); + + return t; + }) + .addText((text) => { + const t = text + .setPlaceholder('Status name') + .setValue(status_type[1]) + .onChange(async (new_name) => { + const index = status_types.indexOf(status_type); + if (index > -1) { + status_types[index][1] = new_name; + updateSettings({ + status_types: status_types, + }); + await this.plugin.saveSettings(); + } + }); + return t; + }) + .addText((text) => { + const t = text + .setPlaceholder('Next status symbol') + .setValue(status_type[2]) + .onChange(async (new_symbol) => { + const index = status_types.indexOf(status_type); + if (index > -1) { + status_types[index][2] = new_symbol; + updateSettings({ + status_types: status_types, + }); + await this.plugin.saveSettings(); + } + }); + + return t; + }); + }); + + this.containerEl.createEl('div'); + + const setting = new Setting(this.containerEl).addButton((button) => { + button + .setButtonText('Add New Task Status') + .setCta() + .onClick(async () => { + status_types.push(['', '', '']); + updateSettings({ + status_types: status_types, + }); + await this.plugin.saveSettings(); + // Force refresh + this.display(); + }); + }); + setting.infoEl.remove(); } } diff --git a/src/Sort.ts b/src/Sort.ts index 42cc7fcba2..1ac0aa07c6 100644 --- a/src/Sort.ts +++ b/src/Sort.ts @@ -75,9 +75,9 @@ export class Sort { } private static compareByStatus(a: Task, b: Task): -1 | 0 | 1 { - if (a.status < b.status) { + if (a.status.indicator > b.status.indicator) { return 1; - } else if (a.status > b.status) { + } else if (a.status.indicator < b.status.indicator) { return -1; } else { return 0; diff --git a/src/Status.ts b/src/Status.ts new file mode 100644 index 0000000000..85b5ee1713 --- /dev/null +++ b/src/Status.ts @@ -0,0 +1,99 @@ +import type { StatusRegistry } from 'StatusRegistry'; + +/** + * Tracks the possible states that a task can be in. + * + * @export + * @class Status + */ +export class Status { + /** + * The indicator used between the two square brackets in the markdown task. + * + * @type {string} + * @memberof Status + */ + public readonly indicator: string; + + /** + * Returns the name of the status for display purposes. + * + * @type {string} + * @memberof Status + */ + public readonly name: string; + + /** + * Returns the next status for a task when toggled. + * + * @type {string} + * @memberof Status + */ + public readonly nextStatusIndicator: string; + + /** + * Returns the next status for a task when toggled. + * + * @type {StatusRegistry} + * @memberof Status + */ + public statusRegistry: StatusRegistry | undefined; + + /** + * The default Done status. + * + * @static + * @type {Status} + * @memberof Status + */ + public static DONE: Status = new Status('x', 'Done', ' '); + + /** + * A default status of empty, used when things go wrong. + * + * @static + * @memberof Status + */ + public static EMPTY: Status = new Status('', 'EMPTY', ''); + + /** + * The default Todo status. + * + * @static + * @type {Status} + * @memberof Status + */ + public static TODO: Status = new Status(' ', 'Todo', 'x'); + + /** + * Creates an instance of Status. The registry will be added later in the case + * of the default statuses. + * + * @param {string} indicator + * @param {string} name + * @param {Status} nextStatusIndicator + * @memberof Status + */ + constructor( + indicator: string, + name: string, + nextStatusIndicator: string, + statusRegistry: StatusRegistry | undefined = undefined, + ) { + this.indicator = indicator; + this.name = name; + this.nextStatusIndicator = nextStatusIndicator; + this.statusRegistry = statusRegistry; + } + + /** + * Returns the completion status for a task, this is only supported + * when the tas is done/x. + * + * @return {*} {boolean} + * @memberof Status + */ + public isCompleted(): boolean { + return this.indicator === 'x'; + } +} diff --git a/src/StatusRegistry.ts b/src/StatusRegistry.ts new file mode 100644 index 0000000000..286b89e881 --- /dev/null +++ b/src/StatusRegistry.ts @@ -0,0 +1,170 @@ +import { Status } from './Status'; + +/** + * Tracks all the registered statuses a task can have. + * + * @export + * @class StatusRegistry + */ +export class StatusRegistry { + private static instance: StatusRegistry; + + private _registeredStatuses: Status[] = []; + + /** + * Creates an instance of Status and registers it for use. It will also check to see + * if the default todo and done are registered and if not handle it internally. + * + * @memberof StatusRegistry + */ + private constructor() { + this.clearStatuses(); + } + + /** + * The static method that controls the access to the StatusRegistry instance. + * + * @static + * @return {*} {StatusRegistry} + * @memberof StatusRegistry + */ + public static getInstance(): StatusRegistry { + if (!StatusRegistry.instance) { + StatusRegistry.instance = new StatusRegistry(); + } + + return StatusRegistry.instance; + } + + /** + * Adds a new Status to the registry if not already registered. + * + * @param {Status} status + * @memberof StatusRegistry + */ + public add(status: Status): void { + if (!this.hasIndicator(status.indicator)) { + status.statusRegistry = this; + this._registeredStatuses.push(status); + } + } + + /** + * Returns the registered status by the indicator between the + * square braces in the markdown task. + * + * @param {string} indicator + * @return {*} {Status} + * @memberof StatusRegistry + */ + public byIndicator(indicator: string): Status { + if (this.hasIndicator(indicator)) { + return this.getIndicator(indicator); + } + + return Status.EMPTY; + } + + /** + * Returns the registered status by the name assigned by the user. + * + * @param {string} name + * @return {*} {Status} + * @memberof StatusRegistry + */ + public byName(nameToFind: string): Status { + if ( + this._registeredStatuses.filter(({ name }) => name === nameToFind) + .length > 0 + ) { + return this._registeredStatuses.filter( + ({ name }) => name === nameToFind, + )[0]; + } + + return Status.EMPTY; + } + + /** + * Resets the array os Status types to be empty. + * + * @memberof StatusRegistry + */ + public clearStatuses(): void { + this._registeredStatuses = []; + this.addDefaultStatusTypes(); + } + + /** + * To allow custom progression of task status each status knows + * which status can come after it as a state transition. + * + * @return {*} {Status} + * @memberof StatusRegistry + */ + public getNextStatus(status: Status): Status { + if (status.indicator === ' ') { + return Status.DONE; + } + + if (status.indicator === 'x') { + return Status.TODO; + } + + if (status.nextStatusIndicator !== '') { + const nextStatus = this.byIndicator(status.nextStatusIndicator); + if (nextStatus !== null) { + return nextStatus; + } + } + return Status.EMPTY; + } + + /** + * Filters the Status types by the indicator and returns the first one found. + * + * @private + * @param {string} indicatorToFind + * @return {*} {Status} + * @memberof StatusRegistry + */ + private getIndicator(indicatorToFind: string): Status { + return this._registeredStatuses.filter( + ({ indicator }) => indicator === indicatorToFind, + )[0]; + } + + /** + * Filters all the Status types by the indicator and returns true if found. + * + * @private + * @param {string} indicatorToFind + * @return {*} {boolean} + * @memberof StatusRegistry + */ + private hasIndicator(indicatorToFind: string): boolean { + return ( + this._registeredStatuses.filter( + ({ indicator }) => indicator === indicatorToFind, + ).length > 0 + ); + } + + /** + * Checks the registry and adds the default status types. + * + * @private + * @memberof StatusRegistry + */ + private addDefaultStatusTypes(): void { + if (!this.hasIndicator('')) { + this.add(Status.EMPTY); + } + if (!this.hasIndicator('x')) { + this.add(Status.DONE); + } + if (!this.hasIndicator(' ')) { + this.add(Status.TODO); + } + } +} diff --git a/src/Task.ts b/src/Task.ts index 572368f499..22653c2c50 100644 --- a/src/Task.ts +++ b/src/Task.ts @@ -1,22 +1,13 @@ import type { Moment } from 'moment'; import { Component, MarkdownRenderer } from 'obsidian'; +import { StatusRegistry } from './StatusRegistry'; +import type { Status } from './Status'; import { replaceTaskWithTasks } from './File'; import { LayoutOptions } from './LayoutOptions'; import { Recurrence } from './Recurrence'; import { getSettings } from './Settings'; import { Urgency } from './Urgency'; -/** - * Collection of status types supported by the plugin. - * TODO: Make this a class so it can support other types and easier mapping to status character. - * @export - * @enum {number} - */ -export enum Status { - Todo = 'Todo', - Done = 'Done', -} - /** * When sorting, make sure low always comes after none. This way any tasks with low will be below any exiting * tasks that have no priority which would be the default. @@ -200,15 +191,11 @@ export class Task { let description = body; const indentation = regexMatch[1]; - // Get the status of the task, only todo and done supported. + // Get the status of the task. const statusString = regexMatch[2].toLowerCase(); - let status: Status; - switch (statusString) { - case ' ': - status = Status.Todo; - break; - default: - status = Status.Done; + const status = StatusRegistry.getInstance().byIndicator(statusString); + if (status === null) { + throw new Error(`Missing status indicator: ${statusString}`); } // Match for block link and remove if found. Always expected to be @@ -407,7 +394,7 @@ export class Task { const checkbox = li.createEl('input'); checkbox.addClass('task-list-item-checkbox'); checkbox.type = 'checkbox'; - if (this.status !== Status.Todo) { + if (this.status.isCompleted()) { checkbox.checked = true; li.addClass('is-checked'); } @@ -527,8 +514,9 @@ export class Task { * task is not recurring, it will return `[toggled]`. */ public toggle(): Task[] { - const newStatus: Status = - this.status === Status.Todo ? Status.Done : Status.Todo; + const newStatus = StatusRegistry.getInstance().getNextStatus( + this.status, + ); let newDoneDate = null; @@ -538,7 +526,7 @@ export class Task { dueDate: Moment | null; } | null = null; - if (newStatus !== Status.Todo) { + if (newStatus.isCompleted()) { // Set done date only if setting value is true const { setDoneDate } = getSettings(); if (setDoneDate) { @@ -555,7 +543,7 @@ export class Task { ...this, status: newStatus, doneDate: newDoneDate, - originalStatusCharacter: newStatus === Status.Done ? 'x' : ' ', + originalStatusCharacter: newStatus.indicator, }); const newTasks: Task[] = []; diff --git a/src/main.ts b/src/main.ts index ab602633f9..043d33cedd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,5 @@ import { Plugin } from 'obsidian'; +import { Status } from './Status'; import { Cache } from './Cache'; import { Commands } from './Commands'; @@ -9,14 +10,18 @@ import { newLivePreviewExtension } from './LivePreviewExtension'; import { QueryRenderer } from './QueryRenderer'; import { getSettings, updateSettings } from './Settings'; import { SettingsTab } from './SettingsTab'; +import { StatusRegistry } from './StatusRegistry'; export default class TasksPlugin extends Plugin { private cache: Cache | undefined; public inlineRenderer: InlineRenderer | undefined; public queryRenderer: QueryRenderer | undefined; + public statusRegistry: StatusRegistry | undefined; async onload() { - console.log('loading plugin "tasks"'); + console.log( + `loading plugin "${this.manifest.name}" v${this.manifest.version}`, + ); await this.loadSettings(); this.addSettingTab(new SettingsTab({ plugin: this })); @@ -26,7 +31,7 @@ export default class TasksPlugin extends Plugin { vault: this.app.vault, }); - const events = new Events({ obsidianEents: this.app.workspace }); + const events = new Events({ obsidianEvents: this.app.workspace }); this.cache = new Cache({ metadataCache: this.app.metadataCache, vault: this.app.vault, @@ -34,13 +39,31 @@ export default class TasksPlugin extends Plugin { }); this.inlineRenderer = new InlineRenderer({ plugin: this }); this.queryRenderer = new QueryRenderer({ plugin: this, events }); + this.statusRegistry = StatusRegistry.getInstance(); + + await this.loadTaskStatuses(); this.registerEditorExtension(newLivePreviewExtension()); new Commands({ plugin: this }); } + async loadTaskStatuses() { + const { status_types } = getSettings(); + + status_types.forEach((status_type) => { + console.log( + `${this.manifest.name}: Adding custom status - [${status_type[0]}] ${status_type[1]} -> ${status_type[2]} `, + ); + this.statusRegistry?.add( + new Status(status_type[0], status_type[1], status_type[2]), + ); + }); + } + onunload() { - console.log('unloading plugin "tasks"'); + console.log( + `unloading plugin "${this.manifest.name}" v${this.manifest.version}`, + ); this.cache?.unload(); } diff --git a/src/ui/EditTask.svelte b/src/ui/EditTask.svelte index d224504752..6a0e49a9e9 100644 --- a/src/ui/EditTask.svelte +++ b/src/ui/EditTask.svelte @@ -1,9 +1,10 @@