Compare commits

..

12 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
Jani Eväkallio
d7b930ff1e Add NoteLink.position to keep track of link ranges (#108) 2020-07-14 08:04:25 +01:00
21 changed files with 430 additions and 177 deletions

View File

@@ -1,29 +0,0 @@
# Proposal: Block Embeds
Embedding content blocks (sections, or any html blocks with id) so that they are visible in the document where they are being linked from.
We could use [VS Code Peek Definition](https://docs.microsoft.com/en-us/visualstudio/ide/how-to-view-and-edit-code-by-using-peek-definition-alt-plus-f12?view=vs-2019) to do this.
You should be able to embed any wiki link, using the same [[proposal-deep-links]] anchor syntax:
```
page#anchor
```
Our options for syntax:
1. HTML style embed
<embed ref="blank-document#section-3-bis"/>
2. Mediawiki hybrid monster
[[blank-document#section-1]]<embed />
3. Our own special ?embed hack
[[blank-document?embed#section-1]]
In order to get these to render inside HTML pages, we will need to customise the renderer. [MDX](https://github.com/mdx-js/mdx)
Before properly implementing embedding with custom sections we need to solve some of the issues described in [[proposal-deep-links]]
[//begin]: # "Autogenerated link references for markdown compatibility"
[proposal-deep-links]: proposal-deep-links "Proposal: Deep links"
[//end]: # "Autogenerated link references"

View File

@@ -1,86 +0,0 @@
# Proposal: Deep links
Linking to a section within a document.
A section is a part of a markdown document.
Sections are automatically created when using a heading.
Custom sections can also be created by using content blocks (see [Declaring content blocks](#declaring-content-blocks) and [Content block](#content-block])
## Section Heading
Step 1: Just link to headings:
[[document#section-heading]]
## Declaring content blocks
Step 2: Declare sections via HTML content blocks
`<div id='content-block'>`
<div id='content-block'>
This is a content block that has been defined using html, instead of automatically generated by a heading.
</div>
`</div>`
This will automatically work because element ids are anchor targets in HTML:
[[document#content-block]]
You can use this for any HTML element (as long as it doesn't get filtered out by e.g. GitHub)
### Challenges
There is a problem with html blocks in md. The content of the block is not rendered as markdown but as html. Unclear how this is accounted for in the [MediaLink](https://www.mediawiki.org/wiki/Help:Links) reference.
> Note that this is only relevant in the context of embedding because when navigating we only care about the beginning of the section.
An alternative for this would be to use self-closing html tags and just assume that a section runs into the beginning of the next block boundary, defined by another HTML element or a section heading, or end of file:
```
<div id="content-block" />
```
## Declaring inline content blocks
Step 3: Use span instead of using a block element (div)
- Outline <span id="1" />
- Inline text <span id="2" />
This does not have the problem with markdown not being treated as markdown inside html blocks, because there is no content in the html blocks.
## Syntax sugar for blocks
Step 4: Syntax sugar (in the future)
```md
- Some ideas. Explore at a future
- Many bullets #a
- Ideas here [#b]
- Then some [^c]
```
## Alternatives:
We could use reference blocks:
[foam:block]: id
[foam:block]: end
```
[foam:block]: id
Hello
[foam:block]: end
```
Challenge here is that we need to have a blank line immediately before each link reference definition.
The `foam:` namespacing is just to ensure we don't collide over any real documents, e.g. one called "block.md"
End of block is optional, and section can run until the next heading

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 { Link, Note, NoteGraph } from './note-graph';
import { Note, NoteLink, NoteLinkDefinition, NoteGraph } from './note-graph';
import { dropExtension } from './utils';
let processor: unified.Processor | null = null;
@@ -29,29 +29,46 @@ export function createNoteFromMarkdown(uri: string, markdown: string): Note {
}
return title === id ? CONTINUE : EXIT;
});
const links: Link[] = [];
const links: NoteLink[] = [];
const definitions: NoteLinkDefinition[] = [];
visit(tree, node => {
if (node.type === 'wikiLink') {
links.push({
from: id,
to: node.value as string,
text: node.value as string,
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,4 +1,5 @@
import { Graph, Edge } from 'graphlib';
import { Position, Point } from 'unist';
type ID = string;
@@ -11,6 +12,14 @@ export interface Link {
export interface NoteLink {
to: ID;
text: string;
position: Position;
}
export interface NoteLinkDefinition {
label: string;
url: string;
title?: string;
position?: Position;
}
export class Note {
@@ -18,12 +27,16 @@ export class Note {
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
) {
@@ -32,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,11 +1,24 @@
import { NoteGraph, Note } from '../src/note-graph';
const position = {
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
@@ -17,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' }],
[{ 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
@@ -39,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' }],
[{ 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
@@ -62,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();
});
@@ -73,7 +100,9 @@ describe('Note graph', () => {
new Note(
'page-a',
'page-a',
[{ to: 'non-existing', text: 'does not exist' }],
[{ to: 'non-existing', text: 'does not exist', position }],
[],
documentEnd,
'/path-b.md',
''
)
@@ -83,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' }],
[{ 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
@@ -118,7 +153,9 @@ describe('Note graph', () => {
new Note(
'page-b',
'page-b',
[{ to: 'page-c', text: 'go' }],
[{ 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"