mirror of
https://github.com/foambubble/foam.git
synced 2026-01-11 15:08:01 -05:00
Compare commits
59 Commits
feature/li
...
cli/demo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d1ec88717 | ||
|
|
6b1407303e | ||
|
|
528213a0f8 | ||
|
|
ff89948a70 | ||
|
|
734982996a | ||
|
|
265ab19e31 | ||
|
|
80a799dff9 | ||
|
|
3ef95628f5 | ||
|
|
626f64aec0 | ||
|
|
e36c285764 | ||
|
|
20ca92f451 | ||
|
|
4557150378 | ||
|
|
43ac90c3c7 | ||
|
|
deb382af2d | ||
|
|
34c775f185 | ||
|
|
22876cd5f0 | ||
|
|
9c1350f64b | ||
|
|
808a453641 | ||
|
|
63521c91e7 | ||
|
|
c652385a97 | ||
|
|
ee7fa79e63 | ||
|
|
ea0a659119 | ||
|
|
b0e587ddb8 | ||
|
|
04d86bc45a | ||
|
|
76b421e800 | ||
|
|
d8c42bd8c4 | ||
|
|
093275ac0c | ||
|
|
c2e998927d | ||
|
|
0ace092c34 | ||
|
|
7f4f90704d | ||
|
|
cb6ad7809f | ||
|
|
896e894aff | ||
|
|
c0f84f66f1 | ||
|
|
d7de46274d | ||
|
|
c9bba037b0 | ||
|
|
942d9b480f | ||
|
|
a5f8050d9c | ||
|
|
c5bd48d86e | ||
|
|
9e07b0f19a | ||
|
|
bb8d0dabba | ||
|
|
b113cafeba | ||
|
|
7c041e0fc8 | ||
|
|
f6c3ecf369 | ||
|
|
9e452aa9c9 | ||
|
|
52f1dc45a6 | ||
|
|
92e4510c2d | ||
|
|
78586be4a3 | ||
|
|
79a5621f31 | ||
|
|
9367e8e495 | ||
|
|
b611b1bb07 | ||
|
|
b987ae7a3f | ||
|
|
6fa858f8d4 | ||
|
|
e57db48f0e | ||
|
|
3e20dc3356 | ||
|
|
d65f724b56 | ||
|
|
6bd9aaa949 | ||
|
|
d905972f61 | ||
|
|
3511ce30e3 | ||
|
|
215dea151f |
6
packages/foam-cli/babel.config.js
Normal file
6
packages/foam-cli/babel.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
['@babel/preset-env', {targets: {node: 'current'}}],
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
};
|
||||
188
packages/foam-cli/jest.config.js
Normal file
188
packages/foam-cli/jest.config.js
Normal file
@@ -0,0 +1,188 @@
|
||||
// For a detailed explanation regarding each configuration property, visit:
|
||||
// https://jestjs.io/docs/en/configuration.html
|
||||
|
||||
module.exports = {
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
|
||||
// Stop running tests after `n` failures
|
||||
// bail: 0,
|
||||
|
||||
// The directory where Jest should store its cached dependency information
|
||||
// cacheDirectory: "/private/var/folders/p6/5y5l8tbs1d32pq9b596lk48h0000gn/T/jest_dx",
|
||||
|
||||
// Automatically clear mock calls and instances between every test
|
||||
clearMocks: true,
|
||||
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
// collectCoverage: false,
|
||||
|
||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||
// collectCoverageFrom: undefined,
|
||||
|
||||
// The directory where Jest should output its coverage files
|
||||
// coverageDirectory: undefined,
|
||||
|
||||
// An array of regexp pattern strings used to skip coverage collection
|
||||
// coveragePathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// Indicates which provider should be used to instrument code for coverage
|
||||
// coverageProvider: "babel",
|
||||
|
||||
// A list of reporter names that Jest uses when writing coverage reports
|
||||
// coverageReporters: [
|
||||
// "json",
|
||||
// "text",
|
||||
// "lcov",
|
||||
// "clover"
|
||||
// ],
|
||||
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
// coverageThreshold: undefined,
|
||||
|
||||
// A path to a custom dependency extractor
|
||||
// dependencyExtractor: undefined,
|
||||
|
||||
// Make calling deprecated APIs throw helpful error messages
|
||||
// errorOnDeprecated: false,
|
||||
|
||||
// Force coverage collection from ignored files using an array of glob patterns
|
||||
// forceCoverageMatch: [],
|
||||
|
||||
// A path to a module which exports an async function that is triggered once before all test suites
|
||||
// globalSetup: undefined,
|
||||
|
||||
// A path to a module which exports an async function that is triggered once after all test suites
|
||||
// globalTeardown: undefined,
|
||||
|
||||
// A set of global variables that need to be available in all test environments
|
||||
// globals: {},
|
||||
|
||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||
// maxWorkers: "50%",
|
||||
|
||||
// An array of directory names to be searched recursively up from the requiring module's location
|
||||
// moduleDirectories: [
|
||||
// "node_modules"
|
||||
// ],
|
||||
|
||||
// An array of file extensions your modules use
|
||||
// moduleFileExtensions: [
|
||||
// "js",
|
||||
// "json",
|
||||
// "jsx",
|
||||
// "ts",
|
||||
// "tsx",
|
||||
// "node"
|
||||
// ],
|
||||
|
||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||
// moduleNameMapper: {},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
// modulePathIgnorePatterns: [],
|
||||
|
||||
// Activates notifications for test results
|
||||
// notify: false,
|
||||
|
||||
// An enum that specifies notification mode. Requires { notify: true }
|
||||
// notifyMode: "failure-change",
|
||||
|
||||
// A preset that is used as a base for Jest's configuration
|
||||
// preset: undefined,
|
||||
|
||||
// Run tests from one or more projects
|
||||
// projects: undefined,
|
||||
|
||||
// Use this configuration option to add custom reporters to Jest
|
||||
// reporters: undefined,
|
||||
|
||||
// Automatically reset mock state between every test
|
||||
// resetMocks: false,
|
||||
|
||||
// Reset the module registry before running each individual test
|
||||
// resetModules: false,
|
||||
|
||||
// A path to a custom resolver
|
||||
// resolver: undefined,
|
||||
|
||||
// Automatically restore mock state between every test
|
||||
// restoreMocks: false,
|
||||
|
||||
// The root directory that Jest should scan for tests and modules within
|
||||
// rootDir: undefined,
|
||||
|
||||
// A list of paths to directories that Jest should use to search for files in
|
||||
// roots: [
|
||||
// "<rootDir>"
|
||||
// ],
|
||||
|
||||
// Allows you to use a custom runner instead of Jest's default test runner
|
||||
// runner: "jest-runner",
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
// setupFiles: [],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
// setupFilesAfterEnv: [],
|
||||
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: "node",
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
|
||||
// Adds a location field to test results
|
||||
// testLocationInResults: false,
|
||||
|
||||
// The glob patterns Jest uses to detect test files
|
||||
// testMatch: [
|
||||
// "**/__tests__/**/*.[jt]s?(x)",
|
||||
// "**/?(*.)+(spec|test).[tj]s?(x)"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
// testPathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// testRegex: [],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: undefined,
|
||||
|
||||
// This option allows use of a custom test runner
|
||||
// testRunner: "jasmine2",
|
||||
|
||||
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||
// testURL: "http://localhost",
|
||||
|
||||
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
||||
// timers: "real",
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
// transform: undefined,
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
// transformIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
// verbose: undefined,
|
||||
|
||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||
// watchPathIgnorePatterns: [],
|
||||
|
||||
// Whether to use watchman for file crawling
|
||||
// watchman: true,
|
||||
};
|
||||
@@ -11,17 +11,28 @@
|
||||
"@oclif/command": "^1",
|
||||
"@oclif/config": "^1",
|
||||
"@oclif/plugin-help": "^3",
|
||||
"chalk": "^4.1.0",
|
||||
"@types/inquirer": "^6.5.0",
|
||||
"foam-core": "^0.2.0",
|
||||
"inquirer": "^7.3.2",
|
||||
"ora": "^4.0.4",
|
||||
"tslib": "^1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.10.4",
|
||||
"@babel/preset-env": "^7.10.4",
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@oclif/dev-cli": "^1",
|
||||
"@types/node": "^10",
|
||||
"babel-jest": "^26.1.0",
|
||||
"chai": "^4",
|
||||
"eslint": "^5.13",
|
||||
"eslint-config-oclif": "^3.1",
|
||||
"eslint-config-oclif-typescript": "^0.1",
|
||||
"foam-core": "^0.2.0",
|
||||
"globby": "^10",
|
||||
"jest": "^26.1.0",
|
||||
"mock-fs": "^4.12.0",
|
||||
"ts-node": "^8",
|
||||
"typescript": "^3.3"
|
||||
},
|
||||
@@ -54,10 +65,9 @@
|
||||
"scripts": {
|
||||
"cli": "./bin/run",
|
||||
"postpack": "rm -f oclif.manifest.json",
|
||||
"posttest": "eslint . --ext .ts --config .eslintrc",
|
||||
"prepack": "rm -rf lib && tsc -b && oclif-dev manifest && oclif-dev readme",
|
||||
"test": "nyc --extension .ts mocha --forbid-only \"test/**/*.test.ts\"",
|
||||
"test": "jest",
|
||||
"version": "oclif-dev readme && git add README.md"
|
||||
},
|
||||
"types": "lib/index.d.ts"
|
||||
}
|
||||
}
|
||||
168
packages/foam-cli/src/commands/init.ts
Normal file
168
packages/foam-cli/src/commands/init.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
/*eslint-disable no-unused-vars*/
|
||||
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import * as inquirer from 'inquirer';
|
||||
import * as ora from 'ora';
|
||||
|
||||
// @todo implement this class, currently it does nothing but collect inputs
|
||||
export default class Init extends Command {
|
||||
static description = 'Initialize a new Foam workspace from template';
|
||||
|
||||
// @todo better examples
|
||||
static examples = [`$ foam init`];
|
||||
|
||||
// @todo validate inputs
|
||||
static flags = {
|
||||
help: flags.help({ char: 'h' }),
|
||||
name: flags.string({
|
||||
char: 'n',
|
||||
description: 'workspace name',
|
||||
}),
|
||||
|
||||
scm: flags.string({
|
||||
char: 's',
|
||||
description: 'source control (github, git, local)'
|
||||
}),
|
||||
|
||||
template: flags.string({
|
||||
char: 't',
|
||||
description: 'template'
|
||||
}),
|
||||
|
||||
gitHubUser: flags.string({
|
||||
char: 'u',
|
||||
description: 'github username'
|
||||
}),
|
||||
|
||||
gitHubPassword: flags.string({
|
||||
description: 'github password'
|
||||
}),
|
||||
|
||||
// @todo make flag
|
||||
githubPages: flags.string({
|
||||
char: 'p',
|
||||
description: 'enable github pages'
|
||||
}),
|
||||
|
||||
repoOwner: flags.string({
|
||||
char: 'p',
|
||||
description: 'github repo owner'
|
||||
}),
|
||||
|
||||
visibility: flags.string({
|
||||
char: 'v',
|
||||
description: 'github repo visibility (public/private)'
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
const { flags } = this.parse(Init);
|
||||
|
||||
const name =
|
||||
flags.name ||
|
||||
(await inquirer.prompt({
|
||||
name: 'name',
|
||||
message: 'Give your workspace a name',
|
||||
type: 'input',
|
||||
default: 'foam',
|
||||
})).name;
|
||||
|
||||
const template =
|
||||
flags.template ||
|
||||
(await inquirer.prompt({
|
||||
name: 'template',
|
||||
message: 'Choose from one of the available templates',
|
||||
type: 'list',
|
||||
choices: [
|
||||
{ name: 'Default (foam-template)' },
|
||||
{ name: 'Gatsby + GitHub Actions (foam-template-gatsby)' },
|
||||
{ name: '11ty + Netlify (foam-template-eleventy)' },
|
||||
{ name: 'MLH Fellowship Workspace (foam-template-mlh)' },
|
||||
],
|
||||
})).template;
|
||||
|
||||
const scm = (await inquirer.prompt([
|
||||
{
|
||||
name: 'scm',
|
||||
message: 'How do you want to store your workspace?',
|
||||
type: 'list',
|
||||
default: 'GitHub',
|
||||
choices: [
|
||||
{ name: 'GitHub' },
|
||||
{ name: 'Local git repository' },
|
||||
{ name: 'Local directory (no source control)' },
|
||||
],
|
||||
},
|
||||
])).scm;
|
||||
|
||||
if (scm === 'GitHub') {
|
||||
const userName =
|
||||
flags.gitHubUser ||
|
||||
(await inquirer.prompt({
|
||||
name: 'username',
|
||||
message: 'GitHub username',
|
||||
type: 'input'
|
||||
})).username;
|
||||
|
||||
const password =
|
||||
flags.gitHubPassword ||
|
||||
(await inquirer.prompt({
|
||||
name: 'password',
|
||||
message: 'GitHub password',
|
||||
type: 'password'
|
||||
})).password;
|
||||
|
||||
const owner =
|
||||
flags.repoOwner ||
|
||||
(await inquirer.prompt({
|
||||
name: 'owner',
|
||||
message: 'GitHub repository owner',
|
||||
type: 'input',
|
||||
default: userName
|
||||
})).owner;
|
||||
|
||||
const visibility =
|
||||
flags.visibility ||
|
||||
(await inquirer.prompt({
|
||||
name: 'visibility',
|
||||
message: 'Should the repository be public or private?',
|
||||
type: 'list',
|
||||
choices: [
|
||||
{ name: 'Public' },
|
||||
{ name: 'Private' }
|
||||
],
|
||||
})).visibility.toLowerCase();
|
||||
|
||||
const pages =
|
||||
flags.githubPages ||
|
||||
((await inquirer.prompt({
|
||||
name: 'pages',
|
||||
message: 'Publish automatically to GitHub pages?',
|
||||
type: 'list',
|
||||
choices: [
|
||||
{ name: 'Yes' },
|
||||
{ name: 'No' }
|
||||
],
|
||||
})).pages === 'Yes');
|
||||
|
||||
|
||||
const sure = (await inquirer.prompt({
|
||||
name: 'sure',
|
||||
type: 'confirm',
|
||||
message: `Create a new ${visibility} Foam in https://github.com/${owner}/${name}?`
|
||||
})).sure;
|
||||
|
||||
if (sure) {
|
||||
const spinner = ora().start();
|
||||
await new Promise(resolve => {
|
||||
setTimeout(() => resolve(), 1000);
|
||||
});
|
||||
spinner.succeed();
|
||||
spinner.succeed('Foam workspace created!');
|
||||
spinner.succeed('Run "code foam" to open your new workspace');
|
||||
}
|
||||
} else {
|
||||
console.log(`Created a private Foam workspace in ./${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
74
packages/foam-cli/src/commands/janitor.ts
Normal file
74
packages/foam-cli/src/commands/janitor.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import * as ora from 'ora';
|
||||
import { initializeNoteGraph, generateLinkReferences, generateHeading, getKebabCaseFileName } from 'foam-core';
|
||||
import { applyTextEdit } from '../utils/apply-text-edit';
|
||||
import { writeFileToDisk } from '../utils/write-file-to-disk';
|
||||
import { isValidDirectory } from '../utils';
|
||||
|
||||
export default class Janitor extends Command {
|
||||
static description = 'Updates link references and heading across all the markdown files in the given workspaces';
|
||||
|
||||
static examples = [
|
||||
`$ foam-cli janitor path-to-foam-workspace
|
||||
Successfully generated link references and heading!
|
||||
`,
|
||||
]
|
||||
|
||||
static flags = {
|
||||
help: flags.help({ char: 'h' }),
|
||||
}
|
||||
|
||||
static args = [{ name: 'workspacePath' }]
|
||||
|
||||
async run() {
|
||||
const spinner = ora('Reading Files').start();
|
||||
|
||||
const { args, flags } = this.parse(Janitor)
|
||||
|
||||
const { workspacePath = './' } = args;
|
||||
|
||||
if (isValidDirectory(workspacePath)) {
|
||||
const graph = await initializeNoteGraph(workspacePath);
|
||||
|
||||
const notes = graph.getNotes().filter(Boolean); // removes undefined notes
|
||||
|
||||
spinner.succeed();
|
||||
spinner.text = `${notes.length} files found`;
|
||||
spinner.succeed();
|
||||
|
||||
// exit early if no files found.
|
||||
if (notes.length === 0) {
|
||||
this.exit();
|
||||
}
|
||||
|
||||
spinner.text = 'Generating link definitions';
|
||||
|
||||
const fileWritePromises = notes.map(note => {
|
||||
// Get edits
|
||||
const heading = generateHeading(note);
|
||||
const definitions = generateLinkReferences(note, graph);
|
||||
|
||||
|
||||
// apply Edits
|
||||
let file = note.source;
|
||||
file = heading ? applyTextEdit(file, heading) : file;
|
||||
file = definitions ? applyTextEdit(file, definitions) : file;
|
||||
|
||||
|
||||
if (heading || definitions) {
|
||||
return writeFileToDisk(note.path, file);
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
})
|
||||
|
||||
await Promise.all(fileWritePromises);
|
||||
|
||||
spinner.succeed();
|
||||
spinner.succeed('Done!');
|
||||
}
|
||||
else {
|
||||
spinner.fail('Directory does not exist!');
|
||||
}
|
||||
}
|
||||
}
|
||||
83
packages/foam-cli/src/commands/lint.ts
Normal file
83
packages/foam-cli/src/commands/lint.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import * as ora from 'ora';
|
||||
import * as chalk from 'chalk';
|
||||
import { initializeNoteGraph, Note, NoteLink } from 'foam-core';
|
||||
import * as fs from 'fs'
|
||||
|
||||
interface InvalidReference {
|
||||
note: Note,
|
||||
link: NoteLink
|
||||
}
|
||||
|
||||
export default class Lint extends Command {
|
||||
static description = 'Shows lint errors and warnings across all the markdown files in the given workspaces';
|
||||
|
||||
// TODO:
|
||||
static examples = [
|
||||
`$ foam-cli Lint path-to-foam-workspace
|
||||
Successfully generated link references and heading!
|
||||
`,
|
||||
]
|
||||
|
||||
static flags = {
|
||||
help: flags.help({ char: 'h' }),
|
||||
}
|
||||
|
||||
static args = [{ name: 'workspacePath' }]
|
||||
|
||||
async run() {
|
||||
const spinner = ora('Reading Files').start();
|
||||
|
||||
const { args, flags } = this.parse(Lint)
|
||||
|
||||
const { workspacePath = './' } = args;
|
||||
|
||||
|
||||
if (fs.existsSync(workspacePath) && fs.lstatSync(workspacePath).isDirectory()) {
|
||||
const graph = await initializeNoteGraph(workspacePath);
|
||||
|
||||
const notes = graph.getNotes();
|
||||
|
||||
spinner.text = `${notes.length} files found`;
|
||||
|
||||
|
||||
|
||||
// TODO: Figure out why there is an undefined note
|
||||
const orphanedNotes = notes.filter(note => note && graph.getBacklinks(note.id).length === 0);
|
||||
|
||||
// Find out invalid references
|
||||
// ⚠️ Warning: Dirty code ahead. This is just a proof of concept.
|
||||
// @ts-ignore
|
||||
const invalidLinks: InvalidReference[] = notes.filter(Boolean).map(note => {
|
||||
return graph
|
||||
.getNoteLinks(note.id)
|
||||
.map(link => {
|
||||
const target = graph.getNote(link.to);
|
||||
return !target ? { note: note, link: link } : false;
|
||||
})
|
||||
.filter(Boolean)
|
||||
}).reduce((acc, curr) => ([...acc, ...curr]), []) // flatten the array
|
||||
|
||||
const warnings = `${orphanedNotes.map(note => {
|
||||
return `→ "${note.title}" is an orphan note.`;
|
||||
}).join('\n')}`;
|
||||
|
||||
|
||||
const errors = `${invalidLinks.map(item => {
|
||||
return `→ Link "${item.link.to}" in "${item.note.title}" points to a non-existing note [${item.link.position.start.line}, ${item.link.position.start.column}] `;
|
||||
}).join('\n')}`;
|
||||
|
||||
|
||||
spinner.stop()
|
||||
|
||||
this.log(chalk.yellowBright('⚠️ Warnings:'));
|
||||
this.log(warnings);
|
||||
|
||||
this.log(chalk.redBright('❌ Errors:'));
|
||||
console.log(errors);
|
||||
}
|
||||
else {
|
||||
spinner.fail('Directory does not exist!');
|
||||
}
|
||||
}
|
||||
}
|
||||
95
packages/foam-cli/src/commands/migrate.ts
Normal file
95
packages/foam-cli/src/commands/migrate.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import * as ora from 'ora';
|
||||
import { initializeNoteGraph, generateLinkReferences, generateHeading, getKebabCaseFileName } from 'foam-core';
|
||||
import { applyTextEdit } from '../utils/apply-text-edit';
|
||||
import { writeFileToDisk } from '../utils/write-file-to-disk';
|
||||
import { renameFile } from '../utils/rename-file';
|
||||
import { isValidDirectory } from '../utils';
|
||||
|
||||
// @todo: Refactor 'migrate' and 'janitor' commands and avoid repeatition
|
||||
export default class Migrate extends Command {
|
||||
static description = 'Updates file names, link references and heading across all the markdown files in the given workspaces';
|
||||
|
||||
static examples = [
|
||||
`$ foam-cli migrate path-to-foam-workspace
|
||||
Successfully generated link references and heading!
|
||||
`,
|
||||
]
|
||||
|
||||
static flags = {
|
||||
help: flags.help({ char: 'h' }),
|
||||
}
|
||||
|
||||
static args = [{ name: 'workspacePath' }]
|
||||
|
||||
async run() {
|
||||
const spinner = ora('Reading Files').start();
|
||||
|
||||
const { args, flags } = this.parse(Migrate)
|
||||
|
||||
const { workspacePath = './' } = args;
|
||||
|
||||
if (isValidDirectory(workspacePath)) {
|
||||
let graph = await initializeNoteGraph(workspacePath);
|
||||
|
||||
let notes = graph.getNotes().filter(Boolean); // removes undefined notes
|
||||
|
||||
spinner.succeed();
|
||||
spinner.text = `${notes.length} files found`;
|
||||
spinner.succeed();
|
||||
|
||||
// exit early if no files found.
|
||||
if (notes.length === 0) {
|
||||
this.exit();
|
||||
}
|
||||
|
||||
// Kebab case file names
|
||||
const fileRename = notes.map(note => {
|
||||
const kebabCasedFileName = getKebabCaseFileName(note.title);
|
||||
if (kebabCasedFileName) {
|
||||
return renameFile(note.path, kebabCasedFileName);
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
})
|
||||
|
||||
await Promise.all(fileRename);
|
||||
|
||||
spinner.text = 'Renaming files';
|
||||
|
||||
// Reinitialize the graph after renaming files
|
||||
graph = await initializeNoteGraph(workspacePath);
|
||||
|
||||
notes = graph.getNotes().filter(Boolean); // remove undefined notes
|
||||
|
||||
spinner.succeed();
|
||||
spinner.text = 'Generating link definitions'
|
||||
|
||||
const fileWritePromises = await Promise.all(notes.map(note => {
|
||||
// Get edits
|
||||
const heading = generateHeading(note);
|
||||
const definitions = generateLinkReferences(note, graph);
|
||||
|
||||
|
||||
// apply Edits
|
||||
let file = note.source;
|
||||
file = heading ? applyTextEdit(file, heading) : file;
|
||||
file = definitions ? applyTextEdit(file, definitions) : file;
|
||||
|
||||
|
||||
if (heading || definitions) {
|
||||
return writeFileToDisk(note.path, file);
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
}))
|
||||
|
||||
await Promise.all(fileWritePromises);
|
||||
|
||||
spinner.succeed();
|
||||
spinner.succeed('Done!');
|
||||
}
|
||||
else {
|
||||
spinner.fail('Directory does not exist!');
|
||||
}
|
||||
}
|
||||
}
|
||||
63
packages/foam-cli/src/commands/publish.ts
Normal file
63
packages/foam-cli/src/commands/publish.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import {Command, flags} from '@oclif/command'
|
||||
import { execSync } from 'child_process';
|
||||
import * as ora from 'ora';
|
||||
|
||||
|
||||
export default class Publish extends Command {
|
||||
static description = 'Push all changes to git repository';
|
||||
|
||||
static examples = [
|
||||
`$ foam publish -m "Optional log message"`,
|
||||
]
|
||||
|
||||
static flags = {
|
||||
message: flags.string({
|
||||
char: 'm',
|
||||
description: "optional message"
|
||||
}),
|
||||
remote: flags.string({
|
||||
char: 'r',
|
||||
description: "remote"
|
||||
}),
|
||||
branch: flags.string({
|
||||
char: 'b',
|
||||
description: "branch"
|
||||
})
|
||||
}
|
||||
|
||||
async execWithSpinner(command: string, message: string) {
|
||||
const spinner = ora(message).start();
|
||||
|
||||
// @todo handle errors
|
||||
const response = execSync(command).toString();
|
||||
|
||||
spinner.succeed(`${message} Done!`);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async printPublishInfo(remote: string) {
|
||||
// @todo actually get this data from GH API
|
||||
|
||||
const [, remotePath] = execSync(`git remote get-url ${remote}`).toString().trim().split(':');
|
||||
const [repo, org] = remotePath.split('/').reverse();
|
||||
console.log('');
|
||||
console.log(`🎉 Your changes will be available shortly at https://${org}.github.io/${repo.replace('.git', '')}`);
|
||||
console.log('');
|
||||
|
||||
}
|
||||
|
||||
async run() {
|
||||
const {flags} = this.parse(Publish);
|
||||
|
||||
// @todo improve
|
||||
const message = flags.message || 'foam publish';
|
||||
const remote = flags.remote || 'origin';
|
||||
const branch = flags.branch || 'master';
|
||||
|
||||
await this.execWithSpinner(`git add -A`, 'Staging changes...');
|
||||
await this.execWithSpinner(`git commit -m "${message}"`, 'Creating a commit...');
|
||||
await this.execWithSpinner(`git push ${remote} ${branch}`, "Publishing...");
|
||||
await this.printPublishInfo(remote);
|
||||
}
|
||||
}
|
||||
18
packages/foam-cli/src/utils/apply-text-edit.ts
Normal file
18
packages/foam-cli/src/utils/apply-text-edit.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { TextEdit } from 'foam-core';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param text text on which the textEdit will be applied
|
||||
* @param textEdit
|
||||
* @returns {string} text with the applied textEdit
|
||||
*/
|
||||
export const applyTextEdit = (text: string, textEdit: TextEdit): string => {
|
||||
const characters = text.split('');
|
||||
const startOffset = textEdit.range.start.offset || 0;
|
||||
const endOffset = textEdit.range.end.offset || 0;
|
||||
const deleteCount = endOffset - startOffset;
|
||||
|
||||
const textToAppend = `${textEdit.newText}`;
|
||||
characters.splice(startOffset, deleteCount, textToAppend);
|
||||
return characters.join('');
|
||||
}
|
||||
4
packages/foam-cli/src/utils/index.ts
Normal file
4
packages/foam-cli/src/utils/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import * as fs from 'fs';
|
||||
|
||||
|
||||
export const isValidDirectory = (path: string) => fs.existsSync(path) && fs.lstatSync(path).isDirectory();
|
||||
15
packages/foam-cli/src/utils/rename-file.ts
Normal file
15
packages/foam-cli/src/utils/rename-file.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param fileUri absolute path for the file that needs to renamed
|
||||
* @param newFileName "new file name" without the extension
|
||||
*/
|
||||
export const renameFile = async (fileUri: string, newFileName: string) => {
|
||||
const dirName = path.dirname(fileUri);
|
||||
const extension = path.extname(fileUri);
|
||||
const newFileUri = path.join(dirName, `${newFileName}${extension}`);
|
||||
|
||||
return fs.promises.rename(fileUri, newFileUri);
|
||||
}
|
||||
5
packages/foam-cli/src/utils/write-file-to-disk.ts
Normal file
5
packages/foam-cli/src/utils/write-file-to-disk.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import * as fs from 'fs';
|
||||
|
||||
export const writeFileToDisk = async (fileUri: string, data: string) => {
|
||||
return fs.promises.writeFile(fileUri, data);
|
||||
}
|
||||
82
packages/foam-cli/test/apply-text-edit.test.ts
Normal file
82
packages/foam-cli/test/apply-text-edit.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
|
||||
import { applyTextEdit } from '../src/utils/apply-text-edit';
|
||||
|
||||
describe('applyTextEdit', () => {
|
||||
it('should return text with applied TextEdit in the end of the string', () => {
|
||||
const textEdit = {
|
||||
newText: `\n 4. this is fourth line`,
|
||||
range: {
|
||||
start: { line: 3, column: 1, offset: 79 }, end: { line: 3, column: 1, offset: 79 },
|
||||
},
|
||||
};
|
||||
|
||||
const text = `
|
||||
1. this is first line
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
1. this is first line
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
4. this is fourth line
|
||||
`;
|
||||
|
||||
|
||||
const actual = applyTextEdit(text, textEdit);
|
||||
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
|
||||
it('should return text with applied TextEdit at the top of the string', () => {
|
||||
const textEdit = {
|
||||
newText: `\n 1. this is first line`,
|
||||
range: {
|
||||
start: { line: 0, column: 0, offset: 0 }, end: { line: 0, column: 0, offset: 0 },
|
||||
},
|
||||
};
|
||||
|
||||
const text = `
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
1. this is first line
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
`;
|
||||
|
||||
|
||||
const actual = applyTextEdit(text, textEdit);
|
||||
|
||||
expect(actual).toBe(expected)
|
||||
});
|
||||
|
||||
it('should return text with applied TextEdit in the middle of the string', () => {
|
||||
const textEdit = {
|
||||
newText: `\n 2. this is the updated second line`,
|
||||
range: {
|
||||
start: { line: 0, column: 0, offset: 26 }, end: { line: 0, column: 0, offset: 53 },
|
||||
},
|
||||
};
|
||||
|
||||
const text = `
|
||||
1. this is first line
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
1. this is first line
|
||||
2. this is the updated second line
|
||||
3. this is third line
|
||||
`;
|
||||
|
||||
|
||||
const actual = applyTextEdit(text, textEdit);
|
||||
|
||||
expect(actual).toBe(expected)
|
||||
});
|
||||
})
|
||||
28
packages/foam-cli/test/rename-file.test.ts
Normal file
28
packages/foam-cli/test/rename-file.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
import { renameFile } from '../src/utils/rename-file'
|
||||
import * as fs from 'fs';
|
||||
import mockFS from 'mock-fs';
|
||||
|
||||
const doesFileExist = (path) => fs.promises.access(path).then(() => true).catch(() => false);
|
||||
|
||||
describe('renameFile', () => {
|
||||
|
||||
const fileUri = './test/oldFileName.md';
|
||||
|
||||
beforeAll(() => {
|
||||
mockFS({ [fileUri]: '' })
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mockFS.restore();
|
||||
});
|
||||
|
||||
it('should rename existing file', async () => {
|
||||
expect(await doesFileExist(fileUri)).toBe(true);
|
||||
|
||||
renameFile(fileUri, 'new-file-name');
|
||||
|
||||
expect(await doesFileExist(fileUri)).toBe(false);
|
||||
expect(await doesFileExist('./test/new-file-name.md')).toBe(true);
|
||||
});
|
||||
});
|
||||
24
packages/foam-cli/test/write-file-to-disk.test.ts
Normal file
24
packages/foam-cli/test/write-file-to-disk.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
import { writeFileToDisk } from '../src/utils/write-file-to-disk'
|
||||
import * as fs from 'fs';
|
||||
import mockFS from 'mock-fs';
|
||||
|
||||
describe('writeFileToDisk', () => {
|
||||
const fileUri = './test-file.md';
|
||||
|
||||
beforeAll(() => {
|
||||
mockFS({ [fileUri]: 'content in the existing file' });
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
fs.unlinkSync(fileUri);
|
||||
mockFS.restore();
|
||||
})
|
||||
|
||||
it('should overrwrite existing file in the disk with the new data', async () => {
|
||||
const expected = `content in the new file`;
|
||||
await writeFileToDisk(fileUri, expected);
|
||||
const actual = await fs.promises.readFile(fileUri, { encoding: 'utf8' });
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
})
|
||||
@@ -15,6 +15,7 @@
|
||||
"prepare": "tsdx build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/github-slugger": "^1.3.0",
|
||||
"@types/graphlib": "^2.1.6",
|
||||
"@types/lodash": "^4.14.157",
|
||||
"husky": "^4.2.5",
|
||||
@@ -23,10 +24,14 @@
|
||||
"typescript": "^3.9.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"detect-newline": "^3.1.0",
|
||||
"github-slugger": "^1.3.0",
|
||||
"glob": "^7.1.6",
|
||||
"graphlib": "^2.1.8",
|
||||
"lodash": "^4.17.19",
|
||||
"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"
|
||||
},
|
||||
|
||||
@@ -3,20 +3,25 @@ import { NoteGraph, Note, NoteLink } from './note-graph';
|
||||
export {
|
||||
createNoteFromMarkdown,
|
||||
createMarkdownReferences,
|
||||
stringifyMarkdownLinkReferenceDefinition,
|
||||
} from './markdown-provider';
|
||||
|
||||
export { NoteGraph, Note, NoteLink }
|
||||
export { TextEdit, generateHeading, generateLinkReferences, getKebabCaseFileName } from './janitor'
|
||||
|
||||
export { initializeNoteGraph } from './initialize-note-graph'
|
||||
|
||||
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,
|
||||
})
|
||||
});
|
||||
|
||||
30
packages/foam-core/src/initialize-note-graph.ts
Normal file
30
packages/foam-core/src/initialize-note-graph.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
|
||||
import glob from 'glob';
|
||||
import { promisify } from 'util';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import detectNewline from 'detect-newline';
|
||||
import { NoteGraph } from './note-graph';
|
||||
import { createNoteFromMarkdown } from './markdown-provider';
|
||||
|
||||
const findAllFiles = promisify(glob);
|
||||
|
||||
export const initializeNoteGraph = async (workspacePath: string) => {
|
||||
// remove trailing slash from workspacePath if exists
|
||||
if (workspacePath.substr(-1) == '/') workspacePath = workspacePath.slice(0, -1);
|
||||
|
||||
const files = await findAllFiles(`${workspacePath}/**/*.md`, {});
|
||||
|
||||
const graph = new NoteGraph();
|
||||
await Promise.all(
|
||||
(await files).map(f => {
|
||||
return fs.promises.readFile(f).then(data => {
|
||||
const markdown = (data || '').toString();
|
||||
const eol = detectNewline(markdown) || os.EOL;
|
||||
graph.setNote(createNoteFromMarkdown(f, markdown, eol));
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
return graph;
|
||||
}
|
||||
87
packages/foam-core/src/janitor/index.ts
Normal file
87
packages/foam-core/src/janitor/index.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Position } from 'unist';
|
||||
import GithubSlugger from 'github-slugger';
|
||||
import { Note, NoteGraph } from '../index';
|
||||
import {
|
||||
createMarkdownReferences,
|
||||
stringifyMarkdownLinkReferenceDefinition,
|
||||
} from '../markdown-provider';
|
||||
import { getHeadingFromFileName } from '../utils'
|
||||
|
||||
const slugger = new GithubSlugger()
|
||||
|
||||
export interface TextEdit {
|
||||
range: Position;
|
||||
newText: string;
|
||||
}
|
||||
|
||||
export const generateLinkReferences = (note: Note, ng: NoteGraph): TextEdit | null => {
|
||||
if (!note) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const newReferences = createMarkdownReferences(ng, note.id).map(
|
||||
stringifyMarkdownLinkReferenceDefinition
|
||||
).join('\n');
|
||||
|
||||
if (note.definitions.length === 0) {
|
||||
if (newReferences.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const padding = note.end.column === 1 ? note.eol : `${note.eol}${note.eol}`;
|
||||
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(note.eol);
|
||||
|
||||
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 => {
|
||||
if (!note) {
|
||||
return 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)}${note.eol}${note.eol}`,
|
||||
range: {
|
||||
start: { line: 0, column: 0, offset: 0 },
|
||||
end: { line: 0, column: 0, offset: 0 }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param fileName
|
||||
* @returns null if file name is already in kebab case otherise returns
|
||||
* the kebab cased file name
|
||||
*/
|
||||
export const getKebabCaseFileName = (fileName: string) => {
|
||||
const kebabCasedFileName = slugger.slug(fileName);
|
||||
return kebabCasedFileName === fileName ? null : kebabCasedFileName;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -18,7 +18,7 @@ function parse(markdown: string): Node {
|
||||
return processor.parse(markdown);
|
||||
}
|
||||
|
||||
export function createNoteFromMarkdown(uri: string, markdown: string): Note {
|
||||
export function createNoteFromMarkdown(uri: string, markdown: string, eol: string): Note {
|
||||
const filename = path.basename(uri);
|
||||
const id = path.parse(filename).name;
|
||||
const tree = parse(markdown);
|
||||
@@ -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, eol);
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -72,7 +89,7 @@ export function createMarkdownReferences(
|
||||
// but int the future we may want to surface these too
|
||||
if (!target) {
|
||||
console.log(
|
||||
`Link '${link.to}' in '${noteId}' points to a non-existing note.`
|
||||
`Warning: Link '${link.to}' in '${noteId}' points to a non-existing note.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
@@ -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.text,
|
||||
url: relativePathWithoutExtension,
|
||||
title: target.title,
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort() as MarkdownReference[];
|
||||
.sort() as NoteLinkDefinition[];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Graph, Edge } from 'graphlib';
|
||||
import { Position } from 'unist';
|
||||
import { Position, Point } from 'unist';
|
||||
import GithubSlugger from 'github-slugger';
|
||||
|
||||
type ID = string;
|
||||
|
||||
@@ -15,25 +16,41 @@ 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 eol: string;
|
||||
public links: NoteLink[];
|
||||
public definitions: NoteLinkDefinition[];
|
||||
|
||||
constructor(
|
||||
id: ID,
|
||||
title: string,
|
||||
links: NoteLink[],
|
||||
definitions: NoteLinkDefinition[],
|
||||
end: Point,
|
||||
path: string,
|
||||
source: string
|
||||
source: string,
|
||||
eol: string
|
||||
) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.source = source;
|
||||
this.path = path;
|
||||
this.links = links;
|
||||
this.definitions = definitions;
|
||||
this.end = end;
|
||||
this.eol = eol;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +69,8 @@ export class NoteGraph {
|
||||
}
|
||||
this.graph.setNode(note.id, note);
|
||||
note.links.forEach(link => {
|
||||
this.graph.setEdge(note.id, link.to, link.text);
|
||||
const slugger = new GithubSlugger();
|
||||
this.graph.setEdge(note.id, slugger.slug(link.to), link.text);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -67,27 +85,34 @@ export class NoteGraph {
|
||||
throw new Error(`Note with ID [${noteId}] not found`);
|
||||
}
|
||||
|
||||
// Note: This is temporary until we figure out how to put
|
||||
// position inside Link (needed for linting)
|
||||
public getNoteLinks(noteId: ID): NoteLink[] {
|
||||
const note = this.getNote(noteId);
|
||||
return note ? note.links : [];
|
||||
}
|
||||
|
||||
public getAllLinks(noteId: ID): Link[] {
|
||||
return (this.graph.nodeEdges(noteId) || []).map(edge =>
|
||||
convertEdgeToLink(edge)
|
||||
convertEdgeToLink(edge, this.graph)
|
||||
);
|
||||
}
|
||||
|
||||
public getForwardLinks(noteId: ID): Link[] {
|
||||
return (this.graph.outEdges(noteId) || []).map(edge =>
|
||||
convertEdgeToLink(edge)
|
||||
convertEdgeToLink(edge, this.graph)
|
||||
);
|
||||
}
|
||||
|
||||
public getBacklinks(noteId: ID): Link[] {
|
||||
return (this.graph.inEdges(noteId) || []).map(edge =>
|
||||
convertEdgeToLink(edge)
|
||||
convertEdgeToLink(edge, this.graph)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const convertEdgeToLink = (edge: Edge): Link => ({
|
||||
from: edge.v,
|
||||
to: edge.w,
|
||||
text: edge.name || edge.w,
|
||||
const convertEdgeToLink = (edge: Edge, graph: Graph): Link => ({
|
||||
from: edge.v,
|
||||
to: edge.w,
|
||||
text: graph.edge(edge.v, edge.w),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
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, ' '));
|
||||
}
|
||||
3
packages/foam-core/test/__migration__/Roam Document.md
Normal file
3
packages/foam-core/test/__migration__/Roam Document.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Roam Document
|
||||
|
||||
[[Second Roam Document]]
|
||||
@@ -0,0 +1 @@
|
||||
# Second Roam Document
|
||||
@@ -0,0 +1 @@
|
||||
This file is missing a title
|
||||
7
packages/foam-core/test/__scaffold__/first-document.md
Normal file
7
packages/foam-core/test/__scaffold__/first-document.md
Normal 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'
|
||||
9
packages/foam-core/test/__scaffold__/index.md
Normal file
9
packages/foam-core/test/__scaffold__/index.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Index
|
||||
|
||||
This file is intentionally missing the link reference definitions
|
||||
|
||||
[[first-document]]
|
||||
|
||||
[[second-document]]
|
||||
|
||||
[[file-without-title]]
|
||||
9
packages/foam-core/test/__scaffold__/second-document.md
Normal file
9
packages/foam-core/test/__scaffold__/second-document.md
Normal 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'
|
||||
11
packages/foam-core/test/__scaffold__/third-document.md
Normal file
11
packages/foam-core/test/__scaffold__/third-document.md
Normal 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"
|
||||
@@ -1,16 +1,25 @@
|
||||
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;
|
||||
const eol = '\n';
|
||||
|
||||
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, eol, '/page-a.md', '')
|
||||
);
|
||||
graph.setNote(
|
||||
new Note('page-b', 'page-b', [], [], documentEnd, eol, '/page-b.md', '')
|
||||
);
|
||||
graph.setNote(
|
||||
new Note('page-c', 'page-c', [], [], documentEnd, eol, '/page-c.md', '')
|
||||
);
|
||||
|
||||
expect(
|
||||
graph
|
||||
@@ -22,17 +31,24 @@ 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, eol, '/page-a.md', '')
|
||||
);
|
||||
graph.setNote(
|
||||
new Note(
|
||||
'page-b',
|
||||
'page-b',
|
||||
[{ to: 'page-a', text: 'go', position }],
|
||||
[],
|
||||
documentEnd,
|
||||
eol,
|
||||
'/page-b.md',
|
||||
''
|
||||
)
|
||||
);
|
||||
graph.setNote(new Note('page-c', 'page-c', [], '/page-c.md', ''));
|
||||
graph.setNote(
|
||||
new Note('page-c', 'page-c', [], [], documentEnd, eol, '/page-c.md', '')
|
||||
);
|
||||
|
||||
expect(
|
||||
graph
|
||||
@@ -44,17 +60,24 @@ 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, eol, '/page-a.md', '')
|
||||
);
|
||||
graph.setNote(
|
||||
new Note(
|
||||
'page-b',
|
||||
'page-b',
|
||||
[{ to: 'page-a', text: 'go', position }],
|
||||
[],
|
||||
documentEnd,
|
||||
eol,
|
||||
'/page-b.md',
|
||||
''
|
||||
)
|
||||
);
|
||||
graph.setNote(new Note('page-c', 'page-c', [], '/page-c.md', ''));
|
||||
graph.setNote(
|
||||
new Note('page-c', 'page-c', [], [], documentEnd, eol, '/page-c.md', '')
|
||||
);
|
||||
|
||||
expect(
|
||||
graph
|
||||
@@ -67,7 +90,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, eol, '/path-b.md', '')
|
||||
);
|
||||
graph.getNote('non-existing');
|
||||
}).toThrow();
|
||||
});
|
||||
@@ -79,6 +104,9 @@ describe('Note graph', () => {
|
||||
'page-a',
|
||||
'page-a',
|
||||
[{ to: 'non-existing', text: 'does not exist', position }],
|
||||
[],
|
||||
documentEnd,
|
||||
eol,
|
||||
'/path-b.md',
|
||||
''
|
||||
)
|
||||
@@ -88,17 +116,24 @@ 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, eol, '/page-a.md', '')
|
||||
);
|
||||
graph.setNote(
|
||||
new Note(
|
||||
'page-b',
|
||||
'page-b',
|
||||
[{ to: 'page-a', text: 'go', position }],
|
||||
[],
|
||||
documentEnd,
|
||||
eol,
|
||||
'/page-b.md',
|
||||
''
|
||||
)
|
||||
);
|
||||
graph.setNote(new Note('page-c', 'page-c', [], '/page-c.md', ''));
|
||||
graph.setNote(
|
||||
new Note('page-c', 'page-c', [], [], documentEnd, eol, '/page-c.md', '')
|
||||
);
|
||||
|
||||
expect(
|
||||
graph
|
||||
@@ -124,6 +159,9 @@ describe('Note graph', () => {
|
||||
'page-b',
|
||||
'page-b',
|
||||
[{ to: 'page-c', text: 'go', position }],
|
||||
[],
|
||||
documentEnd,
|
||||
eol,
|
||||
'/path-2b.md',
|
||||
''
|
||||
)
|
||||
|
||||
53
packages/foam-core/test/janitor/generateHeadings.test.ts
Normal file
53
packages/foam-core/test/janitor/generateHeadings.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import * as path from 'path';
|
||||
import { NoteGraph, Note } from '../../src/note-graph';
|
||||
import { generateHeading } from '../../src/janitor';
|
||||
import { initializeNoteGraph } from '../../src/initialize-note-graph';
|
||||
|
||||
describe('generateHeadings', () => {
|
||||
let _graph: NoteGraph;
|
||||
|
||||
beforeAll(async () => {
|
||||
_graph = await initializeNoteGraph(path.join(__dirname, '../__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);
|
||||
|
||||
})
|
||||
|
||||
});
|
||||
106
packages/foam-core/test/janitor/generateLinkReferences.test.ts
Normal file
106
packages/foam-core/test/janitor/generateLinkReferences.test.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import * as path from 'path';
|
||||
import { NoteGraph, Note } from '../../src/note-graph';
|
||||
import { generateLinkReferences } from '../../src/janitor';
|
||||
import { initializeNoteGraph } from '../../src/initialize-note-graph';
|
||||
|
||||
describe('generateLinkReferences', () => {
|
||||
let _graph: NoteGraph;
|
||||
|
||||
beforeAll(async () => {
|
||||
_graph = await initializeNoteGraph(path.join(__dirname, '../__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);
|
||||
});
|
||||
});
|
||||
@@ -18,12 +18,13 @@ const pageC = `
|
||||
# Page C
|
||||
`;
|
||||
|
||||
// @todo: Add tests for definitions
|
||||
describe('Markdown loader', () => {
|
||||
it('Converts markdown to notes', () => {
|
||||
const graph = new NoteGraph();
|
||||
graph.setNote(createNoteFromMarkdown('page-a', pageA));
|
||||
graph.setNote(createNoteFromMarkdown('page-b', pageB));
|
||||
graph.setNote(createNoteFromMarkdown('page-c', pageC));
|
||||
graph.setNote(createNoteFromMarkdown('page-a', pageA, '\n'));
|
||||
graph.setNote(createNoteFromMarkdown('page-b', pageB, '\n'));
|
||||
graph.setNote(createNoteFromMarkdown('page-c', pageC, '\n'));
|
||||
|
||||
expect(
|
||||
graph
|
||||
@@ -35,9 +36,9 @@ describe('Markdown loader', () => {
|
||||
|
||||
it('Parses wikilinks correctly', () => {
|
||||
const graph = new NoteGraph();
|
||||
graph.setNote(createNoteFromMarkdown('page-a', pageA));
|
||||
graph.setNote(createNoteFromMarkdown('page-b', pageB));
|
||||
graph.setNote(createNoteFromMarkdown('page-c', pageC));
|
||||
graph.setNote(createNoteFromMarkdown('page-a', pageA, '\n'));
|
||||
graph.setNote(createNoteFromMarkdown('page-b', pageB, '\n'));
|
||||
graph.setNote(createNoteFromMarkdown('page-c', pageC, '\n'));
|
||||
|
||||
expect(graph.getBacklinks('page-b').map(link => link.from)).toEqual([
|
||||
'page-a',
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": ["src", "types"],
|
||||
"include": [
|
||||
"src",
|
||||
"types"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
@@ -10,13 +13,14 @@
|
||||
"rootDir": "./src",
|
||||
// for references
|
||||
"baseUrl": "src",
|
||||
"lib": ["esnext"],
|
||||
|
||||
"lib": [
|
||||
"esnext"
|
||||
],
|
||||
"module": "esnext",
|
||||
"importHelpers": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
// "noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
@@ -27,4 +31,4 @@
|
||||
// },
|
||||
// "jsx": "react",
|
||||
},
|
||||
}
|
||||
}
|
||||
2
packages/foam-core/types/utils.d.ts
vendored
2
packages/foam-core/types/utils.d.ts
vendored
@@ -1 +1 @@
|
||||
declare module 'remark-wiki-link';
|
||||
declare module 'remark-wiki-link';
|
||||
@@ -5,16 +5,16 @@
|
||||
"use strict";
|
||||
|
||||
import * as fs from "fs";
|
||||
import { workspace, ExtensionContext } from "vscode";
|
||||
import { workspace, ExtensionContext, window, EndOfLine } from "vscode";
|
||||
|
||||
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) => {
|
||||
@@ -26,7 +26,8 @@ const bootstrap = async (config: FoamConfig) => {
|
||||
.map(f => {
|
||||
return fs.promises.readFile(f.fsPath).then(data => {
|
||||
const markdown = (data || "").toString();
|
||||
foam.notes.setNote(createNoteFromMarkdown(f.fsPath, markdown));
|
||||
const eol = window.activeTextEditor?.document?.eol === EndOfLine.CRLF ? "\r\n" : "\n";
|
||||
foam.notes.setNote(createNoteFromMarkdown(f.fsPath, markdown, eol));
|
||||
});
|
||||
})
|
||||
);
|
||||
@@ -34,8 +35,5 @@ const bootstrap = async (config: FoamConfig) => {
|
||||
};
|
||||
|
||||
const getConfig = () => {
|
||||
return {}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
@@ -13,23 +13,36 @@ 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") {
|
||||
foam.notes.setNote(
|
||||
createNoteFromMarkdown(e.document.fileName, e.document.getText())
|
||||
createNoteFromMarkdown(e.document.fileName, e.document.getText(), docConfig.eol)
|
||||
);
|
||||
e.waitUntil(updateReferenceList(foam.notes));
|
||||
}
|
||||
@@ -42,7 +55,6 @@ const feature: FoamFeature = {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const REFERENCE_HEADER = `[//begin]: # "Autogenerated link references for markdown compatibility"`;
|
||||
const REFERENCE_FOOTER = `[//end]: # "Autogenerated link references"`;
|
||||
|
||||
@@ -54,7 +66,7 @@ async function createReferenceList(foam: NoteGraph) {
|
||||
|
||||
let refs = await generateReferenceList(foam, editor.document);
|
||||
if (refs && refs.length) {
|
||||
await editor.edit(function(editBuilder) {
|
||||
await editor.edit(function (editBuilder) {
|
||||
if (editor) {
|
||||
const spacing = hasEmptyTrailing
|
||||
? docConfig.eol
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user