Skip to content

Commit

Permalink
generate copy without wikilinks (#1365)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
hereistheusername and riccardoferretti authored Jul 10, 2024
1 parent 22b837f commit cef8d2a
Show file tree
Hide file tree
Showing 10 changed files with 575 additions and 9 deletions.
8 changes: 8 additions & 0 deletions packages/foam-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,14 @@
"command": "foam-vscode.open-resource",
"title": "Foam: Open Resource"
},
{
"command": "foam-vscode.convert-link-style-inplace",
"title": "Foam: convert link style in place"
},
{
"command": "foam-vscode.convert-link-style-incopy",
"title": "Foam: convert link format in copy"
},
{
"command": "foam-vscode.views.orphans.group-by:folder",
"title": "Group By Folder",
Expand Down
71 changes: 71 additions & 0 deletions packages/foam-vscode/src/core/janitor/convert-links-format.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { convertLinkFormat } from '.';
import { TEST_DATA_DIR } from '../../test/test-utils';
import { MarkdownResourceProvider } from '../services/markdown-provider';
import { Resource } from '../model/note';
import { FoamWorkspace } from '../model/workspace';
import { Logger } from '../utils/log';
import fs from 'fs';
import { URI } from '../model/uri';
import { createMarkdownParser } from '../services/markdown-parser';
import { FileDataStore } from '../../test/test-datastore';

Logger.setLevel('error');

describe('generateStdMdLink', () => {
let _workspace: FoamWorkspace;
// TODO slug must be reserved for actual slugs, not file names
const findBySlug = (slug: string): Resource => {
return _workspace
.list()
.find(res => res.uri.getName() === slug) as Resource;
};

beforeAll(async () => {
/** Use fs for reading files in units where vscode.workspace is unavailable */
const readFile = async (uri: URI) =>
(await fs.promises.readFile(uri.toFsPath())).toString();
const dataStore = new FileDataStore(
readFile,
TEST_DATA_DIR.joinPath('__scaffold__').toFsPath()
);
const parser = createMarkdownParser();
const mdProvider = new MarkdownResourceProvider(dataStore, parser);
_workspace = await FoamWorkspace.fromProviders([mdProvider], dataStore);
});

it('initialised test graph correctly', () => {
expect(_workspace.list().length).toEqual(11);
});

it('can generate markdown links correctly', async () => {
const note = findBySlug('file-with-different-link-formats');
const actual = note.links
.filter(link => link.type === 'wikilink')
.map(link => convertLinkFormat(link, 'link', _workspace, note));
const expected: string[] = [
'[first-document](first-document.md)',
'[second-document](second-document.md)',
'[[non-exist-file]]',
'[#one section](<file-with-different-link-formats.md#one section>)',
'[another name](<file-with-different-link-formats.md#one section>)',
'[an alias](first-document.md)',
'[first-document](first-document.md)',
];
expect(actual.length).toEqual(expected.length);
const _ = actual.map((LinkReplace, index) => {
expect(LinkReplace.newText).toEqual(expected[index]);
});
});

it('can generate wikilinks correctly', async () => {
const note = findBySlug('file-with-different-link-formats');
const actual = note.links
.filter(link => link.type === 'link')
.map(link => convertLinkFormat(link, 'wikilink', _workspace, note));
const expected: string[] = ['[[first-document|file]]'];
expect(actual.length).toEqual(expected.length);
const _ = actual.map((LinkReplace, index) => {
expect(LinkReplace.newText).toEqual(expected[index]);
});
});
});
83 changes: 83 additions & 0 deletions packages/foam-vscode/src/core/janitor/convert-links-format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Resource, ResourceLink } from '../model/note';
import { URI } from '../model/uri';
import { Range } from '../model/range';
import { FoamWorkspace } from '../model/workspace';
import { isNone } from '../utils';
import { MarkdownLink } from '../services/markdown-link';

export interface LinkReplace {
newText: string;
range: Range /* old range */;
}

/**
* convert a link based on its workspace and the note containing it.
* According to targetFormat parameter to decide output format. If link.type === targetFormat, then it simply copy
* the rawText into LinkReplace. Therefore, it's recommended to filter before conversion.
* If targetFormat isn't supported, or the target resource pointed by link cannot be found, the function will throw
* exception.
* @param link
* @param targetFormat 'wikilink' | 'link'
* @param workspace
* @param note
* @returns LinkReplace { newText: string; range: Range; }
*/
export function convertLinkFormat(
link: ResourceLink,
targetFormat: 'wikilink' | 'link',
workspace: FoamWorkspace,
note: Resource | URI
): LinkReplace {
const resource = note instanceof URI ? workspace.find(note) : note;
const targetUri = workspace.resolveLink(resource, link);
/* If it's already the target format or a placeholder, no transformation happens */
if (link.type === targetFormat || targetUri.scheme === 'placeholder') {
return {
newText: link.rawText,
range: link.range,
};
}

let { target, section, alias } = MarkdownLink.analyzeLink(link);
let sectionDivider = section ? '#' : '';

if (isNone(targetUri)) {
throw new Error(
`Unexpected state: link to: "${link.rawText}" is not resolvable`
);
}

const targetRes = workspace.find(targetUri);
let relativeUri = targetRes.uri.relativeTo(resource.uri.getDirectory());

if (targetFormat === 'wikilink') {
return MarkdownLink.createUpdateLinkEdit(link, {
target: workspace.getIdentifier(relativeUri),
type: 'wikilink',
});
}

if (targetFormat === 'link') {
/* if alias is empty, construct one as target#section */
if (alias === '') {
/* in page anchor have no filename */
if (relativeUri.getBasename() === resource.uri.getBasename()) {
target = '';
}
alias = `${target}${sectionDivider}${section}`;
}

/* if it's originally an embedded note, the markdown link shouldn't be embedded */
const isEmbed = targetRes.type === 'image' ? link.isEmbed : false;

return MarkdownLink.createUpdateLinkEdit(link, {
alias: alias,
target: relativeUri.path,
isEmbed: isEmbed,
type: 'link',
});
}
throw new Error(
`Unexpected state: targetFormat: ${targetFormat} is not supported`
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('generateLinkReferences', () => {
});

it('initialised test graph correctly', () => {
expect(_workspace.list().length).toEqual(10);
expect(_workspace.list().length).toEqual(11);
});

it('should add link references to a file that does not have them', async () => {
Expand Down
1 change: 1 addition & 0 deletions packages/foam-vscode/src/core/janitor/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { generateLinkReferences } from './generate-link-references';
export { generateHeading } from './generate-headings';
export { convertLinkFormat } from './convert-links-format';
181 changes: 181 additions & 0 deletions packages/foam-vscode/src/core/services/markdown-link.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,4 +254,185 @@ describe('MarkdownLink', () => {
expect(edit.range).toEqual(link.range);
});
});

describe('convert wikilink to link', () => {
it('should generate default alias if no one', () => {
const wikilink = parser.parse(getRandomURI(), `[[wikilink]]`).links[0];
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
type: 'link',
});
expect(wikilinkEdit.newText).toEqual(`[wikilink](wikilink)`);
expect(wikilinkEdit.range).toEqual(wikilink.range);

const wikilinkWithSection = parser.parse(
getRandomURI(),
`[[wikilink#section]]`
).links[0];
const wikilinkWithSectionEdit = MarkdownLink.createUpdateLinkEdit(
wikilinkWithSection,
{
type: 'link',
}
);
expect(wikilinkWithSectionEdit.newText).toEqual(
`[wikilink#section](wikilink#section)`
);
expect(wikilinkWithSectionEdit.range).toEqual(wikilinkWithSection.range);
});

it('should use alias in the wikilik the if there has one', () => {
const wikilink = parser.parse(
getRandomURI(),
`[[wikilink#section|alias]]`
).links[0];
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
type: 'link',
});
expect(wikilinkEdit.newText).toEqual(`[alias](wikilink#section)`);
expect(wikilinkEdit.range).toEqual(wikilink.range);
});
});

describe('convert link to wikilink', () => {
it('should reorganize target, section, and alias in wikilink manner', () => {
const link = parser.parse(getRandomURI(), `[link](to/path.md)`).links[0];
const linkEdit = MarkdownLink.createUpdateLinkEdit(link, {
type: 'wikilink',
});
expect(linkEdit.newText).toEqual(`[[to/path.md|link]]`);
expect(linkEdit.range).toEqual(link.range);

const linkWithSection = parser.parse(
getRandomURI(),
`[link](to/path.md#section)`
).links[0];
const linkWithSectionEdit = MarkdownLink.createUpdateLinkEdit(
linkWithSection,
{
type: 'wikilink',
}
);
expect(linkWithSectionEdit.newText).toEqual(
`[[to/path.md#section|link]]`
);
expect(linkWithSectionEdit.range).toEqual(linkWithSection.range);
});

it('should use alias in the wikilik the if there has one', () => {
const wikilink = parser.parse(
getRandomURI(),
`[[wikilink#section|alias]]`
).links[0];
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
type: 'link',
});
expect(wikilinkEdit.newText).toEqual(`[alias](wikilink#section)`);
expect(wikilinkEdit.range).toEqual(wikilink.range);
});
});

describe('convert to its original type', () => {
it('should remain unchanged', () => {
const link = parser.parse(getRandomURI(), `[link](to/path.md#section)`)
.links[0];
const linkEdit = MarkdownLink.createUpdateLinkEdit(link, {
type: 'link',
});
expect(linkEdit.newText).toEqual(`[link](to/path.md#section)`);
expect(linkEdit.range).toEqual(link.range);

const wikilink = parser.parse(
getRandomURI(),
`[[wikilink#section|alias]]`
).links[0];
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
type: 'wikilink',
});
expect(wikilinkEdit.newText).toEqual(`[[wikilink#section|alias]]`);
expect(wikilinkEdit.range).toEqual(wikilink.range);
});
});

describe('change isEmbed property', () => {
it('should change isEmbed only', () => {
const wikilink = parser.parse(getRandomURI(), `[[wikilink]]`).links[0];
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
isEmbed: true,
});
expect(wikilinkEdit.newText).toEqual(`![[wikilink]]`);
expect(wikilinkEdit.range).toEqual(wikilink.range);

const link = parser.parse(getRandomURI(), `![link](to/path.md)`).links[0];
const linkEdit = MarkdownLink.createUpdateLinkEdit(link, {
isEmbed: false,
});
expect(linkEdit.newText).toEqual(`[link](to/path.md)`);
expect(linkEdit.range).toEqual(link.range);
});

it('should be unchanged if the update value is the same as the original one', () => {
const embeddedWikilink = parser.parse(getRandomURI(), `![[wikilink]]`)
.links[0];
const embeddedWikilinkEdit = MarkdownLink.createUpdateLinkEdit(
embeddedWikilink,
{
isEmbed: true,
}
);
expect(embeddedWikilinkEdit.newText).toEqual(`![[wikilink]]`);
expect(embeddedWikilinkEdit.range).toEqual(embeddedWikilink.range);

const link = parser.parse(getRandomURI(), `[link](to/path.md)`).links[0];
const linkEdit = MarkdownLink.createUpdateLinkEdit(link, {
isEmbed: false,
});
expect(linkEdit.newText).toEqual(`[link](to/path.md)`);
expect(linkEdit.range).toEqual(link.range);
});
});

describe('insert angles', () => {
it('should insert angles when meeting space in links', () => {
const link = parser.parse(getRandomURI(), `![link](to/path.md)`).links[0];
const linkAddSection = MarkdownLink.createUpdateLinkEdit(link, {
section: 'one section',
});
expect(linkAddSection.newText).toEqual(
`![link](<to/path.md#one section>)`
);
expect(linkAddSection.range).toEqual(link.range);

const linkChangingTarget = parser.parse(
getRandomURI(),
`[link](to/path.md#one-section)`
).links[0];
const linkEdit = MarkdownLink.createUpdateLinkEdit(linkChangingTarget, {
target: 'to/another path.md',
});
expect(linkEdit.newText).toEqual(
`[link](<to/another path.md#one-section>)`
);
expect(linkEdit.range).toEqual(linkChangingTarget.range);

const wikilink = parser.parse(getRandomURI(), `[[wikilink#one section]]`)
.links[0];
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
type: 'link',
});
expect(wikilinkEdit.newText).toEqual(
`[wikilink#one section](<wikilink#one section>)`
);
expect(wikilinkEdit.range).toEqual(wikilink.range);
});

it('should not insert angles in wikilink', () => {
const wikilink = parser.parse(getRandomURI(), `[[wikilink#one section]]`)
.links[0];
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
target: 'another wikilink',
});
expect(wikilinkEdit.newText).toEqual(`[[another wikilink#one section]]`);
expect(wikilinkEdit.range).toEqual(wikilink.range);
});
});
});
Loading

0 comments on commit cef8d2a

Please sign in to comment.