Compare commits

...

59 Commits

Author SHA1 Message Date
chirag-singhal
3d1ec88717 removed yarn deploy in publish 2020-07-16 22:10:50 +05:30
chirag-singhal
6b1407303e update foam publish command 2020-07-16 20:27:11 +05:30
Ankit Tiwari
528213a0f8 Merge remote-tracking branch 'origin/cli/basic-publish' into cli/demo 2020-07-16 17:38:35 +05:30
Ankit Tiwari
ff89948a70 Merge remote-tracking branch 'origin/cli/fake-init' into cli/demo 2020-07-16 17:38:26 +05:30
Ankit Tiwari
734982996a Merge branch 'cli/lint' into cli/demo 2020-07-16 17:37:25 +05:30
Jani Eväkallio
265ab19e31 Prototype basic publish command (just pushes all changes to git) 2020-07-16 11:57:47 +01:00
Jani Eväkallio
80a799dff9 Prototype the foam init command 2020-07-16 11:11:27 +01:00
Ankit Tiwari
3ef95628f5 Remove posttest command 2020-07-16 13:33:18 +05:30
Ankit Tiwari
626f64aec0 Propagate the error from writeFileToDisk method 2020-07-16 13:32:30 +05:30
Ankit Tiwari
e36c285764 Refactor renameFile to use path module 2020-07-16 13:30:26 +05:30
Ankit Tiwari
20ca92f451 Make fs tests no blocking by using promises 2020-07-16 12:12:50 +05:30
Ankit Tiwari
4557150378 Use Promise.resolve(null) 2020-07-16 11:50:41 +05:30
Ankit Tiwari
43ac90c3c7 Merge branch 'cli/apply-text-edit' into cli/lint 2020-07-16 11:15:04 +05:30
Ankit Tiwari
deb382af2d Mock fs for tests 2020-07-16 11:08:29 +05:30
Ankit Tiwari
34c775f185 Merge branch 'master' of github.com:foambubble/foam into cli/apply-text-edit 2020-07-16 10:34:59 +05:30
Ankit Tiwari
22876cd5f0 Refactor (PR changes)
1.  Renamed initializeNoteGraph.ts to initialize-note-graph.ts to be consistent with naming

2.  Refactored code in janitor and migrate commands
2020-07-16 10:31:55 +05:30
chirag-singhal
9c1350f64b removed excess white space 2020-07-15 22:26:00 +05:30
chirag-singhal
808a453641 removed extra test files 2020-07-15 22:23:59 +05:30
chirag-singhal
63521c91e7 minor fixes 2020-07-15 22:21:41 +05:30
chirag-singhal
c652385a97 Merge branch 'cli/apply-text-edit' of https://github.com/foambubble/foam into cli/apply-text-edit 2020-07-15 22:18:36 +05:30
chirag-singhal
ee7fa79e63 updated foam janitor command 2020-07-15 22:18:17 +05:30
chirag-singhal
ea0a659119 added foam migrate command 2020-07-15 22:12:39 +05:30
Ankit Tiwari
b0e587ddb8 Merge branch 'cli/apply-text-edit' of github.com:foambubble/foam into cli/apply-text-edit 2020-07-15 22:05:07 +05:30
Ankit Tiwari
04d86bc45a Add foam-core as dependency 2020-07-15 22:05:02 +05:30
chirag-singhal
76b421e800 used github slagger instead of dashify 2020-07-15 21:51:04 +05:30
Ankit Tiwari
d8c42bd8c4 Replace dashify by github-slugger 2020-07-15 21:43:32 +05:30
chirag-singhal
093275ac0c fixed core tests 2020-07-15 21:34:14 +05:30
chirag-singhal
c2e998927d Solves issue with roam migration 2020-07-15 21:30:20 +05:30
chirag-singhal
0ace092c34 added tests files for migration 2020-07-15 21:15:30 +05:30
Ankit Tiwari
7f4f90704d Implement basic lint command 2020-07-15 21:07:07 +05:30
chirag-singhal
cb6ad7809f ignore link refrences with no file 2020-07-15 16:55:07 +05:30
chirag-singhal
896e894aff added glob as dependency 2020-07-15 11:42:16 +05:30
chirag-singhal
c0f84f66f1 minor fixes 2020-07-15 11:41:04 +05:30
chirag-singhal
d7de46274d check if given path is valid directory 2020-07-15 11:32:31 +05:30
Ankit Tiwari
c9bba037b0 Use note.eol to append line endings 2020-07-15 00:29:58 +05:30
Ankit Tiwari
942d9b480f Store endOfLine inside Note 2020-07-15 00:20:32 +05:30
Ankit Tiwari
a5f8050d9c Add ora spinner 2020-07-14 22:18:48 +05:30
Ankit Tiwari
c5bd48d86e PR changes 2020-07-14 21:53:04 +05:30
chirag-singhal
9e07b0f19a merge branch janitor/link-reference-definitions 2020-07-14 21:14:23 +05:30
chirag-singhal
bb8d0dabba added tests for generateHeading in janitor 2020-07-14 20:44:54 +05:30
chirag-singhal
b113cafeba added generate Heading function to janitor 2020-07-14 20:40:01 +05:30
Ankit Tiwari
7c041e0fc8 kebab case file names while running janitor 2020-07-14 20:29:39 +05:30
Ankit Tiwari
f6c3ecf369 Implement basic foam-cli janitor command 2020-07-14 19:58:17 +05:30
Ankit Tiwari
9e452aa9c9 Move noteGraph scaffolding to utils 2020-07-14 19:06:25 +05:30
Ankit Tiwari
52f1dc45a6 Implement generateHeading janitor method
Co-authored-by: CHIRAG SINGHAL <csinghal208@gmail.com>
2020-07-14 18:50:43 +05:30
Ankit Tiwari
92e4510c2d Merge branch 'janitor/link-reference-definitions' into cli/apply-text-edit 2020-07-14 18:33:37 +05:30
Ankit Tiwari
78586be4a3 Add partial tests for writeFileToDisk method 2020-07-14 17:31:22 +05:30
chirag-singhal
79a5621f31 Add no change in link definitions test to generateLinkReferences janitor method 2020-07-14 17:18:17 +05:30
Ankit Tiwari
9367e8e495 Implement first version of applyText method 2020-07-14 17:03:56 +05:30
Ankit Tiwari
b611b1bb07 export TextEdit interface 2020-07-14 17:03:12 +05:30
chirag-singhal
b987ae7a3f Add update link definitions test to generateLinkReferences janitor method 2020-07-14 16:36:47 +05:30
chirag-singhal
6fa858f8d4 Add remove link definitions test to generateLinkReferences janitor method 2020-07-14 16:17:56 +05:30
Ankit Tiwari
e57db48f0e Setup Jest 2020-07-14 16:05:49 +05:30
Jani Eväkallio
3e20dc3356 Add partial tests for generateLinkReferenceDefinitions 2020-07-14 10:06:57 +01:00
Jani Eväkallio
d65f724b56 Implement first version of generateLinkReferenceDefinitions janitor method 2020-07-14 10:06:23 +01:00
Jani Eväkallio
6bd9aaa949 Export stringifyMarkdownLinkReferenceDefinition from foam-core 2020-07-14 10:05:47 +01:00
Jani Eväkallio
d905972f61 Add Note.end and Note.definitions to foam-core tests 2020-07-14 10:04:38 +01:00
Jani Eväkallio
3511ce30e3 Use stringifyMarkdownLinkReferenceDefinition in foam-vscode
This commit also applies prettier to previously badly formatted files, so the diff is larger than necessary
2020-07-14 10:03:32 +01:00
Jani Eväkallio
215dea151f Add Note.definitions and Note.end 2020-07-14 10:02:49 +01:00
38 changed files with 2699 additions and 297 deletions

View File

@@ -0,0 +1,6 @@
module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
'@babel/preset-typescript',
],
};

View 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,
};

View File

@@ -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"
}
}

View 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}`);
}
}
}

View 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!');
}
}
}

View 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!');
}
}
}

View 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!');
}
}
}

View 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);
}
}

View 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('');
}

View File

@@ -0,0 +1,4 @@
import * as fs from 'fs';
export const isValidDirectory = (path: string) => fs.existsSync(path) && fs.lstatSync(path).isDirectory();

View 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);
}

View File

@@ -0,0 +1,5 @@
import * as fs from 'fs';
export const writeFileToDisk = async (fileUri: string, data: string) => {
return fs.promises.writeFile(fileUri, data);
}

View 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)
});
})

View 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);
});
});

View 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);
});
})

View File

@@ -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"
},

View File

@@ -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,
})
});

View 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;
}

View 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;
}

View File

@@ -4,7 +4,7 @@ import wikiLinkPlugin from 'remark-wiki-link';
import visit, { CONTINUE, EXIT } from 'unist-util-visit';
import { Node, Parent } from 'unist';
import * as path from 'path';
import { Note, NoteLink, NoteGraph } from './note-graph';
import { Note, NoteLink, NoteLinkDefinition, NoteGraph } from './note-graph';
import { dropExtension } from './utils';
let processor: unified.Processor | null = null;
@@ -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[];
}

View File

@@ -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),
});

View File

@@ -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, ' '));
}

View File

@@ -0,0 +1,3 @@
# Roam Document
[[Second Roam Document]]

View File

@@ -0,0 +1 @@
# Second Roam Document

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
# Third Document
All the link references are correct in this file.
[[first-document]]
[[second-document]]
[first-document]: first-document "First Document"
[second-document]: second-document "Second Document"

View File

@@ -1,16 +1,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',
''
)

View 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);
})
});

View 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);
});
});

View File

@@ -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',

View File

@@ -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",
},
}
}

View File

@@ -1 +1 @@
declare module 'remark-wiki-link';
declare module 'remark-wiki-link';

View File

@@ -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 {};
};

View File

@@ -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(

1543
yarn.lock

File diff suppressed because it is too large Load Diff