Skip to content

Commit cef8d2a

Browse files
generate copy without wikilinks (#1365)
* add generate standalone note command * fix embeded wikilinks * refactor convertLinksFormat function & add 4 user command interfaces * change user interface * modify createUpdateLinkEdit to accomplish convert * only images can be embedded * keey filename when using in page anchor * give a default value to alias in link format combination branch * add tests to createUpdateLinkEdit about changint links' type and isEmbed * get target from getIdentifier --------- Co-authored-by: Riccardo <[email protected]>
1 parent 22b837f commit cef8d2a

10 files changed

+575
-9
lines changed

packages/foam-vscode/package.json

+8
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,14 @@
346346
"command": "foam-vscode.open-resource",
347347
"title": "Foam: Open Resource"
348348
},
349+
{
350+
"command": "foam-vscode.convert-link-style-inplace",
351+
"title": "Foam: convert link style in place"
352+
},
353+
{
354+
"command": "foam-vscode.convert-link-style-incopy",
355+
"title": "Foam: convert link format in copy"
356+
},
349357
{
350358
"command": "foam-vscode.views.orphans.group-by:folder",
351359
"title": "Group By Folder",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { convertLinkFormat } from '.';
2+
import { TEST_DATA_DIR } from '../../test/test-utils';
3+
import { MarkdownResourceProvider } from '../services/markdown-provider';
4+
import { Resource } from '../model/note';
5+
import { FoamWorkspace } from '../model/workspace';
6+
import { Logger } from '../utils/log';
7+
import fs from 'fs';
8+
import { URI } from '../model/uri';
9+
import { createMarkdownParser } from '../services/markdown-parser';
10+
import { FileDataStore } from '../../test/test-datastore';
11+
12+
Logger.setLevel('error');
13+
14+
describe('generateStdMdLink', () => {
15+
let _workspace: FoamWorkspace;
16+
// TODO slug must be reserved for actual slugs, not file names
17+
const findBySlug = (slug: string): Resource => {
18+
return _workspace
19+
.list()
20+
.find(res => res.uri.getName() === slug) as Resource;
21+
};
22+
23+
beforeAll(async () => {
24+
/** Use fs for reading files in units where vscode.workspace is unavailable */
25+
const readFile = async (uri: URI) =>
26+
(await fs.promises.readFile(uri.toFsPath())).toString();
27+
const dataStore = new FileDataStore(
28+
readFile,
29+
TEST_DATA_DIR.joinPath('__scaffold__').toFsPath()
30+
);
31+
const parser = createMarkdownParser();
32+
const mdProvider = new MarkdownResourceProvider(dataStore, parser);
33+
_workspace = await FoamWorkspace.fromProviders([mdProvider], dataStore);
34+
});
35+
36+
it('initialised test graph correctly', () => {
37+
expect(_workspace.list().length).toEqual(11);
38+
});
39+
40+
it('can generate markdown links correctly', async () => {
41+
const note = findBySlug('file-with-different-link-formats');
42+
const actual = note.links
43+
.filter(link => link.type === 'wikilink')
44+
.map(link => convertLinkFormat(link, 'link', _workspace, note));
45+
const expected: string[] = [
46+
'[first-document](first-document.md)',
47+
'[second-document](second-document.md)',
48+
'[[non-exist-file]]',
49+
'[#one section](<file-with-different-link-formats.md#one section>)',
50+
'[another name](<file-with-different-link-formats.md#one section>)',
51+
'[an alias](first-document.md)',
52+
'[first-document](first-document.md)',
53+
];
54+
expect(actual.length).toEqual(expected.length);
55+
const _ = actual.map((LinkReplace, index) => {
56+
expect(LinkReplace.newText).toEqual(expected[index]);
57+
});
58+
});
59+
60+
it('can generate wikilinks correctly', async () => {
61+
const note = findBySlug('file-with-different-link-formats');
62+
const actual = note.links
63+
.filter(link => link.type === 'link')
64+
.map(link => convertLinkFormat(link, 'wikilink', _workspace, note));
65+
const expected: string[] = ['[[first-document|file]]'];
66+
expect(actual.length).toEqual(expected.length);
67+
const _ = actual.map((LinkReplace, index) => {
68+
expect(LinkReplace.newText).toEqual(expected[index]);
69+
});
70+
});
71+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { Resource, ResourceLink } from '../model/note';
2+
import { URI } from '../model/uri';
3+
import { Range } from '../model/range';
4+
import { FoamWorkspace } from '../model/workspace';
5+
import { isNone } from '../utils';
6+
import { MarkdownLink } from '../services/markdown-link';
7+
8+
export interface LinkReplace {
9+
newText: string;
10+
range: Range /* old range */;
11+
}
12+
13+
/**
14+
* convert a link based on its workspace and the note containing it.
15+
* According to targetFormat parameter to decide output format. If link.type === targetFormat, then it simply copy
16+
* the rawText into LinkReplace. Therefore, it's recommended to filter before conversion.
17+
* If targetFormat isn't supported, or the target resource pointed by link cannot be found, the function will throw
18+
* exception.
19+
* @param link
20+
* @param targetFormat 'wikilink' | 'link'
21+
* @param workspace
22+
* @param note
23+
* @returns LinkReplace { newText: string; range: Range; }
24+
*/
25+
export function convertLinkFormat(
26+
link: ResourceLink,
27+
targetFormat: 'wikilink' | 'link',
28+
workspace: FoamWorkspace,
29+
note: Resource | URI
30+
): LinkReplace {
31+
const resource = note instanceof URI ? workspace.find(note) : note;
32+
const targetUri = workspace.resolveLink(resource, link);
33+
/* If it's already the target format or a placeholder, no transformation happens */
34+
if (link.type === targetFormat || targetUri.scheme === 'placeholder') {
35+
return {
36+
newText: link.rawText,
37+
range: link.range,
38+
};
39+
}
40+
41+
let { target, section, alias } = MarkdownLink.analyzeLink(link);
42+
let sectionDivider = section ? '#' : '';
43+
44+
if (isNone(targetUri)) {
45+
throw new Error(
46+
`Unexpected state: link to: "${link.rawText}" is not resolvable`
47+
);
48+
}
49+
50+
const targetRes = workspace.find(targetUri);
51+
let relativeUri = targetRes.uri.relativeTo(resource.uri.getDirectory());
52+
53+
if (targetFormat === 'wikilink') {
54+
return MarkdownLink.createUpdateLinkEdit(link, {
55+
target: workspace.getIdentifier(relativeUri),
56+
type: 'wikilink',
57+
});
58+
}
59+
60+
if (targetFormat === 'link') {
61+
/* if alias is empty, construct one as target#section */
62+
if (alias === '') {
63+
/* in page anchor have no filename */
64+
if (relativeUri.getBasename() === resource.uri.getBasename()) {
65+
target = '';
66+
}
67+
alias = `${target}${sectionDivider}${section}`;
68+
}
69+
70+
/* if it's originally an embedded note, the markdown link shouldn't be embedded */
71+
const isEmbed = targetRes.type === 'image' ? link.isEmbed : false;
72+
73+
return MarkdownLink.createUpdateLinkEdit(link, {
74+
alias: alias,
75+
target: relativeUri.path,
76+
isEmbed: isEmbed,
77+
type: 'link',
78+
});
79+
}
80+
throw new Error(
81+
`Unexpected state: targetFormat: ${targetFormat} is not supported`
82+
);
83+
}

packages/foam-vscode/src/core/janitor/generate-link-references.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ describe('generateLinkReferences', () => {
3636
});
3737

3838
it('initialised test graph correctly', () => {
39-
expect(_workspace.list().length).toEqual(10);
39+
expect(_workspace.list().length).toEqual(11);
4040
});
4141

4242
it('should add link references to a file that does not have them', async () => {
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { generateLinkReferences } from './generate-link-references';
22
export { generateHeading } from './generate-headings';
3+
export { convertLinkFormat } from './convert-links-format';

packages/foam-vscode/src/core/services/markdown-link.test.ts

+181
Original file line numberDiff line numberDiff line change
@@ -254,4 +254,185 @@ describe('MarkdownLink', () => {
254254
expect(edit.range).toEqual(link.range);
255255
});
256256
});
257+
258+
describe('convert wikilink to link', () => {
259+
it('should generate default alias if no one', () => {
260+
const wikilink = parser.parse(getRandomURI(), `[[wikilink]]`).links[0];
261+
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
262+
type: 'link',
263+
});
264+
expect(wikilinkEdit.newText).toEqual(`[wikilink](wikilink)`);
265+
expect(wikilinkEdit.range).toEqual(wikilink.range);
266+
267+
const wikilinkWithSection = parser.parse(
268+
getRandomURI(),
269+
`[[wikilink#section]]`
270+
).links[0];
271+
const wikilinkWithSectionEdit = MarkdownLink.createUpdateLinkEdit(
272+
wikilinkWithSection,
273+
{
274+
type: 'link',
275+
}
276+
);
277+
expect(wikilinkWithSectionEdit.newText).toEqual(
278+
`[wikilink#section](wikilink#section)`
279+
);
280+
expect(wikilinkWithSectionEdit.range).toEqual(wikilinkWithSection.range);
281+
});
282+
283+
it('should use alias in the wikilik the if there has one', () => {
284+
const wikilink = parser.parse(
285+
getRandomURI(),
286+
`[[wikilink#section|alias]]`
287+
).links[0];
288+
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
289+
type: 'link',
290+
});
291+
expect(wikilinkEdit.newText).toEqual(`[alias](wikilink#section)`);
292+
expect(wikilinkEdit.range).toEqual(wikilink.range);
293+
});
294+
});
295+
296+
describe('convert link to wikilink', () => {
297+
it('should reorganize target, section, and alias in wikilink manner', () => {
298+
const link = parser.parse(getRandomURI(), `[link](to/path.md)`).links[0];
299+
const linkEdit = MarkdownLink.createUpdateLinkEdit(link, {
300+
type: 'wikilink',
301+
});
302+
expect(linkEdit.newText).toEqual(`[[to/path.md|link]]`);
303+
expect(linkEdit.range).toEqual(link.range);
304+
305+
const linkWithSection = parser.parse(
306+
getRandomURI(),
307+
`[link](to/path.md#section)`
308+
).links[0];
309+
const linkWithSectionEdit = MarkdownLink.createUpdateLinkEdit(
310+
linkWithSection,
311+
{
312+
type: 'wikilink',
313+
}
314+
);
315+
expect(linkWithSectionEdit.newText).toEqual(
316+
`[[to/path.md#section|link]]`
317+
);
318+
expect(linkWithSectionEdit.range).toEqual(linkWithSection.range);
319+
});
320+
321+
it('should use alias in the wikilik the if there has one', () => {
322+
const wikilink = parser.parse(
323+
getRandomURI(),
324+
`[[wikilink#section|alias]]`
325+
).links[0];
326+
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
327+
type: 'link',
328+
});
329+
expect(wikilinkEdit.newText).toEqual(`[alias](wikilink#section)`);
330+
expect(wikilinkEdit.range).toEqual(wikilink.range);
331+
});
332+
});
333+
334+
describe('convert to its original type', () => {
335+
it('should remain unchanged', () => {
336+
const link = parser.parse(getRandomURI(), `[link](to/path.md#section)`)
337+
.links[0];
338+
const linkEdit = MarkdownLink.createUpdateLinkEdit(link, {
339+
type: 'link',
340+
});
341+
expect(linkEdit.newText).toEqual(`[link](to/path.md#section)`);
342+
expect(linkEdit.range).toEqual(link.range);
343+
344+
const wikilink = parser.parse(
345+
getRandomURI(),
346+
`[[wikilink#section|alias]]`
347+
).links[0];
348+
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
349+
type: 'wikilink',
350+
});
351+
expect(wikilinkEdit.newText).toEqual(`[[wikilink#section|alias]]`);
352+
expect(wikilinkEdit.range).toEqual(wikilink.range);
353+
});
354+
});
355+
356+
describe('change isEmbed property', () => {
357+
it('should change isEmbed only', () => {
358+
const wikilink = parser.parse(getRandomURI(), `[[wikilink]]`).links[0];
359+
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
360+
isEmbed: true,
361+
});
362+
expect(wikilinkEdit.newText).toEqual(`![[wikilink]]`);
363+
expect(wikilinkEdit.range).toEqual(wikilink.range);
364+
365+
const link = parser.parse(getRandomURI(), `![link](to/path.md)`).links[0];
366+
const linkEdit = MarkdownLink.createUpdateLinkEdit(link, {
367+
isEmbed: false,
368+
});
369+
expect(linkEdit.newText).toEqual(`[link](to/path.md)`);
370+
expect(linkEdit.range).toEqual(link.range);
371+
});
372+
373+
it('should be unchanged if the update value is the same as the original one', () => {
374+
const embeddedWikilink = parser.parse(getRandomURI(), `![[wikilink]]`)
375+
.links[0];
376+
const embeddedWikilinkEdit = MarkdownLink.createUpdateLinkEdit(
377+
embeddedWikilink,
378+
{
379+
isEmbed: true,
380+
}
381+
);
382+
expect(embeddedWikilinkEdit.newText).toEqual(`![[wikilink]]`);
383+
expect(embeddedWikilinkEdit.range).toEqual(embeddedWikilink.range);
384+
385+
const link = parser.parse(getRandomURI(), `[link](to/path.md)`).links[0];
386+
const linkEdit = MarkdownLink.createUpdateLinkEdit(link, {
387+
isEmbed: false,
388+
});
389+
expect(linkEdit.newText).toEqual(`[link](to/path.md)`);
390+
expect(linkEdit.range).toEqual(link.range);
391+
});
392+
});
393+
394+
describe('insert angles', () => {
395+
it('should insert angles when meeting space in links', () => {
396+
const link = parser.parse(getRandomURI(), `![link](to/path.md)`).links[0];
397+
const linkAddSection = MarkdownLink.createUpdateLinkEdit(link, {
398+
section: 'one section',
399+
});
400+
expect(linkAddSection.newText).toEqual(
401+
`![link](<to/path.md#one section>)`
402+
);
403+
expect(linkAddSection.range).toEqual(link.range);
404+
405+
const linkChangingTarget = parser.parse(
406+
getRandomURI(),
407+
`[link](to/path.md#one-section)`
408+
).links[0];
409+
const linkEdit = MarkdownLink.createUpdateLinkEdit(linkChangingTarget, {
410+
target: 'to/another path.md',
411+
});
412+
expect(linkEdit.newText).toEqual(
413+
`[link](<to/another path.md#one-section>)`
414+
);
415+
expect(linkEdit.range).toEqual(linkChangingTarget.range);
416+
417+
const wikilink = parser.parse(getRandomURI(), `[[wikilink#one section]]`)
418+
.links[0];
419+
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
420+
type: 'link',
421+
});
422+
expect(wikilinkEdit.newText).toEqual(
423+
`[wikilink#one section](<wikilink#one section>)`
424+
);
425+
expect(wikilinkEdit.range).toEqual(wikilink.range);
426+
});
427+
428+
it('should not insert angles in wikilink', () => {
429+
const wikilink = parser.parse(getRandomURI(), `[[wikilink#one section]]`)
430+
.links[0];
431+
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
432+
target: 'another wikilink',
433+
});
434+
expect(wikilinkEdit.newText).toEqual(`[[another wikilink#one section]]`);
435+
expect(wikilinkEdit.range).toEqual(wikilink.range);
436+
});
437+
});
257438
});

0 commit comments

Comments
 (0)