mirror of
https://github.com/foambubble/foam.git
synced 2026-01-07 21:24:06 -05:00
* Changed replacement approach * Changed tests to check results instead of edits * More tests
This commit is contained in:
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user