Fix #1558 prevent excessive blank lines in link reference definitions (#1561)

* Changed replacement approach

* Changed tests to check results instead of edits

* More tests
This commit is contained in:
Riccardo
2025-12-15 13:15:15 +01:00
committed by GitHub
parent f63b7b2c9b
commit 5d8b9756a9
3 changed files with 370 additions and 297 deletions

View File

@@ -1,297 +1,13 @@
import { generateLinkReferences } from '.';
import { createTestNote, createTestWorkspace } from '../../test/test-utils';
import { Range } from '../model/range';
import { Logger } from '../utils/log';
import { URI } from '../model/uri';
import { EOL } from 'os';
import { createMarkdownParser } from '../services/markdown-parser';
import { TextEdit } from '../services/text-edit';
Logger.setLevel('error');
describe('generateLinkReferences', () => {
it('should add link references to a file that does not have them', async () => {
const parser = createMarkdownParser();
const workspace = createTestWorkspace([URI.file('/')]);
workspace.set(
createTestNote({ uri: '/first-document.md', title: 'First Document' })
);
workspace.set(
createTestNote({ uri: '/second-document.md', title: 'Second Document' })
);
workspace.set(
createTestNote({
uri: '/file-without-title.md',
title: 'file-without-title',
})
);
const noteText = `# Index
This file is intentionally missing the link reference definitions
[[first-document]]
[[second-document]]
[[file-without-title]]
`;
const note = parser.parse(URI.file('/note.md'), textForNote(noteText));
const expected = {
newText: textForNote(
`
[first-document]: first-document "First Document"
[second-document]: second-document "Second Document"
[file-without-title]: file-without-title "file-without-title"`
),
range: Range.create(9, 0, 9, 0),
};
const noteEol = EOL;
const actual = await generateLinkReferences(
note,
noteText,
noteEol,
workspace,
false
);
expect(actual![0].range.start).toEqual(expected.range.start);
expect(actual![0].range.end).toEqual(expected.range.end);
expect(actual![0].newText).toEqual(expected.newText);
});
it('should remove link definitions from a file that has them, if no links are present', async () => {
const parser = createMarkdownParser();
const workspace = createTestWorkspace([URI.file('/')]);
workspace.set(
createTestNote({ uri: '/first-document.md', title: 'First Document' })
);
const noteText = `# Second Document
This is just a link target for now.
We can use it for other things later if needed.
[first-document]: first-document 'First Document'
`;
const note = parser.parse(URI.file('/note.md'), textForNote(noteText));
const expected = {
newText: '',
range: Range.create(6, 0, 6, 49),
};
const actual = await generateLinkReferences(
note,
noteText,
EOL,
workspace,
false
);
expect(actual.length).toBe(1);
expect(actual[0]!.range.start).toEqual(expected.range.start);
expect(actual[0]!.range.end).toEqual(expected.range.end);
expect(actual[0]!.newText).toEqual(expected.newText);
});
it('should update link definitions if they are present but changed', async () => {
const parser = createMarkdownParser();
const workspace = createTestWorkspace([URI.file('/')]);
workspace.set(
createTestNote({ uri: '/second-document.md', title: 'Second Document' })
);
workspace.set(
createTestNote({
uri: '/file-without-title.md',
title: 'file-without-title',
})
);
const noteText = `# First Document
Here's some [unrelated] content.
[unrelated]: http://unrelated.com 'This link should not be changed'
[[file-without-title]]
[second-document]: second-document 'Second Document'
`;
const note = parser.parse(URI.file('/note.md'), textForNote(noteText));
const expected = [
{
newText: '',
range: Range.create(8, 0, 8, 52),
},
{
newText: textForNote(
`\n[file-without-title]: file-without-title "file-without-title"`
),
range: Range.create(9, 0, 9, 0),
},
];
const actual = await generateLinkReferences(
note,
noteText,
EOL,
workspace,
false
);
expect(actual.length).toBe(2);
expect(actual[0]!.range.start).toEqual(expected[0].range.start);
expect(actual[0]!.range.end).toEqual(expected[0].range.end);
expect(actual[0]!.newText).toEqual(expected[0].newText);
expect(actual[1]!.range.start).toEqual(expected[1].range.start);
expect(actual[1]!.range.end).toEqual(expected[1].range.end);
expect(actual[1]!.newText).toEqual(expected[1].newText);
});
it('should not cause any changes if link reference definitions were up to date', async () => {
const parser = createMarkdownParser();
const workspace = createTestWorkspace([URI.file('/')])
.set(
createTestNote({ uri: '/first-document.md', title: 'First Document' })
)
.set(
createTestNote({ uri: '/second-document.md', title: 'Second Document' })
);
const noteText = `# Third Document
All the link references are correct in this file.
[[first-document]]
[[second-document]]
[first-document]: first-document "First Document"
[second-document]: second-document "Second Document"
`;
const note = parser.parse(URI.file('/note.md'), textForNote(noteText));
const expected = [];
const noteEol = EOL;
const actual = await generateLinkReferences(
note,
noteText,
noteEol,
workspace,
false
);
expect(actual).toEqual(expected);
});
it('should put links with spaces in angel brackets', async () => {
const parser = createMarkdownParser();
const workspace = createTestWorkspace([URI.file('/')]).set(
createTestNote({
uri: '/Note being referred as angel.md',
title: 'Note being referred as angel',
})
);
const noteText = `# Angel reference
[[Note being referred as angel]]
`;
const note = parser.parse(URI.file('/note.md'), textForNote(noteText));
const expected = {
newText: textForNote(
`
[Note being referred as angel]: <Note being referred as angel> "Note being referred as angel"`
),
range: Range.create(3, 0, 3, 0),
};
const actual = await generateLinkReferences(
note,
noteText,
EOL,
workspace,
false
);
expect(actual.length).toBe(1);
expect(actual[0]!.range.start).toEqual(expected.range.start);
expect(actual[0]!.range.end).toEqual(expected.range.end);
expect(actual[0]!.newText).toEqual(expected.newText);
});
it('should not remove explicitly entered link references', async () => {
const parser = createMarkdownParser();
const workspace = createTestWorkspace([URI.file('/')]);
workspace.set(
createTestNote({ uri: '/second-document.md', title: 'Second Document' })
);
workspace.set(
createTestNote({
uri: '/file-without-title.md',
title: 'file-without-title',
})
);
const noteText = `# File with explicit link references
A Bug [^footerlink]. Here is [Another link][linkreference]
[^footerlink]: https://foambubble.github.io/
[linkreference]: https://foambubble.github.io/
`;
const note = parser.parse(URI.file('/note.md'), textForNote(noteText));
const expected = [];
const actual = await generateLinkReferences(
note,
noteText,
EOL,
workspace,
false
);
expect(actual).toEqual(expected);
});
it('should not remove explicitly entered link references and have an implicit link', async () => {
const parser = createMarkdownParser();
const workspace = createTestWorkspace([URI.file('/')]);
workspace.set(
createTestNote({ uri: '/second-document.md', title: 'Second Document' })
);
const noteText = `# File with explicit link references
A Bug [^footerlink]. Here is [Another link][linkreference].
I also want a [[first-document]].
[^footerlink]: https://foambubble.github.io/
[linkreference]: https://foambubble.github.io/
[first-document]: first-document 'First Document'
`;
const note = parser.parse(URI.file('/note.md'), textForNote(noteText));
const actual = await generateLinkReferences(
note,
noteText,
EOL,
workspace,
false
);
expect(actual.length).toBe(0);
});
});
/**
* Will adjust a text line separator to match
* what is used by the note
@@ -304,3 +20,347 @@ function textForNote(text: string): string {
const eol = EOL;
return text.split('\n').join(eol);
}
describe('generateLinkReferences', () => {
const parser = createMarkdownParser();
interface TestCase {
case: string;
input: string;
expected: string;
}
const testCases: TestCase[] = [
{
case: 'should add link references for wikilinks present in note',
input: `
# Index
[[doc1]] [[doc2]] [[file-without-title]]
`,
expected: `
# Index
[[doc1]] [[doc2]] [[file-without-title]]
[doc1]: doc1 "First"
[doc2]: doc2 "Second"
[file-without-title]: file-without-title "file-without-title"
`,
},
{
case: '#1558 - should keep a blank line before link references',
input: `
# Test
[[doc1]]
[[doc2]]
`,
expected: `
# Test
[[doc1]]
[[doc2]]
[doc1]: doc1 "First"
[doc2]: doc2 "Second"
`,
},
{
case: 'should remove obsolete link definitions',
input: `
# Document
Some content here.
[doc1]: doc1 "First"
`,
expected: `
# Document
Some content here.
`,
},
{
case: 'should add and remove link definitions as needed',
input: `
# First Document
Here's some [unrelated] content.
[unrelated]: http://unrelated.com 'This link should not be changed'
[[file-without-title]]
[doc2]: doc2 'Second Document'
`,
expected: `
# First Document
Here's some [unrelated] content.
[unrelated]: http://unrelated.com 'This link should not be changed'
[[file-without-title]]
[file-without-title]: file-without-title "file-without-title"
`,
},
{
case: 'should not change correct link references',
input: `
# Third Document
All the link references are correct in this file.
[[doc1]]
[[doc2]]
[doc1]: doc1 "First"
[doc2]: doc2 "Second"
`,
expected: `
# Third Document
All the link references are correct in this file.
[[doc1]]
[[doc2]]
[doc1]: doc1 "First"
[doc2]: doc2 "Second"
`,
},
{
case: 'should put links with spaces in angel brackets',
input: `
# Angel reference
[[Angel note]]
`,
expected: `
# Angel reference
[[Angel note]]
[Angel note]: <Angel note> "Angel note"
`,
},
{
case: 'should not remove explicitly entered link references',
input: `
# File with explicit link references
A Bug [^footerlink]. Here is [Another link][linkreference]
[^footerlink]: https://foambubble.github.io/
[linkreference]: https://foambubble.github.io/
`,
expected: `
# File with explicit link references
A Bug [^footerlink]. Here is [Another link][linkreference]
[^footerlink]: https://foambubble.github.io/
[linkreference]: https://foambubble.github.io/
`,
},
{
case: 'should not change explicitly entered link references',
input: `
# File with explicit link references
A Bug [^footerlink]. Here is [Another link][linkreference].
I also want a [[doc1]].
[^footerlink]: https://foambubble.github.io/
[linkreference]: https://foambubble.github.io/
`,
expected: `
# File with explicit link references
A Bug [^footerlink]. Here is [Another link][linkreference].
I also want a [[doc1]].
[^footerlink]: https://foambubble.github.io/
[linkreference]: https://foambubble.github.io/
[doc1]: doc1 "First"
`,
},
{
case: 'should handle empty file with no wikilinks and no definitions',
input: `
# Empty Document
Just some text without any links.
`,
expected: `
# Empty Document
Just some text without any links.
`,
},
{
case: 'should handle wikilinks with aliases',
input: `
# Document with aliases
[[doc1|Custom Alias]] and [[doc2|Another Alias]]
`,
expected: `
# Document with aliases
[[doc1|Custom Alias]] and [[doc2|Another Alias]]
[doc1|Custom Alias]: doc1 "First"
[doc2|Another Alias]: doc2 "Second"
`,
},
{
case: 'should generate only one definition for multiple references to the same link',
input: `
# Multiple references
First mention: [[doc1]]
Second mention: [[doc1]]
Third mention: [[doc1]]
`,
expected: `
# Multiple references
First mention: [[doc1]]
Second mention: [[doc1]]
Third mention: [[doc1]]
[doc1]: doc1 "First"
`,
},
{
case: 'should handle link definitions in the middle of content',
input: `
# Document
[[doc1]]
[doc1]: doc1 "First"
Some more content here.
[[doc2]]
`,
expected: `
# Document
[[doc1]]
[doc1]: doc1 "First"
Some more content here.
[[doc2]]
[doc2]: doc2 "Second"
`,
},
{
case: 'should handle orphaned wikilinks without corresponding notes',
input: `
# Document with broken links
[[doc1]] [[nonexistent]] [[another-missing]]
`,
expected: `
# Document with broken links
[[doc1]] [[nonexistent]] [[another-missing]]
[doc1]: doc1 "First"
`,
},
{
case: 'should handle file with only blank lines at end',
input: `
`,
expected: `
`,
},
{
case: 'should handle empty files',
input: '',
expected: '',
},
{
case: 'should handle link definitions with different quote styles',
input: `
# Mixed quotes
[[doc1]] [[doc2]]
[doc1]: doc1 'First'
[doc2]: doc2 "Second"
`,
expected: `
# Mixed quotes
[[doc1]] [[doc2]]
[doc1]: doc1 'First'
[doc2]: doc2 "Second"
`,
},
// TODO
// {
// case: 'should append new link references to existing ones without blank lines',
// input: `
// [[doc1]] [[doc2]]
// [doc1]: doc1 "First"
// `,
// expected: `
// [[doc1]] [[doc2]]
// [doc1]: doc1 "First"
// [doc2]: doc2 "Second"
// `,
// },
];
testCases.forEach(testCase => {
// eslint-disable-next-line jest/valid-title
it(testCase.case, async () => {
const workspace = createTestWorkspace([URI.file('/')]);
const workspaceNotes = [
{ uri: '/doc1.md', title: 'First' },
{ uri: '/doc2.md', title: 'Second' },
{ uri: '/file-without-title.md', title: 'file-without-title' },
{ uri: '/Angel note.md', title: 'Angel note' },
];
workspaceNotes.forEach(note => {
workspace.set(createTestNote({ uri: note.uri, title: note.title }));
});
const noteText = testCase.input;
const note = parser.parse(URI.file('/note.md'), textForNote(noteText));
const actual = await generateLinkReferences(
note,
noteText,
EOL,
workspace,
false
);
const updated = TextEdit.apply(noteText, actual);
expect(updated).toBe(textForNote(testCase.expected));
});
});
});

View File

@@ -19,7 +19,8 @@ export const generateLinkReferences = async (
return [];
}
const nLines = currentNoteText.split(eol).length;
const lines = currentNoteText.split(eol);
const nLines = lines.length;
const updatedWikilinkDefinitions = createMarkdownReferences(
workspace,
@@ -51,20 +52,24 @@ export const generateLinkReferences = async (
// Add new definitions
if (toAddWikilinkDefinitions.length > 0) {
const lastLine = currentNoteText.split(eol)[nLines - 1];
const isLastLineEmpty = lastLine.trim().length === 0;
let text = isLastLineEmpty ? '' : eol;
for (const def of toAddWikilinkDefinitions) {
// Choose the correct position for insertion, e.g., end of file or after last reference
text = `${text}${eol}${NoteLinkDefinition.format(def)}`;
// find the last non-empty line to append the definitions after it
const lastLineIndex = nLines - 1;
let insertLineIndex = lastLineIndex;
while (insertLineIndex > 0 && lines[insertLineIndex].trim() === '') {
insertLineIndex--;
}
const definitions = toAddWikilinkDefinitions.map(def =>
NoteLinkDefinition.format(def)
);
const text = eol + eol + definitions.join(eol) + eol;
edits.push({
range: Range.create(
nLines - 1,
lastLine.length,
nLines - 1,
lastLine.length
insertLineIndex,
lines[insertLineIndex].length,
lastLineIndex,
lines[lastLineIndex].length
),
newText: text,
});

View File

@@ -61,6 +61,14 @@ export abstract class NoteLinkDefinition {
return text;
}
static isEqual(def1: NoteLinkDefinition, def2: NoteLinkDefinition): boolean {
return (
def1.label === def2.label &&
def1.url === def2.url &&
def1.title === def2.title
);
}
}
export interface Tag {