Repository: obsidian-tasks-group/obsidian-tasks
Pull request #3245 (2024-12-22 22:45:09)

fix: Allow multi-line properties in {{query.file.property('...')}}
# Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

Changes visible to users:

- [x] **Bug fix** (prefix: `fix` - non-breaking change which fixes an issue)

Internal changes:

- [x] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)

## Description

<!--- Remember to provide a general summary of your changes in the Title above -->

<!--- Describe your changes in detail -->

This fixes one of the limitations from PR #3242 - the following now works:

> Only single-line properties work. Multiple-line properties give an error message 'do not understand query'.


Note that continuation lines are not supported in multi-line properties, when they are used in placeholders.

## Motivation and Context

<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->

Getting closer to releasing access to `query.file.property()` and `query.file.hasProperty()` to users...

See:

- #3083
- #3154

## How has this been tested?

- Extra automated tests
- Exploratory testing

## Screenshots (if appropriate)

Consider the following demo sample markdown note.

I have 3 Tasks searches, and they differ only in the `happens` date filter: I have a bunch of standard instructions that I want to use in all 3 searches.

Now, I can put all my standard instructions in a multi-line `common_instructions` property value, and embed its value as a placeholder in each of the 3 searches 🎉 :

~~~
---
common_instructions: |
  tags include #my/lovely/project
  short mode
  show tree
  show urgency
  group by due
---

- [ ] #task Do stuff - ==overdue== #my/lovely/project 📅 2024-12-21
- [ ] #task Do stuff - ==today== #my/lovely/project 📅 2024-12-22
- [ ] #task Do stuff - ==future== #my/lovely/project 📅 2024-12-23

## Overdue tasks

```tasks
{{query.file.property('common_instructions')}}
happens before today
```

## Tasks for today

```tasks
{{query.file.property('common_instructions')}}
happens today
```

## Future tasks

```tasks
{{query.file.property('common_instructions')}}
happens after today
```
~~~

Here are the search results:

![image](https://github.com/user-attachments/assets/9fd0b600-0006-4973-a1a0-d43608e651c9)


## Checklist

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [x] My change requires a change to the documentation.
- [ ] I have [updated the documentation](https://publish.obsidian.md/tasks-contributing/Documentation/About+Documentation) accordingly.
- [x] My change has adequate [Unit Test coverage](https://publish.obsidian.md/tasks-contributing/Testing/About+Testing).

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://publish.obsidian.md/tasks-contributing) and
[Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md).
Put an x in the boxes to confirm you agree.
-->

- [x] My contribution follows this project's [contributing guide](https://publish.obsidian.md/tasks-contributing)
- [x] I agree to follow this project's [Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md)



Patch:
diff --git a/src/Query/Query.ts b/src/Query/Query.ts
index b0b0e5c4..6261826f 100644
--- a/src/Query/Query.ts
+++ b/src/Query/Query.ts
@@ -57,15 +57,24 @@ export class Query implements IQuery {
 
         this.debug(`Creating query: ${this.formatQueryForLogging()}`);
 
-        continueLines(source).forEach((statement: Statement) => {
-            const line = this.expandPlaceholders(statement, tasksFile);
+        const anyContinuationLinesRemoved = continueLines(source);
+
+        const anyPlaceholdersExpanded: Statement[] = [];
+        for (const statement of anyContinuationLinesRemoved) {
+            const expandedStatements = this.expandPlaceholders(statement, tasksFile);
             if (this.error !== undefined) {
                 // There was an error expanding placeholders.
                 return;
             }
+            anyPlaceholdersExpanded.push(...expandedStatements);
+        }
 
+        for (const statement of anyPlaceholdersExpanded) {
             try {
-                this.parseLine(line, statement);
+                this.parseLine(statement);
+                if (this.error !== undefined) {
+                    return;
+                }
             } catch (e) {
                 let message;
                 if (e instanceof Error) {
@@ -77,7 +86,7 @@ export class Query implements IQuery {
                 this.setError(message, statement);
                 return;
             }
-        });
+        }
     }
 
     public get filePath(): string | undefined {
@@ -88,7 +97,8 @@ export class Query implements IQuery {
         return this._queryId;
     }
 
-    private parseLine(line: string, statement: Statement) {
+    private parseLine(statement: Statement) {
+        const line = statement.anyPlaceholdersExpanded;
         switch (true) {
             case this.shortModeRegexp.test(line):
                 this._queryLayoutOptions.shortMode = true;
@@ -126,7 +136,7 @@ export class Query implements IQuery {
         return `[${this.source.split('\n').join(' ; ')}]`;
     }
 
-    private expandPlaceholders(statement: Statement, tasksFile: OptionalTasksFile) {
+    private expandPlaceholders(statement: Statement, tasksFile: OptionalTasksFile): Statement[] {
         const source = statement.anyContinuationLinesRemoved;
         if (source.includes('{{') && source.includes('}}')) {
             if (this.tasksFile === undefined) {
@@ -134,12 +144,11 @@ export class Query implements IQuery {
 but no file path has been supplied, so cannot expand placeholder values.
 The query is:
 ${source}`;
-                return source;
+                return [statement];
             }
         }
 
         // 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 (tasksFile) {
@@ -152,13 +161,36 @@ ${source}`;
                 } else {
                     this._error = 'Internal error. expandPlaceholders() threw something other than Error.';
                 }
-                return source;
+                return [statement];
             }
         }
 
-        // Save any expanded text back in to the statement:
-        statement.recordExpandedPlaceholders(expandedSource);
-        return expandedSource;
+        return this.createStatementsFromExpandedPlaceholders(expandedSource, statement);
+    }
+
+    private createStatementsFromExpandedPlaceholders(expandedSource: string, statement: Statement) {
+        // Trim and filter empty lines in one step.
+        const expandedSourceLines = expandedSource
+            .split('\n')
+            .map((line) => line.trim())
+            .filter((line) => line.length > 0);
+
+        if (expandedSourceLines.length === 1) {
+            // Save the single expanded line back into the statement.
+            statement.recordExpandedPlaceholders(expandedSourceLines[0]);
+            return [statement];
+        }
+
+        // Handle multiple-line placeholders.
+        return expandedSourceLines.map((expandedSourceLine, index) => {
+            const counter = `: statement ${index + 1} after expansion of placeholder`;
+            const newStatement = new Statement(
+                statement.rawInstruction + counter,
+                statement.anyContinuationLinesRemoved + counter,
+            );
+            newStatement.recordExpandedPlaceholders(expandedSourceLine);
+            return newStatement;
+        });
     }
 
     /**
diff --git a/src/Renderer/QueryRenderer.ts b/src/Renderer/QueryRenderer.ts
index 972028ef..38fdd2d3 100644
--- a/src/Renderer/QueryRenderer.ts
+++ b/src/Renderer/QueryRenderer.ts
@@ -41,8 +41,8 @@ export class QueryRenderer {
         //    not yet available, so empty.
         //  - It does not listen out for edits the properties, so if a property is edited,
         //    the user needs to close and re-open the file.
-        //  - Only single-line properties work. Multiple-line properties give an error message
-        //    'do not understand query'.
+        //  - Multi-line properties are supported, but they cannot contain
+        //    continuation lines.
         const app = this.app;
         const filePath = context.sourcePath;
         const tFile = app.vault.getAbstractFileByPath(filePath);


Test patch:
diff --git a/resources/sample_vaults/Tasks-Demo/Test Data/query_using_properties.md b/resources/sample_vaults/Tasks-Demo/Test Data/query_using_properties.md
index b71a44c9..e6bdff92 100644
--- a/resources/sample_vaults/Tasks-Demo/Test Data/query_using_properties.md
+++ b/resources/sample_vaults/Tasks-Demo/Test Data/query_using_properties.md
@@ -6,7 +6,13 @@ task_instruction: group by filename
 task_instructions: |
   group by root
   group by folder
-  group by filename
+    group by filename
+  # a comment
+    # an indented comment
+task_instructions_with_continuation_line: |
+  path \
+    includes query_using_properties
+task_instruction_with_spaces: "  path includes query_using_properties  "
 ---
 
 # query_using_properties
@@ -24,15 +30,41 @@ ignore global query
 limit 10
 ```
 
+## Use a one-line property: task_instruction_with_spaces
+
+Read a Tasks instruction **that is surrounded by extra spaces** from a property in this file, and embed it in to any number of queries in the file:
+
+```tasks
+explain
+ignore global query
+{{query.file.property('task_instruction_with_spaces')}}
+limit 10
+```
+
 ## Use a multi-line property: task_instructions
 
-This fails as the `task_instructions` contains multiple lines , and placeholders are applied after the query is split at line-endings...
+Read multiple Tasks instructions from a property in this file, and embed them in to any number of queries in the file:
 
 ```tasks
 ignore global query
 folder includes Test Data
 explain
 {{query.file.property('task_instructions')}}
+limit 10
+```
+
+## Use a multi-line property: task_instructions_with_continuation_line
+
+Read multiple Tasks instructions with a continuation line from a property in this file, and embed them in to any number of queries in the file.
+
+Continuation lines are currently unsupported in placeholders.
+
+```tasks
+ignore global query
+folder includes Test Data
+explain
+{{query.file.property('task_instructions_with_continuation_line')}}
+limit 10
 ```
 
 ## Use a list property in a custom filter: root_dirs_to_search
diff --git a/tests/Obsidian/__test_data__/query_using_properties.json b/tests/Obsidian/__test_data__/query_using_properties.json
index ce9baf81..cbcfb3bb 100644
--- a/tests/Obsidian/__test_data__/query_using_properties.json
+++ b/tests/Obsidian/__test_data__/query_using_properties.json
@@ -6,14 +6,16 @@
         "Filters/"
       ],
       "task_instruction": "group by filename",
-      "task_instructions": "group by root\ngroup by folder\ngroup by filename\n"
+      "task_instruction_with_spaces": "  path includes query_using_properties  ",
+      "task_instructions": "group by root\ngroup by folder\n  group by filename\n# a comment\n  # an indented comment\n",
+      "task_instructions_with_continuation_line": "path \\\n  includes query_using_properties\n"
     },
     "frontmatterLinks": [],
     "frontmatterPosition": {
       "end": {
         "col": 3,
-        "line": 9,
-        "offset": 165
+        "line": 15,
+        "offset": 369
       },
       "start": {
         "col": 0,
@@ -28,13 +30,13 @@
         "position": {
           "end": {
             "col": 24,
-            "line": 11,
-            "offset": 191
+            "line": 17,
+            "offset": 395
           },
           "start": {
             "col": 0,
-            "line": 11,
-            "offset": 167
+            "line": 17,
+            "offset": 371
           }
         }
       },
@@ -44,13 +46,29 @@
         "position": {
           "end": {
             "col": 44,
-            "line": 15,
-            "offset": 283
+            "line": 21,
+            "offset": 487
           },
           "start": {
             "col": 0,
-            "line": 15,
-            "offset": 239
+            "line": 21,
+            "offset": 443
+          }
+        }
+      },
+      {
+        "heading": "Use a one-line property: task_instruction_with_spaces",
+        "level": 2,
+        "position": {
+          "end": {
+            "col": 56,
+            "line": 32,
+            "offset": 750
+          },
+          "start": {
+            "col": 0,
+            "line": 32,
+            "offset": 694
           }
         }
       },
@@ -60,13 +78,29 @@
         "position": {
           "end": {
             "col": 47,
-            "line": 26,
-            "offset": 537
+            "line": 43,
+            "offset": 1055
           },
           "start": {
             "col": 0,
-            "line": 26,
-            "offset": 490
+            "line": 43,
+            "offset": 1008
+          }
+        }
+      },
+      {
+        "heading": "Use a multi-line property: task_instructions_with_continuation_line",
+        "level": 2,
+        "position": {
+          "end": {
+            "col": 70,
+            "line": 55,
+            "offset": 1369
+          },
+          "start": {
+            "col": 0,
+            "line": 55,
+            "offset": 1299
           }
         }
       },
@@ -76,30 +110,30 @@
         "position": {
           "end": {
             "col": 62,
-            "line": 37,
-            "offset": 852
+            "line": 69,
+            "offset": 1786
           },
           "start": {
             "col": 0,
-            "line": 37,
-            "offset": 790
+            "line": 69,
+            "offset": 1724
           }
         }
       }
     ],
     "listItems": [
       {
-        "parent": -13,
+        "parent": -19,
         "position": {
           "end": {
             "col": 44,
-            "line": 13,
-            "offset": 237
+            "line": 19,
+            "offset": 441
           },
           "start": {
             "col": 0,
-            "line": 13,
-            "offset": 193
+            "line": 19,
+            "offset": 397
           }
         },
         "task": " "
@@ -110,8 +144,8 @@
         "position": {
           "end": {
             "col": 3,
-            "line": 9,
-            "offset": 165
+            "line": 15,
+            "offset": 369
           },
           "start": {
             "col": 0,
@@ -125,13 +159,13 @@
         "position": {
           "end": {
             "col": 24,
-            "line": 11,
-            "offset": 191
+            "line": 17,
+            "offset": 395
           },
           "start": {
             "col": 0,
-            "line": 11,
-            "offset": 167
+            "line": 17,
+            "offset": 371
           }
         },
         "type": "heading"
@@ -140,13 +174,13 @@
         "position": {
           "end": {
             "col": 44,
-            "line": 13,
-            "offset": 237
+            "line": 19,
+            "offset": 441
           },
           "start": {
             "col": 0,
-            "line": 13,
-            "offset": 193
+            "line": 19,
+            "offset": 397
           }
         },
         "type": "list"
@@ -155,13 +189,13 @@
         "position": {
           "end": {
             "col": 44,
-            "line": 15,
-            "offset": 283
+            "line": 21,
+            "offset": 487
           },
           "start": {
             "col": 0,
-            "line": 15,
-            "offset": 239
+            "line": 21,
+            "offset": 443
           }
         },
         "type": "heading"
@@ -170,13 +204,13 @@
         "position": {
           "end": {
             "col": 108,
-            "line": 17,
-            "offset": 393
+            "line": 23,
+            "offset": 597
           },
           "start": {
             "col": 0,
-            "line": 17,
-            "offset": 285
+            "line": 23,
+            "offset": 489
           }
         },
         "type": "paragraph"
@@ -185,13 +219,58 @@
         "position": {
           "end": {
             "col": 3,
-            "line": 24,
-            "offset": 488
+            "line": 30,
+            "offset": 692
           },
           "start": {
             "col": 0,
-            "line": 19,
-            "offset": 395
+            "line": 25,
+            "offset": 599
+          }
+        },
+        "type": "code"
+      },
+      {
+        "position": {
+          "end": {
+            "col": 56,
+            "line": 32,
+            "offset": 750
+          },
+          "start": {
+            "col": 0,
+            "line": 32,
+            "offset": 694
+          }
+        },
+        "type": "heading"
+      },
+      {
+        "position": {
+          "end": {
+            "col": 147,
+            "line": 34,
+            "offset": 899
+          },
+          "start": {
+            "col": 0,
+            "line": 34,
+            "offset": 752
+          }
+        },
+        "type": "paragraph"
+      },
+      {
+        "position": {
+          "end": {
+            "col": 3,
+            "line": 41,
+            "offset": 1006
+          },
+          "start": {
+            "col": 0,
+            "line": 36,
+            "offset": 901
           }
         },
         "type": "code"
@@ -200,13 +279,13 @@
         "position": {
           "end": {
             "col": 47,
-            "line": 26,
-            "offset": 537
+            "line": 43,
+            "offset": 1055
           },
           "start": {
             "col": 0,
-            "line": 26,
-            "offset": 490
+            "line": 43,
+            "offset": 1008
           }
         },
         "type": "heading"
@@ -214,14 +293,14 @@
       {
         "position": {
           "end": {
-            "col": 136,
-            "line": 28,
-            "offset": 675
+            "col": 118,
+            "line": 45,
+            "offset": 1175
           },
           "start": {
             "col": 0,
-            "line": 28,
-            "offset": 539
+            "line": 45,
+            "offset": 1057
           }
         },
         "type": "paragraph"
@@ -230,13 +309,73 @@
         "position": {
           "end": {
             "col": 3,
-            "line": 35,
-            "offset": 788
+            "line": 53,
+            "offset": 1297
           },
           "start": {
             "col": 0,
-            "line": 30,
-            "offset": 677
+            "line": 47,
+            "offset": 1177
+          }
+        },
+        "type": "code"
+      },
+      {
+        "position": {
+          "end": {
+            "col": 70,
+            "line": 55,
+            "offset": 1369
+          },
+          "start": {
+            "col": 0,
+            "line": 55,
+            "offset": 1299
+          }
+        },
+        "type": "heading"
+      },
+      {
+        "position": {
+          "end": {
+            "col": 143,
+            "line": 57,
+            "offset": 1514
+          },
+          "start": {
+            "col": 0,
+            "line": 57,
+            "offset": 1371
+          }
+        },
+        "type": "paragraph"
+      },
+      {
+        "position": {
+          "end": {
+            "col": 61,
+            "line": 59,
+            "offset": 1577
+          },
+          "start": {
+            "col": 0,
+            "line": 59,
+            "offset": 1516
+          }
+        },
+        "type": "paragraph"
+      },
+      {
+        "position": {
+          "end": {
+            "col": 3,
+            "line": 67,
+            "offset": 1722
+          },
+          "start": {
+            "col": 0,
+            "line": 61,
+            "offset": 1579
           }
         },
         "type": "code"
@@ -245,13 +384,13 @@
         "position": {
           "end": {
             "col": 62,
-            "line": 37,
-            "offset": 852
+            "line": 69,
+            "offset": 1786
           },
           "start": {
             "col": 0,
-            "line": 37,
-            "offset": 790
+            "line": 69,
+            "offset": 1724
           }
         },
         "type": "heading"
@@ -260,13 +399,13 @@
         "position": {
           "end": {
             "col": 3,
-            "line": 52,
-            "offset": 1240
+            "line": 84,
+            "offset": 2174
           },
           "start": {
             "col": 0,
-            "line": 39,
-            "offset": 854
+            "line": 71,
+            "offset": 1788
           }
         },
         "type": "code"
@@ -277,20 +416,20 @@
         "position": {
           "end": {
             "col": 11,
-            "line": 13,
-            "offset": 204
+            "line": 19,
+            "offset": 408
           },
           "start": {
             "col": 6,
-            "line": 13,
-            "offset": 199
+            "line": 19,
+            "offset": 403
           }
         },
         "tag": "#task"
       }
     ]
   },
-  "fileContents": "---\nroot_dirs_to_search:\n  - Formats/\n  - Filters/\ntask_instruction: group by filename\ntask_instructions: |\n  group by root\n  group by folder\n  group by filename\n---\n\n# query_using_properties\n\n- [ ] #task Task in 'query_using_properties'\n\n## Use a one-line property: task_instruction\n\nRead a Tasks instruction from a property in this file, and embed it in to any number of queries in the file:\n\n```tasks\nexplain\nignore global query\n{{query.file.property('task_instruction')}}\nlimit 10\n```\n\n## Use a multi-line property: task_instructions\n\nThis fails as the `task_instructions` contains multiple lines , and placeholders are applied after the query is split at line-endings...\n\n```tasks\nignore global query\nfolder includes Test Data\nexplain\n{{query.file.property('task_instructions')}}\n```\n\n## Use a list property in a custom filter: root_dirs_to_search\n\n```tasks\nignore global query\nexplain\n\nfilter by function \\\n    if (!query.file.hasProperty('root_dirs_to_search')) { \\\n        throw Error('Please set the \"root_dirs_to_search\" list property, with each value ending in a backslash...'); \\\n    } \\\n    const roots = query.file.property('root_dirs_to_search'); \\\n    return roots.includes(task.file.root);\n\nlimit groups 5\ngroup by root\n```\n",
+  "fileContents": "---\nroot_dirs_to_search:\n  - Formats/\n  - Filters/\ntask_instruction: group by filename\ntask_instructions: |\n  group by root\n  group by folder\n    group by filename\n  # a comment\n    # an indented comment\ntask_instructions_with_continuation_line: |\n  path \\\n    includes query_using_properties\ntask_instruction_with_spaces: \"  path includes query_using_properties  \"\n---\n\n# query_using_properties\n\n- [ ] #task Task in 'query_using_properties'\n\n## Use a one-line property: task_instruction\n\nRead a Tasks instruction from a property in this file, and embed it in to any number of queries in the file:\n\n```tasks\nexplain\nignore global query\n{{query.file.property('task_instruction')}}\nlimit 10\n```\n\n## Use a one-line property: task_instruction_with_spaces\n\nRead a Tasks instruction **that is surrounded by extra spaces** from a property in this file, and embed it in to any number of queries in the file:\n\n```tasks\nexplain\nignore global query\n{{query.file.property('task_instruction_with_spaces')}}\nlimit 10\n```\n\n## Use a multi-line property: task_instructions\n\nRead multiple Tasks instructions from a property in this file, and embed them in to any number of queries in the file:\n\n```tasks\nignore global query\nfolder includes Test Data\nexplain\n{{query.file.property('task_instructions')}}\nlimit 10\n```\n\n## Use a multi-line property: task_instructions_with_continuation_line\n\nRead multiple Tasks instructions with a continuation line from a property in this file, and embed them in to any number of queries in the file.\n\nContinuation lines are currently unsupported in placeholders.\n\n```tasks\nignore global query\nfolder includes Test Data\nexplain\n{{query.file.property('task_instructions_with_continuation_line')}}\nlimit 10\n```\n\n## Use a list property in a custom filter: root_dirs_to_search\n\n```tasks\nignore global query\nexplain\n\nfilter by function \\\n    if (!query.file.hasProperty('root_dirs_to_search')) { \\\n        throw Error('Please set the \"root_dirs_to_search\" list property, with each value ending in a backslash...'); \\\n    } \\\n    const roots = query.file.property('root_dirs_to_search'); \\\n    return roots.includes(task.file.root);\n\nlimit groups 5\ngroup by root\n```\n",
   "filePath": "Test Data/query_using_properties.md",
   "getAllTags": [
     "#task"
diff --git a/tests/Query/Explain/DocsSamplesForExplain.test.explain_placeholders.approved.explanation.text b/tests/Query/Explain/DocsSamplesForExplain.test.explain_placeholders.approved.explanation.text
index efb69fe9..71df1953 100644
--- a/tests/Query/Explain/DocsSamplesForExplain.test.explain_placeholders.approved.explanation.text
+++ b/tests/Query/Explain/DocsSamplesForExplain.test.explain_placeholders.approved.explanation.text
@@ -13,7 +13,7 @@ Explanation of this Tasks code block query:
   filename includes file path.md
 
   description includes Some Cryptic String {{! Inline comments are removed before search }} =>
-  description includes Some Cryptic String 
+  description includes Some Cryptic String
 
   No grouping instructions supplied.
 
diff --git a/tests/Query/Query.test.ts b/tests/Query/Query.test.ts
index 02ed13c2..94fc2924 100644
--- a/tests/Query/Query.test.ts
+++ b/tests/Query/Query.test.ts
@@ -769,24 +769,49 @@ Problem statement:
             );
             expect(query.filters.length).toEqual(0);
         });
+
+        it('should report first error if non-existent placeholder used', () => {
+            // Arrange
+            const source = `{{error 1}}
+{{error 2}}
+{{error 3}}`;
+            const tasksFile = new TasksFile('a/b/path with space.md');
+
+            // Act
+            const query = new Query(source, tasksFile);
+
+            // 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: error 1\n' +
+                    '\n' +
+                    'The problem is in:\n' +
+                    '    {{error 1}}',
+            );
+            expect(query.filters.length).toEqual(0);
+        });
     });
 
     describe('properties in the query file', () => {
         const file = getTasksFileFromMockData(query_using_properties);
 
+        function makeQueryFromPropertyWithValue(propertyName: string, propertyValue: string) {
+            const source = "{{query.file.property('" + propertyName + "')}}";
+            const query = new Query(source, file);
+
+            expect(file.property(propertyName)).toEqual(propertyValue);
+            return query;
+        }
+
         describe('via placeholders', () => {
             it('should use query.file.property() via placeholder', () => {
-                // Act
-                const source = "{{query.file.property('task_instruction')}}";
-                const query = new Query(source, file);
-
-                // Assert
-                expect(file.property('task_instruction')).toEqual('group by filename');
+                const propertyValue = 'group by filename';
+                const query = makeQueryFromPropertyWithValue('task_instruction', propertyValue);
 
                 expect(query.error).toBeUndefined();
-                expect(query.grouping.length).toEqual(1);
-                expect(query.grouping[0].instruction).toEqual('group by filename');
-
                 expect(query.explainQuery()).toMatchInlineSnapshot(`
                     "No filters supplied. All tasks will match the query.
 
@@ -798,43 +823,61 @@ Problem statement:
                 `);
             });
 
-            it('cannot yet access multi-line property with query.file.property via placeholder', () => {
-                // Act
-                const source = '{{query.file.property("task_instructions")}}';
-                const query = new Query(source, file);
+            it('should use query.file.property() via placeholder that has spaces around value', () => {
+                const propertyValue = '  path includes query_using_properties  ';
+                const query = makeQueryFromPropertyWithValue('task_instruction_with_spaces', propertyValue);
 
-                // This fails because the placeholder is replaced by multiple lines.
-                // And currently, the parsing of lines in Query assumes that placeholders
-                // do not increase the number of lines in the query.
-
-                // Assert
-                expect(file.frontmatter.task_instructions).toEqual(`group by root
-group by folder
-group by filename
-`);
+                expect(query.error).toBeUndefined();
+                expect(query.explainQuery()).toMatchInlineSnapshot(`
+                    "{{query.file.property('task_instruction_with_spaces')}} =>
+                    path includes query_using_properties
 
-                expect(query.error).not.toBeUndefined();
-                expect(query.error).toMatchInlineSnapshot(`
-                    "do not understand query
-                    Problem statement:
-                        {{query.file.property("task_instructions")}} =>
-                        group by root
-                    group by folder
-                    group by filename
+                    No grouping instructions supplied.
 
+                    No sorting instructions supplied.
                     "
                 `);
+            });
 
+            it('should access multi-line property with query.file.property via placeholder', () => {
+                const propertyValue = `group by root
+group by folder
+  group by filename
+# a comment
+  # an indented comment
+`;
+                const query = makeQueryFromPropertyWithValue('task_instructions', propertyValue);
+
+                expect(query.error).toBeUndefined();
                 expect(query.explainQuery()).toMatchInlineSnapshot(`
-                    "Query has an error:
-                    do not understand query
-                    Problem statement:
-                        {{query.file.property("task_instructions")}} =>
-                        group by root
+                    "No filters supplied. All tasks will match the query.
+
+                    {{query.file.property('task_instructions')}}: statement 1 after expansion of placeholder =>
+                    group by root
+
+                    {{query.file.property('task_instructions')}}: statement 2 after expansion of placeholder =>
                     group by folder
+
+                    {{query.file.property('task_instructions')}}: statement 3 after expansion of placeholder =>
                     group by filename
 
+                    No sorting instructions supplied.
+                    "
+                `);
+            });
+
+            it('does not work with continuation lines in multi-line property with query.file.property via placeholder', () => {
+                const propertyValue = `path \\
+  includes query_using_properties
+`;
+                const query = makeQueryFromPropertyWithValue('task_instructions_with_continuation_line', propertyValue);
 
+                expect(query.error).not.toBeUndefined();
+                expect(query.error).toMatchInlineSnapshot(`
+                    "do not understand query
+                    Problem statement:
+                        {{query.file.property('task_instructions_with_continuation_line')}}: statement 1 after expansion of placeholder =>
+                        path \\
                     "
                 `);
             });