├── .gitignore ├── LICENSE ├── README.md ├── manifest.json ├── package.json ├── rollup.config.js ├── src ├── URIModal.ts ├── iconPicker.ts ├── icon_list.ts ├── main.ts └── settings.ts ├── styles.css ├── tsconfig.json └── versions.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij 2 | *.iml 3 | .idea 4 | 5 | # npm 6 | node_modules 7 | package-lock.json 8 | 9 | # build 10 | main.js 11 | *.js.map 12 | 13 | # obsidian 14 | data.json 15 | 16 | #misc 17 | .vscode 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 kzhovn 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | This plugin allows you to add custom URI commands to the command palette. Can be used with the [Obsidian URI scheme](https://help.obsidian.md/Advanced+topics/Using+obsidian+URI), as well as any other URI scheme your computer supports. 3 | 4 | ### Placeholders 5 | You can use the placeholders below in your URI. All of these are [URL-encoded](https://en.wikipedia.org/wiki/Percent-encoding) for you unless you turn off URL-encoding, so you don't need to worry about your text having any unescaped illegal or reserved characters. 6 | 7 | All commands with placeholders are hidden when there is no active file. 8 | 9 | | Placeholder | Description | | 10 | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | 11 | | {{fileName}} | Just the base name of the file, without the filepath or file extension. | | 12 | | {{filePath}} | Path, relative to the vault, to the current file. E.g. `FolderName/filename.md` | | 13 | | {{fileText}} | Entire contents of the file, including frontmatter. Available only in markdown files. | | 14 | | {{selection}} | Your current selection. If nothing is selected, placeholder is replaced by the empty string. | | 15 | | {{line}} | Current line. | | 16 | | {{vaultName}} | Name of the current vault. | | 17 | | {{meta:FIELD_NAME}} | The value of the metadata field corresponding to FIELD_NAME. Note that if there are multiple values in one field (as a comma-separated list or [array]), the values in the field will be inserted in the URI as a comma-separated list. Requires MetaEdit. | | 18 | 19 | ## Examples 20 | ### Obsidian 21 | - Open the vault `work vault`: `obsidian://open?vault=work%20vault` 22 | - Open the note `hotkey reference` in the vault `my vault`: `obsidian://open?vault=my%20vault&file=hotkey%20reference` 23 | - Append your selection to today's daily note (requires Advanced URI plugin): `obsidian://advanced-uri?vault=&daily=true&data={{selection}}&mode=append` 24 | - Open this plugin's settings page (requires Hotkey Helper plugin): `obsidian://goto-plugin?id=uri-commands&show=config` 25 | 26 | ### Other programs 27 | - Open an email draft of your current note in your mail client: `mailto:friend@example.com?subject={{fileName}}&body={{fileText}}` 28 | - [Email your current note to Roam](http://www.sendtoroam.com/): `mailto:me@sendtoroam.com?subject={{fileName}}&body={{fileText}}` 29 | - Open a spotify album: `spotify:album:4niKC11eq7dRXiDVWsTdEy` 30 | - Open a new [HackMD](https://hackmd.io/) collaborative markdown pad: `https://hackmd.io/new` 31 | - Note that for websites, you *must* start your URI with `https://` or `http://`, not `www.` 32 | - Open the wikipedia page for the contents of the YAML field "topic": `https://en.wikipedia.org/wiki/{{meta:topic}}` 33 | - Look up your selection in your Calibre library: `calibre://search/_?q={{selection}}` 34 | - Open the url in the "external-link" metadata field: `{{meta:external-link}}` 35 | - Note that for this to work, URL encoding must be turned off 36 | 37 | ## Related plugins 38 | - [Advanced URI](https://github.com/Vinzent03/obsidian-advanced-uri): enables URIs for daily note, appending text to a file, jump to heading, search and replace, and more 39 | - [Hotkey Helper](https://github.com/pjeby/hotkey-helper): enables Obsidian URIs for plugin READMEs, settings, and hotkey configurations 40 | 41 | ## Help 42 | For more information on URIs in Obsidian, see the [Obsidian documentation](https://help.obsidian.md/Advanced+topics/Using+obsidian+URI). An incomplete list of other URI schemes can be found [here](https://en.wikipedia.org/wiki/List_of_URI_schemes). 43 | 44 | ## Thanks 45 | Parts of this code, especially the icon picker, borrow heavily from [phibr0](https://github.com/phibr0)'s plugins, including [Obsidian Macros](https://github.com/phibr0/obsidian-macros) and [Customizable Sidebar](https://github.com/phibr0/obsidian-customizable-sidebar). -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "uri-commands", 3 | "name": "URI Commands", 4 | "version": "1.0.1", 5 | "minAppVersion": "0.9.12", 6 | "description": "Execute URIs from the Obsidian command palette.", 7 | "author": "kzhovn", 8 | "authorUrl": "https://github.com/kzhovn", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uri-commands", 3 | "version": "1.0.1", 4 | "description": "Execute URIs from the Obsidian command palette.", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "rollup --config rollup.config.js -w", 8 | "build": "rollup --config rollup.config.js --environment BUILD:production" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "@rollup/plugin-commonjs": "^18.0.0", 15 | "@rollup/plugin-node-resolve": "^11.2.1", 16 | "@rollup/plugin-typescript": "^8.3.2", 17 | "@types/node": "^14.18.16", 18 | "obsidian": "^0.12.17", 19 | "rollup": "^2.74.1", 20 | "tslib": "^2.4.0", 21 | "typescript": "^4.6.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | 5 | const isProd = (process.env.BUILD === 'production'); 6 | 7 | const banner = 8 | `/* 9 | THIS IS A GENERATED/BUNDLED FILE BY ROLLUP 10 | if you want to view the source visit the plugins github repository 11 | */ 12 | `; 13 | 14 | export default { 15 | input: 'src/main.ts', 16 | output: { 17 | dir: '.', 18 | sourcemap: 'inline', 19 | sourcemapExcludeSources: isProd, 20 | format: 'cjs', 21 | exports: 'default', 22 | banner, 23 | }, 24 | external: ['obsidian'], 25 | plugins: [ 26 | typescript(), 27 | nodeResolve({browser: true}), 28 | commonjs(), 29 | ] 30 | }; -------------------------------------------------------------------------------- /src/URIModal.ts: -------------------------------------------------------------------------------- 1 | import { Modal, Setting, moment } from "obsidian"; 2 | import { IconPicker } from "./iconPicker"; 3 | import URIPlugin from "./main"; 4 | import { URISettingTab, URICommand } from "./settings"; 5 | 6 | 7 | export default class URIModal extends Modal { 8 | settingTab: URISettingTab; 9 | plugin: URIPlugin; 10 | uriCommand: URICommand; 11 | editMode: boolean; 12 | 13 | constructor(plugin: URIPlugin, settingTab: URISettingTab, command: URICommand = null, editMode = false) { 14 | super(plugin.app); 15 | this.settingTab = settingTab; 16 | this.plugin = plugin; 17 | this.editMode = editMode; 18 | 19 | if (command === null) { 20 | this.uriCommand = { 21 | name: "", 22 | id: "", 23 | URITemplate: "", 24 | encode: true, 25 | } 26 | } else { 27 | this.uriCommand = command; 28 | } 29 | } 30 | 31 | onOpen() { 32 | this.display(); 33 | } 34 | 35 | display() { 36 | let {contentEl} = this; 37 | contentEl.empty(); 38 | 39 | new Setting(contentEl) 40 | .setName("Command name") 41 | .addText((textEl) => { 42 | textEl.setValue(this.uriCommand.name) 43 | .onChange((value) => { 44 | this.uriCommand.name = value; 45 | }); 46 | }); 47 | 48 | new Setting(contentEl) 49 | .setName("URI") 50 | .setDesc("Accepts {{fileName}}, {{fileText}}, {{selection}}, {{line}}, {{filePath}}, {{vaultName}} and {{meta:FIELD_NAME}} placeholders.") 51 | .addText((textEl) => { 52 | textEl.setValue(this.uriCommand.URITemplate) 53 | .onChange((value) => { 54 | this.uriCommand.URITemplate = value; 55 | }); 56 | }); 57 | 58 | //heavily borrowing https://github.com/phibr0/obsidian-macros/blob/master/src/ui/macroModal.ts#L66 59 | new Setting(contentEl) 60 | .setName("Add icon") 61 | .setDesc("Optional") 62 | .addButton(button => { 63 | if (this.uriCommand.icon) { //button looks like the existing icon 64 | button.setIcon(this.uriCommand.icon); 65 | } else { //or if no existing icon 66 | button.setButtonText("Pick icon"); 67 | } 68 | 69 | button.onClick(() => { 70 | new IconPicker(this.plugin, this.uriCommand, this).open() 71 | }); 72 | }); 73 | 74 | new Setting(contentEl) 75 | .setName("URL-encode input") 76 | .setDesc("Automatically URL-encode any user input text. Should only be off if content is already encoded or itself a URI scheme (e.g.. a bare URL with https://).") 77 | .addToggle(toggle => { 78 | toggle.setValue(this.uriCommand.encode) 79 | .onChange(value => { 80 | this.uriCommand.encode = value; 81 | }) 82 | }) 83 | 84 | 85 | //https://github.com/phibr0/obsidian-macros/blob/master/src/ui/macroModal.ts#L132 86 | const buttonDiv = contentEl.createDiv({ cls: "URI-flex-center" }); 87 | const saveButton = createEl("button", {text: "Save command"}); 88 | buttonDiv.appendChild(saveButton); 89 | 90 | saveButton.onClickEvent(async () => { 91 | if (this.editMode === false) { //creating a new command 92 | //replace spaces with - and add unix millisec timestamp (to ensure uniqueness) 93 | this.uriCommand.id = this.uriCommand.name.trim().replace(" ", "-").toLowerCase() + moment().valueOf(); 94 | this.plugin.settings.URICommands.push(this.uriCommand); 95 | this.plugin.addURICommand(this.uriCommand); 96 | } else { //remove and readd command, works around forcing the user to reload the entire app 97 | (this.app as any).commands.removeCommand(`${this.plugin.manifest.id}:${this.uriCommand.id}`); 98 | this.plugin.addURICommand(this.uriCommand); 99 | } 100 | 101 | await this.plugin.saveSettings(); 102 | this.settingTab.display(); //refresh settings tab 103 | this.close(); 104 | }); 105 | } 106 | 107 | onClose() { 108 | let {contentEl} = this; 109 | contentEl.empty(); 110 | } 111 | } -------------------------------------------------------------------------------- /src/iconPicker.ts: -------------------------------------------------------------------------------- 1 | //From https://github.com/phibr0/obsidian-macros/blob/a56fb9a7259564a9345e0d1ed0af4331f4dba104/src/ui/iconPicker.ts#L4 2 | 3 | import { FuzzyMatch, FuzzySuggestModal, setIcon } from "obsidian"; 4 | import URIPlugin from "src/main"; 5 | import { ICON_LIST } from "./icon_list"; 6 | import { URICommand } from "./settings"; 7 | import URIModal from "./URIModal"; 8 | 9 | 10 | export class IconPicker extends FuzzySuggestModal{ 11 | plugin: URIPlugin; 12 | command: URICommand; 13 | modal: URIModal; 14 | 15 | constructor(plugin: URIPlugin, command: URICommand, modal: URIModal) { 16 | super(plugin.app); 17 | this.plugin = plugin; 18 | this.command = command; 19 | this.modal = modal; 20 | this.setPlaceholder("Pick an icon"); 21 | } 22 | 23 | private cap(string: string): string { 24 | const words = string.split(" "); 25 | 26 | return words.map((word) => { 27 | return word[0].toUpperCase() + word.substring(1); 28 | }).join(" "); 29 | } 30 | 31 | getItems(): string[] { 32 | return ICON_LIST; 33 | } 34 | 35 | getItemText(item: string): string { 36 | return this.cap(item.replace(/-/ig, " ")); 37 | } 38 | 39 | renderSuggestion(item: FuzzyMatch, el: HTMLElement): void { 40 | el.addClass("URI-icon-container"); 41 | const div = createDiv({ cls: "URI-icon" }); 42 | el.appendChild(div); 43 | setIcon(div, item.item); 44 | super.renderSuggestion(item, el); 45 | } 46 | 47 | onChooseItem(item: string): void { 48 | this.command.icon = item; 49 | this.modal.display(); 50 | this.close(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/icon_list.ts: -------------------------------------------------------------------------------- 1 | export const ICON_LIST = ['activity', 'airplay', 'alarm-check', 'alarm-clock-off', 'alarm-clock', 'alarm-minus', 'alarm-plus', 'album', 'alert-circle', 'alert-octagon', 'alert-triangle', 'align-left', 'align-right', 'align-center', 'align-justify', 'anchor', 'aperture', 'archive', 'arrow-big-down', 'arrow-big-left', 'arrow-big-right', 'arrow-big-up', 'arrow-down-circle', 'arrow-down-left', 'arrow-down-right', 'arrow-down', 'arrow-left-circle', 'arrow-left-right', 'arrow-left', 'arrow-right-circle', 'arrow-right', 'arrow-up-circle', 'arrow-up-left', 'arrow-up-right', 'arrow-up', 'asterisk', 'at-sign', 'award', 'axe', 'banknote', 'bar-chart-2', 'bar-chart', 'battery-charging', 'battery-full', 'battery-low', 'battery-medium', 'battery', 'beaker', 'bell-minus', 'bell-off', 'bell-plus', 'bell-ring', 'bell', 'bike', 'binary', 'bitcoin', 'bluetooth-connected', 'bluetooth-off', 'bluetooth-searching', 'bluetooth', 'bold', 'book-open', 'book', 'bookmark-minus', 'bookmark-plus', 'bookmark', 'bot', 'box-select', 'box', 'briefcase', 'brush', 'bug', 'building', 'bus', 'calculator', 'calendar', 'camera-off', 'camera', 'car', 'carrot', 'cast', 'check-circle-2', 'check-circle', 'check-square', 'check', 'chevron-down', 'chevron-first', 'chevron-last', 'chevron-left', 'chevron-right', 'chevron-up', 'chevrons-down-up', 'chevrons-down', 'chevrons-left', 'chevrons-right', 'chevrons-up-down', 'chevrons-up', 'chrome', 'circle-slashed', 'circle', 'clipboard-check', 'clipboard-copy', 'clipboard-list', 'clipboard-x', 'clipboard', 'clock-1', 'clock-10', 'clock-11', 'clock-12', 'clock-2', 'clock-3', 'clock-4', 'clock-5', 'clock-6', 'clock-7', 'clock-8', 'clock-9', 'clock', 'cloud-drizzle', 'cloud-fog', 'cloud-hail', 'cloud-lightning', 'cloud-moon', 'cloud-off', 'cloud-rain-wind', 'cloud-rain', 'cloud-snow', 'cloud-sun', 'cloud', 'cloudy', 'clover', 'code-2', 'code', 'codepen', 'codesandbox', 'coffee', 'coins', 'columns', 'command', 'compass', 'contact', 'contrast', 'cookie', 'copy', 'copyleft', 'copyright', 'corner-down-left', 'corner-down-right', 'corner-left-down', 'corner-left-up', 'corner-right-down', 'corner-right-up', 'corner-up-left', 'corner-up-right', 'cpu', 'credit-card', 'crop', 'cross', 'crosshair', 'crown', 'currency', 'database', 'delete', 'dice', 'disc', 'divide-circle', 'divide-square', 'divide', 'dollar-sign', 'download-cloud', 'download', 'dribbble', 'droplet', 'droplets', 'drumstick', 'edit-2', 'edit-3', 'edit', 'egg', 'equal-not', 'equal', 'eraser', 'euro', 'expand', 'external-link', 'eye-off', 'eye', 'facebook', 'fast-forward', 'feather', 'figma', 'file-check-2', 'file-check', 'file-code', 'file-digit', 'file-input', 'file-minus-2', 'file-minus', 'file-output', 'file-plus-2', 'file-plus', 'file-search', 'file-text', 'file-x-2', 'file-x', 'file', 'files', 'film', 'filter', 'flag-triangle-left', 'flag-triangle-right', 'flag', 'flame', 'flashlight-off', 'flashlight', 'flask-conical', 'flask-round', 'folder-minus', 'folder-open', 'folder-plus', 'folder', 'form-input', 'forward', 'framer', 'frown', 'function-square', 'gamepad-2', 'gamepad', 'gauge', 'gavel', 'gem', 'ghost', 'gift', 'git-branch-plus', 'git-branch', 'git-commit', 'git-merge', 'git-pull-request', 'github', 'gitlab', 'glasses', 'globe-2', 'globe', 'grab', 'graduation-cap', 'grid', 'grip-horizontal', 'grip-vertical', 'hammer', 'hand-metal', 'hand', 'hard-drive', 'hard-hat', 'hash', 'haze', 'headphones', 'heart', 'help-circle', 'hexagon', 'highlighter', 'history', 'home', 'image-minus', 'image-off', 'image-plus', 'image', 'import', 'inbox', 'indent', 'indian-rupee', 'infinity', 'info', 'inspect', 'instagram', 'italic', 'japanese-yen', 'key', 'landmark', 'languages', 'laptop-2', 'laptop', 'lasso-select', 'lasso', 'layers', 'layout-dashboard', 'layout-grid', 'layout-list', 'layout-template', 'layout', 'library', 'life-buoy', 'lightbulb-off', 'lightbulb', 'link-2-off', 'link-2', 'link', 'linkedin', 'list-checks', 'list-minus', 'list-ordered', 'list-plus', 'list-x', 'list', 'loader-2', 'loader', 'locate-fixed', 'locate', 'lock', 'log-in', 'log-out', 'mail', 'map-pin', 'map', 'maximize-2', 'maximize', 'megaphone', 'meh', 'message-circle', 'message-square', 'mic-off', 'mic', 'minimize-2', 'minimize', 'minus-circle', 'minus-square', 'minus', 'monitor-off', 'monitor-speaker', 'monitor', 'moon', 'more-horizontal', 'more-vertical', 'mountain-snow', 'mountain', 'mouse-pointer-2', 'mouse-pointer-click', 'mouse-pointer', 'move-diagonal-2', 'move-diagonal', 'move-horizontal', 'move-vertical', 'move', 'music', 'navigation-2', 'navigation', 'network', 'octagon', 'option', 'outdent', 'package-check', 'package-minus', 'package-plus', 'package-search', 'package-x', 'package', 'palette', 'paperclip', 'pause-circle', 'pause-octagon', 'pause', 'pen-tool', 'pencil', 'percent', 'person-standing', 'phone-call', 'phone-forwarded', 'phone-incoming', 'phone-missed', 'phone-off', 'phone-outgoing', 'phone', 'pie-chart', 'piggy-bank', 'pin', 'pipette', 'plane', 'play-circle', 'play', 'plug-zap', 'plus-circle', 'plus-square', 'plus', 'pocket', 'podcast', 'pointer', 'pound-sterling', 'power-off', 'power', 'printer', 'qr-code', 'quote', 'radio-receiver', 'radio', 'redo', 'refresh-ccw', 'refresh-cw', 'regex', 'repeat-1', 'repeat', 'reply-all', 'reply', 'rewind', 'rocking-chair', 'rotate-ccw', 'rotate-cw', 'rss', 'ruler', 'russian-ruble', 'save', 'scale', 'scan-line', 'scan', 'scissors', 'screen-share-off', 'screen-share', 'search', 'send', 'separator-horizontal', 'separator-vertical', 'server-crash', 'server-off', 'server', 'settings-2', 'settings', 'share-2', 'share', 'sheet', 'shield-alert', 'shield-check', 'shield-close', 'shield-off', 'shield', 'shirt', 'shopping-bag', 'shopping-cart', 'shovel', 'shrink', 'shuffle', 'sidebar-close', 'sidebar-open', 'sidebar', 'sigma', 'signal-high', 'signal-low', 'signal-medium', 'signal-zero', 'signal', 'skip-back', 'skip-forward', 'skull', 'slack', 'slash', 'sliders', 'smartphone-charging', 'smartphone', 'smile', 'snowflake', 'sort-asc', 'sort-desc', 'speaker', 'sprout', 'square', 'star-half', 'star', 'stop-circle', 'strikethrough', 'subscript', 'sun', 'sunrise', 'sunset', 'superscript', 'swiss-franc', 'switch-camera', 'table', 'tablet', 'target', 'tent', 'terminal-square', 'terminal', 'text-cursor-input', 'text-cursor', 'thermometer-snowflake', 'thermometer-sun', 'thermometer', 'thumbs-down', 'thumbs-up', 'ticket', 'timer-off', 'timer-reset', 'timer', 'toggle-left', 'toggle-right', 'tornado', 'trash-2', 'trash', 'trello', 'trending-down', 'trending-up', 'triangle', 'truck', 'tv-2', 'tv', 'twitch', 'twitter', 'type', 'umbrella', 'underline', 'undo', 'unlink-2', 'unlink', 'unlock', 'upload-cloud', 'upload', 'user-check', 'user-minus', 'user-plus', 'user-x', 'user', 'users', 'verified', 'vibrate', 'video-off', 'video', 'view', 'voicemail', 'volume-1', 'volume-2', 'volume-x', 'volume', 'wallet', 'wand', 'watch', 'webcam', 'wifi-off', 'wifi', 'wind', 'wrap-text', 'wrench', 'x-circle', 'x-octagon', 'x-square', 'x', 'youtube', 'zap-off', 'zap', 'zoom-in', 'zoom-out', 'lucide-clock', 'lucide-cloud', 'lucide-cross', 'lucide-folder', 'lucide-info', 'lucide-languages', 'lucide-link', 'lucide-pencil', 'lucide-pin', 'lucide-search', 'lucide-star', 'lucide-trash'] 2 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Editor, Plugin, Notice, TFile, MarkdownView } from 'obsidian'; 2 | import { URISettingTab, URIPluginSettings, DEFAULT_SETTINGS, URICommand } from './settings'; 3 | 4 | const SELECTION_TEMPLATE = "{{selection}}"; 5 | const FILE_TEXT_TEMPLATE = "{{fileText}}"; 6 | const FILE_NAME_TEMPLATE = "{{fileName}}"; 7 | const LINE_TEMPLATE = "{{line}}"; 8 | const FILE_PATH_TEMPLATE = "{{filePath}}" 9 | const VAULT_NAME_TEMPLATE = "{{vaultName}}" 10 | const METADATA_REGEX = /{{meta:([^}]*)}}/; //note that this will *not* match if the metadata name has a } in it 11 | 12 | const editorTemplates = [SELECTION_TEMPLATE, LINE_TEMPLATE]; // templates that require an editor to extract 13 | const fileTemplates = [FILE_NAME_TEMPLATE, FILE_TEXT_TEMPLATE, FILE_PATH_TEMPLATE]; // templates that require an active file (but not an editor) 14 | 15 | export default class URIPlugin extends Plugin { 16 | settings: URIPluginSettings; 17 | async onload() { 18 | console.log('Loading URI commands...'); 19 | 20 | await this.loadSettings(); 21 | this.addSettingTab(new URISettingTab(this.app, this)); 22 | 23 | this.addCommands(); 24 | } 25 | 26 | onunload() { 27 | console.log('Unloading URI commands'); 28 | } 29 | 30 | async loadSettings() { 31 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); 32 | } 33 | 34 | async saveSettings() { 35 | await this.saveData(this.settings); 36 | } 37 | 38 | addCommands() { 39 | this.settings.URICommands.forEach(command => { 40 | this.addURICommand(command); 41 | }) 42 | } 43 | 44 | addURICommand(command: URICommand) { 45 | this.addCommand({ 46 | id: command.id, 47 | name: command.name, 48 | icon: command.icon, 49 | 50 | checkCallback: (check: boolean) => { 51 | const view: MarkdownView = this.app.workspace.getActiveViewOfType(MarkdownView); 52 | const file: TFile = this.app.workspace.getActiveFile(); 53 | const editor: Editor = view?.editor; 54 | if (!editor) { //don't show commands that require an editor 55 | const uriContainsEditorTemplates = editorTemplates.some(template => command.URITemplate.includes(template)); //https://stackoverflow.com/a/66980203 56 | if (uriContainsEditorTemplates) return false; 57 | } 58 | 59 | if (!file) { // don't show commands that require a file 60 | const uriContainsFileTemplates = fileTemplates.some(template => command.URITemplate.includes(template)); 61 | if (uriContainsFileTemplates || METADATA_REGEX.test(command.URITemplate)) return false; 62 | } else if (file.extension !== "md") { // don't show commands that require a markdown file 63 | if (command.URITemplate.includes(FILE_TEXT_TEMPLATE)) return false; 64 | } 65 | 66 | if (!check) this.runCommand(command, editor, file); 67 | return true; 68 | } 69 | }); 70 | } 71 | 72 | async runCommand(command: URICommand, editor?: Editor, file?: TFile) { 73 | let uriString = command.URITemplate; 74 | 75 | if(METADATA_REGEX.test(uriString)) { //specified metadata values 76 | //checks if you can use metadata placeholder 77 | if (!(this.app as any).plugins.plugins["metaedit"].api) { 78 | new Notice("Must have MetaEdit enabled to use metadata placeholders") 79 | return; 80 | } 81 | 82 | //@ts-ignore 83 | const {getPropertyValue} = this.app.plugins.plugins["metaedit"].api; 84 | 85 | //for every instance of the placeholder: extract the name of the field, get the corresponding value, and replace the placeholder with the encoded value 86 | //https://stackoverflow.com/questions/432493/how-do-you-access-the-matched-groups-in-a-javascript-regular-expression 87 | let metadataMatch = METADATA_REGEX.exec(uriString); //grab a matched group, where match[0] is the full regex and match [1] is the (first) group 88 | 89 | while (metadataMatch !== null) { //loop through all the matched until exec() isn't spitting out any more 90 | let metadataValue = await getPropertyValue(metadataMatch[1], file); 91 | if (!metadataValue) { //if this value doesn't exist on the file 92 | new Notice(`The field ${metadataMatch[1]} does not exist on this file.`) 93 | return; 94 | } 95 | uriString = replacePlaceholder(command, uriString, metadataMatch[0], metadataValue); 96 | metadataMatch = METADATA_REGEX.exec(uriString); 97 | } 98 | } 99 | 100 | if (uriString.includes(FILE_NAME_TEMPLATE)) { // base name of file 101 | uriString = replacePlaceholder(command, uriString, FILE_NAME_TEMPLATE, file.basename); 102 | } 103 | 104 | if (uriString.includes(FILE_TEXT_TEMPLATE)) { //entire text of file 105 | const fileText = await this.app.vault.read(file); 106 | uriString = replacePlaceholder(command, uriString, FILE_TEXT_TEMPLATE, fileText); 107 | } 108 | 109 | if (uriString.includes(SELECTION_TEMPLATE)) { //current selection 110 | uriString = replacePlaceholder(command, uriString, SELECTION_TEMPLATE, editor.getSelection()); //currently replaced with empty string if no selection 111 | } 112 | 113 | if (uriString.includes(LINE_TEMPLATE)) { //current line 114 | const currentLine = editor.getCursor().line; 115 | uriString = replacePlaceholder(command, uriString, LINE_TEMPLATE, editor.getLine(currentLine)); 116 | } 117 | 118 | if (uriString.includes(FILE_PATH_TEMPLATE)) { //path inside the vault to the current file 119 | uriString = replacePlaceholder(command, uriString, FILE_PATH_TEMPLATE, file.path); 120 | } 121 | 122 | if (uriString.includes(VAULT_NAME_TEMPLATE)) { //name of the current vault 123 | uriString = replacePlaceholder(command, uriString, VAULT_NAME_TEMPLATE, this.app.vault.getName()); 124 | } 125 | 126 | window.open(uriString); 127 | if (this.settings.notification === true) { 128 | new Notice(`Opening ${uriString}`); 129 | } 130 | } 131 | } 132 | 133 | function replacePlaceholder(command: URICommand, uriString: string, placeholder: string | RegExp, replacementString: string) { 134 | if (command.encode) { 135 | replacementString = encodeURIComponent(replacementString); 136 | } 137 | 138 | return uriString.replace(placeholder, replacementString); 139 | } 140 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import { App, PluginSettingTab, Setting, Notice, setIcon, Command } from 'obsidian'; 2 | import URIPlugin from './main'; 3 | import URIModal from './URIModal'; 4 | 5 | export interface URICommand extends Command { 6 | URITemplate: string; 7 | encode: boolean; 8 | } 9 | 10 | export interface URIPluginSettings { 11 | URICommands: URICommand[]; 12 | notification: boolean; 13 | } 14 | 15 | export const DEFAULT_SETTINGS: URIPluginSettings = { 16 | URICommands: [], 17 | notification: false, 18 | } 19 | 20 | //borrowed in part from phibr0's Customizable Sidebar plugin 21 | export class URISettingTab extends PluginSettingTab { 22 | plugin: URIPlugin; 23 | 24 | constructor(app: App, plugin: URIPlugin) { 25 | super(app, plugin); 26 | this.plugin = plugin; 27 | } 28 | 29 | display(): void { 30 | let { containerEl } = this; 31 | containerEl.empty(); 32 | 33 | containerEl.createEl('h2', { text: 'Commands' }); 34 | 35 | new Setting(containerEl) 36 | .setName("Add URI") 37 | .setDesc("Add a new URI to the command palette") 38 | .addButton((button) => { 39 | button.setButtonText("Add Command") 40 | .onClick(() => { 41 | new URIModal(this.plugin, this).open(); 42 | }); 43 | }); 44 | 45 | this.plugin.settings.URICommands.forEach(command => { 46 | if (command === null) { //this should *not* happen 47 | this.plugin.settings.URICommands.remove(command); 48 | console.log("Command was null, removing.") 49 | return; 50 | } 51 | 52 | let iconDiv: HTMLElement; 53 | if (command.icon) { //do want the "if null or empty string or undefined or etc" behavior 54 | iconDiv = createDiv({ cls: "URI-settings-icon" }); 55 | setIcon(iconDiv, command.icon, 20); 56 | } 57 | 58 | 59 | const setting = new Setting(containerEl) 60 | .setName(command.name) 61 | .setDesc(command.URITemplate) 62 | .addExtraButton(button => { 63 | button.setIcon("trash") 64 | .setTooltip("Remove command") 65 | .onClick(async () => { 66 | // Unregister the command from the palette 67 | (this.app as any).commands.removeCommand(`${this.plugin.manifest.id}:${command.id}`); 68 | this.plugin.settings.URICommands.remove(command); 69 | await this.plugin.saveSettings(); 70 | this.display(); 71 | }) 72 | }) 73 | .addExtraButton(button => { 74 | button.setIcon("gear") 75 | .setTooltip("Edit command") 76 | .onClick(() => { 77 | new URIModal(this.plugin, this, command, true).open() 78 | }) 79 | }); 80 | 81 | if (command.icon) { 82 | setting.nameEl.prepend(iconDiv); 83 | } 84 | setting.nameEl.addClass("URI-flex"); 85 | }); 86 | 87 | containerEl.createEl('h2', { text: 'Settings' }); 88 | 89 | new Setting(containerEl) 90 | .setName("Notification on launch") 91 | .setDesc("Display a notification with the command URI on launch.") 92 | .addToggle(toggle => { 93 | toggle.setValue(this.plugin.settings.notification) 94 | .onChange(value => { 95 | this.plugin.settings.notification = value; 96 | this.plugin.saveSettings(); 97 | }) 98 | }) 99 | 100 | 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /*Icon Picker*/ 2 | .URI-icon { 3 | transform: translateY(3px); 4 | margin-right: 8px; 5 | } 6 | 7 | .URI-icon-container{ 8 | display: flex; 9 | } 10 | 11 | .URI-settings-icon { 12 | height: 20px; 13 | margin-right: 0.5rem; 14 | transform: translateY(2px); 15 | } 16 | 17 | .URI-flex { 18 | display: flex; 19 | } 20 | 21 | .URI-flex-center { 22 | display: flex; 23 | place-content: center; 24 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "es6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "lib": [ 13 | "dom", 14 | "es5", 15 | "scripthost", 16 | "es2015" 17 | ] 18 | }, 19 | "include": [ 20 | "**/*.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.1": "0.9.12", 3 | "1.0.0": "0.9.7" 4 | } 5 | --------------------------------------------------------------------------------