diff --git a/README.md b/README.md index 4bca833..06c583a 100644 --- a/README.md +++ b/README.md @@ -1,140 +1,132 @@ -# User Plugins for Obsidian +# Obsidian User Plugins -User Plugins is a simple plugin that lets you use Obsidian plugin API in your snippets or javascript files to modify -the behaviour of Obsidian just as if you created a plugin, but without the hassle. +Lets you use the Obsidian plugin API in your snippets or JavaScript files to modify the behavior of Obsidian, just as if you created a plugin, but without the hassle. # Stop and read -> :warning: **This plugin is for advanced users only**: DO NOT use code in scripts you do not fully -> understand. It can delete your notes or worse. See [legal notice](#Notice). +> :warning: **This plugin is for advanced users only**: DO NOT use code in scripts you do not fully understand. +> It can delete your notes or worse. See [legal notice](#Notice). -## Use cases +# Caveats -- [adding custom commands](https://docs.obsidian.md/Reference/TypeScript+API/Command) -- testing an idea for a plugin -- using plugin API to do anything you want +## Obsidian API compatibility -## Motivating example +> :warning: Do not assume user scripts can run any Obsidian API functions other +> than what is shown in the examples. Creating settings is especially not supported, but it is on the [roadmap](#roadmap). -Add command `Create new note in given folder` that allows you to choose -a folder before creating a note. +## Versioning + +Newer versions of this plugin will usually require the newest Obsidian version due to its API changes. Consider this plugin "bleeding edge" for now. + +# Use cases + +- [Adding custom commands](https://docs.obsidian.md/Reference/TypeScript+API/Command) +- Testing an idea for a plugin +- Using Obsidian plugin API to do anything you want + +# Motivating example + +Add command `Create new note in folder` that allows you to choose a folder +before creating a note: ```javascript -module.exports = {} - -module.exports.onload = async (plugin) => { - const MarkdownView = plugin.passedModules.obsidian.MarkdownView - plugin.addCommand({ - id: 'new-note-in-folder', - name: 'Create new note in a folder', - callback: async () => { - const folders = plugin.app.vault.getAllLoadedFiles().filter(i => i.children).map(folder => folder.path); - const folder = await plugin.helpers.suggest(folders, folders); - const created_note = await plugin.app.vault.create(folder + "/Untitled.md", "") - const active_leaf = plugin.app.workspace.activeLeaf; - if (!active_leaf) { - return; - } - await active_leaf.openFile(created_note, { - state: { mode: "source" }, - }); - plugin.app.workspace.trigger("create",created_note) - const view = app.workspace.getActiveViewOfType(MarkdownView); - if (view) { - const editor = view.editor; - editor.focus() - } - } - }); -} +module.exports = { + async onload(plugin) { + plugin.addCommand({ + id: "new-note-in-folder", + name: "Create new note in a folder", + callback: async () => { + const api = plugin.api; + const folders = api.getAllFolders(); + const folder = await api.suggest(folders, folders); + const createdNote = await plugin.app.vault.create(folder + "/Hello World.md", "Hello World!"); + api.openFile(createdNote); + }, + }); + }, + async onunload() { + // optional + console.log("unloading plugin"); + }, +}; + ``` ![Command in palette](https://user-images.githubusercontent.com/8244123/167032593-0dbe59b1-2c2a-4700-83f4-01609cf0d30a.png) -## Quick start - -### Installation +# Quick start -~This plugin is not yet available in the Community Plugins panel.~ +## Installation You can easily add this plugin from Community Plugins panel. Alternatively, here's a manual way: -`git clone` this repo to `/.obsidian/plugins` folder and then execute: +Clone this repository into the `/.obsidian/plugins` folder and then execute: ```bash npm install npm run build ``` -### Usage +## Usage -Scripts can be added either by manually adding snippets or enabling each individual file in -a scripts directory in a vault. Scripts have access to a `Plugin` object. Its API is declared [here](https://github.com/obsidianmd/obsidian-api/blob/master/obsidian.d.ts). -`plugin` has two additional members: +You can add scripts either by manually adding snippets or enabling each individual file in the defined scripts directory in your vault. -- `helpers` +To use scripts, specify a scripts folder in settings, hit the reload button to search for scripts in the specified path, +then enable each script found using a toggle button. - Currently it has a single method that opens a suggester modal: +There are a few types of scripts that you can use. - ```javascript - suggest( - textItems: string[] | ((item: T) => string), - items: T[], - placeholder?: string, - limit?: number - ) - ``` +### Obsidian plugin type -- `passedModules` +Has the basic structure of an Obsidian plugin: - Currently only gives access to the `obsidian` module +```typescript +import { Plugin } from "obsidian"; -#### Snippet +export default class MyPlugin extends Plugin { + async onload() { + // code here + } +} +``` +Written in either [Typescript](./examples/ts-plugin/main.ts) or [Javascript](./examples/js-plugin/plugin.js) flavour. -A snippet is just a javascript block of code that has access to a `plugin` variable. -It is executed in the `onload` method of the plugin. -Example: +You have access to [Helper API](#helper-api) by getting `obsidian-user-plugins` via `this.app.plugins.getPlugin`. -```javascript -plugin.addCommand({ - id: 'foo-bar', - name: 'FooBar', - callback: () => { - console.log('foobar'); - } -}); -``` +### Module type -#### Script file +A JavaScript module that exports at least an `async onload(plugin): void` method and +optionally an `async onnunload(): void` method. It has access to the global function +`require` to get the `obsidian` module. +The `plugin` parameter is an instance of `obsidian-user-plugins` with the [Helper API](#helper-api). -A script file follows CommonJS module specification and exports at least `onload` function that -takes a single argument `plugin` as an input. You must specify `onload` function in the exported -module and you can also specify `onunload` if needed. +See [example](./examples/js-module/module.js). -To use scripts specify scripts folder in settings, hit the reload button to search for scripts in the specified path -and then enable each found script using a toggle button. +### Snippet -Example: -```javascript -module.exports = {} -module.exports.onload = function(plugin) { - plugin.addCommand({ - id: 'foo-bar', - name: 'FooBar', - callback: () => { - console.log('foobar'); - } - }); -} -module.exports.onunload = function(plugin) { - console.log('unload') -} -``` -## Obsidian developer documentation +A snippet is a JavaScript block of code that has access to the global `plugin` +variable. It also has access to the global function `require` to get the `obsidian` +module. Snippets are executed during the plugin initialization phase in `onload()`. +You can also access [Helper API](#helper-api) via the `plugin.api` object. + +See [example](./examples/js-snippet/snippet.js). + +# Helper API + +This plugin exposes an `api` object with handy methods. Check them out [here](./src/helpers/Helpers.ts). + +# Roadmap + +- [ ] Custom configuration per script file +- [ ] Additional functions in the [Helper API](#helper-api) + +# Obsidian Plugin API + +The Obsidian plugin API is declared [here](https://github.com/obsidianmd/obsidian-api/blob/master/obsidian.d.ts). The [Obsidian Developer platform](https://docs.obsidian.md/Reference/TypeScript+API/) contains extensive documentation for the various plugin methods and interfaces, e.g. for the [Command](https://docs.obsidian.md/Reference/TypeScript+API/Command) interface or the [Plugin](https://docs.obsidian.md/Reference/TypeScript+API/Plugin) class. -## Notice +# Notice THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/examples/js-module/README.md b/examples/js-module/README.md new file mode 100644 index 0000000..204bff8 --- /dev/null +++ b/examples/js-module/README.md @@ -0,0 +1,3 @@ +# Sample JS module + +Put it in the directory set in Obsidian User Plugins configuration. Should appear on the list of available scripts. diff --git a/examples/js-module/module.js b/examples/js-module/module.js new file mode 100644 index 0000000..13c47a1 --- /dev/null +++ b/examples/js-module/module.js @@ -0,0 +1,19 @@ +module.exports = { + async onload(plugin) { + plugin.addCommand({ + id: "new-note-in-folder", + name: "Create new note in a folder", + callback: async () => { + const api = plugin.api; + const folders = api.getAllFolders(); + const folder = await api.suggest(folders, folders); + const createdNote = await plugin.app.vault.create(folder + "/Hello World.md", "Hello World!"); + api.openFile(createdNote); + }, + }); + }, + async onunload() { + // optional + console.log("unloading plugin"); + }, +}; diff --git a/examples/js-plugin/README.md b/examples/js-plugin/README.md new file mode 100644 index 0000000..0d8301e --- /dev/null +++ b/examples/js-plugin/README.md @@ -0,0 +1,3 @@ +# Sample JS plugin + +Put it in the directory set in Obsidian User Plugins configuration. Should appear on the list of available scripts. diff --git a/examples/js-plugin/plugin.js b/examples/js-plugin/plugin.js new file mode 100644 index 0000000..03723fc --- /dev/null +++ b/examples/js-plugin/plugin.js @@ -0,0 +1,17 @@ +module.exports = {}; +const Plugin = require('obsidian').Plugin; +module.exports.default = class MyPlugin extends Plugin { + async onload() { + this.addCommand({ + id: "new-note-in-folder", + name: "Create new note in a folder", + callback: async () => { + const api = this.app.plugins.getPlugin("obsidian-user-plugins").api; + const folders = api.getAllFolders(); + const folder = await api.suggest(folders, folders); + const createdNote = await this.app.vault.create(folder + "/Hello World.md", "Hello World!"); + api.openFile(createdNote); + }, + }); + } +} diff --git a/examples/js-snippet/README.md b/examples/js-snippet/README.md new file mode 100644 index 0000000..460f7e5 --- /dev/null +++ b/examples/js-snippet/README.md @@ -0,0 +1,3 @@ +# Sample JS snippet + +Paste it as one of the snippets in Obsidian User Plugins configuration. diff --git a/examples/js-snippet/snippet.js b/examples/js-snippet/snippet.js new file mode 100644 index 0000000..8d8860b --- /dev/null +++ b/examples/js-snippet/snippet.js @@ -0,0 +1,10 @@ +plugin.addCommand({ + id: "new-note-in-folder", + name: "Create new note in a folder", + callback: async () => { + const folders = plugin.api.getAllFolders(); + const folder = await plugin.api.suggest(folders, folders); + const createdNote = await plugin.app.vault.create(folder + "/Hello World.md", "Hello World!"); + plugin.api.openFile(createdNote); + }, +}); diff --git a/examples/ts-plugin/.editorconfig b/examples/ts-plugin/.editorconfig new file mode 100644 index 0000000..d7d168a --- /dev/null +++ b/examples/ts-plugin/.editorconfig @@ -0,0 +1,10 @@ +# top-most EditorConfig file +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = tab +indent_size = 2 +tab_width = 2 diff --git a/examples/ts-plugin/.eslintignore b/examples/ts-plugin/.eslintignore new file mode 100644 index 0000000..e019f3c --- /dev/null +++ b/examples/ts-plugin/.eslintignore @@ -0,0 +1,3 @@ +node_modules/ + +main.js diff --git a/examples/ts-plugin/.eslintrc b/examples/ts-plugin/.eslintrc new file mode 100644 index 0000000..0807290 --- /dev/null +++ b/examples/ts-plugin/.eslintrc @@ -0,0 +1,23 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "env": { "node": true }, + "plugins": [ + "@typescript-eslint" + ], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "parserOptions": { + "sourceType": "module" + }, + "rules": { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], + "@typescript-eslint/ban-ts-comment": "off", + "no-prototype-builtins": "off", + "@typescript-eslint/no-empty-function": "off" + } + } \ No newline at end of file diff --git a/examples/ts-plugin/.gitignore b/examples/ts-plugin/.gitignore new file mode 100644 index 0000000..e09a007 --- /dev/null +++ b/examples/ts-plugin/.gitignore @@ -0,0 +1,22 @@ +# vscode +.vscode + +# Intellij +*.iml +.idea + +# npm +node_modules + +# Don't include the compiled main.js file in the repo. +# They should be uploaded to GitHub releases instead. +main.js + +# Exclude sourcemaps +*.map + +# obsidian +data.json + +# Exclude macOS Finder (System Explorer) View States +.DS_Store diff --git a/examples/ts-plugin/.npmrc b/examples/ts-plugin/.npmrc new file mode 100644 index 0000000..b973752 --- /dev/null +++ b/examples/ts-plugin/.npmrc @@ -0,0 +1 @@ +tag-version-prefix="" \ No newline at end of file diff --git a/examples/ts-plugin/README.md b/examples/ts-plugin/README.md new file mode 100644 index 0000000..b609f77 --- /dev/null +++ b/examples/ts-plugin/README.md @@ -0,0 +1,14 @@ +# Sample Plugin + +This is just cloned [Obsidian sample plugin](https://github.com/obsidianmd/obsidian-sample-plugin) with [main.ts](./main.ts) modified. + +Compile it with + +```bash +npm install +npm run build +``` + +and set the directory in Obsidian User Plugins. +The file `main.js` should appear on the list of available scripts. +Change `esbuild.config.mjs` to modify the name of the output file `main.js`. diff --git a/examples/ts-plugin/esbuild.config.mjs b/examples/ts-plugin/esbuild.config.mjs new file mode 100644 index 0000000..a5de8b8 --- /dev/null +++ b/examples/ts-plugin/esbuild.config.mjs @@ -0,0 +1,49 @@ +import esbuild from "esbuild"; +import process from "process"; +import builtins from "builtin-modules"; + +const banner = +`/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +if you want to view the source, please visit the github repository of this plugin +*/ +`; + +const prod = (process.argv[2] === "production"); + +const context = await esbuild.context({ + banner: { + js: banner, + }, + entryPoints: ["main.ts"], + bundle: true, + external: [ + "obsidian", + "electron", + "@codemirror/autocomplete", + "@codemirror/collab", + "@codemirror/commands", + "@codemirror/language", + "@codemirror/lint", + "@codemirror/search", + "@codemirror/state", + "@codemirror/view", + "@lezer/common", + "@lezer/highlight", + "@lezer/lr", + ...builtins], + format: "cjs", + target: "es2018", + logLevel: "info", + sourcemap: prod ? false : "inline", + treeShaking: true, + outfile: "main.js", + minify: prod, +}); + +if (prod) { + await context.rebuild(); + process.exit(0); +} else { + await context.watch(); +} diff --git a/examples/ts-plugin/main.ts b/examples/ts-plugin/main.ts new file mode 100644 index 0000000..3ad63bf --- /dev/null +++ b/examples/ts-plugin/main.ts @@ -0,0 +1,22 @@ +import { Plugin } from "obsidian"; + +export default class MyPlugin extends Plugin { + async onload() { + this.addCommand({ + id: "new-note-in-folder", + name: "Create new note in a folder", + callback: async () => { + const api = (this.app as any).plugins.getPlugin( + "obsidian-user-plugins" + ).api; + const folders = api.getAllFolders(); + const folder = await api.suggest(folders, folders); + const createdNote = await this.app.vault.create( + folder + "/Hello World.md", + "Hello World!" + ); + api.openFile(createdNote); + }, + }); + } +} diff --git a/examples/ts-plugin/manifest.json b/examples/ts-plugin/manifest.json new file mode 100644 index 0000000..dfa940e --- /dev/null +++ b/examples/ts-plugin/manifest.json @@ -0,0 +1,11 @@ +{ + "id": "sample-plugin", + "name": "Sample Plugin", + "version": "1.0.0", + "minAppVersion": "0.15.0", + "description": "Demonstrates some of the capabilities of the Obsidian API.", + "author": "Obsidian", + "authorUrl": "https://obsidian.md", + "fundingUrl": "https://obsidian.md/pricing", + "isDesktopOnly": false +} diff --git a/examples/ts-plugin/package.json b/examples/ts-plugin/package.json new file mode 100644 index 0000000..9bce297 --- /dev/null +++ b/examples/ts-plugin/package.json @@ -0,0 +1,24 @@ +{ + "name": "obsidian-sample-plugin", + "version": "1.0.0", + "description": "This is a sample plugin for Obsidian (https://obsidian.md)", + "main": "main.js", + "scripts": { + "dev": "node esbuild.config.mjs", + "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", + "version": "node version-bump.mjs && git add manifest.json versions.json" + }, + "keywords": [], + "author": "", + "license": "MIT", + "devDependencies": { + "@types/node": "^16.11.6", + "@typescript-eslint/eslint-plugin": "5.29.0", + "@typescript-eslint/parser": "5.29.0", + "builtin-modules": "3.3.0", + "esbuild": "0.17.3", + "obsidian": "^1.5.7-1", + "tslib": "2.4.0", + "typescript": "4.7.4" + } +} diff --git a/examples/ts-plugin/styles.css b/examples/ts-plugin/styles.css new file mode 100644 index 0000000..71cc60f --- /dev/null +++ b/examples/ts-plugin/styles.css @@ -0,0 +1,8 @@ +/* + +This CSS file will be included with your plugin, and +available in the app when your plugin is enabled. + +If your plugin does not need CSS, delete this file. + +*/ diff --git a/examples/ts-plugin/tsconfig.json b/examples/ts-plugin/tsconfig.json new file mode 100644 index 0000000..c44b729 --- /dev/null +++ b/examples/ts-plugin/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "inlineSourceMap": true, + "inlineSources": true, + "module": "ESNext", + "target": "ES6", + "allowJs": true, + "noImplicitAny": true, + "moduleResolution": "node", + "importHelpers": true, + "isolatedModules": true, + "strictNullChecks": true, + "lib": [ + "DOM", + "ES5", + "ES6", + "ES7" + ] + }, + "include": [ + "**/*.ts" + ] +} diff --git a/manifest.json b/manifest.json index 4a82611..0718a78 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,10 @@ { "id": "obsidian-user-plugins", "name": "User Plugins", - "minAppVersion": "0.12.0", + "minAppVersion": "1.7.2", "description": "Use ts and js modules or js snippets to code your own plugins", "author": "mnowotnik", "authorUrl": "https://mnowotnik.com", "isDesktopOnly": false, - "version": "1.3.0" + "version": "1.4.0" } diff --git a/src/helpers/Helpers.ts b/src/helpers/Helpers.ts index 52b98bd..f042612 100644 --- a/src/helpers/Helpers.ts +++ b/src/helpers/Helpers.ts @@ -1,4 +1,12 @@ -import { App, Notice } from "obsidian"; +import { + App, + Notice, + PaneType, + SplitDirection, + TFile, + TFolder, + WorkspaceLeaf, +} from "obsidian"; import { suggest } from "./Suggester"; export class Helpers { @@ -16,4 +24,43 @@ export class Helpers { notify(message: string, time: number = 5000) { new Notice(message, time); } + + getAllFolders() { + return this.app.vault + .getAllLoadedFiles() + .filter((f) => f instanceof TFolder) + .map((folder) => folder.path); + } + + async openFile( + file: TFile, + params: { + paneType?: PaneType; + openInNewTab?: boolean; + direction?: SplitDirection; + mode?: "source" | "preview" | "default"; + focus?: boolean; + } = {} + ) { + let leaf: WorkspaceLeaf; + + if (params.paneType === "split") { + leaf = this.app.workspace.getLeaf( + params.paneType, + params.direction + ); + } else { + leaf = this.app.workspace.getLeaf(params.paneType); + } + + if (params.mode) { + await leaf.openFile(file, { state: { mode: params.mode } }); + } else { + await leaf.openFile(file); + } + + if (params.focus) { + this.app.workspace.setActiveLeaf(leaf, { focus: true }); + } + } } diff --git a/src/loaders/cjsModuleLoader.ts b/src/loaders/cjsModuleLoader.ts index 5e22bd9..88896a1 100644 --- a/src/loaders/cjsModuleLoader.ts +++ b/src/loaders/cjsModuleLoader.ts @@ -44,7 +44,7 @@ export default class CjsModuleLoader implements Loader { async runSnippet(snippet: string, idx: number) { try { - Function("plugin", snippet)(this); + Function("plugin", "require", snippet)(this.plugin, require); } catch (e) { Error.captureStackTrace(e); new Notice( @@ -123,8 +123,7 @@ export default class CjsModuleLoader implements Loader { }, ]); } - } - if (userModule.exports.default) { + } else if (userModule.exports.default) { const userPlugin = new userModule.exports.default( this.app, this.plugin.manifest diff --git a/src/main.ts b/src/main.ts index e29a34f..8414fe1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,18 +5,28 @@ import { SettingsManager, SettingTab } from "./settings/Settings"; import { Helpers } from "./helpers/Helpers"; export default class UserPlugins extends Plugin { - private commonJsModuleLoader: CjsModuleLoader; - private settingsManager: SettingsManager; + api: Helpers; + /** + * @deprecated + */ passedModules: Record; + /** + * @deprecated + */ helpers: Helpers; - require: (s: string) => Promise; + private commonJsModuleLoader: CjsModuleLoader; + private settingsManager: SettingsManager; async onload() { this.settingsManager = new SettingsManager(this, await this.loadData()); + this.commonJsModuleLoader = new CjsModuleLoader( + this, + this.settingsManager.settings + ); - this.require = (s: string) => require(s); this.passedModules = { obsidian }; this.helpers = new Helpers(this.app); + this.api = new Helpers(this.app); this.addSettingTab( new SettingTab({ settingsManager: this.settingsManager, @@ -29,10 +39,6 @@ export default class UserPlugins extends Plugin { // wait for vault files to load // FIXME maybe there's a better hook this.app.workspace.onLayoutReady(async () => { - this.commonJsModuleLoader = new CjsModuleLoader( - this, - this.settingsManager.settings - ); try { await this.commonJsModuleLoader.onload(); } catch (e) { diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 5e8b5aa..16d45eb 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -78,6 +78,9 @@ export class SettingTab extends PluginSettingTab { this.app, this.settingsManager.settings.scriptsFolder ); + files = files.filter( + (f) => f.path.contains("node_modules") == false + ); } catch (e) { console.error(`Failed to read user scripts folder: ${e.message}`); new Notice("Failed to read user scripts folder"); diff --git a/versions.json b/versions.json index 93eef73..2e1c531 100644 --- a/versions.json +++ b/versions.json @@ -6,5 +6,6 @@ "1.1.0": "0.12.0", "1.1.1": "0.12.0", "1.2.0": "0.12.0", - "1.3.0": "0.12.0" + "1.3.0": "0.12.0", + "1.4.0": "1.7.2" }