Compare commits

...

11 Commits

Author SHA1 Message Date
chirag-singhal
bb8d0dabba added tests for generateHeading in janitor 2020-07-14 20:44:54 +05:30
chirag-singhal
b113cafeba added generate Heading function to janitor 2020-07-14 20:40:01 +05:30
chirag-singhal
79a5621f31 Add no change in link definitions test to generateLinkReferences janitor method 2020-07-14 17:18:17 +05:30
chirag-singhal
b987ae7a3f Add update link definitions test to generateLinkReferences janitor method 2020-07-14 16:36:47 +05:30
chirag-singhal
6fa858f8d4 Add remove link definitions test to generateLinkReferences janitor method 2020-07-14 16:17:56 +05:30
Jani Eväkallio
3e20dc3356 Add partial tests for generateLinkReferenceDefinitions 2020-07-14 10:06:57 +01:00
Jani Eväkallio
d65f724b56 Implement first version of generateLinkReferenceDefinitions janitor method 2020-07-14 10:06:23 +01:00
Jani Eväkallio
6bd9aaa949 Export stringifyMarkdownLinkReferenceDefinition from foam-core 2020-07-14 10:05:47 +01:00
Jani Eväkallio
d905972f61 Add Note.end and Note.definitions to foam-core tests 2020-07-14 10:04:38 +01:00
Jani Eväkallio
3511ce30e3 Use stringifyMarkdownLinkReferenceDefinition in foam-vscode
This commit also applies prettier to previously badly formatted files, so the diff is larger than necessary
2020-07-14 10:03:32 +01:00
Jani Eväkallio
215dea151f Add Note.definitions and Note.end 2020-07-14 10:02:49 +01:00
19 changed files with 420 additions and 59 deletions

View File

@@ -17,6 +17,7 @@
"devDependencies": {
"@types/graphlib": "^2.1.6",
"@types/lodash": "^4.14.157",
"glob": "^7.1.6",
"husky": "^4.2.5",
"tsdx": "^0.13.2",
"tslib": "^2.0.0",
@@ -27,6 +28,7 @@
"lodash": "^4.17.15",
"remark-parse": "^8.0.2",
"remark-wiki-link": "^0.0.4",
"title-case": "^3.0.2",
"unified": "^9.0.0",
"unist-util-visit": "^2.0.2"
},

View File

@@ -3,20 +3,21 @@ import { NoteGraph, Note, NoteLink } from './note-graph';
export {
createNoteFromMarkdown,
createMarkdownReferences,
stringifyMarkdownLinkReferenceDefinition,
} from './markdown-provider';
export { NoteGraph, Note, NoteLink }
export { NoteGraph, Note, NoteLink };
export interface FoamConfig {
// TODO
}
export interface Foam {
notes: NoteGraph
notes: NoteGraph;
// config: FoamConfig
}
export const createFoam = (config: FoamConfig) => ({
notes: new NoteGraph(),
config: config,
})
});

View File

@@ -0,0 +1,67 @@
import { Note, NoteGraph } from '../index';
import {
createMarkdownReferences,
stringifyMarkdownLinkReferenceDefinition,
} from '../markdown-provider';
import { Position } from 'unist';
import { getHeadingFromFileName } from '../utils';
interface TextEdit {
range: Position;
newText: string;
}
export const generateLinkReferences = (note: Note, ng: NoteGraph): TextEdit | null => {
const newReferences = createMarkdownReferences(ng, note.id).map(
stringifyMarkdownLinkReferenceDefinition
).join('\n');
if (note.definitions.length === 0) {
if (newReferences.length === 0) {
return null;
}
// @todo: how to abstract new line?
const padding = note.end.column === 1 ? '\n' : '\n\n';
return {
newText: `${padding}${newReferences}`,
range: {
start: note.end,
end: note.end,
},
};
} else {
const first = note.definitions[0];
const last = note.definitions[note.definitions.length - 1];
const oldRefrences = note.definitions.map(stringifyMarkdownLinkReferenceDefinition).join('\n');
if(oldRefrences === newReferences){
return null;
}
return {
// @todo: do we need to ensure new lines?
newText: `${newReferences}`,
range: {
start: first.position!.start,
end: last.position!.end,
},
};
}
};
export const generateHeading = (note: Note): TextEdit | null => {
// Note: This may not work if the heading is same as the file name
if (note.title !== note.id) {
return null;
}
return {
newText: `# ${getHeadingFromFileName(note.id)}\n\n`,
range: {
start: { line: 0, column: 0, offset: 0 },
end: { line: 0, column: 0, offset: 0 }
}
}
};

View File

@@ -4,7 +4,7 @@ import wikiLinkPlugin from 'remark-wiki-link';
import visit, { CONTINUE, EXIT } from 'unist-util-visit';
import { Node, Parent } from 'unist';
import * as path from 'path';
import { Note, NoteLink, NoteGraph } from './note-graph';
import { Note, NoteLink, NoteLinkDefinition, NoteGraph } from './note-graph';
import { dropExtension } from './utils';
let processor: unified.Processor | null = null;
@@ -30,28 +30,45 @@ export function createNoteFromMarkdown(uri: string, markdown: string): Note {
return title === id ? CONTINUE : EXIT;
});
const links: NoteLink[] = [];
const definitions: NoteLinkDefinition[] = [];
visit(tree, node => {
if (node.type === 'wikiLink') {
links.push({
to: node.value as string,
text: node.value as string,
position: node.position!
position: node.position!,
});
}
if (node.type === 'definition') {
definitions.push({
label: node.label as string,
url: node.url as string,
title: node.title as string,
position: node.position,
});
}
});
return new Note(id, title, links, uri, markdown);
const end = tree.position!.end;
return new Note(id, title, links, definitions, end, uri, markdown);
}
interface MarkdownReference {
linkText: string;
wikiLink: string;
pageTitle: string;
}
export function stringifyMarkdownLinkReferenceDefinition(
definition: NoteLinkDefinition
) {
let text = `[${definition.label}]: ${definition.url}`;
if (definition.title) {
text = `${text} "${definition.title}"`;
}
return text;
}
export function createMarkdownReferences(
graph: NoteGraph,
noteId: string
): MarkdownReference[] {
): NoteLinkDefinition[] {
const source = graph.getNote(noteId);
// Should never occur since we're already in a file,
@@ -85,11 +102,11 @@ export function createMarkdownReferences(
// [wiki-link-text]: wiki-link "Page title"
return {
linkText: link.to,
wikiLink: relativePathWithoutExtension,
pageTitle: target.title,
label: link.to,
url: relativePathWithoutExtension,
title: target.title,
};
})
.filter(Boolean)
.sort() as MarkdownReference[];
.sort() as NoteLinkDefinition[];
}

View File

@@ -1,5 +1,5 @@
import { Graph, Edge } from 'graphlib';
import { Position } from 'unist';
import { Position, Point } from 'unist';
type ID = string;
@@ -15,17 +15,28 @@ export interface NoteLink {
position: Position;
}
export interface NoteLinkDefinition {
label: string;
url: string;
title?: string;
position?: Position;
}
export class Note {
public id: ID;
public title: string;
public source: string;
public path: string;
public end: Point;
public links: NoteLink[];
public definitions: NoteLinkDefinition[];
constructor(
id: ID,
title: string,
links: NoteLink[],
definitions: NoteLinkDefinition[],
end: Point,
path: string,
source: string
) {
@@ -34,6 +45,8 @@ export class Note {
this.source = source;
this.path = path;
this.links = links;
this.definitions = definitions;
this.end = end;
}
}

View File

@@ -1,5 +1,17 @@
import { titleCase } from 'title-case';
export function dropExtension(path: string): string {
const parts = path.split('.');
parts.pop();
return parts.join('.');
}
/**
*
* @param filename
* @returns title cased heading after removing special characters
*/
export const getHeadingFromFileName = (filename: string): string => {
return titleCase(filename.replace(/[^\w\s]/gi, ' '));
}

View File

@@ -0,0 +1 @@
This file is missing a title

View File

@@ -0,0 +1,7 @@
# First Document
[[file-without-title]]
[//begin]: # 'Autogenerated link references for markdown compatibility'
[second-document]: second-document 'Second Document'
[//end]: # 'Autogenerated link references'

View File

@@ -0,0 +1,9 @@
# Index
This file is intentionally missing the link reference definitions
[[first-document]]
[[second-document]]
[[file-without-title]]

View File

@@ -0,0 +1,22 @@
import glob from 'glob';
import { promisify } from 'util';
import fs from 'fs';
import { NoteGraph} from '../../src/note-graph';
import { createNoteFromMarkdown } from '../../src/markdown-provider';
const findAllFiles = promisify(glob);
export const scaffold = async () => {
const files = await findAllFiles('test/__scaffold__/**/*.md', {});
const graph = new NoteGraph();
await Promise.all(
(await files).map(f => {
return fs.promises.readFile(f).then(data => {
const markdown = (data || '').toString();
graph.setNote(createNoteFromMarkdown(f, markdown));
});
})
);
return graph;
};

View File

@@ -0,0 +1,9 @@
# Second Document
This is just a link target for now.
We can use it for other things later if needed.
[//begin]: # 'Autogenerated link references for markdown compatibility'
[first-document]: first-document 'First Document'
[//end]: # 'Autogenerated link references'

View File

@@ -0,0 +1,11 @@
# 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"

View File

@@ -1,16 +1,24 @@
import { NoteGraph, Note } from '../src/note-graph';
const position = {
start: { line: 0, column: 0},
end: { line: 0, column: 0}
start: { line: 0, column: 0 },
end: { line: 0, column: 0 },
};
const documentEnd = position.end;
describe('Note graph', () => {
it('Adds notes to graph', () => {
const graph = new NoteGraph();
graph.setNote(new Note('page-a', 'page-a', [], '/page-a.md', ''));
graph.setNote(new Note('page-b', 'page-b', [], '/page-b.md', ''));
graph.setNote(new Note('page-c', 'page-c', [], '/page-c.md', ''));
graph.setNote(
new Note('page-a', 'page-a', [], [], documentEnd, '/page-a.md', '')
);
graph.setNote(
new Note('page-b', 'page-b', [], [], documentEnd, '/page-b.md', '')
);
graph.setNote(
new Note('page-c', 'page-c', [], [], documentEnd, '/page-c.md', '')
);
expect(
graph
@@ -22,17 +30,23 @@ describe('Note graph', () => {
it('Detects forward links', () => {
const graph = new NoteGraph();
graph.setNote(new Note('page-a', 'page-a', [], '/page-a.md', ''));
graph.setNote(
new Note('page-a', 'page-a', [], [], documentEnd, '/page-a.md', '')
);
graph.setNote(
new Note(
'page-b',
'page-b',
[{ to: 'page-a', text: 'go', position }],
[],
documentEnd,
'/page-b.md',
''
)
);
graph.setNote(new Note('page-c', 'page-c', [], '/page-c.md', ''));
graph.setNote(
new Note('page-c', 'page-c', [], [], documentEnd, '/page-c.md', '')
);
expect(
graph
@@ -44,17 +58,23 @@ describe('Note graph', () => {
it('Detects backlinks', () => {
const graph = new NoteGraph();
graph.setNote(new Note('page-a', 'page-a', [], '/page-a.md', ''));
graph.setNote(
new Note('page-a', 'page-a', [], [], documentEnd, '/page-a.md', '')
);
graph.setNote(
new Note(
'page-b',
'page-b',
[{ to: 'page-a', text: 'go', position }],
[],
documentEnd,
'/page-b.md',
''
)
);
graph.setNote(new Note('page-c', 'page-c', [], '/page-c.md', ''));
graph.setNote(
new Note('page-c', 'page-c', [], [], documentEnd, '/page-c.md', '')
);
expect(
graph
@@ -67,7 +87,9 @@ describe('Note graph', () => {
it('Fails when accessing non-existing node', () => {
expect(() => {
const graph = new NoteGraph();
graph.setNote(new Note('page-a', 'page-a', [], '/path-b.md', ''));
graph.setNote(
new Note('page-a', 'page-a', [], [], documentEnd, '/path-b.md', '')
);
graph.getNote('non-existing');
}).toThrow();
});
@@ -79,6 +101,8 @@ describe('Note graph', () => {
'page-a',
'page-a',
[{ to: 'non-existing', text: 'does not exist', position }],
[],
documentEnd,
'/path-b.md',
''
)
@@ -88,17 +112,23 @@ describe('Note graph', () => {
it('Updates links when modifying note', () => {
const graph = new NoteGraph();
graph.setNote(new Note('page-a', 'page-a', [], '/page-a.md', ''));
graph.setNote(
new Note('page-a', 'page-a', [], [], documentEnd, '/page-a.md', '')
);
graph.setNote(
new Note(
'page-b',
'page-b',
[{ to: 'page-a', text: 'go', position }],
[],
documentEnd,
'/page-b.md',
''
)
);
graph.setNote(new Note('page-c', 'page-c', [], '/page-c.md', ''));
graph.setNote(
new Note('page-c', 'page-c', [], [], documentEnd, '/page-c.md', '')
);
expect(
graph
@@ -124,6 +154,8 @@ describe('Note graph', () => {
'page-b',
'page-b',
[{ to: 'page-c', text: 'go', position }],
[],
documentEnd,
'/path-2b.md',
''
)

View File

@@ -0,0 +1,52 @@
import { NoteGraph, Note } from '../../src/note-graph';
import { generateHeading } from '../../src/janitor';
import { scaffold } from '../__scaffold__';
describe('generateHeadings', () => {
let _graph: NoteGraph;
beforeAll(async () => {
_graph = await scaffold();
});
it('should add heading to a file that does not have them', () => {
const note = _graph.getNote('file-without-title') as Note;
const expected = {
newText: `# File without Title
`,
range: {
start: {
line: 0,
column: 0,
offset: 0,
},
end: {
line: 0,
column: 0,
offset: 0,
},
},
}
const actual = generateHeading(note!);
expect(actual!.range.start).toEqual(expected.range.start);
expect(actual!.range.end).toEqual(expected.range.end);
expect(actual!.newText).toEqual(expected.newText);
});
it('should not cause any changes to a file that does heading', () => {
const note = _graph.getNote('index') as Note;
const expected = null;
const actual = generateHeading(note!);
expect(actual).toEqual(expected);
})
});

View File

@@ -0,0 +1,105 @@
import { NoteGraph, Note } from '../../src/note-graph';
import { generateLinkReferences } from '../../src/janitor';
import { scaffold } from '../__scaffold__';
describe('generateLinkReferences', () => {
let _graph: NoteGraph;
beforeAll(async () => {
_graph = await scaffold();
});
it('initialised test graph correctly', () => {
expect(_graph.getNotes().length).toEqual(5);
});
it('should add link references to a file that does not have them', () => {
const note = _graph.getNote('index') as Note;
const expected = {
newText: `
[first-document]: first-document "First Document"
[second-document]: second-document "Second Document"
[file-without-title]: file-without-title "file-without-title"`,
range: {
start: {
line: 10,
column: 1,
offset: 140,
},
end: {
line: 10,
column: 1,
offset: 140,
},
},
};
const actual = generateLinkReferences(note!, _graph);
expect(actual!.range.start).toEqual(expected.range.start);
expect(actual!.range.end).toEqual(expected.range.end);
expect(actual!.newText).toEqual(expected.newText);
});
it('should remove link definitions from a file that has them, if no links are present', () => {
const note = _graph.getNote('second-document') as Note;
const expected = {
newText: "",
range: {
start: {
line: 7,
column: 1,
offset: 105,
},
end: {
line: 9,
column: 43,
offset: 269,
},
},
};
const actual = generateLinkReferences(note!, _graph);
expect(actual!.range.start).toEqual(expected.range.start);
expect(actual!.range.end).toEqual(expected.range.end);
expect(actual!.newText).toEqual(expected.newText);
});
it('should update link definitions if they are present but changed', () => {
const note = _graph.getNote('first-document') as Note;
const expected = {
newText: `[file-without-title]: file-without-title "file-without-title"`,
range: {
start: {
line: 5,
column: 1,
offset: 42,
},
end: {
line: 7,
column: 43,
offset: 209,
},
},
};
const actual = generateLinkReferences(note!, _graph);
expect(actual!.range.start).toEqual(expected.range.start);
expect(actual!.range.end).toEqual(expected.range.end);
expect(actual!.newText).toEqual(expected.newText);
});
it('should not cause any changes if link reference definitions were up to date', () => {
const note = _graph.getNote('third-document') as Note;
const expected = null;
const actual = generateLinkReferences(note!, _graph);
expect(actual).toEqual(expected);
});
});

View File

@@ -18,6 +18,7 @@ const pageC = `
# Page C
`;
// @todo: Add tests for definitions
describe('Markdown loader', () => {
it('Converts markdown to notes', () => {
const graph = new NoteGraph();

View File

@@ -11,10 +11,10 @@ import { createNoteFromMarkdown, createFoam, FoamConfig } from "foam-core";
import { features } from "./features";
export function activate(context: ExtensionContext) {
const foamPromise = bootstrap(getConfig())
const foamPromise = bootstrap(getConfig());
features.forEach(f => {
f.activate(context, foamPromise);
})
});
}
const bootstrap = async (config: FoamConfig) => {
@@ -34,8 +34,5 @@ const bootstrap = async (config: FoamConfig) => {
};
const getConfig = () => {
return {}
}
return {};
};

View File

@@ -13,18 +13,31 @@ import {
Position
} from "vscode";
import { createMarkdownReferences, createNoteFromMarkdown, NoteGraph, Foam } from "foam-core";
import {
createMarkdownReferences,
stringifyMarkdownLinkReferenceDefinition,
createNoteFromMarkdown,
NoteGraph,
Foam
} from "foam-core";
import { basename } from "path";
import { hasEmptyTrailing, docConfig, loadDocConfig, isMdEditor, mdDocSelector, getText, dropExtension } from "../utils";
import {
hasEmptyTrailing,
docConfig,
loadDocConfig,
isMdEditor,
mdDocSelector,
getText,
dropExtension
} from "../utils";
import { FoamFeature } from "../types";
const feature: FoamFeature = {
activate: async (context: ExtensionContext, foamPromise: Promise<Foam>) => {
activate: async (context: ExtensionContext, foamPromise: Promise<Foam>) => {
const foam = await foamPromise;
context.subscriptions.push(
commands.registerCommand(
"foam-vscode.update-wikilinks",
() => updateReferenceList(foam.notes)
commands.registerCommand("foam-vscode.update-wikilinks", () =>
updateReferenceList(foam.notes)
),
workspace.onWillSaveTextDocument(e => {
if (e.document.languageId === "markdown") {
@@ -42,7 +55,6 @@ const feature: FoamFeature = {
}
};
const REFERENCE_HEADER = `[//begin]: # "Autogenerated link references for markdown compatibility"`;
const REFERENCE_FOOTER = `[//end]: # "Autogenerated link references"`;
@@ -97,14 +109,17 @@ async function updateReferenceList(foam: NoteGraph) {
}
}
async function generateReferenceList(foam: NoteGraph, doc: TextDocument): Promise<string[]> {
async function generateReferenceList(
foam: NoteGraph,
doc: TextDocument
): Promise<string[]> {
const filePath = doc.fileName;
const id = dropExtension(basename(filePath));
const references = uniq(
createMarkdownReferences(foam, id).map(
link => `[${link.linkText}]: ${link.wikiLink} "${link.pageTitle}"`
stringifyMarkdownLinkReferenceDefinition
)
);
@@ -146,10 +161,10 @@ function detectReferenceListRange(doc: TextDocument): Range {
}
class WikilinkReferenceCodeLensProvider implements CodeLensProvider {
private foam: NoteGraph
private foam: NoteGraph;
constructor(foam: NoteGraph) {
this.foam = foam
this.foam = foam;
}
public provideCodeLenses(

View File

@@ -4749,18 +4749,6 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
foam-core@0.2.0-alpha.0:
version "0.2.0-alpha.0"
resolved "https://registry.yarnpkg.com/foam-core/-/foam-core-0.2.0-alpha.0.tgz#41377dd1280aca370f4ef046d6e4b27fadd9a35a"
integrity sha512-PNqRsDJILU233WJuooyo/pxXT3G7gSeH/omD2b7AmJvGtHGhuX7csY32iwmWoh1aVoGN5pAhSW4wM/CX45MgFw==
dependencies:
graphlib "^2.1.8"
lodash "^4.17.15"
remark-parse "^8.0.2"
remark-wiki-link "^0.0.4"
unified "^9.0.0"
unist-util-visit "^2.0.2"
for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"