Wikilink navigation in markdown preview panel (#521)

* `FoamWorkspace.find` to return `null`  when no reference is provided for relative path

* turning wikilinks into browsable links in markdown preview

* moved preview styles in css file and reorganized code in static folder

Static was previous used only for the dataviz graph. Now we have 2 subdirectories: dataviz for the graph, and preview for the markdown preview.
For now the css style is a bit of an overkill, but sets up the right foundation for further customization down the line.

* chore: explicitly disabling gitdoc extension, removing unnecessary async keyword

* fix: fixed test utility fn (and linter warning)

* test: added tests for preview link generation

* changed launch configuration to support both foam-core and foam-vscode packages
This commit is contained in:
Riccardo
2021-03-11 15:31:05 +01:00
committed by GitHub
parent a6db7815f0
commit fa4b9d57aa
18 changed files with 224 additions and 43 deletions

11
.vscode/launch.json vendored
View File

@@ -4,12 +4,21 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"inputs": [
{
"id": "packageName",
"type": "pickString",
"description": "Select the package in which this test is located",
"options": ["foam-core", "foam-vscode"],
"default": "foam-core"
}
],
"configurations": [
{
"type": "node",
"name": "vscode-jest-tests",
"request": "launch",
"runtimeArgs": ["workspace", "foam-core", "run", "test"], // ${yarnWorkspaceName} is what we're missing
"runtimeArgs": ["workspace", "${input:packageName}", "run", "test"], // ${yarnWorkspaceName} is what we're missing
"args": [
"--runInBand"
],

View File

@@ -22,5 +22,10 @@
"prettier.requireConfig": true,
"editor.formatOnSave": true,
"editor.tabSize": 2,
"jest.debugCodeLens.showWhenTestStateIn": ["fail", "unknown", "pass"]
"jest.debugCodeLens.showWhenTestStateIn": [
"fail",
"unknown",
"pass"
],
"gitdoc.enabled": false
}

View File

@@ -90,7 +90,7 @@ export class FoamWorkspace implements IDisposable {
get(uri: URI) {
return FoamWorkspace.get(this, uri);
}
find(uri: URI) {
find(uri: URI | string) {
return FoamWorkspace.find(this, uri);
}
set(resource: Resource) {
@@ -312,10 +312,7 @@ export class FoamWorkspace implements IDisposable {
case 'relative-path':
if (isNone(reference)) {
throw new Error(
'Cannot find note defined by relative path without reference note: ' +
resourceId
);
return null;
}
const relativePath = resourceId as string;
const targetUri = computeRelativeURI(reference, relativePath);

View File

@@ -1,6 +1,7 @@
import { NoteLinkDefinition, Note, Attachment } from '../src/model/note';
import { URI } from '../src/common/uri';
import { Logger } from '../src/utils/log';
import { parseUri } from '../src/utils';
Logger.setLevel('error');
@@ -37,7 +38,7 @@ export const createTestNote = (params: {
}): Note => {
const root = params.root ?? URI.file('/');
return {
uri: strToUri(params.uri),
uri: parseUri(root, params.uri),
type: 'note',
properties: {},
title: params.title ?? null,

View File

@@ -32,6 +32,10 @@
],
"main": "./out/extension.js",
"contributes": {
"markdown.markdownItPlugins": true,
"markdown.previewStyles": [
"./static/preview/style.css"
],
"views": {
"explorer": [
{
@@ -360,6 +364,7 @@
"@babel/preset-typescript": "^7.10.4",
"@types/dateformat": "^3.0.1",
"@types/glob": "^7.1.1",
"@types/markdown-it": "^12.0.1",
"@types/node": "^13.11.0",
"@types/remove-markdown": "^0.1.1",
"@types/vscode": "^1.47.1",
@@ -371,6 +376,7 @@
"jest": "^26.2.2",
"jest-environment-vscode": "^1.0.0",
"jest-extended": "^0.11.5",
"markdown-it": "^12.0.4",
"rimraf": "^3.0.2",
"ts-jest": "^26.4.4",
"typescript": "^3.8.3",
@@ -380,6 +386,7 @@
"dateformat": "^3.0.3",
"foam-core": "^0.11.0",
"gray-matter": "^4.0.2",
"markdown-it-regex": "^0.2.0",
"micromatch": "^4.0.2",
"remove-markdown": "^0.3.0"
}

View File

@@ -29,14 +29,22 @@ export async function activate(context: ExtensionContext) {
};
const foamPromise: Promise<Foam> = bootstrap(config, services);
features.forEach(f => {
f.activate(context, foamPromise);
});
const resPromises = features.map(f => f.activate(context, foamPromise));
const foam = await foamPromise;
Logger.info(`Loaded ${foam.workspace.list().length} notes`);
context.subscriptions.push(dataStore, foam, watcher);
const res = (await Promise.all(resPromises)).filter(r => r != null);
return {
extendMarkdownIt: (md: markdownit) => {
return res.reduce((acc: markdownit, r: any) => {
return r.extendMarkdownIt ? r.extendMarkdownIt(acc) : acc;
}, md);
},
};
} catch (e) {
Logger.error('An error occurred while bootstrapping Foam', e);
window.showErrorMessage(

View File

@@ -156,41 +156,26 @@ async function getWebviewContent(
context: vscode.ExtensionContext,
panel: vscode.WebviewPanel
) {
const webviewPath = vscode.Uri.file(
path.join(context.extensionPath, 'static', 'dataviz.html')
);
const file = await vscode.workspace.fs.readFile(webviewPath);
const text = new TextDecoder('utf-8').decode(file);
const datavizPath = [context.extensionPath, 'static', 'dataviz'];
const webviewUri = (fileName: string) =>
panel.webview
.asWebviewUri(
vscode.Uri.file(path.join(context.extensionPath, 'static', fileName))
)
.toString();
const graphDirectory = path.join('graphs', 'default');
const textWithVariables = text
.replace(
'${graphPath}', // eslint-disable-line
'{{' + path.join(graphDirectory, 'graph.js') + '}}'
)
.replace(
'${graphStylesPath}', // eslint-disable-line
'{{' + path.join(graphDirectory, 'graph.css') + '}}'
const getWebviewUri = (fileName: string) =>
panel.webview.asWebviewUri(
vscode.Uri.file(path.join(...datavizPath, fileName))
);
// Basic templating. Will replace the script paths with the
// appropriate webview URI.
const filled = textWithVariables.replace(
/<script data-replace src="([^"]+")/g,
match => {
const indexHtml = await vscode.workspace.fs.readFile(
vscode.Uri.file(path.join(...datavizPath, 'index.html'))
);
// Replace the script paths with the appropriate webview URI.
const filled = new TextDecoder('utf-8')
.decode(indexHtml)
.replace(/<script data-replace src="([^"]+")/g, match => {
const fileName = match
.slice('<script data-replace src="'.length, -1)
.trim();
return '<script src="' + webviewUri(fileName) + '"';
}
);
return '<script src="' + getWebviewUri(fileName).toString() + '"';
});
return filled;
}

View File

@@ -11,6 +11,7 @@ import orphans from './orphans';
import placeholders from './placeholders';
import backlinks from './backlinks';
import utilityCommands from './utility-commands';
import previewNavigation from './preview-navigation';
import { FoamFeature } from '../types';
export const features: FoamFeature[] = [
@@ -27,4 +28,5 @@ export const features: FoamFeature[] = [
placeholders,
backlinks,
utilityCommands,
previewNavigation,
];

View File

@@ -0,0 +1,34 @@
import MarkdownIt from 'markdown-it';
import { FoamWorkspace } from 'foam-core';
import { createPlaceholder, createTestNote } from '../test/test-utils';
import { markdownItWithFoamLinks } from './preview-navigation';
describe('Link generation in preview', () => {
const noteA = createTestNote({
uri: 'note-a.md',
title: 'My note title',
});
const placeholder = createPlaceholder({
uri: 'placeholder',
});
const ws = new FoamWorkspace().set(noteA).set(placeholder);
const md = markdownItWithFoamLinks(MarkdownIt(), ws);
it('generates a link to a note', () => {
expect(md.render(`[[note-a]]`)).toEqual(
`<p><a class='foam-note-link' title='${noteA.title}' href='${noteA.uri.fsPath}'>note-a</a></p>\n`
);
});
it('generates a link to a placeholder resource', () => {
expect(md.render(`[[placeholder]]`)).toEqual(
`<p><a class='foam-placeholder-link' title="Link to non-existing resource" href="javascript:void(0);">placeholder</a></p>\n`
);
});
it('generates a placeholder link to an unknown slug', () => {
expect(md.render(`[[random-text]]`)).toEqual(
`<p><a class='foam-placeholder-link' title="Link to non-existing resource" href="javascript:void(0);">random-text</a></p>\n`
);
});
});

View File

@@ -0,0 +1,55 @@
import * as vscode from 'vscode';
import markdownItRegex from 'markdown-it-regex';
import { Foam, FoamWorkspace, Logger } from 'foam-core';
import { FoamFeature } from '../types';
const feature: FoamFeature = {
activate: async (
_context: vscode.ExtensionContext,
foamPromise: Promise<Foam>
) => {
const foam = await foamPromise;
return {
extendMarkdownIt: (md: markdownit) =>
markdownItWithFoamLinks(md, foam.workspace),
};
},
};
export const markdownItWithFoamLinks = (
md: markdownit,
workspace: FoamWorkspace
) => {
return md.use(markdownItRegex, {
name: 'connect-wikilinks',
regex: /\[\[([^\[\]]+?)\]\]/,
replace: (wikilink: string) => {
try {
const resource = workspace.find(wikilink);
if (resource == null) {
return getPlaceholderLink(wikilink);
}
switch (resource.type) {
case 'note':
return `<a class='foam-note-link' title='${resource.title}' href='${resource.uri.fsPath}'>${wikilink}</a>`;
case 'attachment':
return `<a class='foam-attachment-link' title='attachment' href='${resource.uri.fsPath}'>${wikilink}</a>`;
case 'placeholder':
return getPlaceholderLink(wikilink);
}
} catch (e) {
Logger.error(
`Error while creating link for [[${wikilink}]] in Preview panel`,
e
);
return getPlaceholderLink(wikilink);
}
},
});
};
const getPlaceholderLink = (content: string) =>
`<a class='foam-placeholder-link' title="Link to non-existing resource" href="javascript:void(0);">${content}</a>`;
export default feature;

View File

@@ -2,5 +2,8 @@ import { ExtensionContext } from 'vscode';
import { Foam } from 'foam-core';
export interface FoamFeature {
activate: (context: ExtensionContext, foamPromise: Promise<Foam>) => void;
activate: (
context: ExtensionContext,
foamPromise: Promise<Foam>
) => Promise<any> | void;
}

View File

@@ -251,7 +251,7 @@ function getNodeColor(nodeId, model) {
case 'regular':
return { fill: typeFill, border: typeFill };
case 'lessened':
const transparent = d3.rgb(typeFill).copy({ opacity: 0.3 });
const transparent = d3.rgb(typeFill).copy({ opacity: 0.05 });
return { fill: transparent, border: transparent };
case 'highlighted':
return {

View File

@@ -22,6 +22,6 @@
4. open this file in a browser
-->
<!-- <script src="./test-data.js"></script> -->
<script data-replace src="./graphs/default/graph.js"></script>
<script data-replace src="./graph.js"></script>
</body>
</html>

View File

@@ -0,0 +1,8 @@
.foam-placeholder-link {
color: red;
cursor: default;
}
.foam-note-link,
.foam-attachment-link {
}

View File

@@ -2742,6 +2742,11 @@
dependencies:
"@types/node" "*"
"@types/highlight.js@^9.7.0":
version "9.12.4"
resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-9.12.4.tgz#8c3496bd1b50cc04aeefd691140aa571d4dbfa34"
integrity sha512-t2szdkwmg2JJyuCM20e8kR2X59WCE5Zkl4bzm1u1Oukjm79zpbiAv+QjnwLnuuV0WHEcX2NgUItu0pAMKuOPww==
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
@@ -2794,11 +2799,30 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
"@types/linkify-it@*":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.0.tgz#c0ca4c253664492dbf47a646f31cfd483a6bbc95"
integrity sha512-x9OaQQTb1N2hPZ/LWJsqushexDvz7NgzuZxiRmZio44WPuolTZNHDBCrOxCzRVOMwamJRO2dWax5NbygOf1OTQ==
"@types/lodash@^4.14.157":
version "4.14.157"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.157.tgz#fdac1c52448861dfde1a2e1515dbc46e54926dc8"
integrity sha512-Ft5BNFmv2pHDgxV5JDsndOWTRJ+56zte0ZpYLowp03tW+K+t8u8YMOzAnpuqPgzX6WO1XpDIUm7u04M8vdDiVQ==
"@types/markdown-it@^12.0.1":
version "12.0.1"
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-12.0.1.tgz#8391e19fea4796ff863edda55800c7e669beb358"
integrity sha512-mHfT8j/XkPb1uLEfs0/C3se6nd+webC2kcqcy8tgcVr0GDEONv/xaQzAN+aQvkxQXk/jC0Q6mPS+0xhFwRF35g==
dependencies:
"@types/highlight.js" "^9.7.0"
"@types/linkify-it" "*"
"@types/mdurl" "*"
"@types/mdurl@*":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9"
integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==
"@types/micromatch@^4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@types/micromatch/-/micromatch-4.0.1.tgz#9381449dd659fc3823fd2a4190ceacc985083bc7"
@@ -3173,6 +3197,11 @@ argparse@^1.0.7:
dependencies:
sprintf-js "~1.0.2"
argparse@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
aria-query@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b"
@@ -4853,6 +4882,11 @@ enquirer@^2.3.4:
dependencies:
ansi-colors "^4.1.1"
entities@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
env-paths@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43"
@@ -8318,6 +8352,13 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
linkify-it@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.2.tgz#f55eeb8bc1d3ae754049e124ab3bb56d97797fb8"
integrity sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ==
dependencies:
uc.micro "^1.0.1"
load-json-file@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
@@ -8635,6 +8676,27 @@ markdown-escapes@^1.0.0:
resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535"
integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==
markdown-it-regex@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/markdown-it-regex/-/markdown-it-regex-0.2.0.tgz#e09ad2d75209720d591d3949e1142c75c0fbecf6"
integrity sha512-111UnMGJSt37gy+DlgcpQNwEfS2jvscOFSztzGhuXUHk7K1J5eAEj6C3jifmKb0cWtTuxdpHgIt4PyGQ+DtDjw==
markdown-it@^12.0.4:
version "12.0.4"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.0.4.tgz#eec8247d296327eac3ba9746bdeec9cfcc751e33"
integrity sha512-34RwOXZT8kyuOJy25oJNJoulO8L0bTHYWXcdZBYZqFnjIy3NgjeoM3FmPXIOFQ26/lSHYMr8oc62B6adxXcb3Q==
dependencies:
argparse "^2.0.1"
entities "~2.1.0"
linkify-it "^3.0.1"
mdurl "^1.0.1"
uc.micro "^1.0.5"
mdurl@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=
meow@^3.3.0:
version "3.7.0"
resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
@@ -11709,6 +11771,11 @@ typescript@^3.3, typescript@^3.7.3, typescript@^3.8.3, typescript@^3.9.5:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.6.tgz#8f3e0198a34c3ae17091b35571d3afd31999365a"
integrity sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw==
uc.micro@^1.0.1, uc.micro@^1.0.5:
version "1.0.6"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
uglify-js@^3.1.4:
version "3.10.0"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.10.0.tgz#397a7e6e31ce820bfd1cb55b804ee140c587a9e7"