diff --git a/.vscode/launch.json b/.vscode/launch.json index 7752da2..ad9c5d3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -24,7 +24,7 @@ "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" ], "outFiles": ["${workspaceFolder}/out/test/**/*.js"], - "preLaunchTask": "${defaultBuildTask}" + "preLaunchTask": "npm: watch" } ] } diff --git a/README.md b/README.md index 1bffc0f..c48a096 100644 --- a/README.md +++ b/README.md @@ -78,3 +78,12 @@ Refer to the [CHANGELOG.md](CHANGELOG.md) file. You are very welcome to open an issue or a pull request with changes. If it is your first time with vscode extension, make sure to checkout [Official Guides](https://code.visualstudio.com/api/get-started/your-first-extension). + +If you want to run test locally you'll need to yarn install: + +```terminal +yarn install +``` + +Then to run tests use the debugger > `Extention Tests` option + diff --git a/package.json b/package.json index d9fddc0..dfbc93c 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "version": "0.6.0", "publisher": "tchayen", "engines": { - "vscode": "^1.45.0" + "vscode": "^1.47.2" }, "categories": [ "Other" @@ -68,18 +68,21 @@ "lint": "eslint src --ext ts", "watch": "tsc -watch -p ./", "pretest": "yarn run compile && yarn run lint", - "test": "node ./out/test/runTest.js" + "test": "node ./src/test/runTest.js" }, "devDependencies": { "@types/glob": "^7.1.1", + "@types/md5": "^2.2.0", "@types/mocha": "^7.0.2", "@types/node": "^13.11.0", + "@types/sinon": "^9.0.4", "@types/vscode": "^1.45.0", "@typescript-eslint/eslint-plugin": "^2.33.0", "@typescript-eslint/parser": "^2.33.0", "eslint": "^6.8.0", "glob": "^7.1.6", "mocha": "^7.1.2", + "sinon": "^9.0.2", "ts-loader": "^7.0.4", "typescript": "^3.8.3", "vscode-test": "^1.3.0", @@ -87,7 +90,6 @@ "webpack-cli": "^3.3.11" }, "dependencies": { - "@types/md5": "^2.2.0", "md5": "^2.2.1", "remark-frontmatter": "^2.0.0", "remark-parse": "^8.0.2", diff --git a/src/extension.ts b/src/extension.ts index 25532ee..9ae9dc4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,8 +1,9 @@ import * as vscode from "vscode"; import { TextDecoder } from "util"; import * as path from "path"; -import { parseFile, parseDirectory, learnFileId } from "./parsing"; -import { filterNonExistingEdges, getColumnSetting, getConfiguration, getFileTypesSetting } from "./utils"; +import { parseDirectory, learnFileId, processFile } from "./parsing"; +import { filterNonExistingEdges, getColumnSetting, getConfiguration, getFileTypesSetting, getDot } from "./utils"; + import { Graph } from "./types"; const watch = ( @@ -30,7 +31,7 @@ const watch = ( // Watch file changes in case user adds a link. watcher.onDidChange(async (event) => { - await parseFile(graph, event.path); + processFile(graph, event.path); filterNonExistingEdges(graph); sendGraph(); }); @@ -129,7 +130,7 @@ export function activate(context: vscode.ExtensionContext) { }; await parseDirectory(graph, vscode.workspace.rootPath, learnFileId); - await parseDirectory(graph, vscode.workspace.rootPath, parseFile); + await parseDirectory(graph, vscode.workspace.rootPath, processFile); filterNonExistingEdges(graph); const d3Uri = panel.webview.asWebviewUri( @@ -138,6 +139,7 @@ export function activate(context: vscode.ExtensionContext) { panel.webview.html = await getWebviewContent(context, graph, d3Uri); + console.log(getDot(graph)); watch(context, panel, graph); }) ); diff --git a/src/parsing.ts b/src/parsing.ts index c58eb60..8ebee08 100644 --- a/src/parsing.ts +++ b/src/parsing.ts @@ -1,16 +1,24 @@ import * as vscode from "vscode"; import * as path from "path"; +import * as fs from "fs"; +import * as util from "util"; import * as unified from "unified"; import * as markdown from "remark-parse"; import * as wikiLinkPlugin from "remark-wiki-link"; import * as frontmatter from "remark-frontmatter"; import { MarkdownNode, Graph } from "./types"; import { TextDecoder } from "util"; -import { findTitle, findLinks, id, FILE_ID_REGEXP, getFileTypesSetting, getConfiguration } from "./utils"; +import { findTitle, findLinks, id, getFileIdRegexp, getFileTypesSetting } from "./utils"; import { basename } from "path"; let idToPath: Record = {}; +const readFileAsync = util.promisify(fs.readFile); + +/** + * ?? + * @param id ?? + */ export const idResolver = (id: string) => { const filePath = idToPath[id]; if (filePath === undefined) { @@ -25,9 +33,32 @@ const parser = unified() .use(wikiLinkPlugin, { pageResolver: idResolver }) .use(frontmatter); -export const parseFile = async (graph: Graph, filePath: string) => { - const buffer = await vscode.workspace.fs.readFile(vscode.Uri.file(filePath)); - const content = new TextDecoder("utf-8").decode(buffer); +/** + * Wrapper for `parseFile` that reads a file. Uses `vscode.workspace.fs`. + * @param graph object that will be altered. + * @param filePath absolute path to the file. Used for reading the file. + */ +export const processFile = async (graph: Graph, filePath: string) => { + let content = await readFile(filePath); + return parseFile(graph, filePath, content); +}; + +/** + * Read file, return text. + * @param filePath absolute path to the file. Used for reading the file. + */ +export const readFile = async (filePath: string) => { + return await readFileAsync(filePath, {encoding:'utf8'}); +}; + +/** + * Alters given graph, adding a node and some edges if file is properly + * structured. + * @param graph object that will be altered. + * @param filePath absolute path to the file. Isn't used for reading the file. + * @param content content of the file as utf-8 string. + */ +export const parseFile = (graph: Graph, filePath: string, content: string) => { const ast: MarkdownNode = parser.parse(content); let title: string | null = findTitle(ast); @@ -64,14 +95,23 @@ export const parseFile = async (graph: Graph, filePath: string) => { } }; +/** + * For given file path, returns its ID or null. Uses `vscode.workspace.fs`. + * @param filePath absolute path of the file. + */ export const findFileId = async (filePath: string): Promise => { const buffer = await vscode.workspace.fs.readFile(vscode.Uri.file(filePath)); const content = new TextDecoder("utf-8").decode(buffer); - const match = content.match(FILE_ID_REGEXP); + const match = content.match(getFileIdRegexp()); return match ? match[1] : null; }; +/** + * Populates `idToPath` with ID from the file if one is found there. + * @param _graph unused. + * @param filePath absolute path of the file. + */ export const learnFileId = async (_graph: Graph, filePath: string) => { const id = await findFileId(filePath); if (id !== null) { @@ -85,6 +125,13 @@ export const learnFileId = async (_graph: Graph, filePath: string) => { idToPath[fileNameWithoutExt] = filePath; }; +/** + * Recursively reads content of the given directory and calls specified + * callback on each file (not a symlink) with `*.md` extensions. + * @param graph object to be altered. + * @param directory path of the directory. + * @param fileCallback + */ export const parseDirectory = async ( graph: Graph, directory: string, diff --git a/src/test/data/1.md b/src/test/data/1.md new file mode 100644 index 0000000..21e60f8 --- /dev/null +++ b/src/test/data/1.md @@ -0,0 +1 @@ +# Test \ No newline at end of file diff --git a/src/test/data/2.md b/src/test/data/2.md new file mode 100644 index 0000000..87302df --- /dev/null +++ b/src/test/data/2.md @@ -0,0 +1,3 @@ +# Test 2 + +This links to [1](1.md) diff --git a/src/test/suite/ast.json b/src/test/suite/ast.json new file mode 100644 index 0000000..0fa7da6 --- /dev/null +++ b/src/test/suite/ast.json @@ -0,0 +1,235 @@ +{ + "type": "root", + "children": [ + { + "type": "heading", + "depth": 1, + "children": [ + { + "type": "text", + "value": "Links", + "position": { + "start": { "line": 1, "column": 3, "offset": 2 }, + "end": { "line": 1, "column": 8, "offset": 7 }, + "indent": [] + } + } + ], + "position": { + "start": { "line": 1, "column": 1, "offset": 0 }, + "end": { "line": 1, "column": 8, "offset": 7 }, + "indent": [] + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "link", + "title": null, + "url": "another.md", + "children": [ + { + "type": "text", + "value": "Another", + "position": { + "start": { "line": 3, "column": 2, "offset": 10 }, + "end": { "line": 3, "column": 9, "offset": 17 }, + "indent": [] + } + } + ], + "position": { + "start": { "line": 3, "column": 1, "offset": 9 }, + "end": { "line": 3, "column": 22, "offset": 30 }, + "indent": [] + } + }, + { + "type": "text", + "value": "\n", + "position": { + "start": { "line": 3, "column": 22, "offset": 30 }, + "end": { "line": 4, "column": 1, "offset": 31 }, + "indent": [1] + } + }, + { + "type": "link", + "title": null, + "url": "other.md", + "children": [ + { + "type": "text", + "value": "Other", + "position": { + "start": { "line": 4, "column": 2, "offset": 32 }, + "end": { "line": 4, "column": 7, "offset": 37 }, + "indent": [] + } + } + ], + "position": { + "start": { "line": 4, "column": 1, "offset": 31 }, + "end": { "line": 4, "column": 18, "offset": 48 }, + "indent": [] + } + }, + { + "type": "text", + "value": "\n", + "position": { + "start": { "line": 4, "column": 18, "offset": 48 }, + "end": { "line": 5, "column": 1, "offset": 49 }, + "indent": [1] + } + }, + { + "type": "link", + "title": null, + "url": "nested.md", + "children": [ + { + "type": "text", + "value": "Nested", + "position": { + "start": { "line": 5, "column": 2, "offset": 50 }, + "end": { "line": 5, "column": 8, "offset": 56 }, + "indent": [] + } + } + ], + "position": { + "start": { "line": 5, "column": 1, "offset": 49 }, + "end": { "line": 5, "column": 20, "offset": 68 }, + "indent": [] + } + }, + { + "type": "text", + "value": "\n", + "position": { + "start": { "line": 5, "column": 20, "offset": 68 }, + "end": { "line": 6, "column": 1, "offset": 69 }, + "indent": [1] + } + }, + { + "type": "link", + "title": null, + "url": "error.md", + "children": [ + { + "type": "text", + "value": "Error", + "position": { + "start": { "line": 6, "column": 2, "offset": 70 }, + "end": { "line": 6, "column": 7, "offset": 75 }, + "indent": [] + } + } + ], + "position": { + "start": { "line": 6, "column": 1, "offset": 69 }, + "end": { "line": 6, "column": 18, "offset": 86 }, + "indent": [] + } + } + ], + "position": { + "start": { "line": 3, "column": 1, "offset": 9 }, + "end": { "line": 6, "column": 18, "offset": 86 }, + "indent": [1, 1, 1] + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "linkReference", + "identifier": "more", + "label": "More", + "referenceType": "shortcut", + "children": [ + { + "type": "text", + "value": "More", + "position": { + "start": { "line": 8, "column": 2, "offset": 89 }, + "end": { "line": 8, "column": 6, "offset": 93 }, + "indent": [] + } + } + ], + "position": { + "start": { "line": 8, "column": 1, "offset": 88 }, + "end": { "line": 8, "column": 7, "offset": 94 }, + "indent": [] + } + } + ], + "position": { + "start": { "line": 8, "column": 1, "offset": 88 }, + "end": { "line": 8, "column": 7, "offset": 94 }, + "indent": [] + } + }, + { + "type": "definition", + "identifier": "more", + "label": "more", + "title": null, + "url": "more.md", + "position": { + "start": { "line": 10, "column": 1, "offset": 96 }, + "end": { "line": 10, "column": 16, "offset": 111 }, + "indent": [] + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "linkReference", + "identifier": "nowhere", + "label": "nowhere", + "referenceType": "shortcut", + "children": [ + { + "type": "text", + "value": "nowhere", + "position": { + "start": { "line": 12, "column": 2, "offset": 114 }, + "end": { "line": 12, "column": 9, "offset": 121 }, + "indent": [] + } + } + ], + "position": { + "start": { "line": 12, "column": 1, "offset": 113 }, + "end": { "line": 12, "column": 10, "offset": 122 }, + "indent": [] + } + }, + { + "type": "text", + "value": ": <>", + "position": { + "start": { "line": 12, "column": 10, "offset": 122 }, + "end": { "line": 12, "column": 14, "offset": 126 }, + "indent": [] + } + } + ], + "position": { + "start": { "line": 12, "column": 1, "offset": 113 }, + "end": { "line": 12, "column": 14, "offset": 126 }, + "indent": [] + } + } + ], + "position": { + "start": { "line": 1, "column": 1, "offset": 0 }, + "end": { "line": 13, "column": 1, "offset": 127 } + } +} diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts index 08a6f78..dcf6b8f 100644 --- a/src/test/suite/extension.test.ts +++ b/src/test/suite/extension.test.ts @@ -1,15 +1,233 @@ -import * as assert from 'assert'; - +import * as assert from "assert"; +import * as sinon from "sinon"; +import * as path from "path"; // You can import and use all API from the 'vscode' module // as well as import your extension to test it -import * as vscode from 'vscode'; -// import * as myExtension from '../../extension'; +import * as vscode from "vscode"; + +import * as unified from "unified"; +import * as markdown from "remark-parse"; + +import * as ast from "./ast.json"; +import { + findLinks, + findTitle, + id, + getDot, + exists, + filterNonExistingEdges, + getColumnSetting, + getFileIdRegexp, +} from "../../utils"; +import { Graph } from "../../types"; +import { + parseFile, + findFileId, + parseDirectory, + processFile, + readFile +} from "../../parsing"; +import { TextEncoder } from "util"; + +const testFolder = (file) => { + let dir = __dirname; + return path.join(dir + "/../../../src/test/data/" + file); +}; +describe("Tests", () => { + let stub; + + before(() => { + stub = sinon.stub(vscode.workspace, "getConfiguration").returns({ + openColumn: "one", + fileIdRegexp: "\\d{10}", + } as any); + }); + + after(() => { + stub.reset(); + }); + + vscode.window.showInformationMessage("Start all tests."); + + const getGraph = () => ({ + nodes: [ + { id: "1", label: "First", path: "/Users/test/Desktop/notes/1.md" }, + { id: "2", label: "Second", path: "/Users/test/Desktop/notes/2.md" }, + { id: "3", label: "Third", path: "/Users/test/Desktop/notes/3.md" }, + ], + edges: [ + { source: "1", target: "2" }, + { source: "1", target: "3" }, + { source: "3", target: "2" }, + ], + }); + + const parser = unified().use(markdown); + + it("findLinks works", () => { + const links = findLinks(ast); + const expected = [ + "another.md", + "other.md", + "nested.md", + "error.md", + "more.md", + ]; + + assert.deepStrictEqual(links, expected); + }); + + describe("findTitle", () => { + it("works", () => { + assert.equal(findTitle(ast), "Links"); + }); + + it("selects correct title out of many", () => { + assert.equal(findTitle(parser.parse("# First\n\n# Second")), "First"); + }); + + it("does not find title if none exists", () => { + assert.equal(findTitle(parser.parse("No title\n\nAnywhere here.")), null); + }); + }); + + it("id works", () => { + assert.equal( + id("./some/random/sub/directory/1.md"), + "1" + ); + }); + + it("getColumnSetting works", () => { + const setting = getColumnSetting("openColumn"); + assert.equal(setting, vscode.ViewColumn.One); + }); + + describe("File ID Parser for Zettlr", () => { + + it("Parses 10 digits from a file as per the Zettlr ID reference", () => { + const regexp = getFileIdRegexp(); + assert.equal( + regexp.test("# Title\n\n1234567890\n\nThat was an ID."), + true + ); + }); + + it("Does not parse 9 digits, that would be an invalid reference", () => { + const regexp = getFileIdRegexp(); + + assert.equal(regexp.test("# Title\n\n123456789\n\nThat was an invalid ID."), false); + }); + + xit("You can override the default regular expression", () => {}); + }); + + it("getDot works", () => { + const graph = getGraph(); + const dot = getDot(graph); + const expected = + 'digraph g {\n 1 [label="First"];\n 2 [label="Second"];\n 3 [label="Third"];\n 1 -> 2\n 1 -> 3\n 3 -> 2\n}'; + + assert.equal(dot, expected); + }); + + it("exists works", () => { + const graph = getGraph(); + assert.equal(exists(graph, "1"), true); + assert.equal(exists(graph, "First"), false); + }); + + it("filterNonExistingEdges", () => { + const graph = getGraph(); + graph.edges.push({ source: "2", target: "https://wikipedia.org/" }); + graph.edges.push({ source: "2", target: "4" }); + + assert.equal(graph.edges.length, 5); + filterNonExistingEdges(graph); + assert.equal(graph.edges.length, 3); + }); + + xit("idResolver works", () => {}); + + describe("parseFile", () => { + it("readFile reads content", async () => { + const graph: Graph = { + nodes: [], + edges: [], + }; + const content = await readFile(testFolder("1.md")); + assert.equal(content, "# Test"); + }); + + it("works", () => { + const graph: Graph = { + nodes: [], + edges: [], + }; + + const path = "/Users/test/Desktop/notes"; + const firstFileName = "1.md"; + const firstFilePath = `${path}/${firstFileName}`; + const secondFileName = "2.md"; + const title = "Test"; + const content = `# ${title}\n\n[Link](${secondFileName})\n`; + + parseFile(graph, firstFilePath, content); + + assert.deepStrictEqual(graph.nodes, [ + { id: id(firstFilePath), label: title, path: firstFilePath }, + ]); + assert.deepStrictEqual(graph.edges, [ + { source: id(firstFilePath), target: id(`${path}/${secondFileName}`) }, + ]); + }); + }); + + it("findFileId works", async () => { + const file = "# Title\n\n1234567890\n\nThat was an ID."; + + const promise: Promise = new Promise((resolve) => + resolve(new TextEncoder().encode(file)) + ); + + const stub = sinon.stub(vscode.workspace.fs, "readFile").returns(promise); + + assert.equal( + await findFileId("/Users/test/Desktop/notes/1.md"), + "1234567890" + ); + + stub.reset(); + }); + + xit("learnFileId works", () => { + // TODO: mocks. + }); + + describe("parseDirectory", () => { + it("works", async () => { + + const graph = { + nodes: [], + edges: [], + }; + + // TODO: + // - mock readFile to give proper content + // - assert to make check if parseDirectory populates the graph + + await parseDirectory(graph, testFolder(""), processFile); + + assert.equal(graph.nodes.length, 2); + const getData = ({label, id}) => { + return { label, id }; + }; + assert.deepStrictEqual(getData(graph.nodes[0]), {label: "Test", id: "1"}); + assert.deepStrictEqual(getData(graph.nodes[1]), {label: "Test 2", id: "2"}); + + }); -suite('Extension Test Suite', () => { - vscode.window.showInformationMessage('Start all tests.'); + xit("returns empty graph for non-existing directory", async () => {}); - test('Sample test', () => { - assert.equal(-1, [1, 2, 3].indexOf(5)); - assert.equal(-1, [1, 2, 3].indexOf(0)); - }); + }); }); diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts index 7029e38..d3cd395 100644 --- a/src/test/suite/index.ts +++ b/src/test/suite/index.ts @@ -1,38 +1,38 @@ -import * as path from 'path'; -import * as Mocha from 'mocha'; -import * as glob from 'glob'; +import * as path from "path"; +import * as Mocha from "mocha"; +import * as glob from "glob"; export function run(): Promise { - // Create the mocha test - const mocha = new Mocha({ - ui: 'tdd', - color: true - }); + // Create the mocha test + const mocha = new Mocha({ + ui: "bdd", + color: true, + }); - const testsRoot = path.resolve(__dirname, '..'); + const testsRoot = path.resolve(__dirname, ".."); - return new Promise((c, e) => { - glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { - if (err) { - return e(err); - } + return new Promise((c, e) => { + glob("**/**.test.js", { cwd: testsRoot }, (err, files) => { + if (err) { + return e(err); + } - // Add files to the test suite - files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); + // Add files to the test suite + files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); - try { - // Run the mocha test - mocha.run(failures => { - if (failures > 0) { - e(new Error(`${failures} tests failed.`)); - } else { - c(); - } - }); - } catch (err) { - console.error(err); - e(err); - } - }); - }); + try { + // Run the mocha test + mocha.run((failures) => { + if (failures > 0) { + e(new Error(`${failures} tests failed.`)); + } else { + c(); + } + }); + } catch (err) { + console.error(err); + e(err); + } + }); + }); } diff --git a/src/types.ts b/src/types.ts index 6da48df..52d1666 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,19 +1,41 @@ +/** + * Defines directed edge between two nodes. + * @param source ID of the starting node of the edge. + * @param target ID of ending node of the edge. + */ export type Edge = { source: string; target: string; }; +/** + * @param id ID of the node. Should be unique. Used for identifying the nodes + * and matching them with edges. + * @param path absolute path of the file. Used for opening files. + * @param label label used while displaying the node. + */ export type Node = { id: string; path: string; label: string; }; +/** + * Graph representation using list of nodes and list of edges. + */ export type Graph = { nodes: Node[]; edges: Edge[]; }; +/** + * Based on the output of `remark-parse` with `remark-wiki-link` and + * `remark-frontmatter` plugins. + * To make it simpler, most fields are made + * optional. + * The reason it is used instead of the official type is because it + * was too general. + */ export type MarkdownNode = { type: string; children?: MarkdownNode[]; diff --git a/src/utils.ts b/src/utils.ts index 202239d..5bac013 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,6 +2,13 @@ import * as vscode from "vscode"; import * as md5 from "md5"; import { MarkdownNode, Graph } from "./types"; +/** + * Finds links to other Markdown files in the given file. Handles: + * - simple local links `[Link](1.md)`. + * - references `[Reference] [reference]: 1.md`. + * - wiki-style links `[[Link]]`. + * @param ast abstract syntax tree of a Markdown file. + */ export const findLinks = (ast: MarkdownNode): string[] => { if (ast.type === "link" || ast.type === "definition") { return [ast.url!]; @@ -23,6 +30,11 @@ export const findLinks = (ast: MarkdownNode): string[] => { return links; }; +/** + * Finds title of the given file which is the first heading of depth one + * (# Like this) encountered. If there is no such thing, returns null. + * @param ast abstract syntax tree of a Markdown file. + */ export const findTitle = (ast: MarkdownNode): string | null => { if (!ast.children) { return null; @@ -38,18 +50,29 @@ export const findTitle = (ast: MarkdownNode): string | null => { return child.children[0].value!; } } + return null; }; +/** + * Translates given path to ID. IDs are unique as long as paths are (which + * should remain true). + * @param path absolute path of the file. + */ export const id = (path: string): string => { - return md5(path); + // return md5(path); // Extracting file name without extension: - // const fullPath = path.split("/"); - // const fileName = fullPath[fullPath.length - 1]; - // return fileName.split(".")[0]; + const fullPath = path.split("/"); + const fileName = fullPath[fullPath.length - 1]; + return fileName.split(".")[0]; }; +/** + * Returns value of configuration for the given key. + * @param key configuration key. For example in `markdown-links.showColumn`, + * configuration key is `showColumn`. + */ export const getConfiguration = (key: string) => vscode.workspace.getConfiguration("markdown-links")[key]; @@ -67,21 +90,34 @@ const settingToValue: { [key: string]: vscode.ViewColumn | undefined } = { nine: 9, }; +/** + * For given configuration key, returns mapping to the vscode.ViewColumn value. + * @param key configuration key. + */ export const getColumnSetting = (key: string) => { const column = getConfiguration(key); return settingToValue[column] || vscode.ViewColumn.One; }; +/** + * Returns regular expression for finding file IDs in a file. + */ export const getFileIdRegexp = () => { const DEFAULT_VALUE = "\\d{14}"; const userValue = getConfiguration("fileIdRegexp") || DEFAULT_VALUE; - // Ensure the id is not preceeded by [[, which would make it a part of + // TODO: make `[[` also configurable. + // Ensure the id is not preceeded by `[[`, which would make it a part of // wiki-style link, and put the user-supplied regex in a capturing group to // retrieve matching string. return new RegExp(`(? { @@ -90,15 +126,23 @@ export const getFileTypesSetting = () => { }; export const getDot = (graph: Graph) => `digraph g { - ${graph.nodes - .map((node) => ` ${node.id} [label="${node.label}"];`) - .join("\n")} - ${graph.edges.map((edge) => ` ${edge.source} -> ${edge.target}`).join("\n")} - }`; - +${graph.nodes.map((node) => ` ${node.id} [label="${node.label}"];`).join("\n")} +${graph.edges.map((edge) => ` ${edge.source} -> ${edge.target}`).join("\n")} +}`; + +/** + * Returns whether file with given ID is present in the given graph. + * @param graph graph to check. + * @param id ID of the file to look for. + */ export const exists = (graph: Graph, id: string) => !!graph.nodes.find((node) => node.id === id); +/** + * Filters `edges` array to leave only those that have node in `nodes` for + * both `source` and `target` of given edge. + * @param graph object to alter. + */ export const filterNonExistingEdges = (graph: Graph) => { graph.edges = graph.edges.filter( (edge) => exists(graph, edge.source) && exists(graph, edge.target) diff --git a/static/webview.html b/static/webview.html index bd8d2cb..7cfe94d 100644 --- a/static/webview.html +++ b/static/webview.html @@ -64,11 +64,30 @@ //const activeNodeColor = "#0050ff"; //const nodeColor = "#777"; + let nodesData = []; let linksData = []; const vscode = acquireVsCodeApi(); + const colors = [ + "#ECD1C9", + "#FBB5AE", + "#FFEFBC", + "#B7D1DF", + "#D1E2CE", + "#FADAE5", + "#ECE3D5", + "#F2F2F2", + "#ECE3C1", + "#BEDFC8", + "#F9F2B6", + "#EFD0BD", + "#DDD0E5", + "#F2E4C8", + "#CBCBCB", + ]; + const onClick = (d) => { vscode.postMessage({ type: "click", payload: d }); }; @@ -112,11 +131,10 @@ return true; }; - // const getNodeColor = (node, active) => { - // console.log(node, active); - // return node.path === active ? activeNodeColor : nodeColor; - // }; + const getNodeColor = (node, active) => { + return node.path === active ? activeNodeColor : nodeColor; + }; const element = document.createElementNS( "http://www.w3.org/2000/svg", diff --git a/tsconfig.json b/tsconfig.json index f113fdc..ea4a595 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "sourceMap": true, "rootDir": "src", "noImplicitAny": false, + "resolveJsonModule": true, "strict": true /* enable all strict type-checking options */ /* Additional Checks */ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ diff --git a/yarn.lock b/yarn.lock index 04bed6d..c2779a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30,6 +30,42 @@ dependencies: regenerator-runtime "^0.13.4" +"@sinonjs/commons@^1", "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.7.2": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.0.tgz#c8d68821a854c555bba172f3b06959a0039b236d" + integrity sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^6.0.0", "@sinonjs/fake-timers@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40" + integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA== + dependencies: + "@sinonjs/commons" "^1.7.0" + +"@sinonjs/formatio@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-5.0.1.tgz#f13e713cb3313b1ab965901b01b0828ea6b77089" + integrity sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ== + dependencies: + "@sinonjs/commons" "^1" + "@sinonjs/samsam" "^5.0.2" + +"@sinonjs/samsam@^5.0.2", "@sinonjs/samsam@^5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-5.0.3.tgz#86f21bdb3d52480faf0892a480c9906aa5a52938" + integrity sha512-QucHkc2uMJ0pFGjJUDP3F9dq5dx8QIaqISl9QgwLOh6P9yv877uONPGXh/OH/0zmM3tW1JjuJltAZV2l7zU+uQ== + dependencies: + "@sinonjs/commons" "^1.6.0" + lodash.get "^4.4.2" + type-detect "^4.0.8" + +"@sinonjs/text-encoding@^0.7.1": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" + integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== + "@types/color-name@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" @@ -81,6 +117,18 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.5.tgz#96ec3b0afafd64a4ccea9107b75bf8489f0e5765" integrity sha512-3ySmiBYJPqgjiHA7oEaIo2Rzz0HrOZ7yrNO5HWyaE5q0lQ3BppDZ3N53Miz8bw2I7gh1/zir2MGVZBvpb1zq9g== +"@types/sinon@^9.0.4": + version "9.0.4" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.4.tgz#e934f904606632287a6e7f7ab0ce3f08a0dad4b1" + integrity sha512-sJmb32asJZY6Z2u09bl0G2wglSxDlROlAejCjsnor+LzBMz17gu8IU7vKC/vWDnv9zEq2wqADHVXFjf4eE8Gdw== + dependencies: + "@types/sinonjs__fake-timers" "*" + +"@types/sinonjs__fake-timers@*": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e" + integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA== + "@types/unist@^2.0.0", "@types/unist@^2.0.2": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e" @@ -1100,6 +1148,11 @@ diff@3.5.0: resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== +diff@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -2163,6 +2216,11 @@ is-wsl@^1.1.0: resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -2220,6 +2278,11 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" +just-extend@^4.0.2: + version "4.1.0" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.1.0.tgz#7278a4027d889601640ee0ce0e5a00b992467da4" + integrity sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA== + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -2290,6 +2353,11 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + lodash@^4.17.14, lodash@^4.17.15: version "4.17.19" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" @@ -2576,6 +2644,17 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +nise@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/nise/-/nise-4.0.3.tgz#9f79ff02fa002ed5ffbc538ad58518fa011dc913" + integrity sha512-EGlhjm7/4KvmmE6B/UFsKh7eHykRl9VH+au8dduHLCyWUO/hr7+N+WtTvDUwc9zHuM1IaIJs/0lQ6Ag1jDkQSg== + dependencies: + "@sinonjs/commons" "^1.7.0" + "@sinonjs/fake-timers" "^6.0.0" + "@sinonjs/text-encoding" "^0.7.1" + just-extend "^4.0.2" + path-to-regexp "^1.7.0" + node-environment-flags@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.6.tgz#a30ac13621f6f7d674260a54dede048c3982c088" @@ -2847,6 +2926,13 @@ path-key@^2.0.0, path-key@^2.0.1: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + pbkdf2@^3.0.3: version "3.0.17" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6" @@ -3302,6 +3388,19 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== +sinon@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.0.2.tgz#b9017e24633f4b1c98dfb6e784a5f0509f5fd85d" + integrity sha512-0uF8Q/QHkizNUmbK3LRFqx5cpTttEVXudywY9Uwzy8bTfZUhljZ7ARzSxnRHWYWtVTeh4Cw+tTb3iU21FQVO9A== + dependencies: + "@sinonjs/commons" "^1.7.2" + "@sinonjs/fake-timers" "^6.0.1" + "@sinonjs/formatio" "^5.0.1" + "@sinonjs/samsam" "^5.0.3" + diff "^4.0.2" + nise "^4.0.1" + supports-color "^7.1.0" + slice-ansi@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" @@ -3740,6 +3839,11 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" +type-detect@4.0.8, type-detect@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + type-fest@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1"