Skip to content

Commit a339554

Browse files
authored
Merge pull request #3555 from obsidian-tasks-group/store-destination-path
feat: Add `Link.destinationPath`
2 parents 3a85ddd + 4805cbd commit a339554

File tree

111 files changed

+2913
-113
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

111 files changed

+2913
-113
lines changed

resources/sample_vaults/Tasks-Demo/How To/Access links.md

Lines changed: 111 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
---
22
TQ_short_mode: true
3-
TQ_extra_instructions: |-
4-
group by folder
5-
group by function task.file.filenameWithoutExtension
3+
TQ_extra_instructions:
64
---
75

86
# Accessing Links
@@ -16,6 +14,14 @@ TQ_extra_instructions: |-
1614

1715
### `task.outlinks`: Task lines containing an outgoing link
1816

17+
````text
18+
```tasks
19+
# Task line has a link
20+
filter by function task.outlinks.length > 0
21+
limit groups 1
22+
```
23+
````
24+
1925
```tasks
2026
# Task line has a link
2127
filter by function task.outlinks.length > 0
@@ -24,32 +30,132 @@ limit groups 1
2430

2531
### `task.file.outlinksInProperties`: Tasks in files whose properties/frontmatter contains a link
2632

33+
````text
34+
```tasks
35+
filter by function task.file.outlinksInProperties.length > 0
36+
limit groups 1
37+
```
38+
````
39+
2740
```tasks
2841
filter by function task.file.outlinksInProperties.length > 0
2942
limit groups 1
3043
```
3144

3245
### `task.file.outlinksInBody`: Tasks in files whose markdown body contains a link
3346

47+
````text
48+
```tasks
49+
filter by function task.file.outlinksInBody.length > 0
50+
limit groups 1
51+
```
52+
````
53+
3454
```tasks
3555
filter by function task.file.outlinksInBody.length > 0
3656
limit groups 1
3757
```
3858

3959
### `task.file.outlinks`: Tasks in files whose file contains a link anywhere
4060

61+
````text
62+
```tasks
63+
filter by function task.file.outlinks.length > 0
64+
limit groups 1
65+
```
66+
````
67+
4168
```tasks
4269
filter by function task.file.outlinks.length > 0
4370
limit groups 1
4471
```
4572

73+
### Task lines that contain broken links
74+
75+
````text
76+
```tasks
77+
filter by function task.outlinks.some(link => link.destinationPath === null)
78+
```
79+
````
80+
81+
```tasks
82+
filter by function task.outlinks.some(link => link.destinationPath === null)
83+
```
84+
85+
### Tasks lines that link to the file containing the query
86+
87+
#### Tasks - version 1
88+
89+
This should match one task, in [[Link to Access links file]].
90+
91+
````text
92+
```tasks
93+
filter by function task.outlinks.some(link => link.destinationPath === query.file.path)
94+
```
95+
````
96+
97+
```tasks
98+
filter by function task.outlinks.some(link => link.destinationPath === query.file.path)
99+
```
100+
101+
#### Tasks - version 2
102+
103+
This should match one task, in [[Link to Access links file]].
104+
105+
There is a bug: this does not yet find the task it should do.
106+
107+
````text
108+
```tasks
109+
filter by function task.outlinks.some(link => link.isLinkTo(query.file))
110+
```
111+
````
112+
113+
```tasks
114+
filter by function task.outlinks.some(link => link.isLinkTo(query.file))
115+
```
116+
117+
#### Dataview version
118+
119+
````text
120+
```dataview
121+
TASK
122+
WHERE contains(file.outlinks, this.file.link)
123+
```
124+
````
125+
126+
```dataview
127+
TASK
128+
WHERE contains(file.outlinks, this.file.link)
129+
```
130+
46131
## Grouping
47132

48133
### Group by task outlinks
49134

135+
````text
50136
```tasks
51-
# Task line has a link
52137
filter by function task.outlinks.length > 0
53138
group by function task.outlinks.map(link => link.markdown).sort().join(' · ')
54-
limit groups 1
139+
```
140+
````
141+
142+
```tasks
143+
filter by function task.outlinks.length > 0
144+
group by function task.outlinks.map(link => link.markdown).sort().join(' · ')
145+
```
146+
147+
### Group tasks by the files they link to
148+
149+
The value of `link.destinationPath` is null if the link is broken.
150+
151+
````text
152+
```tasks
153+
filter by function task.outlinks.length > 0
154+
group by function task.outlinks.map(link => link.destinationPath).sort().join(' · ')
155+
```
156+
````
157+
158+
```tasks
159+
filter by function task.outlinks.length > 0
160+
group by function task.outlinks.map(link => link.destinationPath).sort().join(' · ')
55161
```
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Link to Access links file
2+
3+
- [ ] #task I link to [[Access links]]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# markdownLink
2+
3+
This file exists to ensure all links in [[all_link_types]] exist.
4+
5+
## heading
6+
7+
### sub-heading
8+
9+
block ^block
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# wikilink
2+
3+
This file exists to ensure all links in [[all_link_types]] exist.
4+
5+
## heading
6+
7+
### sub-heading
8+
9+
block ^block
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# all_link_types
2+
3+
1. [ ] #task 1 in 'all_link_types' [markdownLink](markdownLink.md)
4+
2. [ ] #task 2 in 'all_link_types' [markdownLink with alias](markdownLink.md)
5+
6+
3. [ ] #task 3 in 'all_link_types' [[wikilink]]
7+
4. [ ] #task 4 in 'all_link_types' [[wikilink|wikilink with alias]]
8+
9+
5. [ ] #task 5 in 'all_link_types' [[#heading]]
10+
6. [ ] #task 6 in 'all_link_types' [[#heading|internal wikilink with alias and heading]]
11+
7. [ ] #task 7 in 'all_link_types' [[#heading#sub-heading]]
12+
8. [ ] #task 7 in 'all_link_types' [[#heading#sub-heading||internal wikilink with alias and two headings]]
13+
14+
9. [ ] #task 8 in 'all_link_types' [[wikilink#heading]]
15+
10. [ ] #task 9 in 'all_link_types' [[wikilink#heading|wikilink with alias and heading]]
16+
11. [ ] #task 10 in 'all_link_types' [[wikilink#heading#sub-heading]]
17+
12. [ ] #task 11 in 'all_link_types' [[wikilink#heading#sub-heading|wikilink with alias and two headings]]
18+
19+
13. [ ] #task 12 in 'all_link_types' [markdownLink](markdownLink.md#heading)
20+
14. [ ] #task 13 in 'all_link_types' [markdownLink with alias and heading](markdownLink.md#heading)
21+
15. [ ] #task 14 in 'all_link_types' [markdownLink](markdownLink.md#heading#sub-heading)
22+
16. [ ] #task 15 in 'all_link_types' [markdownLink with alias and two headings](markdownLink.md#heading#sub-heading)
23+
24+
17. [ ] #task 16 in 'all_link_types' [[#^block]]
25+
18. [ ] #task 17 in 'all_link_types' [[#^block|wikilink with alias and block]]
26+
27+
19. [ ] #task 18 in 'all_link_types' [[wikilink#^block]]
28+
20. [ ] #task 19 in 'all_link_types' [[wikilink#^block|wikilink with alias and block]]
29+
30+
21. [ ] #task 20 in 'all_link_types' [markdownLink](markdownLink.md#^block)
31+
22. [ ] #task 21 in 'all_link_types' [markdownLink with alias and block](markdownLink.md#^block)
32+
33+
## heading
34+
35+
### sub-heading
36+
37+
block ^block
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# embed_link_in_task
2+
3+
- [ ] #task Task in 'embed_link_in_task' ![[empty_yaml]]

resources/sample_vaults/Tasks-Demo/_meta/templater_scripts/convert_test_data_markdown_to_js.js

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const fs = require('node:fs');
22
const path = require('node:path');
33

44
const vault = app.vault;
5+
const metadataCache = app.metadataCache;
56

67
async function getMarkdownFiles() {
78
// Get all files from Test Data/ directory
@@ -13,6 +14,12 @@ function getBasename(filePath) {
1314
return path.basename(filePath, '.md');
1415
}
1516

17+
function writeDataAsJson(outputPath, sortedData) {
18+
const testSourceFile = getOutputFilePath(outputPath);
19+
const content = JSON.stringify(sortedData, null, 2);
20+
writeFile(testSourceFile, content);
21+
}
22+
1623
function getOutputFilePath(outputFile) {
1724
const rootOfVault = vault.adapter.getBasePath();
1825
return path.join(rootOfVault, '../../../tests/Obsidian', outputFile);
@@ -48,6 +55,21 @@ function sortObjectKeys(obj) {
4855
return obj;
4956
}
5057

58+
/**
59+
* Sorts an object's keys in alphabetical order at the top level only.
60+
*/
61+
function sortTopLevelKeys(obj) {
62+
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
63+
return Object.keys(obj)
64+
.sort()
65+
.reduce((acc, key) => {
66+
acc[key] = obj[key]; // No recursive call here
67+
return acc;
68+
}, {});
69+
}
70+
return obj;
71+
}
72+
5173
async function convertMarkdownFileToTestFunction(filePath, tp) {
5274
const tFile = vault.getAbstractFileByPath(filePath);
5375

@@ -63,7 +85,24 @@ async function convertMarkdownFileToTestFunction(filePath, tp) {
6385

6486
const getAllTags = tp.obsidian.getAllTags(cachedMetadata);
6587
const parseFrontMatterTags = tp.obsidian.parseFrontMatterTags(cachedMetadata.frontmatter);
66-
const data = { filePath, fileContents, cachedMetadata, getAllTags, parseFrontMatterTags };
88+
89+
// Resolve all links in body of this file
90+
const allLinks = [...(cachedMetadata.links ?? []), ...(cachedMetadata.frontmatterLinks ?? [])];
91+
const resolveLinkToPath = {};
92+
allLinks.forEach((link) => {
93+
const linkpath = tp.obsidian.getLinkpath(link.link);
94+
const tFile = app.metadataCache.getFirstLinkpathDest(linkpath, filePath);
95+
resolveLinkToPath[link.link] = tFile ? tFile.path : null;
96+
});
97+
98+
const data = {
99+
filePath,
100+
fileContents,
101+
cachedMetadata,
102+
getAllTags,
103+
parseFrontMatterTags,
104+
resolveLinkToPath,
105+
};
67106

68107
const filename = getBasename(filePath);
69108
if (filename.includes(' ')) {
@@ -77,12 +116,10 @@ async function convertMarkdownFileToTestFunction(filePath, tp) {
77116
return '';
78117
}
79118

80-
const testSourceFile = getOutputFilePath(`__test_data__/${filename}.json`);
81-
82119
// Sort keys in the data object to ensure stable order
83120
const sortedData = sortObjectKeys(data);
84-
const content = JSON.stringify(sortedData, null, 2);
85-
writeFile(testSourceFile, content);
121+
122+
writeDataAsJson(`__test_data__/${filename}.json`, sortedData);
86123
}
87124

88125
async function writeListOfAllTestFunctions(files) {
@@ -118,6 +155,12 @@ ${functions.join('\n')}
118155
writeFile(testSourceFile, content);
119156
}
120157

158+
function writeMetadataCacheData() {
159+
const outputPath = '__test_data__/metadataCache/';
160+
writeDataAsJson(outputPath + 'resolvedLinks.json', sortTopLevelKeys(metadataCache.resolvedLinks));
161+
writeDataAsJson(outputPath + 'unresolvedLinks.json', sortTopLevelKeys(metadataCache.unresolvedLinks));
162+
}
163+
121164
async function export_files(tp) {
122165
const markdownFiles = await getMarkdownFiles();
123166

@@ -127,6 +170,8 @@ async function export_files(tp) {
127170

128171
await writeListOfAllTestFunctions(markdownFiles);
129172

173+
writeMetadataCacheData();
174+
130175
showNotice('Success.');
131176
return '';
132177
}

src/Scripting/TasksFile.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { type CachedMetadata, type FrontMatterCache, getAllTags, parseFrontMatterTags } from 'obsidian';
2-
import { Link } from '../Task/Link';
1+
import { type CachedMetadata, type FrontMatterCache, type Reference, getAllTags, parseFrontMatterTags } from 'obsidian';
2+
import type { Link } from '../Task/Link';
3+
import { LinkResolver } from '../Task/LinkResolver';
34

45
export type OptionalTasksFile = TasksFile | undefined;
56

@@ -13,6 +14,9 @@ export class TasksFile {
1314
private readonly _frontmatter = { tags: [] } as any;
1415
private readonly _tags: string[] = [];
1516

17+
private readonly _outlinksInProperties: Readonly<Link[]> = [];
18+
private readonly _outlinksInBody: Readonly<Link[]> = [];
19+
1620
constructor(path: string, cachedMetadata: CachedMetadata = {}) {
1721
this._path = path;
1822
this._cachedMetadata = cachedMetadata;
@@ -22,13 +26,19 @@ export class TasksFile {
2226
this._frontmatter = JSON.parse(JSON.stringify(rawFrontmatter));
2327
this._frontmatter.tags = parseFrontMatterTags(rawFrontmatter) ?? [];
2428
}
29+
this._outlinksInProperties = this.createLinks(this.cachedMetadata.frontmatterLinks);
30+
this._outlinksInBody = this.createLinks(this.cachedMetadata.links);
2531

2632
if (Object.keys(cachedMetadata).length !== 0) {
2733
const tags = getAllTags(this.cachedMetadata) ?? [];
2834
this._tags = [...new Set(tags)];
2935
}
3036
}
3137

38+
private createLinks(obsidianRawLinks: Reference[] | undefined) {
39+
return obsidianRawLinks?.map((link) => LinkResolver.getInstance().resolve(link, this.path)) ?? [];
40+
}
41+
3242
/**
3343
* Return the path to the file.
3444
*/
@@ -60,15 +70,15 @@ export class TasksFile {
6070
/**
6171
* Return an array of {@link Link} in the file's properties/frontmatter.
6272
*/
63-
get outlinksInProperties(): Link[] {
64-
return this.cachedMetadata.frontmatterLinks?.map((link) => new Link(link, this.path)) ?? [];
73+
get outlinksInProperties(): Readonly<Link[]> {
74+
return this._outlinksInProperties;
6575
}
6676

6777
/**
6878
* Return an array of {@link Link} in the body of the file.
6979
*/
70-
get outlinksInBody(): Link[] {
71-
return this.cachedMetadata?.links?.map((link) => new Link(link, this.path)) ?? [];
80+
get outlinksInBody(): Readonly<Link[]> {
81+
return this._outlinksInBody;
7282
}
7383

7484
/**

0 commit comments

Comments
 (0)