Skip to content

Commit 691baa4

Browse files
authored
Merge pull request #3391 from obsidian-tasks-group/refactor-description-render
refactor: Remove need to construct temporary Task objects
2 parents cb44ad0 + 6e9ce47 commit 691baa4

File tree

6 files changed

+148
-54
lines changed

6 files changed

+148
-54
lines changed

resources/sample_vaults/Tasks-Demo/Test Data/internal_heading_links.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,13 @@ This file contains test cases for internal heading links in tasks.
4141
## Escaped Links
4242

4343
- [ ] #task Task with \[\[#Escaped Links\]\] escaped link
44+
45+
## Search
46+
47+
Hover over each heading in these tasks, and to make the Page Preview plugin show that the links exist.
48+
49+
```tasks
50+
path includes {{query.file.path}}
51+
group by heading
52+
hide backlinks
53+
```

src/Obsidian/InlineRenderer.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,11 @@ export class InlineRenderer {
133133
}
134134
const dataLine: string = renderedElement.getAttr('data-line') ?? '0';
135135
const taskIndex: number = Number.parseInt(dataLine, 10);
136-
const taskElement = await taskLineRenderer.renderTaskLine(task, taskIndex);
136+
const taskElement = await taskLineRenderer.renderTaskLine({
137+
task: task,
138+
taskIndex: taskIndex,
139+
isTaskInQueryFile: true,
140+
});
137141

138142
// If the rendered element contains a sub-list or sub-div (e.g. the
139143
// folding arrow), we need to keep it.

src/Renderer/QueryResultsRenderer.ts

Lines changed: 6 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -372,9 +372,12 @@ export class QueryResultsRenderer {
372372
queryRendererParameters: QueryRendererParameters,
373373
) {
374374
const isFilenameUnique = this.isFilenameUnique({ task }, queryRendererParameters.allMarkdownFiles);
375-
const processedTask = this.processTaskLinks(task);
376-
377-
const listItem = await taskLineRenderer.renderTaskLine(processedTask, taskIndex, isFilenameUnique);
375+
const listItem = await taskLineRenderer.renderTaskLine({
376+
task: task,
377+
taskIndex: taskIndex,
378+
isTaskInQueryFile: this.filePath === task.path,
379+
isFilenameUnique: isFilenameUnique,
380+
});
378381

379382
// Remove all footnotes. They don't re-appear in another document.
380383
const footnotes = listItem.querySelectorAll('[data-footnote-id]');
@@ -500,45 +503,6 @@ export class QueryResultsRenderer {
500503
backLink.append(')');
501504
}
502505
}
503-
504-
private processTaskLinks(task: Task): Task {
505-
// Skip if task is from the same file as the query
506-
if (this.filePath === task.path) {
507-
return task;
508-
}
509-
510-
const linkCache = task.file.cachedMetadata.links;
511-
if (!linkCache) return task;
512-
513-
// Find links in the task description
514-
const taskLinks = linkCache.filter((link) => {
515-
return (
516-
link.position.start.line === task.taskLocation.lineNumber &&
517-
task.description.includes(link.original) &&
518-
link.link.startsWith('#')
519-
);
520-
});
521-
if (taskLinks.length === 0) return task;
522-
523-
let description = task.description;
524-
525-
// a task can only be from one file, so we can replace all the internal links
526-
//in the description with the new file path
527-
for (const link of taskLinks) {
528-
const fullLink = `[[${task.path}${link.link}|${link.displayText}]]`;
529-
// Replace the first instance of this link:
530-
description = description.replace(link.original, fullLink);
531-
}
532-
533-
// Return a new Task with the updated description
534-
return new Task({
535-
...task,
536-
description,
537-
// Preserve the original task's location information
538-
taskLocation: task.taskLocation,
539-
});
540-
}
541-
542506
private addPostponeButton(listItem: HTMLElement, task: Task, shortMode: boolean) {
543507
const amount = 1;
544508
const timeUnit = 'day';

src/Renderer/TaskLineRenderer.ts

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -127,18 +127,28 @@ export class TaskLineRenderer {
127127
* @note Output is based on the {@link DefaultTaskSerializer}'s format, with default (emoji) symbols
128128
* @param task The task to be rendered.
129129
* @param taskIndex Task's index in the list. This affects `data-line` data attributes of the list item.
130+
* @param isTaskInQueryFile
130131
* @param isFilenameUnique Whether the name of the file that contains the task is unique in the vault.
131132
* If it is undefined, the outcome will be the same as with a unique file name:
132133
* the file name only. If set to `true`, the full path will be returned.
133134
*/
134-
public async renderTaskLine(task: Task, taskIndex: number, isFilenameUnique?: boolean): Promise<HTMLLIElement> {
135+
public async renderTaskLine({
136+
task,
137+
taskIndex,
138+
isTaskInQueryFile,
139+
isFilenameUnique,
140+
}: {
141+
task: Task;
142+
taskIndex: number;
143+
isTaskInQueryFile: boolean;
144+
isFilenameUnique?: boolean;
145+
}): Promise<HTMLLIElement> {
135146
const li = createAndAppendElement('li', this.parentUlElement);
136-
137147
li.classList.add('task-list-item', 'plugin-tasks-list-item');
138148

139149
const textSpan = createAndAppendElement('span', li);
140150
textSpan.classList.add('tasks-list-text');
141-
await this.taskToHtml(task, textSpan, li);
151+
await this.taskToHtml(task, textSpan, li, isTaskInQueryFile);
142152

143153
// NOTE: this area is mentioned in `CONTRIBUTING.md` under "How does Tasks handle status changes". When
144154
// moving the code, remember to update that reference too.
@@ -193,7 +203,12 @@ export class TaskLineRenderer {
193203
return li;
194204
}
195205

196-
private async taskToHtml(task: Task, parentElement: HTMLElement, li: HTMLLIElement): Promise<void> {
206+
private async taskToHtml(
207+
task: Task,
208+
parentElement: HTMLElement,
209+
li: HTMLLIElement,
210+
isTaskInQueryFile: boolean,
211+
): Promise<void> {
197212
const fieldRenderer = new TaskFieldRenderer();
198213
const emojiSerializer = TASK_FORMATS.tasksPluginEmoji.taskSerializer;
199214
// Render and build classes for all the task's visible components
@@ -212,7 +227,7 @@ export class TaskLineRenderer {
212227
// to differentiate between the container of the text and the text itself, so it will be possible
213228
// to do things like surrounding only the text (rather than its whole placeholder) with a highlight
214229
const internalSpan = createAndAppendElement('span', span);
215-
await this.renderComponentText(internalSpan, componentString, component, task);
230+
await this.renderComponentText(internalSpan, componentString, component, task, isTaskInQueryFile);
216231
this.addInternalClasses(component, internalSpan);
217232

218233
// Add the component's CSS class describing what this component is (priority, due date etc.)
@@ -269,15 +284,17 @@ export class TaskLineRenderer {
269284
componentString: string,
270285
component: TaskLayoutComponent,
271286
task: Task,
287+
isTaskInQueryFile: boolean,
272288
) {
273289
if (component === TaskLayoutComponent.Description) {
274-
return await this.renderDescription(task, span);
290+
return await this.renderDescription(task, span, isTaskInQueryFile);
275291
}
276292
span.innerHTML = componentString;
277293
}
278294

279-
private async renderDescription(task: Task, span: HTMLSpanElement) {
280-
let description = GlobalFilter.getInstance().removeAsWordFromDependingOnSettings(task.description);
295+
private async renderDescription(task: Task, span: HTMLSpanElement, isTaskInQueryFile: boolean) {
296+
let description = this.adjustRelativeLinksInDescription(task, isTaskInQueryFile);
297+
description = GlobalFilter.getInstance().removeAsWordFromDependingOnSettings(description);
281298

282299
const { debugSettings } = getSettings();
283300
if (debugSettings.showTaskHiddenData) {
@@ -314,6 +331,40 @@ export class TaskLineRenderer {
314331
});
315332
}
316333

334+
private adjustRelativeLinksInDescription(task: Task, isTaskInQueryFile: boolean) {
335+
// Skip if task is from the same file as the query
336+
if (isTaskInQueryFile) {
337+
return task.description;
338+
}
339+
340+
// Skip if the task is in a file with no links
341+
const linkCache = task.file.cachedMetadata.links;
342+
if (!linkCache) {
343+
return task.description;
344+
}
345+
346+
// Find links in the task description
347+
const taskLinks = linkCache.filter((link) => {
348+
return (
349+
link.position.start.line === task.taskLocation.lineNumber &&
350+
task.description.includes(link.original) &&
351+
link.link.startsWith('#')
352+
);
353+
});
354+
355+
let description = task.description;
356+
if (taskLinks.length !== 0) {
357+
// a task can only be from one file, so we can replace all the internal links
358+
//in the description with the new file path
359+
for (const link of taskLinks) {
360+
const fullLink = `[[${task.path}${link.link}|${link.displayText}]]`;
361+
// Replace the first instance of this link:
362+
description = description.replace(link.original, fullLink);
363+
}
364+
}
365+
return description;
366+
}
367+
317368
/*
318369
* Adds internal classes for various components (right now just tags actually), meaning that we modify the existing
319370
* rendered element to add classes inside it.

tests/Obsidian/__test_data__/internal_heading_links.json

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,22 @@
192192
"offset": 902
193193
}
194194
}
195+
},
196+
{
197+
"heading": "Search",
198+
"level": 2,
199+
"position": {
200+
"end": {
201+
"col": 9,
202+
"line": 44,
203+
"offset": 988
204+
},
205+
"start": {
206+
"col": 0,
207+
"line": 44,
208+
"offset": 979
209+
}
210+
}
195211
}
196212
],
197213
"links": [
@@ -859,6 +875,51 @@
859875
}
860876
},
861877
"type": "list"
878+
},
879+
{
880+
"position": {
881+
"end": {
882+
"col": 9,
883+
"line": 44,
884+
"offset": 988
885+
},
886+
"start": {
887+
"col": 0,
888+
"line": 44,
889+
"offset": 979
890+
}
891+
},
892+
"type": "heading"
893+
},
894+
{
895+
"position": {
896+
"end": {
897+
"col": 102,
898+
"line": 46,
899+
"offset": 1092
900+
},
901+
"start": {
902+
"col": 0,
903+
"line": 46,
904+
"offset": 990
905+
}
906+
},
907+
"type": "paragraph"
908+
},
909+
{
910+
"position": {
911+
"end": {
912+
"col": 3,
913+
"line": 52,
914+
"offset": 1172
915+
},
916+
"start": {
917+
"col": 0,
918+
"line": 48,
919+
"offset": 1094
920+
}
921+
},
922+
"type": "code"
862923
}
863924
],
864925
"tags": [
@@ -1014,7 +1075,7 @@
10141075
}
10151076
]
10161077
},
1017-
"fileContents": "# Internal Heading Links Test\n\nThis file contains test cases for internal heading links in tasks.\n\n## Simple Headers\n\n## Another Header\n\n## Basic Internal Links\n\n- [ ] #task Task with<br>[[#Basic Internal Links]]\n\n## Multiple Links In One Task\n\n- [ ] #task Task with<br>[[#Multiple Links In One Task]] and<br>[[#Simple Headers]]\n\n## External File Links\n\n- [ ] #task Task with<br>[[Other File]]\n\n## Mixed Link Types\n\n- [ ] #task Task with<br>[[Other File]] and<br>[[#Mixed Link Types]]\n\n## Header Links With File Reference\n\n- [ ] #task<br>[[#Header Links With File Reference]] then<br>[[Other File#Some Header]] and<br>[[#Another Header]]\n\n## Headers-With_Special Characters\n\n- [ ] #task Task with<br>[[#Headers-With_Special Characters]]\n\n## Aliased Links\n\n- [ ] #task Task with<br>[[#Aliased Links|I am an alias]]\n\n## Links In Code Blocks\n\n- [ ] #task Task with `[[#Links In Code Blocks]]` code block\n\n## Escaped Links\n\n- [ ] #task Task with \\[\\[#Escaped Links\\]\\] escaped link\n",
1078+
"fileContents": "# Internal Heading Links Test\n\nThis file contains test cases for internal heading links in tasks.\n\n## Simple Headers\n\n## Another Header\n\n## Basic Internal Links\n\n- [ ] #task Task with<br>[[#Basic Internal Links]]\n\n## Multiple Links In One Task\n\n- [ ] #task Task with<br>[[#Multiple Links In One Task]] and<br>[[#Simple Headers]]\n\n## External File Links\n\n- [ ] #task Task with<br>[[Other File]]\n\n## Mixed Link Types\n\n- [ ] #task Task with<br>[[Other File]] and<br>[[#Mixed Link Types]]\n\n## Header Links With File Reference\n\n- [ ] #task<br>[[#Header Links With File Reference]] then<br>[[Other File#Some Header]] and<br>[[#Another Header]]\n\n## Headers-With_Special Characters\n\n- [ ] #task Task with<br>[[#Headers-With_Special Characters]]\n\n## Aliased Links\n\n- [ ] #task Task with<br>[[#Aliased Links|I am an alias]]\n\n## Links In Code Blocks\n\n- [ ] #task Task with `[[#Links In Code Blocks]]` code block\n\n## Escaped Links\n\n- [ ] #task Task with \\[\\[#Escaped Links\\]\\] escaped link\n\n## Search\n\nHover over each heading in these tasks, and to make the Page Preview plugin show that the links exist.\n\n```tasks\npath includes {{query.file.path}}\ngroup by heading\nhide backlinks\n```\n",
10181079
"filePath": "Test Data/internal_heading_links.md",
10191080
"getAllTags": [
10201081
"#task",

tests/Renderer/TaskLineRenderer.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ async function renderListItem(
4646
taskLayoutOptions: taskLayoutOptions ?? new TaskLayoutOptions(),
4747
queryLayoutOptions: queryLayoutOptions ?? new QueryLayoutOptions(),
4848
});
49-
return await taskLineRenderer.renderTaskLine(task, 0);
49+
return await taskLineRenderer.renderTaskLine({ task: task, taskIndex: 0, isTaskInQueryFile: true });
5050
}
5151

5252
function getTextSpan(listItem: HTMLElement) {
@@ -89,7 +89,11 @@ describe('task line rendering - HTML', () => {
8989
taskLayoutOptions: new TaskLayoutOptions(),
9090
queryLayoutOptions: new QueryLayoutOptions(),
9191
});
92-
const listItem = await taskLineRenderer.renderTaskLine(new TaskBuilder().build(), 0);
92+
const listItem = await taskLineRenderer.renderTaskLine({
93+
task: new TaskBuilder().build(),
94+
taskIndex: 0,
95+
isTaskInQueryFile: true,
96+
});
9397

9498
// Just one element
9599
expect(ulElement.children.length).toEqual(1);

0 commit comments

Comments
 (0)