diff --git a/docs/How To/How to get tasks in current file.md b/docs/How To/How to get tasks in current file.md
index 1846845552..abd7868656 100644
--- a/docs/How To/How to get tasks in current file.md
+++ b/docs/How To/How to get tasks in current file.md
@@ -4,7 +4,7 @@ publish: true
# How to get all tasks in the current file
-#plugin/dataview
+#feature/scripting #plugin/dataview
## Motivation and assumptions
@@ -13,37 +13,45 @@ for example to make sure no task gets accidentally missed.
This page documents ways of setting this up.
-Assumptions:
+## Using pure Tasks blocks - with placeholders
-- We assume that you know how to install and enable the [Dataview](https://github.com/blacksmithgu/obsidian-dataview) plugin.
+> [!released]
+> Placeholders were introduced in Tasks X.Y.Z.
-## Using pure Tasks blocks - fragile and error-prone
+We want to search for tasks in the file with the same `path` that the query is in.
-Tasks does not provide an automated way to include the location of the `tasks` block in a query.
+Tasks now provides an automated way to include the location of the `tasks` block in a query.
-It is possible to use the `path` instruction, but unfortunately you have to insert the path to the file yourself:
+We can use the `path` instruction with the placeholder text `{{query.file.path}}` which will be replaced with the path of the file containing the current query, like this:
## Summary of Tasks within this note
```tasks
not done
- path includes [insert current note's name or full path]
+ path includes path includes {{query.file.path}}
```
-For example:
+The following placeholders are available:
- ## Summary of Tasks within this note
+```text
+{{query.file.path}}
+{{query.file.root}}
+{{query.file.folder}}
+{{query.file.filename}}
+```
- ```tasks
- not done
- path includes Obsidian/tasks/tasks user support/03 Done - tasks user support/1.11.0 release
- ```
+They can be used with any text filter, not just `path`, `file`, `folder`, `filename`. For example, they might be useful with `description` and `heading` filters.
-> [!warning]
-> Using `path includes` to search for a particular file name or folder is error-prone, as if you rename the file,
-you have to remember to manually update the location in the tasks block, and this is very error-prone.
+For more information, see:
-## Using Dataview to generate Tasks blocks - safe and convenient
+- [[Placeholders]]
+- [[Query Properties]]
+
+## Using Dataview to generate Tasks blocks - the old way
+
+:
+
+- We assume that you know how to install and enable the [Dataview](https://github.com/blacksmithgu/obsidian-dataview) plugin.
There is a nice property that the [Dataview](https://github.com/blacksmithgu/obsidian-dataview) plugin can write out code blocks that are then processed by other plugins.
diff --git a/docs/Introduction.md b/docs/Introduction.md
index 20edd1118b..2acc3d680e 100644
--- a/docs/Introduction.md
+++ b/docs/Introduction.md
@@ -6,6 +6,7 @@ publish: true
## What's New?
+- X.Y.Z: 🔥 Use [[Query Properties]] and [[Placeholders]] to filter and group with the query's file path, root, folder and name.
- 4.6.0: 🔥 Add `on or before` and `on or after` to [[Filters#Date search options|date search options]]
- 4.6.0: 🔥 Add `in or before` and `in or after` to [[Filters#Date range options|date range search search options]]
- 4.5.0: 🔥 Support task in list items starting with [[Getting Started#Finding tasks in your vault|`+` signs]]
diff --git a/docs/Queries/Explaining Queries.md b/docs/Queries/Explaining Queries.md
index af8f322909..44d2b2748b 100644
--- a/docs/Queries/Explaining Queries.md
+++ b/docs/Queries/Explaining Queries.md
@@ -190,6 +190,39 @@ due next week =>
```
+### Template values are expanded
+
+> [!released]
+> Templating was introduced in Tasks X.Y.Z.
+
+For example, when the following query with [[Query Properties]] in [[Placeholders|placeholders]] is placed in a tasks query block in the file `some/sample/file path.md`:
+
+
+```text
+explain
+path includes {{query.file.path}}
+root includes {{query.file.root}}
+folder includes {{query.file.folder}}
+filename includes {{query.file.filename}}
+```
+
+
+the results begin with the following:
+
+
+```text
+Explanation of this Tasks code block query:
+
+path includes some/sample/file path.md
+
+root includes some/
+
+folder includes some/sample/
+
+filename includes file path.md
+```
+
+
## Styling explain results
### Default style
diff --git a/docs/Queries/Filters.md b/docs/Queries/Filters.md
index 5329a99466..1f2da7ebe6 100644
--- a/docs/Queries/Filters.md
+++ b/docs/Queries/Filters.md
@@ -975,12 +975,17 @@ Note that the path includes the `.md` extension.
- `path (includes|does not include) `
- Matches case-insensitive (disregards capitalization).
+ - Use `{{query.file.path}}` as a placeholder for the path of the file containing the current query.
+ - For example, `path equals {{query.file.path}}`
+ - Useful reading: [[Query Properties]] and [[Placeholders]]
- `path (regex matches|regex does not match) //`
- Does regular expression match (case-sensitive by default).
- Essential reading: [[Regular Expressions|Regular Expression Searches]].
> [!released]
-`regex matches` and `regex does not match` were introduced in Tasks 1.12.0.
+>
+> - `regex matches` and `regex does not match` were introduced in Tasks 1.12.0.
+> - Placeholders were released in Tasks X.Y.Z.
Since Tasks 4.2.0, **[[Custom Filters|custom filtering]] by file path** is now possible, using `task.file.path`.
@@ -1000,12 +1005,17 @@ Since Tasks 4.2.0, **[[Custom Filters|custom filtering]] by file path** is now p
### Root
> [!released]
-> Introduced in Tasks 3.4.0.
+>
+> - Introduced in Tasks 3.4.0.
+> - Placeholders were released in Tasks X.Y.Z.
The `root` is the top-level folder of the file that contains the task, that is, the first directory in the path, which will be `/` for files in the root of the vault.
- `root (includes|does not include) `
- Matches case-insensitive (disregards capitalization).
+ - Use `{{query.file.root}}` as a placeholder for the root of the file containing the current query.
+ - For example, `root includes {{query.file.root}}`
+ - Useful reading: [[Query Properties]] and [[Placeholders]]
- `root (regex matches|regex does not match) //`
- Does regular expression match (case-sensitive by default).
- Essential reading: [[Regular Expressions|Regular Expression Searches]].
@@ -1026,12 +1036,17 @@ Since Tasks 4.2.0, **[[Custom Filters|custom filtering]] by root folder** is now
### Folder
> [!released]
-> Introduced in Tasks 3.4.0.
+>
+> - Introduced in Tasks 3.4.0.
+> - Placeholders were released in Tasks X.Y.Z.
This is the `folder` to the file that contains the task, which will be `/` for files in root of the vault.
- `folder (includes|does not include) `
- Matches case-insensitive (disregards capitalization).
+ - Use `{{query.file.folder}}` as a placeholder for the folder of the file containing the current query.
+ - For example, `folder includes {{query.file.folder}}`, which will match tasks in the folder containing the query **and all sub-folders**.
+ - Useful reading: [[Query Properties]] and [[Placeholders]]
- `folder (regex matches|regex does not match) //`
- Does regular expression match (case-sensitive by default).
- Essential reading: [[Regular Expressions|Regular Expression Searches]].
@@ -1044,7 +1059,12 @@ Since Tasks 4.2.0, **[[Custom Filters|custom filtering]] by folder** is now poss
- Find tasks in files in any file in the given folder **only**, and not any sub-folders.
- The equality test, `===`, requires that the trailing slash (`/`) be included.
- ```filter by function task.file.folder.includes("Work/Projects/")```
- - Find tasks in files in any folder **and any sub-folders**.
+ - Find tasks in files in a specific folder **and any sub-folders**.
+- ```filter by function task.file.folder.includes( '{{query.file.folder}}' )```
+ - Find tasks in files in the folder that contains the query **and any sub-folders**.
+ - Note that the placeholder text is expanded to a raw string, so needs to be inside quotes.
+- ```filter by function task.file.folder === '{{query.file.folder}}'```
+ - Find tasks in files in the folder that contains the query only (**not tasks in any sub-folders**).
- ```filter by function task.file.folder.includes("Work/Projects")```
- By leaving off the trailing slash (`/`) this would also find tasks in any file inside folders such as:
- `Work/Projects 2023/`
@@ -1055,12 +1075,17 @@ Since Tasks 4.2.0, **[[Custom Filters|custom filtering]] by folder** is now poss
### File Name
> [!released]
-Introduced in Tasks 1.13.0.
+>
+> - Introduced in Tasks 3.4.0.
+> - Placeholders were released in Tasks X.Y.Z.
Note that the file name includes the `.md` extension.
- `filename (includes|does not include) `
- Matches case-insensitive (disregards capitalization).
+ - Use `{{query.file.filename}}` as a placeholder for the file name of the file containing the current query.
+ - For example, `filename includes {{query.file.filename}}`
+ - Useful reading: [[Query Properties]] and [[Placeholders]]
- `filename (regex matches|regex does not match) //`
- Does regular expression match (case-sensitive by default).
- Essential reading: [[Regular Expressions|Regular Expression Searches]].
diff --git a/docs/Queries/Grouping.md b/docs/Queries/Grouping.md
index 506b0ec174..7fc4d646e0 100644
--- a/docs/Queries/Grouping.md
+++ b/docs/Queries/Grouping.md
@@ -522,9 +522,20 @@ Since Tasks 4.0.0, **[[Custom Grouping|custom grouping]] by file path** is now p
- ```group by function task.file.path```
- Like 'group by path' but includes the file extension.
+- ```group by function task.file.path.replace('{{query.file.folder}}', '')```
+ - Group by the task's file path, but remove the query's folder from the group.
+ - For tasks in the query's folder or a sub-folder, this is a nice way of seeing shortened paths.
+ - Note that the placeholder text is expanded to a raw string, so needs to be inside quotes.
+ - This is provided to give ideas: it's a bit of a lazy implementation, as it doesn't check that `'{{query.file.folder}}'` is at the start of the line.
+Since Tasks X.Y.Z, the query's file path can be used in custom groups.
+
+- It must be quoted: `'{{query.file.folder}}'`
+- Beware if using placeholder text in regular expressions: Any special characters in filenames would need to be escaped.
+- Useful reading: [[Query Properties]] and [[Placeholders]].
+
### Root
- `group by root` (the top-level folder of the file that contains the task, that is, the first directory in the path, which will be `/` for files in root of the vault)
@@ -541,6 +552,12 @@ Since Tasks 4.0.0, **[[Custom Grouping|custom grouping]] by root folder** is now
+Since Tasks X.Y.Z, the query's file root can be used in custom groups.
+
+- It must be quoted: `'{{query.file.root}}'`
+- Beware if using placeholder text in regular expressions: Any special characters in filenames would need to be escaped.
+- Useful reading: [[Query Properties]] and [[Placeholders]].
+
### Folder
- `group by folder` (the folder to the file that contains the task, which always ends in `/` and will be exactly `/` for files in root of the vault)
@@ -561,6 +578,12 @@ Since Tasks 4.0.0, **[[Custom Grouping|custom grouping]] by folder** is now poss
+Since Tasks X.Y.Z, the query's folder can be used in custom groups.
+
+- It must be quoted: `'{{query.file.folder}}'`
+- Beware if using placeholder text in regular expressions: Any special characters in filenames would need to be escaped.
+- Useful reading: [[Query Properties]] and [[Placeholders]].
+
### File Name
- `group by filename` (the link to the file that contains the task, without the `.md` extension)
@@ -577,6 +600,12 @@ Since Tasks 4.0.0, **[[Custom Grouping|custom grouping]] by file name** is now p
+Since Tasks X.Y.Z, the query's file name can be used in custom groups.
+
+- It must be quoted: `'{{query.file.filename}}'`
+- Beware if using placeholder text in regular expressions: Any special characters in filenames would need to be escaped.
+- Useful reading: [[Query Properties]] and [[Placeholders]].
+
### Backlink
- `group by backlink` (the text that would be shown in the task's [[Backlinks|backlink]], combining the task's file name and heading, but with no link added)
diff --git a/docs/Quick Reference.md b/docs/Quick Reference.md
index c6c19c58bc..ae32ccb804 100644
--- a/docs/Quick Reference.md
+++ b/docs/Quick Reference.md
@@ -8,57 +8,57 @@ aliases:
This table summarizes the filters and other options available inside a `tasks` block.
-| [[Filters]] | [[Sorting\|Sort]] | [[Grouping\|Group]] | [[Layout\|Display]] | [[About Scripting\|Scripting]] |
-| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | ---------------------- | ---------------------- | --------------------------------------------------- |
-| **[[Filters#Filters for Dates in Tasks\|Status]]** | | | | |
-| `done`
`not done` | `sort by status` | `group by status` | | `task.isDone` |
-| `status.name (includes, does not include) `
`status.name (regex matches, regex does not match) /regex/i` | `sort by status.name` | `group by status.name` | | `task.status.name` |
-| `status.type (is, is not) (TODO, DONE, IN_PROGRESS, CANCELLED, NON_TASK)` | `sort by status.type` | `group by status.type` | | `task.status.type` |
-| | | | | `task.status.symbol` |
-| | | | | `task.status.nextSymbol` |
-| **[[Filters#Filters for Dates in Tasks\|Dates]]** | | | | |
+| [[Filters]] | [[Sorting\|Sort]] | [[Grouping\|Group]] | [[Layout\|Display]] | [[About Scripting\|Scripting]] |
+| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | ---------------------- | ---------------------- | --------------------------------------------------- |
+| **[[Filters#Filters for Dates in Tasks\|Status]]** | | | | |
+| `done`
`not done` | `sort by status` | `group by status` | | `task.isDone` |
+| `status.name (includes, does not include) `
`status.name (regex matches, regex does not match) /regex/i` | `sort by status.name` | `group by status.name` | | `task.status.name` |
+| `status.type (is, is not) (TODO, DONE, IN_PROGRESS, CANCELLED, NON_TASK)` | `sort by status.type` | `group by status.type` | | `task.status.type` |
+| | | | | `task.status.symbol` |
+| | | | | `task.status.nextSymbol` |
+| **[[Filters#Filters for Dates in Tasks\|Dates]]** | | | | |
| `done (on, before, after, on or before, on or after) `
`done (in, before, after, in or before, in or after) ...`
`... YYYY-MM-DD YYYY-MM-DD`
`... (last, this, next) (week, month, quarter, year)`
`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`
`has done date`
`no done date`
`done date is invalid` | `sort by done` | `group by done` | `hide done date` | `task.done` |
| `created (on, before, after, on or before, on or after) `
`created (in, before, after, in or before, in or after) ...`
`... YYYY-MM-DD YYYY-MM-DD`
`... (last, this, next) (week, month, quarter, year)`
`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`
`has created date`
`no created date`
`created date is invalid` | `sort by created` | `group by created` | `hide created date` | `task.created` |
| `starts (on, before, after, on or before, on or after) `
`starts (in, before, after, in or before, in or after) ...`
`... YYYY-MM-DD YYYY-MM-DD`
`... (last, this, next) (week, month, quarter, year)`
`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`
`has start date`
`no start date`
`start date is invalid` | `sort by start` | `group by start` | `hide start date` | `task.start` |
| `scheduled (on, before, after, on or before, on or after) `
`scheduled (in, before, after, in or before, in or after) ...`
`... YYYY-MM-DD YYYY-MM-DD`
`... (last, this, next) (week, month, quarter, year)`
`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`
`has scheduled date`
`no scheduled date`
`scheduled date is invalid` | `sort by scheduled` | `group by scheduled` | `hide scheduled date` | `task.scheduled` |
| `due (on, before, after, on or before, on or after) `
`due (in, before, after, in or before, in or after) ...`
`... YYYY-MM-DD YYYY-MM-DD`
`... (last, this, next) (week, month, quarter, year)`
`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`
`has due date`
`no due date`
`due date is invalid` | `sort by due` | `group by due` | `hide due date` | `task.due` |
| `happens (on, before, after, on or before, on or after) `
`happens (in, before, after, in or before, in or after) ...`
`... YYYY-MM-DD YYYY-MM-DD`
`... (last, this, next) (week, month, quarter, year)`
`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`
`has happens date`
`no happens date` | `sort by happens` | `group by happens` | | `task.happens` |
-| **[[Filters#Recurrence\|Recurrence]]** | | | | |
-| `is recurring`
`is not recurring` | `sort by recurring` | `group by recurring` | | `task.isRecurring` |
-| `recurrence (includes, does not include) `
`recurrence (regex matches, regex does not match) /regex/i` | | `group by recurrence` | `hide recurrence rule` | `task.recurrenceRule` |
-| **[[Filters#Priority\|Priority]]** and **[[Urgency\|urgency]]** | | | | |
-| `priority is (above, below, not)? (lowest, low, none, medium, high, highest)` | `sort by priority` | `group by priority` | `hide priority` | `task.priorityName`
`task.priorityNumber` |
-| | `sort by urgency` | `group by urgency` | `show urgency` | `task.urgency` |
-| **[[Filters#Filters for File Properties\|File properties]]** | | | | |
-| `path (includes, does not include) `
`path (regex matches, regex does not match) /regex/i` | `sort by path` | `group by path` | | `task.file.path` |
-| `root (includes, does not include) `
`root (regex matches, regex does not match) /regex/i` | | `group by root` | | `task.file.root` |
-| `folder (includes, does not include) `
`folder (regex matches, regex does not match) /regex/i` | | `group by folder` | | `task.file.folder` |
-| `filename (includes, does not include) `
`filename (regex matches, regex does not match) /regex/i` | `sort by filename` | `group by filename` | | `task.file.filename` |
-| `heading (includes, does not include) `
`heading (regex matches, regex does not match) /regex/i` | `sort by heading` | `group by heading` | | `task.hasHeading`
`task.heading` |
-| | | `group by backlink` | `hide backlink` | |
-| **[[Filters#Description\|Description]]**, **[[Filters#Tags\|Tags]]** and other odds and ends | | | | |
-| `description (includes, does not include) `
`description (regex matches, regex does not match) /regex/i` | `sort by description` | | | `task.description`
`task.descriptionWithoutTags` |
-| `has tags`
`no tags`
`tag (includes, does not include) `
`tags (include, do not include) `
`tag (regex matches, regex does not match) /regex/i`
`tags (regex matches, regex does not match) /regex/i` | `sort by tag`
`sort by tag ` | `group by tags` | `hide tags` | `task.tags` |
-| | | | | `task.originalMarkdown` |
-| **[[About Scripting\|Scripting]]** | | | | |
-| `filter by function` | | `group by function` | | |
-| **[[Combining Filters]]** | | | | |
-| `(filter 1) AND (filter 2)` | | | | |
-| `(filter 1) OR (filter 2)` | | | | |
-| `NOT (filter 1)` | | | | |
-| `(filter 1) XOR (filter 2)` | | | | |
-| `(filter 1) AND NOT (filter 2)` | | | | |
-| `(filter 1) OR NOT (filter 2)` | | | | |
-| `(filter 1) AND ((filter 2) OR (filter 3))` | | | | |
-| **Other Filter Options** | | | | |
-| `exclude sub-items` | | | | |
-| `limit to tasks`
`limit ` | | | | |
-| `limit groups to tasks`
`limit groups ` | | | | |
-| **Other Layout Options** | | | | |
-| `hide edit button` | | | | |
-| `hide task count` | | | | |
-| `short mode` | | | | |
-| **Other Instructions** | | | | |
-| `ignore global query` | | | | |
-| `explain` | | | | |
-| `# comment` | | | | |
+| **[[Filters#Recurrence\|Recurrence]]** | | | | |
+| `is recurring`
`is not recurring` | `sort by recurring` | `group by recurring` | | `task.isRecurring` |
+| `recurrence (includes, does not include) `
`recurrence (regex matches, regex does not match) /regex/i` | | `group by recurrence` | `hide recurrence rule` | `task.recurrenceRule` |
+| **[[Filters#Priority\|Priority]]** and **[[Urgency\|urgency]]** | | | | |
+| `priority is (above, below, not)? (lowest, low, none, medium, high, highest)` | `sort by priority` | `group by priority` | `hide priority` | `task.priorityName`
`task.priorityNumber` |
+| | `sort by urgency` | `group by urgency` | `show urgency` | `task.urgency` |
+| **[[Filters#Filters for File Properties\|File properties]]** | | | | |
+| `path (includes, does not include) `
`path (regex matches, regex does not match) /regex/i`
`path includes {{query.file.path}}` | `sort by path` | `group by path` | | `task.file.path` |
+| `root (includes, does not include) `
`root (regex matches, regex does not match) /regex/i`
`root includes {{query.file.root}}` | | `group by root` | | `task.file.root` |
+| `folder (includes, does not include) `
`folder (regex matches, regex does not match) /regex/i`
`folder includes {{query.file.folder}}` | | `group by folder` | | `task.file.folder` |
+| `filename (includes, does not include) `
`filename (regex matches, regex does not match) /regex/i`
`filename includes {{query.file.filename}}` | `sort by filename` | `group by filename` | | `task.file.filename` |
+| `heading (includes, does not include) `
`heading (regex matches, regex does not match) /regex/i` | `sort by heading` | `group by heading` | | `task.hasHeading`
`task.heading` |
+| | | `group by backlink` | `hide backlink` | |
+| **[[Filters#Description\|Description]]**, **[[Filters#Tags\|Tags]]** and other odds and ends | | | | |
+| `description (includes, does not include) `
`description (regex matches, regex does not match) /regex/i` | `sort by description` | | | `task.description`
`task.descriptionWithoutTags` |
+| `has tags`
`no tags`
`tag (includes, does not include) `
`tags (include, do not include) `
`tag (regex matches, regex does not match) /regex/i`
`tags (regex matches, regex does not match) /regex/i` | `sort by tag`
`sort by tag ` | `group by tags` | `hide tags` | `task.tags` |
+| | | | | `task.originalMarkdown` |
+| **[[About Scripting\|Scripting]]** | | | | |
+| `filter by function` | | `group by function` | | |
+| **[[Combining Filters]]** | | | | |
+| `(filter 1) AND (filter 2)` | | | | |
+| `(filter 1) OR (filter 2)` | | | | |
+| `NOT (filter 1)` | | | | |
+| `(filter 1) XOR (filter 2)` | | | | |
+| `(filter 1) AND NOT (filter 2)` | | | | |
+| `(filter 1) OR NOT (filter 2)` | | | | |
+| `(filter 1) AND ((filter 2) OR (filter 3))` | | | | |
+| **Other Filter Options** | | | | |
+| `exclude sub-items` | | | | |
+| `limit to tasks`
`limit ` | | | | |
+| `limit groups to tasks`
`limit groups ` | | | | |
+| **Other Layout Options** | | | | |
+| `hide edit button` | | | | |
+| `hide task count` | | | | |
+| `short mode` | | | | |
+| **Other Instructions** | | | | |
+| `ignore global query` | | | | |
+| `explain` | | | | |
+| `# comment` | | | | |
diff --git a/docs/Scripting/About Scripting.md b/docs/Scripting/About Scripting.md
index d0417569b3..2f0dac927d 100644
--- a/docs/Scripting/About Scripting.md
+++ b/docs/Scripting/About Scripting.md
@@ -15,6 +15,10 @@ We are using the word 'scripting' in a very loose sense here:
- For now, it refers only to writing JavaScript expressions in Tasks query blocks.
- It is intended to evolve in to something broader over time.
+## Templating capabilities
+
+- [[Placeholders]] - use placeholder text in native Tasks queries, such as `{{query.file.path}}` to refer to some properties of the file containing the query.
+
## Scripting capabilities
- [[Custom Filters]] - write short JavaScript expressions to create task search filters.
@@ -26,4 +30,5 @@ We are using the word 'scripting' in a very loose sense here:
- [[Task Properties]] - all the available task properties, such as `task.description`, `task.file.path`.
- Note: The properties are also listed in [[Quick Reference]].
+- [[Query Properties]] - all the available task properties, such as `query.file.path`, `query.file.path` - available for use via [[Placeholders]].
- [[Expressions]] - some background about how JavaScript expressions work, for use in Tasks code blocks.
diff --git a/docs/Scripting/Custom Filters.md b/docs/Scripting/Custom Filters.md
index 4311c928b8..3a5f8417ae 100644
--- a/docs/Scripting/Custom Filters.md
+++ b/docs/Scripting/Custom Filters.md
@@ -38,6 +38,15 @@ The Reference section [[Task Properties]] shows all the task properties availabl
The available task properties are also shown in the [[Quick Reference]] table.
+### Available Query Properties
+
+The Reference section [[Query Properties]] shows all the query properties available for use via [[Placeholders]] in custom filters.
+
+Any placeholders in custom filters must be surrounded by quotes.
+
+> [!released]
+> Query properties and placeholders were introduced in Tasks X.Y.Z.
+
### Expressions
The instructions look like this:
@@ -136,7 +145,12 @@ For users who are comfortable with JavaScript, these more complicated examples m
- Find tasks in files in any file in the given folder **only**, and not any sub-folders.
- The equality test, `===`, requires that the trailing slash (`/`) be included.
- ```filter by function task.file.folder.includes("Work/Projects/")```
- - Find tasks in files in any folder **and any sub-folders**.
+ - Find tasks in files in a specific folder **and any sub-folders**.
+- ```filter by function task.file.folder.includes( '{{query.file.folder}}' )```
+ - Find tasks in files in the folder that contains the query **and any sub-folders**.
+ - Note that the placeholder text is expanded to a raw string, so needs to be inside quotes.
+- ```filter by function task.file.folder === '{{query.file.folder}}'```
+ - Find tasks in files in the folder that contains the query only (**not tasks in any sub-folders**).
- ```filter by function task.file.folder.includes("Work/Projects")```
- By leaving off the trailing slash (`/`) this would also find tasks in any file inside folders such as:
- `Work/Projects 2023/`
diff --git a/docs/Scripting/Custom Grouping.md b/docs/Scripting/Custom Grouping.md
index f84f1a87e1..93cd00585a 100644
--- a/docs/Scripting/Custom Grouping.md
+++ b/docs/Scripting/Custom Grouping.md
@@ -37,6 +37,15 @@ The Reference section [[Task Properties]] shows all the task properties availabl
The available task properties are also shown in the [[Quick Reference]] table.
+### Available Query Properties
+
+The Reference section [[Query Properties]] shows all the query properties available for use via [[Placeholders]] in custom grouping.
+
+Any placeholders in custom groups must be surrounded by quotes.
+
+> [!released]
+> Query properties and placeholders were introduced in Tasks X.Y.Z.
+
### Expressions
The instructions look like this:
diff --git a/docs/Scripting/Placeholders.md b/docs/Scripting/Placeholders.md
new file mode 100644
index 0000000000..838e8ad7ea
--- /dev/null
+++ b/docs/Scripting/Placeholders.md
@@ -0,0 +1,124 @@
+---
+publish: true
+---
+
+# Placeholders
+
+#feature/scripting
+
+> [!released]
+> Placeholders were introduced in Tasks X.Y.Z.
+
+## Summary
+
+- Tasks provides a placeholder facility to enable filters to access the location of the query file.
+- Any known property inside a pair of `{{` and `}}` strings is expanded to a value obtained from the query file's path.
+- For example,:
+ - `{{query.file.path}}` might get expanded to
+ - `some/sample/actions on my hobby.md` - for any Tasks queries inside that file.
+- The available values for use in placeholders are listed in [[Query Properties]].
+
+## Checking template variables
+
+The [[Explaining Queries|explain]] instruction shows how any placeholders in the query are interpreted. This can be used to understand how placeholders are expanded generally.
+
+For example, when the following query with [[Query Properties]] in [[Placeholders|placeholders]] is placed in a tasks query block in the file `some/sample/file path.md`:
+
+
+```text
+explain
+path includes {{query.file.path}}
+root includes {{query.file.root}}
+folder includes {{query.file.folder}}
+filename includes {{query.file.filename}}
+```
+
+
+the results begin with the following, which demonstrates how each value inside `{{...}}` was expanded:
+
+
+```text
+Explanation of this Tasks code block query:
+
+path includes some/sample/file path.md
+
+root includes some/
+
+folder includes some/sample/
+
+filename includes file path.md
+```
+
+
+## Error checking: invalid variables
+
+If there are any unknown properties in the placeholders, a clear message is written.
+
+For example, the following shows that the names of query properties are case-sensitive:
+
+
+```text
+# query.file.fileName is invalid, because of the capital N.
+# query.file.filename is the correct property name.
+filename includes {{query.file.fileName}}
+```
+
+
+... generates this output:
+
+```text
+Tasks query: There was an error expanding one or more placeholders.
+
+The error message was:
+ Unknown property: query.file.fileName
+
+The problem is in:
+ filename includes {{query.file.fileName}}
+```
+
+%% ---------------------------------------------------------------------------
+IF THIS TEXT CHANGES, IT MEANS THE HARD-CODED OUTPUT ABOVE NEEDS TO BE UPDATED:
+
+
+```text
+Explanation of this Tasks code block query:
+
+Query has an error:
+There was an error expanding one or more placeholders.
+
+The error message was:
+ Unknown property: query.file.fileName
+
+The problem is in:
+ filename includes {{query.file.fileName}}
+```
+
+--------------------------------------------------------------------------- %%
+
+## Things to be aware of
+
+- The symbols are case-sensitive:
+ - `query.file.fileName` is not recognised
+- When placeholders are used in custom filters and groups, they must be surrounded by quotes.
+ - For example: `'{{query.file.folder}}'`
+
+## Known Limitations
+
+- It complains about any unrecognised placeholders in comments, even though comments are then ignored.
+- Explanations:
+ - `explain` instructions only show the expanded text.
+ - It would be nice to also show the original variable name, and then the expanded text.
+- Use in regular expressions is allowed
+ - but due to [[Regular Expressions#Special characters|characters with special meanings]] in regular expressions, it is not recommended to use them.
+- When you rename a file containing a tasks query block with variable names in, the query block is not automatically updated:
+ - the workaround is to close and re-open the file containing the query.
+
+## Missing Features
+
+- Searching by today's date or time
+- Getting date strings from file names
+
+## Technical Details
+
+- The templating library used is [mustache.js](https://www.npmjs.com/package/mustache).
+- Error-checking to detect use of unknown variables is implemented via [mustache-validator](https://www.npmjs.com/package/mustache-validator).
diff --git a/docs/Scripting/Query Properties.md b/docs/Scripting/Query Properties.md
new file mode 100644
index 0000000000..3532eff90e
--- /dev/null
+++ b/docs/Scripting/Query Properties.md
@@ -0,0 +1,41 @@
+---
+publish: true
+---
+
+# Query Properties
+
+#feature/scripting
+
+> [!released]
+> Query Properties were introduced in Tasks X.Y.Z.
+
+## Introduction
+
+In a growing number of locations, Tasks allows programmatic/scripting access to properties of the file containing the search query:
+
+- [[Placeholders]]
+
+This page documents all the available pieces of information in Queries that you can access.
+
+> [!warning]
+>
+> - These properties can currently only be used in [[Placeholders]].
+> - Placeholders can be in [[Custom Filters]] and [[Custom Grouping]], but must be surrounded by quotes. For example: `'{{query.file.folder}}'`.
+> - In a future release, we will allow expressions such as `query.file.folder` to be used directly in custom filters and groups.
+
+## Values for Query File Properties
+
+
+
+| Field | Type | Example |
+| ----- | ----- | ----- |
+| `query.file.path` | `string` | `'root/sub-folder/file containing query.md'` |
+| `query.file.root` | `string` | `'root/'` |
+| `query.file.folder` | `string` | `'root/sub-folder/'` |
+| `query.file.filename` | `string` | `'file containing query.md'` |
+
+
+
+1. `query.file` is a `TasksFile` object.
+1. You can see the current [TasksFile source code](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/src/Scripting/TasksFile.ts), to explore its capabilities.
+1. The presence of `.md` filename extensions is chosen to match the existing conventions in the Tasks filter instructions [[Filters#File Path|path]] and [[Filters#File Name|filename]].
diff --git a/package.json b/package.json
index 037290a06a..8bb6287982 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
"@testing-library/svelte": "^3.2.2",
"@tsconfig/svelte": "^3.0.0",
"@types/jest": "^29.5.2",
+ "@types/mustache": "^4.2.2",
"@typescript-eslint/eslint-plugin": "^5.59.8",
"@typescript-eslint/parser": "^5.59.8",
"approvals": "^6.2.1",
@@ -59,6 +60,8 @@
"boon-js": "^2.0.3",
"chrono-node": "2.3.9",
"eventemitter2": "^6.4.5",
+ "mustache": "^4.2.0",
+ "mustache-validator": "^0.2.0",
"rrule": "^2.7.1"
}
}
diff --git a/resources/sample_vaults/Tasks-Demo/Filters/Search for tasks in file or folder containing the Query.md b/resources/sample_vaults/Tasks-Demo/Filters/Search for tasks in file or folder containing the Query.md
new file mode 100644
index 0000000000..0c13c5727e
--- /dev/null
+++ b/resources/sample_vaults/Tasks-Demo/Filters/Search for tasks in file or folder containing the Query.md
@@ -0,0 +1,20 @@
+# Search for tasks in file or folder containing the Query
+
+See [How to get all tasks in the current file](https://publish.obsidian.md/tasks/How+To/How+to+get+tasks+in+current+file).
+
+## Sample Tasks
+
+- [ ] #task Task 1
+- [ ] #task Task 2
+- [ ] #task Task 3
+- [ ] #task Task 4
+
+## Search
+
+```tasks
+explain
+path includes {{query.file.path}}
+root includes {{query.file.root}}
+folder includes {{query.file.folder}}
+filename includes {{query.file.filename}}
+```
diff --git a/src/Query/Query.ts b/src/Query/Query.ts
index 3e74786430..8d2ac45f39 100644
--- a/src/Query/Query.ts
+++ b/src/Query/Query.ts
@@ -1,3 +1,5 @@
+import { expandPlaceholders } from '../Scripting/ExpandPlaceholders';
+import { makeQueryContext } from '../Scripting/QueryContext';
import { LayoutOptions } from '../TaskLayout';
import type { Task } from '../Task';
import type { IQuery } from '../IQuery';
@@ -12,7 +14,9 @@ import type { Filter } from './Filter/Filter';
import { QueryResult } from './QueryResult';
export class Query implements IQuery {
- public source: string;
+ /** Note: source is the raw source, before expanding any placeholders */
+ public readonly source: string;
+ public readonly filePath: string | undefined;
private _limit: number | undefined = undefined;
private _taskGroupLimit: number | undefined = undefined;
@@ -33,12 +37,20 @@ export class Query implements IQuery {
private readonly commentRegexp = /^#.*/;
- constructor({ source }: { source: string }) {
+ constructor({ source }: { source: string }, path: string | undefined = undefined) {
this.source = source;
+ this.filePath = path;
+
source
.split('\n')
- .map((line: string) => line.trim())
- .forEach((line: string) => {
+ .map((rawLine: string) => rawLine.trim())
+ .forEach((rawLine: string) => {
+ const line = this.expandPlaceholders(rawLine, path);
+ if (this.error !== undefined) {
+ // There was an error expanding placeholders.
+ return;
+ }
+
switch (true) {
case line === '':
break;
@@ -72,6 +84,37 @@ export class Query implements IQuery {
});
}
+ private expandPlaceholders(source: string, path: string | undefined) {
+ if (source.includes('{{') && source.includes('}}')) {
+ if (this.filePath === undefined) {
+ this._error = `The query looks like it contains a placeholder, with "{{" and "}}"
+but no file path has been supplied, so cannot expand placeholder values.
+The query is:
+${source}`;
+ return source;
+ }
+ }
+
+ // TODO Do not complain about any placeholder errors in comment lines
+ // TODO Show the original and expanded text in explanations
+ // TODO Give user error info if they try and put a string in a regex search
+ let expandedSource: string = source;
+ if (path) {
+ const queryContext = makeQueryContext(path);
+ try {
+ expandedSource = expandPlaceholders(source, queryContext);
+ } catch (error) {
+ if (error instanceof Error) {
+ this._error = error.message;
+ } else {
+ this._error = 'Internal error. expandPlaceholders() threw something other than Error.';
+ }
+ return source;
+ }
+ }
+ return expandedSource;
+ }
+
/**
*
* Appends {@link q2} to this query.
@@ -95,7 +138,7 @@ export class Query implements IQuery {
public append(q2: Query): Query {
if (this.source === '') return q2;
if (q2.source === '') return this;
- return new Query({ source: `${this.source}\n${q2.source}` });
+ return new Query({ source: `${this.source}\n${q2.source}` }, this.filePath);
}
/**
@@ -107,6 +150,12 @@ export class Query implements IQuery {
public explainQuery(): string {
let result = '';
+ if (this.error !== undefined) {
+ result += 'Query has an error:\n';
+ result += this.error + '\n';
+ return result;
+ }
+
const numberOfFilters = this.filters.length;
if (numberOfFilters === 0) {
result += 'No filters supplied. All tasks will match the query.';
diff --git a/src/QueryRenderer.ts b/src/QueryRenderer.ts
index dfcc6551d0..b430491de0 100644
--- a/src/QueryRenderer.ts
+++ b/src/QueryRenderer.ts
@@ -89,12 +89,12 @@ class QueryRenderChild extends MarkdownRenderChild {
// added later.
switch (this.containerEl.className) {
case 'block-language-tasks':
- this.query = getQueryForQueryRenderer(this.source);
+ this.query = getQueryForQueryRenderer(this.source, this.filePath);
this.queryType = 'tasks';
break;
default:
- this.query = getQueryForQueryRenderer(this.source);
+ this.query = getQueryForQueryRenderer(this.source, this.filePath);
this.queryType = 'tasks';
break;
}
@@ -135,7 +135,7 @@ class QueryRenderChild extends MarkdownRenderChild {
const millisecondsToMidnight = midnight.getTime() - now.getTime();
this.queryReloadTimeout = setTimeout(() => {
- this.query = getQueryForQueryRenderer(this.source);
+ this.query = getQueryForQueryRenderer(this.source, this.filePath);
// Process the current cache state:
this.events.triggerRequestCacheUpdate(this.render.bind(this));
this.reloadQueryAtMidnight();
@@ -198,7 +198,7 @@ class QueryRenderChild extends MarkdownRenderChild {
// Use the 'explain' instruction to enable this
private createExplanation(content: HTMLDivElement) {
- const explanationAsString = explainResults(this.source);
+ const explanationAsString = explainResults(this.source, this.filePath);
const explanationsBlock = content.createEl('pre');
explanationsBlock.addClasses(['plugin-tasks-query-explanation']);
diff --git a/src/Scripting/ExpandPlaceholders.ts b/src/Scripting/ExpandPlaceholders.ts
new file mode 100644
index 0000000000..e93806e615
--- /dev/null
+++ b/src/Scripting/ExpandPlaceholders.ts
@@ -0,0 +1,45 @@
+import Mustache from 'mustache';
+import proxyData from 'mustache-validator';
+
+// https://github.com/janl/mustache.js
+
+/**
+ * Expand any placeholder strings - {{....}} - in the given template, and return the result.
+ *
+ * The template implementation is currently provided by: [mustache.js](https://github.com/janl/mustache.js).
+ *
+ * @param template - A template string, typically with placeholders such as {{query.task.folder}}
+ * @param view - The property values
+ *
+ * @throws Error
+ *
+ * By using mustache-validator's proxyData, we ensure that any accesses of property names that are
+ * not in the view, we ensure that errors are detected immediately.
+ * The first unknown placeholder is included in Error.message.
+ */
+export function expandPlaceholders(template: string, view: any): string {
+ // Turn off HTML escaping of things like '/' in file paths:
+ // https://github.com/janl/mustache.js#variables
+ Mustache.escape = function (text) {
+ return text;
+ };
+
+ try {
+ return Mustache.render(template, proxyData(view));
+ } catch (error) {
+ let message = '';
+ if (error instanceof Error) {
+ message = `There was an error expanding one or more placeholders.
+
+The error message was:
+ ${error.message.replace(/ > /g, '.').replace('Missing Mustache data property', 'Unknown property')}`;
+ } else {
+ message = 'Unknown error expanding placeholders.';
+ }
+ message += `
+
+The problem is in:
+ ${template}`;
+ throw Error(message);
+ }
+}
diff --git a/src/Scripting/QueryContext.ts b/src/Scripting/QueryContext.ts
new file mode 100644
index 0000000000..d0ee986706
--- /dev/null
+++ b/src/Scripting/QueryContext.ts
@@ -0,0 +1,30 @@
+import { TasksFile } from './TasksFile';
+
+/**
+ * This interface is part of the implementation of placeholders.
+ * Use {@link makeQueryContext} to make a QueryContext.
+ *
+ * QueryContext is a 'view' to pass in to {@link expandPlaceholders}.
+ *
+ * It provides the following:
+ * `queryContext.query.file` - where query.file is a {@link TasksFile} object.
+ * So it supplies `query.file.path`, `query.file.folder`.
+ */
+export interface QueryContext {
+ query: {
+ file: TasksFile;
+ };
+}
+
+/**
+ * Create a {@link QueryContext} to represent a query in note at the give path.
+ * @param path
+ */
+export function makeQueryContext(path: string): QueryContext {
+ const tasksFile = new TasksFile(path);
+ return {
+ query: {
+ file: tasksFile,
+ },
+ };
+}
diff --git a/src/lib/QueryRendererHelper.ts b/src/lib/QueryRendererHelper.ts
index dc746c9e5e..021a48557b 100644
--- a/src/lib/QueryRendererHelper.ts
+++ b/src/lib/QueryRendererHelper.ts
@@ -20,19 +20,20 @@ import { Query } from '../Query/Query';
* * Explains the query described by {@link source}
*
* @param {string} source The source of the task block to explain
+ * @param {string} path The location of the task block, if known
* @returns {string}
*/
-export function explainResults(source: string): string {
+export function explainResults(source: string, path: string | undefined = undefined): string {
let result = '';
if (!GlobalFilter.isEmpty()) {
result += `Only tasks containing the global filter '${GlobalFilter.get()}'.\n\n`;
}
- const tasksBlockQuery = new Query({ source });
+ const tasksBlockQuery = new Query({ source }, path);
if (!tasksBlockQuery.ignoreGlobalQuery) {
- const globalQuery: IQuery = new Query(getGlobalQuerySource());
+ const globalQuery: IQuery = new Query(getGlobalQuerySource(), path);
if (globalQuery.source.trim() !== '') {
result += `Explanation of the global query:\n\n${globalQuery.explainQuery()}\n`;
@@ -50,11 +51,12 @@ export function explainResults(source: string): string {
* This query is the result of joining the global query with the query in the task block
*
* @param {string} source The query source from the task block
+ * @param {string | undefined} path The path to the file containing the query, if available.
* @returns {Query} The query to execute
*/
-export function getQueryForQueryRenderer(source: string): Query {
- const globalQuery = new Query(getGlobalQuerySource());
- const tasksBlockQuery = new Query({ source });
+export function getQueryForQueryRenderer(source: string, path: string | undefined): Query {
+ const globalQuery = new Query(getGlobalQuerySource(), path);
+ const tasksBlockQuery = new Query({ source }, path);
if (tasksBlockQuery.ignoreGlobalQuery) {
return tasksBlockQuery;
diff --git a/tests/Query.test.ts b/tests/Query.test.ts
index 8d2c6afd6f..59313373d1 100644
--- a/tests/Query.test.ts
+++ b/tests/Query.test.ts
@@ -506,6 +506,62 @@ Problem line: "${source}"`);
Problem line: "${source}"`);
});
});
+
+ describe('parsing placeholders', () => {
+ it('should expand placeholder values in filters, but not source', () => {
+ // Arrange
+ const rawQuery = 'path includes {{query.file.path}}';
+ const path = 'a/b/path with space.md';
+
+ // Act
+ const query = new Query({ source: rawQuery }, path);
+
+ // Assert
+ expect(query.source).toEqual(rawQuery); // Interesting that query.source still has the placeholder text
+ expect(query.filters.length).toEqual(1);
+ expect(query.filters[0].instruction).toEqual('path includes a/b/path with space.md');
+ });
+
+ it('should report error if placeholders used without query location', () => {
+ // Arrange
+ const input = 'path includes {{query.file.path}}';
+
+ // Act
+ const query = new Query({ source: input });
+
+ // Assert
+ expect(query).not.toBeValid();
+ expect(query.error).toEqual(
+ 'The query looks like it contains a placeholder, with "{{" and "}}"\n' +
+ 'but no file path has been supplied, so cannot expand placeholder values.\n' +
+ 'The query is:\n' +
+ 'path includes {{query.file.path}}',
+ );
+ expect(query.filters.length).toEqual(0);
+ });
+
+ it('should report error if non-existent placeholder used', () => {
+ // Arrange
+ const input = 'path includes {{query.file.noSuchProperty}}';
+ const path = 'a/b/path with space.md';
+
+ // Act
+ const query = new Query({ source: input }, path);
+
+ // Assert
+ expect(query).not.toBeValid();
+ expect(query.error).toEqual(
+ 'There was an error expanding one or more placeholders.\n' +
+ '\n' +
+ 'The error message was:\n' +
+ ' Unknown property: query.file.noSuchProperty\n' +
+ '\n' +
+ 'The problem is in:\n' +
+ ' path includes {{query.file.noSuchProperty}}',
+ );
+ expect(query.filters.length).toEqual(0);
+ });
+ });
});
describe('Query', () => {
@@ -1031,6 +1087,17 @@ due 2012-01-23 =>
expect(query.explainQuery()).toEqual(expectedDisplayText);
});
+ it('should include any error message in the explanation', () => {
+ const input = 'i am a nonsense query';
+ const query = new Query({ source: input });
+
+ const expectedDisplayText = `Query has an error:
+do not understand query
+Problem line: "i am a nonsense query"
+`;
+ expect(query.explainQuery()).toEqual(expectedDisplayText);
+ });
+
it('should explain limit 5', () => {
const input = 'limit 5';
const query = new Query({ source: input });
diff --git a/tests/Query/Explain/DocsSamplesForExplain.test.explain_placeholders.approved.explanation.text b/tests/Query/Explain/DocsSamplesForExplain.test.explain_placeholders.approved.explanation.text
new file mode 100644
index 0000000000..6ebedc10da
--- /dev/null
+++ b/tests/Query/Explain/DocsSamplesForExplain.test.explain_placeholders.approved.explanation.text
@@ -0,0 +1,9 @@
+Explanation of this Tasks code block query:
+
+path includes some/sample/file path.md
+
+root includes some/
+
+folder includes some/sample/
+
+filename includes file path.md
diff --git a/tests/Query/Explain/DocsSamplesForExplain.test.explain_placeholders.approved.query.text b/tests/Query/Explain/DocsSamplesForExplain.test.explain_placeholders.approved.query.text
new file mode 100644
index 0000000000..e3f3cb837b
--- /dev/null
+++ b/tests/Query/Explain/DocsSamplesForExplain.test.explain_placeholders.approved.query.text
@@ -0,0 +1,6 @@
+
+explain
+path includes {{query.file.path}}
+root includes {{query.file.root}}
+folder includes {{query.file.folder}}
+filename includes {{query.file.filename}}
diff --git a/tests/Query/Explain/DocsSamplesForExplain.test.explain_placeholders_error.approved.explanation.text b/tests/Query/Explain/DocsSamplesForExplain.test.explain_placeholders_error.approved.explanation.text
new file mode 100644
index 0000000000..e582014b12
--- /dev/null
+++ b/tests/Query/Explain/DocsSamplesForExplain.test.explain_placeholders_error.approved.explanation.text
@@ -0,0 +1,10 @@
+Explanation of this Tasks code block query:
+
+Query has an error:
+There was an error expanding one or more placeholders.
+
+The error message was:
+ Unknown property: query.file.fileName
+
+The problem is in:
+ filename includes {{query.file.fileName}}
diff --git a/tests/Query/Explain/DocsSamplesForExplain.test.explain_placeholders_error.approved.query.text b/tests/Query/Explain/DocsSamplesForExplain.test.explain_placeholders_error.approved.query.text
new file mode 100644
index 0000000000..0041a99683
--- /dev/null
+++ b/tests/Query/Explain/DocsSamplesForExplain.test.explain_placeholders_error.approved.query.text
@@ -0,0 +1,4 @@
+
+# query.file.fileName is invalid, because of the capital N.
+# query.file.filename is the correct property name.
+filename includes {{query.file.fileName}}
diff --git a/tests/Query/Explain/DocsSamplesForExplain.test.ts b/tests/Query/Explain/DocsSamplesForExplain.test.ts
index b360b6079f..56bbe8571f 100644
--- a/tests/Query/Explain/DocsSamplesForExplain.test.ts
+++ b/tests/Query/Explain/DocsSamplesForExplain.test.ts
@@ -90,4 +90,30 @@ explain`;
checkExplainPresentAndVerify(blockQuery);
verifyTaskBlockExplanation(blockQuery);
});
+
+ it('placeholders', () => {
+ // Arrange
+ const instructions: string = `
+explain
+path includes {{query.file.path}}
+root includes {{query.file.root}}
+folder includes {{query.file.folder}}
+filename includes {{query.file.filename}}`;
+
+ // Act, Assert
+ checkExplainPresentAndVerify(instructions);
+ verifyTaskBlockExplanation(instructions);
+ });
+
+ it('placeholders error', () => {
+ // Arrange
+ const instructions: string = `
+# query.file.fileName is invalid, because of the capital N.
+# query.file.filename is the correct property name.
+filename includes {{query.file.fileName}}`;
+
+ // Act, Assert
+ verifyQuery(instructions); // This does not have an explain, so does not call checkExplainPresentAndVerify()
+ verifyTaskBlockExplanation(instructions);
+ });
});
diff --git a/tests/Query/Filter/BacklinkField.test.ts b/tests/Query/Filter/BacklinkField.test.ts
index f5abb5c955..dc89ae554c 100644
--- a/tests/Query/Filter/BacklinkField.test.ts
+++ b/tests/Query/Filter/BacklinkField.test.ts
@@ -76,6 +76,7 @@ describe('grouping by backlink', () => {
'\\_c\\_',
'\\_c\\_ > heading _italic text_',
'a\\_b\\_c',
+ 'b',
'c',
'c > heading',
'Unknown Location',
diff --git a/tests/Query/Filter/FilenameField.test.ts b/tests/Query/Filter/FilenameField.test.ts
index d29a59975b..783239db2f 100644
--- a/tests/Query/Filter/FilenameField.test.ts
+++ b/tests/Query/Filter/FilenameField.test.ts
@@ -126,6 +126,6 @@ describe('grouping by filename', () => {
const grouper = new FilenameField().createNormalGrouper();
// Assert
- expect({ grouper, tasks }).groupHeadingsToBe(['[[_c_]]', '[[a_b_c]]', '[[c]]', 'Unknown Location']);
+ expect({ grouper, tasks }).groupHeadingsToBe(['[[_c_]]', '[[a_b_c]]', '[[b]]', '[[c]]', 'Unknown Location']);
});
});
diff --git a/tests/Query/Filter/FolderField.test.ts b/tests/Query/Filter/FolderField.test.ts
index 3d1265165e..8f1685617d 100644
--- a/tests/Query/Filter/FolderField.test.ts
+++ b/tests/Query/Filter/FolderField.test.ts
@@ -55,6 +55,6 @@ describe('grouping by folder', () => {
const grouper = new FolderField().createNormalGrouper();
// Assert
- expect({ grouper, tasks }).groupHeadingsToBe(['/', 'a/b/', 'a/d/', 'e/d/']);
+ expect({ grouper, tasks }).groupHeadingsToBe(['/', 'a/', 'a/b/', 'a/d/', 'e/d/']);
});
});
diff --git a/tests/Query/Filter/PathField.test.ts b/tests/Query/Filter/PathField.test.ts
index 7725f8821d..f53513dc93 100644
--- a/tests/Query/Filter/PathField.test.ts
+++ b/tests/Query/Filter/PathField.test.ts
@@ -157,6 +157,7 @@ describe('grouping by path', () => {
// Assert
expect({ grouper, tasks }).groupHeadingsToBe([
// Why there is no path for empty path?
+ 'a/b',
'a/b/\\_c\\_',
'a/b/c',
'a/d/c',
diff --git a/tests/Scripting/ExpandPlaceholders.test.ts b/tests/Scripting/ExpandPlaceholders.test.ts
new file mode 100644
index 0000000000..d0573fd176
--- /dev/null
+++ b/tests/Scripting/ExpandPlaceholders.test.ts
@@ -0,0 +1,67 @@
+import { expandPlaceholders } from '../../src/Scripting/ExpandPlaceholders';
+import { makeQueryContext } from '../../src/Scripting/QueryContext';
+
+describe('ExpandTemplate', () => {
+ const path = 'a/b/path with space.md';
+
+ it('hard-coded call', () => {
+ const view = {
+ title: 'Joe',
+ calc: () => 2 + 4,
+ };
+
+ const output = expandPlaceholders('{{ title }} spends {{ calc }}', view);
+ expect(output).toMatchInlineSnapshot('"Joe spends 6"');
+ });
+
+ it('fake query - with file path', () => {
+ const rawString = `path includes {{query.file.path}}
+filename includes {{query.file.filename}}`;
+
+ const queryContext = makeQueryContext(path);
+ expect(expandPlaceholders(rawString, queryContext)).toMatchInlineSnapshot(`
+ "path includes a/b/path with space.md
+ filename includes path with space.md"
+ `);
+ });
+
+ it('should return the input string if no {{ in line', function () {
+ const queryContext = makeQueryContext(path);
+ const line = 'no braces here';
+
+ const result = expandPlaceholders(line, queryContext);
+
+ // This test revealed that Mustache itself returns the input string if no {{ present.
+ expect(Object.is(line, result)).toEqual(true);
+ });
+
+ it('should throw an error if unknown template field used', () => {
+ const view = {
+ title: 'Joe',
+ };
+
+ const source = '{{ title }} spends {{ unknownField }}';
+ expect(() => expandPlaceholders(source, view)).toThrow(`There was an error expanding one or more placeholders.
+
+The error message was:
+ Unknown property: unknownField
+
+The problem is in:
+ {{ title }} spends {{ unknownField }}`);
+ });
+
+ it('should throw an error if unknown template nested field used', () => {
+ const queryContext = makeQueryContext('stuff.md');
+ const source = '{{ query.file.nonsense }}';
+
+ expect(() => expandPlaceholders(source, queryContext)).toThrow(
+ `There was an error expanding one or more placeholders.
+
+The error message was:
+ Unknown property: query.file.nonsense
+
+The problem is in:
+ {{ query.file.nonsense }}`,
+ );
+ });
+});
diff --git a/tests/Scripting/QueryContext.test.ts b/tests/Scripting/QueryContext.test.ts
new file mode 100644
index 0000000000..b46a60688a
--- /dev/null
+++ b/tests/Scripting/QueryContext.test.ts
@@ -0,0 +1,38 @@
+import { TaskBuilder } from '../TestingTools/TaskBuilder';
+import { FilenameField } from '../../src/Query/Filter/FilenameField';
+import { FolderField } from '../../src/Query/Filter/FolderField';
+import { PathField } from '../../src/Query/Filter/PathField';
+import { RootField } from '../../src/Query/Filter/RootField';
+import { makeQueryContext } from '../../src/Scripting/QueryContext';
+
+describe('QueryContext', () => {
+ describe('values should all match their corresponding filters', () => {
+ const path = 'a/b/c.md';
+ const task = new TaskBuilder().path(path).build();
+ const queryContext = makeQueryContext(path);
+
+ it('root', () => {
+ const instruction = `root includes ${queryContext.query.file.root}`;
+ const filter = new RootField().createFilterOrErrorMessage(instruction);
+ expect(filter).toMatchTask(task);
+ });
+
+ it('path', () => {
+ const instruction = `path includes ${queryContext.query.file.path}`;
+ const filter = new PathField().createFilterOrErrorMessage(instruction);
+ expect(filter).toMatchTask(task);
+ });
+
+ it('folder', () => {
+ const instruction = `folder includes ${queryContext.query.file.folder}`;
+ const filter = new FolderField().createFilterOrErrorMessage(instruction);
+ expect(filter).toMatchTask(task);
+ });
+
+ it('filename', () => {
+ const instruction = `filename includes ${queryContext.query.file.filename}`;
+ const filter = new FilenameField().createFilterOrErrorMessage(instruction);
+ expect(filter).toMatchTask(task);
+ });
+ });
+});
diff --git a/tests/Scripting/QueryProperties.test.query_file_properties.approved.md b/tests/Scripting/QueryProperties.test.query_file_properties.approved.md
new file mode 100644
index 0000000000..fd6709cd93
--- /dev/null
+++ b/tests/Scripting/QueryProperties.test.query_file_properties.approved.md
@@ -0,0 +1,11 @@
+
+
+| Field | Type | Example |
+| ----- | ----- | ----- |
+| `query.file.path` | `string` | `'root/sub-folder/file containing query.md'` |
+| `query.file.root` | `string` | `'root/'` |
+| `query.file.folder` | `string` | `'root/sub-folder/'` |
+| `query.file.filename` | `string` | `'file containing query.md'` |
+
+
+
diff --git a/tests/Scripting/QueryProperties.test.ts b/tests/Scripting/QueryProperties.test.ts
new file mode 100644
index 0000000000..ed4e4dc883
--- /dev/null
+++ b/tests/Scripting/QueryProperties.test.ts
@@ -0,0 +1,32 @@
+import { expandPlaceholders } from '../../src/Scripting/ExpandPlaceholders';
+import { makeQueryContext } from '../../src/Scripting/QueryContext';
+
+import { MarkdownTable } from '../TestingTools/VerifyMarkdownTable';
+import { addBackticks, determineExpressionType, formatToRepresentType } from './ScriptingTestHelpers';
+
+describe('query', () => {
+ function verifyFieldDataForReferenceDocs(fields: string[]) {
+ const markdownTable = new MarkdownTable(['Field', 'Type', 'Example']);
+ const path = 'root/sub-folder/file containing query.md';
+ const queryContext = makeQueryContext(path);
+ for (const field of fields) {
+ const value1 = expandPlaceholders('{{' + field + '}}', queryContext);
+ const cells = [
+ addBackticks(field),
+ addBackticks(determineExpressionType(value1)),
+ addBackticks(formatToRepresentType(value1)),
+ ];
+ markdownTable.addRow(cells);
+ }
+ markdownTable.verifyForDocs();
+ }
+
+ it('file properties', () => {
+ verifyFieldDataForReferenceDocs([
+ 'query.file.path',
+ 'query.file.root',
+ 'query.file.folder',
+ 'query.file.filename',
+ ]);
+ });
+});
diff --git a/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.file_properties_task.file.folder_docs.approved.md b/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.file_properties_task.file.folder_docs.approved.md
index 7517c02e93..a718898426 100644
--- a/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.file_properties_task.file.folder_docs.approved.md
+++ b/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.file_properties_task.file.folder_docs.approved.md
@@ -4,7 +4,12 @@
- Find tasks in files in any file in the given folder **only**, and not any sub-folders.
- The equality test, `===`, requires that the trailing slash (`/`) be included.
- ```filter by function task.file.folder.includes("Work/Projects/")```
- - Find tasks in files in any folder **and any sub-folders**.
+ - Find tasks in files in a specific folder **and any sub-folders**.
+- ```filter by function task.file.folder.includes( '{{query.file.folder}}' )```
+ - Find tasks in files in the folder that contains the query **and any sub-folders**.
+ - Note that the placeholder text is expanded to a raw string, so needs to be inside quotes.
+- ```filter by function task.file.folder === '{{query.file.folder}}'```
+ - Find tasks in files in the folder that contains the query only (**not tasks in any sub-folders**).
- ```filter by function task.file.folder.includes("Work/Projects")```
- By leaving off the trailing slash (`/`) this would also find tasks in any file inside folders such as:
- `Work/Projects 2023/`
diff --git a/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.file_properties_task.file.folder_results.approved.txt b/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.file_properties_task.file.folder_results.approved.txt
index bbc1b8770f..ea0916b978 100644
--- a/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.file_properties_task.file.folder_results.approved.txt
+++ b/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.file_properties_task.file.folder_results.approved.txt
@@ -11,13 +11,33 @@ The equality test, `===`, requires that the trailing slash (`/`) be included.
filter by function task.file.folder.includes("Work/Projects/")
-Find tasks in files in any folder **and any sub-folders**.
+Find tasks in files in a specific folder **and any sub-folders**.
=>
- [ ] In Work/Projects/general projects stuff.md
- [ ] In Work/Projects/Detail/detailed.md
====================================================================================
+filter by function task.file.folder.includes( '{{query.file.folder}}' )
+Find tasks in files in the folder that contains the query **and any sub-folders**.
+Note that the placeholder text is expanded to a raw string, so needs to be inside quotes.
+=>
+- [ ] xyz in a/b.md
+- [ ] xyz in a/b/c.md
+- [ ] xyz in a/d/c.md
+- [ ] xyz in a/b/c.md
+- [ ] xyz in a/b/_c_.md
+- [ ] xyz in a/b/_c_.md
+====================================================================================
+
+
+filter by function task.file.folder === '{{query.file.folder}}'
+Find tasks in files in the folder that contains the query only (**not tasks in any sub-folders**).
+=>
+- [ ] xyz in a/b.md
+====================================================================================
+
+
filter by function task.file.folder.includes("Work/Projects")
By leaving off the trailing slash (`/`) this would also find tasks in any file inside folders such as:
`Work/Projects 2023/`
diff --git a/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.ts b/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.ts
index ffeb41009d..427e9f260e 100644
--- a/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.ts
+++ b/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.ts
@@ -232,7 +232,16 @@ describe('file properties', () => {
],
[
'filter by function task.file.folder.includes("Work/Projects/")',
- 'Find tasks in files in any folder **and any sub-folders**',
+ 'Find tasks in files in a specific folder **and any sub-folders**',
+ ],
+ [
+ "filter by function task.file.folder.includes( '{{query.file.folder}}' )",
+ 'Find tasks in files in the folder that contains the query **and any sub-folders**',
+ 'Note that the placeholder text is expanded to a raw string, so needs to be inside quotes.',
+ ],
+ [
+ "filter by function task.file.folder === '{{query.file.folder}}'",
+ 'Find tasks in files in the folder that contains the query only (**not tasks in any sub-folders**)',
],
[
'filter by function task.file.folder.includes("Work/Projects")',
diff --git a/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.file_properties_task.file.filename_results.approved.txt b/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.file_properties_task.file.filename_results.approved.txt
index 0d43a87b80..f4c0fe0845 100644
--- a/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.file_properties_task.file.filename_results.approved.txt
+++ b/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.file_properties_task.file.filename_results.approved.txt
@@ -7,6 +7,7 @@ Like 'group by filename' but does not link to the file.
=>
_c_.md
a_b_c.md
+b.md
c.md
====================================================================================
@@ -18,6 +19,7 @@ Like 'group by backlink' but links to the heading in the file.
[[_c_#heading _italic text_]]
[[#heading]]
[[a_b_c#a_b_c]]
+[[b]]
[[c]]
[[c#c]]
[[c#heading]]
diff --git a/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.file_properties_task.file.folder_results.approved.txt b/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.file_properties_task.file.folder_results.approved.txt
index 580946a83d..f3f5f79e09 100644
--- a/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.file_properties_task.file.folder_results.approved.txt
+++ b/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.file_properties_task.file.folder_results.approved.txt
@@ -6,6 +6,7 @@ group by function task.file.folder
Same as 'group by folder'.
=>
/
+a/
a/b/
a/d/
e/d/
@@ -21,6 +22,7 @@ Here's how it works:
Then the trailing slash is added back, to ensure we do not get an empty string for files in the top level of the vault.
=>
/
+a/
b/
d/
====================================================================================
diff --git a/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.file_properties_task.file.path_docs.approved.md b/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.file_properties_task.file.path_docs.approved.md
index 579ee7512d..39bb1a8159 100644
--- a/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.file_properties_task.file.path_docs.approved.md
+++ b/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.file_properties_task.file.path_docs.approved.md
@@ -2,6 +2,11 @@
- ```group by function task.file.path```
- Like 'group by path' but includes the file extension.
+- ```group by function task.file.path.replace('{{query.file.folder}}', '')```
+ - Group by the task's file path, but remove the query's folder from the group.
+ - For tasks in the query's folder or a sub-folder, this is a nice way of seeing shortened paths.
+ - Note that the placeholder text is expanded to a raw string, so needs to be inside quotes.
+ - This is provided to give ideas: it's a bit of a lazy implementation, as it doesn't check that `'{{query.file.folder}}'` is at the start of the line.
diff --git a/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.file_properties_task.file.path_results.approved.txt b/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.file_properties_task.file.path_results.approved.txt
index 3d6273a5de..435747ff61 100644
--- a/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.file_properties_task.file.path_results.approved.txt
+++ b/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.file_properties_task.file.path_results.approved.txt
@@ -6,9 +6,25 @@ group by function task.file.path
Like 'group by path' but includes the file extension.
=>
a_b_c.md
+a/b.md
a/b/_c_.md
a/b/c.md
a/d/c.md
e/d/c.md
====================================================================================
+
+group by function task.file.path.replace('{{query.file.folder}}', '')
+Group by the task's file path, but remove the query's folder from the group.
+For tasks in the query's folder or a sub-folder, this is a nice way of seeing shortened paths.
+Note that the placeholder text is expanded to a raw string, so needs to be inside quotes.
+This is provided to give ideas: it's a bit of a lazy implementation, as it doesn't check that `'{{query.file.folder}}'` is at the start of the line.
+=>
+a_b_c.md
+b.md
+b/_c_.md
+b/c.md
+d/c.md
+e/d/c.md
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.ts b/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.ts
index 699cbc9803..998e5025e9 100644
--- a/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.ts
+++ b/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.ts
@@ -191,6 +191,13 @@ describe('file properties', () => {
[
// comment to force line break
['group by function task.file.path', "Like 'group by path' but includes the file extension"],
+ [
+ "group by function task.file.path.replace('{{query.file.folder}}', '')",
+ "Group by the task's file path, but remove the query's folder from the group.",
+ "For tasks in the query's folder or a sub-folder, this is a nice way of seeing shortened paths.",
+ 'Note that the placeholder text is expanded to a raw string, so needs to be inside quotes.',
+ "This is provided to give ideas: it's a bit of a lazy implementation, as it doesn't check that `'{{query.file.folder}}'` is at the start of the line.",
+ ],
],
SampleTasks.withAllRootsPathsHeadings(),
],
diff --git a/tests/Scripting/ScriptingReference/VerifyFunctionFieldSamples.ts b/tests/Scripting/ScriptingReference/VerifyFunctionFieldSamples.ts
index c4bac506e4..bc0cffa631 100644
--- a/tests/Scripting/ScriptingReference/VerifyFunctionFieldSamples.ts
+++ b/tests/Scripting/ScriptingReference/VerifyFunctionFieldSamples.ts
@@ -3,6 +3,8 @@ import { FunctionField } from '../../../src/Query/Filter/FunctionField';
import type { Task } from '../../../src/Task';
import { groupHeadingsForTask } from '../../CustomMatchers/CustomMatchersForGrouping';
import { verifyMarkdownForDocs } from '../../TestingTools/VerifyMarkdownTable';
+import { expandPlaceholders } from '../../../src/Scripting/ExpandPlaceholders';
+import { makeQueryContext } from '../../../src/Scripting/QueryContext';
/** For example, 'task.due' */
type TaskPropertyName = string;
@@ -63,7 +65,8 @@ export function verifyFunctionFieldFilterSamplesOnTasks(filters: QueryInstructio
const instruction = filter[0];
const comment = filter.slice(1);
- const filterOrErrorMessage = new FunctionField().createFilterOrErrorMessage(instruction);
+ const expandedInstruction = expandPlaceholders(instruction, makeQueryContext('a/b.md'));
+ const filterOrErrorMessage = new FunctionField().createFilterOrErrorMessage(expandedInstruction);
expect(filterOrErrorMessage).toBeValid();
const filterFunction = filterOrErrorMessage.filterFunction!;
@@ -104,7 +107,11 @@ export function verifyFunctionFieldGrouperSamplesOnTasks(
verifyAll('Results of custom groupers', customGroups, (group) => {
const instruction = group[0];
const comment = group.slice(1);
- const grouper = new FunctionField().createGrouperFromLine(instruction);
+
+ const expandedInstruction = expandPlaceholders(instruction, makeQueryContext('a/b.md'));
+ const grouper = new FunctionField().createGrouperFromLine(expandedInstruction);
+ expect(grouper).not.toBeNull();
+
const headings = groupHeadingsForTask(grouper!, tasks);
return formatQueryAndResultsForApproving(instruction, comment, headings);
});
diff --git a/tests/TestHelpers.ts b/tests/TestHelpers.ts
index cd48b4fcf2..ddf8c831a7 100644
--- a/tests/TestHelpers.ts
+++ b/tests/TestHelpers.ts
@@ -112,6 +112,7 @@ export class SampleTasks {
['', 'heading'],
// no heading supplied
+ ['a/b.md', null],
['a/b/c.md', null],
// File and heading, nominal case
diff --git a/tests/TestingTools/ApprovalTestHelpers.ts b/tests/TestingTools/ApprovalTestHelpers.ts
index 7509284fa3..098af6993e 100644
--- a/tests/TestingTools/ApprovalTestHelpers.ts
+++ b/tests/TestingTools/ApprovalTestHelpers.ts
@@ -73,7 +73,7 @@ export function verifyQueryExplanation(instructions: string, options?: Options):
* @param options
*/
export function verifyTaskBlockExplanation(instructions: string, options?: Options): void {
- const explanation = explainResults(instructions);
+ const explanation = explainResults(instructions, 'some/sample/file path.md');
options = options || new Options();
options = options.forFile().withFileExtention('explanation.text');
diff --git a/tests/lib/QueryRendererHelper.test.ts b/tests/lib/QueryRendererHelper.test.ts
index 81a57df65a..96056ad8c2 100644
--- a/tests/lib/QueryRendererHelper.test.ts
+++ b/tests/lib/QueryRendererHelper.test.ts
@@ -102,16 +102,30 @@ describe('query used for QueryRenderer', () => {
});
it('should be the result of combining the global query and the actual query', () => {
+ // Arrange
const querySource = 'description includes world';
const globalQuerySource = 'description includes hello';
updateSettings({ globalQuery: globalQuerySource });
- expect(getQueryForQueryRenderer(querySource).source).toEqual(`${globalQuerySource}\n${querySource}`);
+ const filePath = 'a/b/c.md';
+
+ // Act
+ const query = getQueryForQueryRenderer(querySource, filePath);
+
+ // Assert
+ expect(query.source).toEqual(`${globalQuerySource}\n${querySource}`);
+ expect(query.filePath).toEqual(filePath);
});
it('should ignore the global query if "ignore global query" is set', () => {
+ // Arrange
updateSettings({ globalQuery: 'path includes from_global_query' });
- expect(getQueryForQueryRenderer('description includes from_block_query\nignore global query').source).toEqual(
- 'description includes from_block_query\nignore global query',
- );
+ const filePath = 'a/b/c.md';
+
+ // Act
+ const query = getQueryForQueryRenderer('description includes from_block_query\nignore global query', filePath);
+
+ // Assert
+ expect(query.source).toEqual('description includes from_block_query\nignore global query');
+ expect(query.filePath).toEqual(filePath);
});
});
diff --git a/yarn.lock b/yarn.lock
index 06c0c7cb1a..107d5277d9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1240,6 +1240,11 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
+"@types/mustache@^4.2.2":
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/@types/mustache/-/mustache-4.2.2.tgz#825bf5c214c3ab84d0b23fef2c8eb898f3ff8717"
+ integrity sha512-MUSpfpW0yZbTgjekDbH0shMYBUD+X/uJJJMm9LXN1d5yjl5lCY1vN/eWKD6D1tOtjA6206K0zcIPnUaFMurdNA==
+
"@types/node@*":
version "18.0.3"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.3.tgz#463fc47f13ec0688a33aec75d078a0541a447199"
@@ -4454,6 +4459,16 @@ ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+mustache-validator@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/mustache-validator/-/mustache-validator-0.2.0.tgz#b9ef770263fe560429a7f90cb5866331f4274cb1"
+ integrity sha512-L4RIboVYTXHFHNWv5Sac3Ru63SUMgDRSY1G7YapeasgMf7IqpYIO/h8f+/5GJJIwcCcfyQXDFffD+VO6/F5f+Q==
+
+mustache@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64"
+ integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==
+
natives@^1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.6.tgz#a603b4a498ab77173612b9ea1acdec4d980f00bb"