Skip to content

Commit 98d4fdd

Browse files
authored
Merge pull request #3437 from ilandikov/feat-include-instruction
feat: Partial implementation of 'include' instruction
2 parents 65442cb + e8425f7 commit 98d4fdd

File tree

4 files changed

+178
-0
lines changed

4 files changed

+178
-0
lines changed

docs/Scripting/Includes.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
publish: true
3+
---
4+
5+
# Includes
6+
7+
<span class="related-pages">#feature/scripting</span>
8+
9+
```text
10+
include my_snippet_from_settings
11+
```
12+
13+
You can only include full lines (valid instructions).

docs/Scripting/Placeholders.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@ Explanation of this Tasks code block query:
7575
7676
It is now possible to use properties in the query file. See [[Obsidian Properties#Using Query Properties in Searches]]
7777

78+
## Using includes.xxx
79+
80+
You can do the following:
81+
82+
```text
83+
{{includes.my_snippet_from_settings}}
84+
```
85+
86+
See also [[Includes]].
87+
7888
## Error checking: invalid variables
7989

8090
If there are any unknown properties in the placeholders, a clear message is written.

src/Query/Query.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export class Query implements IQuery {
5858
private readonly limitRegexp = /^limit (groups )?(to )?(\d+)( tasks?)?/i;
5959

6060
private readonly commentRegexp = /^#.*/;
61+
private readonly includeRegexp = /^include +(.*)/i;
6162

6263
constructor(source: string, tasksFile: OptionalTasksFile = undefined) {
6364
this._queryId = this.generateQueryId(10);
@@ -117,6 +118,9 @@ export class Query implements IQuery {
117118
private parseLine(statement: Statement) {
118119
const line = statement.anyPlaceholdersExpanded;
119120
switch (true) {
121+
case this.includeRegexp.test(line):
122+
this.parseInclude(line, statement);
123+
break;
120124
case this.shortModeRegexp.test(line):
121125
this._queryLayoutOptions.shortMode = true;
122126
this.saveLayoutStatement(statement);
@@ -443,6 +447,24 @@ ${statement.explainStatement(' ')}
443447
return false;
444448
}
445449

450+
private parseInclude(_line: string, _statement: Statement) {
451+
const include = this.includeRegexp.exec(_line);
452+
if (include) {
453+
const includeName = include[1].trim();
454+
const includeValue = getSettings().includes[includeName];
455+
if (!includeValue) {
456+
this.setError(`Cannot find include "${includeName}" in the Tasks settings`, _statement);
457+
return;
458+
}
459+
460+
includeValue.split('\n').forEach((instruction) => {
461+
const statement = new Statement(_statement.rawInstruction, _statement.anyContinuationLinesRemoved);
462+
statement.recordExpandedPlaceholders(instruction);
463+
this.parseLine(statement);
464+
});
465+
}
466+
}
467+
446468
/**
447469
* Creates a unique ID for correlation of console logging.
448470
*

tests/Scripting/Includes.test.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
5+
import moment from 'moment';
16
import { getSettings, resetSettings, updateSettings } from '../../src/Config/Settings';
27
import { Query } from '../../src/Query/Query';
38
import { TasksFile } from '../../src/Scripting/TasksFile';
49

10+
window.moment = moment;
11+
12+
beforeEach(() => {
13+
jest.useFakeTimers();
14+
jest.setSystemTime(new Date('2025-04-28'));
15+
});
16+
517
afterEach(() => {
618
resetSettings();
719
});
@@ -17,6 +29,127 @@ describe('include tests', () => {
1729
expect(query.filters.length).toEqual(1);
1830
expect(query.filters[0].statement.anyPlaceholdersExpanded).toEqual('not done');
1931
});
32+
33+
it('should accept whole-line include filter instruction', () => {
34+
updateSettings({ includes: { not_done: 'not done' } });
35+
36+
const source = 'include not_done';
37+
const query = new Query(source, new TasksFile('stuff.md'));
38+
39+
expect(query.error).toBeUndefined();
40+
expect(query.source).toEqual('include not_done');
41+
expect(query.filters.length).toEqual(1);
42+
expect(query.filters[0].statement.anyPlaceholdersExpanded).toEqual('not done');
43+
});
44+
45+
it('should accept whole-line include layout instruction', () => {
46+
updateSettings({ includes: { show_tree: 'show tree' } });
47+
48+
const source = 'include show_tree';
49+
const query = new Query(source, new TasksFile('stuff.md'));
50+
51+
expect(query.error).toBeUndefined();
52+
expect(query.source).toEqual('include show_tree');
53+
expect(query.queryLayoutOptions.hideTree).toEqual(false);
54+
expect(query.layoutStatements[0].anyPlaceholdersExpanded).toEqual('show tree');
55+
});
56+
57+
it('should accept multi-line include', () => {
58+
updateSettings({ includes: { multi_line: 'scheduled tomorrow\nhide backlink' } });
59+
60+
const source = 'include multi_line';
61+
const query = new Query(source, new TasksFile('stuff.md'));
62+
63+
expect(query.error).toBeUndefined();
64+
65+
expect(query.filters.length).toEqual(1);
66+
expect(query.filters[0].statement.anyPlaceholdersExpanded).toEqual('scheduled tomorrow');
67+
68+
expect(query.queryLayoutOptions.hideBacklinks).toEqual(true);
69+
expect(query.layoutStatements[0].anyPlaceholdersExpanded).toEqual('hide backlink');
70+
});
71+
72+
it('should give a meaningful error for non-existent include', () => {
73+
updateSettings({ includes: {} });
74+
75+
const source = 'include not_existent';
76+
const query = new Query(source, new TasksFile('stuff.md'));
77+
78+
expect(query.error).toMatchInlineSnapshot(`
79+
"Cannot find include "not_existent" in the Tasks settings
80+
Problem line: "include not_existent""
81+
`);
82+
expect(query.source).toEqual('include not_existent');
83+
});
84+
85+
it('should support nested include instructions', () => {
86+
updateSettings({
87+
includes: {
88+
inside: 'not done',
89+
out: 'include inside\nhide edit button',
90+
},
91+
});
92+
93+
const source = 'include out';
94+
const query = new Query(source, new TasksFile('stuff.md'));
95+
96+
expect(query.error).toBeUndefined();
97+
expect(query.source).toEqual('include out');
98+
99+
expect(query.filters.length).toEqual(1);
100+
expect(query.filters[0].statement.anyPlaceholdersExpanded).toEqual('not done');
101+
102+
expect(query.queryLayoutOptions.hideEditButton).toEqual(true);
103+
expect(query.layoutStatements[0].anyPlaceholdersExpanded).toEqual('hide edit button');
104+
});
105+
106+
it('should explain two levels of nested includes', () => {
107+
updateSettings({
108+
includes: {
109+
inside: '(happens this week) AND (starts before today)',
110+
out: 'include inside\nnot done',
111+
},
112+
});
113+
114+
const source = 'include out';
115+
const query = new Query(source, new TasksFile('stuff.md'));
116+
117+
expect(query.explainQuery()).toMatchInlineSnapshot(`
118+
"include out =>
119+
(happens this week) AND (starts before today) =>
120+
AND (All of):
121+
happens this week =>
122+
due, start or scheduled date is between:
123+
2025-04-28 (Monday 28th April 2025) and
124+
2025-05-04 (Sunday 4th May 2025) inclusive
125+
starts before today =>
126+
start date is before 2025-04-28 (Monday 28th April 2025) OR no start date
127+
128+
include out =>
129+
not done
130+
"
131+
`);
132+
});
133+
134+
it('should give meaningful error message about included text', () => {
135+
updateSettings({
136+
includes: {
137+
inside: 'apple sauce',
138+
out: 'include inside',
139+
},
140+
});
141+
142+
const source = 'include out';
143+
const query = new Query(source, new TasksFile('stuff.md'));
144+
145+
expect(query.error).toMatchInlineSnapshot(`
146+
"do not understand query
147+
Problem statement:
148+
include out =>
149+
apple sauce
150+
"
151+
`);
152+
});
20153
});
21154

22155
describe('include settings tests', () => {

0 commit comments

Comments
 (0)