Skip to content

Commit

Permalink
Simplified ResourceLink model and added utility functions to manipula…
Browse files Browse the repository at this point in the history
…te it
  • Loading branch information
riccardoferretti committed Apr 6, 2022
1 parent 88227d4 commit 5b7a2ab
Show file tree
Hide file tree
Showing 13 changed files with 289 additions and 60 deletions.
2 changes: 0 additions & 2 deletions packages/foam-vscode/src/core/model/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ export interface NoteSource {

export interface ResourceLink {
type: 'wikilink' | 'link';
target: string;
label: string;
rawText: string;
range: Range;
}
Expand Down
5 changes: 4 additions & 1 deletion packages/foam-vscode/src/core/model/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { isSome } from '../utils';
import { Emitter } from '../common/event';
import { ResourceProvider } from './provider';
import { IDisposable } from '../common/lifecycle';
import { Logger } from '../utils/log';

export class FoamWorkspace implements IDisposable {
private onDidAddEmitter = new Emitter<Resource>();
Expand Down Expand Up @@ -137,7 +138,9 @@ export class FoamWorkspace implements IDisposable {
return provider.resolveLink(this, resource, link);
}
}
return URI.placeholder(link.target);
throw new Error(
`Couldn't find provider for resource "${resource.uri.toString()}"`
);
}

public read(uri: URI): Promise<string | null> {
Expand Down
200 changes: 200 additions & 0 deletions packages/foam-vscode/src/core/services/markdown-link.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { getRandomURI } from '../../test/test-utils';
import { ResourceLink } from '../model/note';
import { Range } from '../model/range';
import { createMarkdownParser } from '../services/markdown-parser';
import { MarkdownLink } from './markdown-link';

describe('MarkdownLink', () => {
const parser = createMarkdownParser([]);
describe('parse wikilink', () => {
it('should parse target', () => {
const link = parser.parse(getRandomURI(), `this is a [[wikilink]]`)
.links[0];
const parsed = MarkdownLink.analyzeLink(link);
expect(parsed.target).toEqual('wikilink');
expect(parsed.section).toBeUndefined();
expect(parsed.alias).toBeUndefined();
});
it('should parse target and section', () => {
const link = parser.parse(
getRandomURI(),
`this is a [[wikilink#section]]`
).links[0];
const parsed = MarkdownLink.analyzeLink(link);
expect(parsed.target).toEqual('wikilink');
expect(parsed.section).toEqual('section');
expect(parsed.alias).toBeUndefined();
});
it('should parse target and alias', () => {
const link = parser.parse(getRandomURI(), `this is a [[wikilink|alias]]`)
.links[0];
const parsed = MarkdownLink.analyzeLink(link);
expect(parsed.target).toEqual('wikilink');
expect(parsed.section).toBeUndefined();
expect(parsed.alias).toEqual('alias');
});
it('should parse target and alias with escaped separator', () => {
const link = parser.parse(
getRandomURI(),
`this is a [[wikilink\\|alias]]`
).links[0];
const parsed = MarkdownLink.analyzeLink(link);
expect(parsed.target).toEqual('wikilink');
expect(parsed.section).toBeUndefined();
expect(parsed.alias).toEqual('alias');
});
it('should parse target section and alias', () => {
const link = parser.parse(
getRandomURI(),
`this is a [[wikilink with spaces#section with spaces|alias with spaces]]`
).links[0];
const parsed = MarkdownLink.analyzeLink(link);
expect(parsed.target).toEqual('wikilink with spaces');
expect(parsed.section).toEqual('section with spaces');
expect(parsed.alias).toEqual('alias with spaces');
});
it('should parse section', () => {
const link = parser.parse(getRandomURI(), `this is a [[#section]]`)
.links[0];
const parsed = MarkdownLink.analyzeLink(link);
expect(parsed.target).toBeUndefined();
expect(parsed.section).toEqual('section');
expect(parsed.alias).toBeUndefined();
});
});

describe('parse direct link', () => {
it('should parse target', () => {
const link = parser.parse(getRandomURI(), `this is a [link](to/path.md)`)
.links[0];
const parsed = MarkdownLink.analyzeLink(link);
expect(parsed.target).toEqual('to/path.md');
expect(parsed.section).toBeUndefined();
expect(parsed.alias).toEqual('link');
});
it('should parse target and section', () => {
const link = parser.parse(
getRandomURI(),
`this is a [link](to/path.md#section)`
).links[0];
const parsed = MarkdownLink.analyzeLink(link);
expect(parsed.target).toEqual('to/path.md');
expect(parsed.section).toEqual('section');
expect(parsed.alias).toEqual('link');
});
it('should parse section only', () => {
const link: ResourceLink = {
type: 'link',
rawText: '[link](#section)',
range: Range.create(0, 0),
};
const parsed = MarkdownLink.analyzeLink(link);
expect(parsed.target).toBeUndefined();
expect(parsed.section).toEqual('section');
expect(parsed.alias).toEqual('link');
});
});

describe('rename wikilink', () => {
it.skip('should rename the target only', () => {
const link = parser.parse(
getRandomURI(),
`this is a [[wikilink#section]]`
).links[0];
const edit = MarkdownLink.createUpdateLinkEdit(link, {
target: 'new-link',
});
expect(edit.newText).toEqual(`[[new-link#section]]`);
expect(edit.selection).toEqual(link.range);
});
it('should rename the section only', () => {
const link = parser.parse(
getRandomURI(),
`this is a [[wikilink#section]]`
).links[0];
const edit = MarkdownLink.createUpdateLinkEdit(link, {
section: 'new-section',
});
expect(edit.newText).toEqual(`[[wikilink#new-section]]`);
expect(edit.selection).toEqual(link.range);
});
it('should rename both target and section', () => {
const link = parser.parse(
getRandomURI(),
`this is a [[wikilink#section]]`
).links[0];
const edit = MarkdownLink.createUpdateLinkEdit(link, {
target: 'new-link',
section: 'new-section',
});
expect(edit.newText).toEqual(`[[new-link#new-section]]`);
expect(edit.selection).toEqual(link.range);
});
it('should be able to remove the section', () => {
const link = parser.parse(
getRandomURI(),
`this is a [[wikilink#section]]`
).links[0];
const edit = MarkdownLink.createUpdateLinkEdit(link, {
section: '',
});
expect(edit.newText).toEqual(`[[wikilink]]`);
expect(edit.selection).toEqual(link.range);
});
it('should be able to rename the alias', () => {
const link = parser.parse(getRandomURI(), `this is a [[wikilink|alias]]`)
.links[0];
const edit = MarkdownLink.createUpdateLinkEdit(link, {
alias: 'new-alias',
});
expect(edit.newText).toEqual(`[[wikilink|new-alias]]`);
expect(edit.selection).toEqual(link.range);
});
});

describe('rename direct link', () => {
it('should rename the target only', () => {
const link = parser.parse(getRandomURI(), `this is a [link](to/path.md)`)
.links[0];
const edit = MarkdownLink.createUpdateLinkEdit(link, {
target: 'to/another-path.md',
});
expect(edit.newText).toEqual(`[link](to/another-path.md)`);
expect(edit.selection).toEqual(link.range);
});
it('should rename the section only', () => {
const link = parser.parse(
getRandomURI(),
`this is a [link](to/path.md#section)`
).links[0];
const edit = MarkdownLink.createUpdateLinkEdit(link, {
section: 'section2',
});
expect(edit.newText).toEqual(`[link](to/path.md#section2)`);
expect(edit.selection).toEqual(link.range);
});
it('should rename both target and section', () => {
const link = parser.parse(
getRandomURI(),
`this is a [link](to/path.md#section)`
).links[0];
const edit = MarkdownLink.createUpdateLinkEdit(link, {
target: 'to/another-path.md',
section: 'section2',
});
expect(edit.newText).toEqual(`[link](to/another-path.md#section2)`);
expect(edit.selection).toEqual(link.range);
});
it('should be able to remove the section', () => {
const link = parser.parse(
getRandomURI(),
`this is a [link](to/path.md#section)`
).links[0];
const edit = MarkdownLink.createUpdateLinkEdit(link, {
section: '',
});
expect(edit.newText).toEqual(`[link](to/path.md)`);
expect(edit.selection).toEqual(link.range);
});
});
});
57 changes: 57 additions & 0 deletions packages/foam-vscode/src/core/services/markdown-link.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { ResourceLink } from '../model/note';

export abstract class MarkdownLink {
private static wikilinkRegex = new RegExp(
/\[\[([^#\|]+)?#?([^\|]+)?\|?(.*)?\]\]/
);
private static directLinkRegex = new RegExp(
/\[([^\]]+)\]\(([^#]*)?#?([^\]]+)?\)/
);

public static analyzeLink(link: ResourceLink) {
if (link.type === 'wikilink') {
const [_, target, section, alias] = this.wikilinkRegex.exec(link.rawText);
return {
target: target?.replace(/\\/g, ''),
section,
alias,
};
}
if (link.type === 'link') {
const [_, alias, target, section] = this.directLinkRegex.exec(
link.rawText
);
return { target, section, alias };
}
throw new Error(
`Unexpected state: link of type ${link.type} is not supported`
);
}

public static createUpdateLinkEdit(
link: ResourceLink,
delta: { target?: string; section?: string; alias?: string }
) {
const { target, section, alias } = MarkdownLink.analyzeLink(link);
const newTarget = delta.target ?? target;
const newSection = delta.section ?? section ?? '';
const newAlias = delta.alias ?? alias ?? '';
const sectionDivider = newSection ? '#' : '';
const aliasDivider = newAlias ? '|' : '';
if (link.type === 'wikilink') {
return {
newText: `[[${newTarget}${sectionDivider}${newSection}${aliasDivider}${newAlias}]]`,
selection: link.range,
};
}
if (link.type === 'link') {
return {
newText: `[${newAlias}](${newTarget}${sectionDivider}${newSection})`,
selection: link.range,
};
}
throw new Error(
`Unexpected state: link of type ${link.type} is not supported`
);
}
}
24 changes: 6 additions & 18 deletions packages/foam-vscode/src/core/services/markdown-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ describe('Markdown parsing', () => {
expect(note.links.length).toEqual(1);
const link = note.links[0];
expect(link.type).toEqual('link');
expect(link.label).toEqual('link to page b');
expect(link.rawText).toEqual('[link to page b](../doc/page-b.md)');
expect(link.target).toEqual('../doc/page-b.md');
});

it('should detect links that have formatting in label', () => {
Expand All @@ -51,8 +49,6 @@ describe('Markdown parsing', () => {
expect(note.links.length).toEqual(1);
const link = note.links[0];
expect(link.type).toEqual('link');
expect(link.label).toEqual('link with formatting');
expect(link.target).toEqual('../doc/page-b.md');
});

it('should detect wikilinks', () => {
Expand All @@ -63,13 +59,9 @@ describe('Markdown parsing', () => {
let link = note.links[0];
expect(link.type).toEqual('wikilink');
expect(link.rawText).toEqual('[[a link]]');
expect(link.label).toEqual('a link');
expect(link.target).toEqual('a link');
link = note.links[1];
expect(link.type).toEqual('wikilink');
expect(link.rawText).toEqual('[[a file]]');
expect(link.label).toEqual('a file');
expect(link.target).toEqual('a file');
});

it('should detect wikilinks that have aliases', () => {
Expand All @@ -80,13 +72,9 @@ describe('Markdown parsing', () => {
let link = note.links[0];
expect(link.type).toEqual('wikilink');
expect(link.rawText).toEqual('[[link|link alias]]');
expect(link.label).toEqual('link alias');
expect(link.target).toEqual('link');
link = note.links[1];
expect(link.type).toEqual('wikilink');
expect(link.rawText).toEqual('[[other link | spaced]]');
expect(link.label).toEqual('spaced');
expect(link.target).toEqual('other link');
});

it('should skip wikilinks in codeblocks', () => {
Expand All @@ -99,9 +87,9 @@ this is inside a [[codeblock]]
this is some text with our [[second-wikilink]].
`);
expect(noteA.links.map(l => l.label)).toEqual([
'first-wikilink',
'second-wikilink',
expect(noteA.links.map(l => l.rawText)).toEqual([
'[[first-wikilink]]',
'[[second-wikilink]]',
]);
});

Expand All @@ -113,9 +101,9 @@ this is \`inside a [[codeblock]]\`
this is some text with our [[second-wikilink]].
`);
expect(noteA.links.map(l => l.label)).toEqual([
'first-wikilink',
'second-wikilink',
expect(noteA.links.map(l => l.rawText)).toEqual([
'[[first-wikilink]]',
'[[second-wikilink]]',
]);
});
});
Expand Down
Loading

0 comments on commit 5b7a2ab

Please sign in to comment.