Skip to content

Commit 7b532b0

Browse files
authored
refactor: Simplify BacklinkField & add test for #1927 (#1949)
* test: Add test to show that #1927 is already fixed It was fixed in f3c6a41. * test: Fix fromLine() test function behaviour when no heading The actual behaviour of the Tasks plugin when there is no available preceding heading is to use null, not an empty string. This makes the test helper fromLine() in TestHelpers.ts behave the same way. All tests still pass. (TestBuilder already used null as the default heading, so no extra work was needed there.) * test: Add full test coverage for Task backlinks code * test: BacklinkField.test.ts heading types now match those in Task Task allows heading to be string or null, not string or undefined. * test: BacklinkField.test.ts path type now matches that in Task Task only allows path to be string, not null or undefined. * refactor: Simplify BacklinkField.value() Now the test types for BacklinkField match the types used in Task, it turns out we can use Task.getLinkText() directly and all tests pass. * refactor: Simplify BacklinkField.grouper() This avoids doing substring calculations, which makes the code a bit easier to reason about. * test: Fix names of tests - heading was accidentally labelled path * test: Format test code to match the start of this branch To make the pull request easier to review. * comment: Clarify descriptive comments in grouper() * comment: Clarify some comments
1 parent 05481b6 commit 7b532b0

File tree

4 files changed

+97
-34
lines changed

4 files changed

+97
-34
lines changed

src/Query/Filter/BacklinkField.ts

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { Task } from '../../Task';
22
import type { GrouperFunction } from '../Grouper';
33
import { TextField } from './TextField';
4-
import { HeadingField } from './HeadingField';
54
import { FilterOrErrorMessage } from './Filter';
65

76
export class BacklinkField extends TextField {
@@ -14,26 +13,7 @@ export class BacklinkField extends TextField {
1413
if (linkText === null) {
1514
return 'Unknown Location';
1615
}
17-
18-
let filenameComponent = 'Unknown Location';
19-
20-
if (task.filename !== null) {
21-
filenameComponent = task.filename;
22-
}
23-
24-
if (task.precedingHeader === null || task.precedingHeader.length === 0) {
25-
return filenameComponent;
26-
}
27-
28-
// Markdown characters in the heading must NOT be escaped.
29-
const headingGrouper = new HeadingField().createGrouper().grouper;
30-
const headingComponent = headingGrouper(task)[0];
31-
32-
if (filenameComponent === headingComponent) {
33-
return filenameComponent;
34-
} else {
35-
return `${filenameComponent} > ${headingComponent}`;
36-
}
16+
return linkText;
3717
}
3818

3919
createFilterOrErrorMessage(line: string): FilterOrErrorMessage {
@@ -51,15 +31,19 @@ export class BacklinkField extends TextField {
5131
public grouper(): GrouperFunction {
5232
return (task: Task) => {
5333
const filename = task.filename;
54-
if (filename !== null) {
55-
const backlink = this.value(task);
56-
if (filename !== backlink) {
57-
// In case backlink is 'file_name > heading', the filename only shall be escaped
58-
return [TextField.escapeMarkdownCharacters(filename) + backlink.substring(filename.length)];
59-
}
34+
if (filename === null) {
35+
return ['Unknown Location'];
6036
}
6137

62-
return [TextField.escapeMarkdownCharacters(this.value(task))];
38+
// Always escape the filename, to prevent any underscores being interpreted as markdown:
39+
let result = TextField.escapeMarkdownCharacters(filename);
40+
41+
// Only append the heading if it differs from the un-escaped fileanme:
42+
if (task.precedingHeader && task.precedingHeader !== filename) {
43+
// We don't escape the heading, as any markdown characters in it really are markdown styling:
44+
result += ' > ' + task.precedingHeader;
45+
}
46+
return [result];
6347
};
6448
}
6549
}

tests/Query/Filter/BacklinkField.test.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,25 +35,28 @@ describe('grouping by backlink', () => {
3535

3636
it.each([
3737
// no location supplied
38-
[undefined, 'heading', ['Unknown Location']],
38+
['', 'heading', ['Unknown Location']],
3939

4040
// no heading supplied
41-
['a/b/c.md', undefined, ['c']],
41+
['a/b/c.md', null, ['c']],
4242

4343
// File and heading, nominal case
4444
['a/b/c.md', 'heading', ['c > heading']],
4545

4646
// If file name and heading are identical, avoid duplication ('c > c')
4747
['a/b/c.md', 'c', ['c']],
4848

49+
// If file name and heading are identical, avoid duplication, even if there are underscores in the file name
50+
['a_b_c.md', 'a_b_c', ['a\\_b\\_c']],
51+
4952
// Underscores in file name component are escaped
50-
['a/b/_c_.md', undefined, ['\\_c\\_']],
53+
['a/b/_c_.md', null, ['\\_c\\_']],
5154

5255
// But underscores in the heading component are not
5356
['a/b/_c_.md', 'heading _italic text_', ['\\_c\\_ > heading _italic text_']],
5457
])(
55-
'task "%s" with path "%s" should have groups: %s',
56-
(path: string | undefined, heading: string | undefined, groups: string[]) => {
58+
'path "%s" and heading "%s" should have groups: %s',
59+
(path: string, heading: string | null, groups: string[]) => {
5760
// Arrange
5861
const grouper = new BacklinkField().createGrouper().grouper;
5962
const t = '- [ ] xyz';

tests/Task.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,82 @@ describe('parsing tags', () => {
414414
);
415415
});
416416

417+
describe('backlinks', () => {
418+
function shouldGiveLinkText(
419+
path: string,
420+
heading: string | null,
421+
filenameUnique: boolean,
422+
expected: string | null,
423+
) {
424+
expect(
425+
new TaskBuilder()
426+
.path(path)
427+
.precedingHeader(heading)
428+
.build()
429+
.getLinkText({ isFilenameUnique: filenameUnique }),
430+
).toEqual(expected);
431+
}
432+
433+
describe('valid and unique paths', () => {
434+
it('valid, unique path without heading should use filename as backlink', () => {
435+
shouldGiveLinkText('a/b/c.md', null, true, 'c');
436+
});
437+
438+
it('valid, unique path with different heading should use filename and heading as backlink', () => {
439+
shouldGiveLinkText('a/b/c.md', 'heading', true, 'c > heading');
440+
});
441+
442+
it('valid, unique path with same heading should use just filename as backlink', () => {
443+
shouldGiveLinkText('a/b/c.md', 'c', true, 'c');
444+
});
445+
});
446+
447+
describe('valid non-unique paths', () => {
448+
it('valid, non-unique path without heading should use full path as backlink', () => {
449+
shouldGiveLinkText('a/b/c.md', null, false, '/a/b/c.md');
450+
});
451+
452+
it('valid, non-unique path with different heading should use full path and heading as backlink', () => {
453+
shouldGiveLinkText('a/b/c.md', 'heading', false, '/a/b/c.md > heading');
454+
});
455+
456+
it('valid, non-unique path with same heading as file name should use full path and heading as backlink', () => {
457+
shouldGiveLinkText('a/b/c.md', 'c', false, '/a/b/c.md > c');
458+
});
459+
460+
it('valid, non-unique path with same heading as path name with initial slash should use just the path as backlink', () => {
461+
// This is not a realistic use-case, but is included to show the current behaviour of the code
462+
shouldGiveLinkText('a/b/c.md', '/a/b/c.md', false, '/a/b/c.md');
463+
});
464+
});
465+
466+
describe('invalid unique paths', () => {
467+
// use invalid file extension to generate null filename
468+
// This is not a realistic use-case, but is included to show the current behaviour of the code
469+
470+
it('invalid, unique path without heading should give null for backlink', () => {
471+
shouldGiveLinkText('a/b/c.markdown', null, true, null);
472+
});
473+
474+
it('invalid, unique path with heading should give null for backlink', () => {
475+
shouldGiveLinkText('a/b/c.markdown', 'heading', true, null);
476+
});
477+
});
478+
479+
describe('invalid non-unique paths', () => {
480+
// use invalid file extension to generate null filename
481+
// This is not a realistic use-case, but is included to show the current behaviour of the code
482+
483+
it('invalid, non-unique path without heading uses path anyway for backlink', () => {
484+
shouldGiveLinkText('a/b/c.markdown', null, false, '/a/b/c.markdown');
485+
});
486+
487+
it('invalid, non-unique path with heading uses path and backlink anyway for backlink', () => {
488+
shouldGiveLinkText('a/b/c.markdown', 'heading', false, '/a/b/c.markdown > heading');
489+
});
490+
});
491+
});
492+
417493
describe('to string', () => {
418494
it('retains the indentation', () => {
419495
const line = '> > > - [ ] Task inside a nested blockquote or callout';

tests/TestHelpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { TaskLocation } from '../src/TaskLocation';
44
export function fromLine({
55
line,
66
path = '',
7-
precedingHeader = '',
7+
precedingHeader = null,
88
}: {
99
line: string;
1010
path?: string;

0 commit comments

Comments
 (0)