├── .eslintrc.json ├── .github └── workflows │ └── release.yml ├── .gitignore ├── GitVersion.yml ├── README.md ├── docs ├── command-palette.png ├── settings.png ├── sidebar.png └── templates-prompt.png ├── manifest.json ├── package.json ├── rollup.config.js ├── src ├── ObsidianService.ts ├── Symbols.ts ├── TempleFuzzySuggestModal.ts ├── TempleService.ts ├── constants.ts ├── main.ts ├── providers │ ├── ClipboardContext.ts │ ├── ClipboardTempleProvider.ts │ ├── DateTimeContext.ts │ ├── DateTimeFilters.ts │ ├── DateTimeProvider.ts │ ├── DateTimeTempleProvider.ts │ ├── FileInfoContext.ts │ ├── FileInfoTempleProvider.ts │ ├── ITempleProvider.ts │ ├── TempleContext.ts │ ├── TempleDocsContext.ts │ ├── ZettelContext.ts │ └── ZettelTempleProvider.ts ├── settings │ ├── TempleSettings.ts │ ├── TempleSettingsProvider.ts │ └── TempleSettingsTab.ts └── styles.css ├── tsconfig.json └── versions.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/eslint-recommended", 9 | "plugin:@typescript-eslint/recommended" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaVersion": 12 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint" 17 | ], 18 | "rules": { 19 | "quotes": ["warn", "single"] 20 | } 21 | } -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # modified from https://github.com/deathau/workflow-experiments/blob/main/.github/workflows/release.yml 2 | 3 | name: Release Obsidian Plugin 4 | on: 5 | push: 6 | 7 | env: 8 | dist_dir: ./dist/ 9 | out_dir: ./out/ 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Use Node.js 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: '14.x' 25 | 26 | - name: Install GitVersion 27 | uses: gittools/actions/gitversion/setup@v0.9.5 28 | with: 29 | versionSpec: "5.x" 30 | 31 | - name: Versioning 32 | id: gitversion 33 | uses: gittools/actions/gitversion/execute@v0.9.5 34 | 35 | - name: Update manifest version 36 | uses: jossef/action-set-json-field@v1 37 | with: 38 | file: manifest.json 39 | field: version 40 | value: ${{ steps.gitversion.outputs.majorMinorPatch }} 41 | 42 | - name: Commit manifest version changes 43 | if: steps.gitversion.outputs.preReleaseTag == '' 44 | uses: EndBug/add-and-commit@v6 45 | with: 46 | add: manifest.json 47 | message: "build: update version" 48 | 49 | - name: Build 50 | id: build 51 | run: | 52 | npm install 53 | npm run build --if-present 54 | 55 | - name: Copy built files 56 | id: copy 57 | run: | 58 | mkdir -p ${{ env.out_dir }} 59 | cp -a ${{ env.dist_dir }}. ${{ env.out_dir }} 60 | 61 | - name: Package 62 | run: | 63 | zip -r ${{ env.out_dir }}${{ github.event.repository.name }}-${{ steps.gitversion.outputs.majorMinorPatch }}.zip ${{ env.dist_dir }} 64 | 65 | - name: Debug 66 | run: | 67 | ls ${{ env.out_dir }} 68 | 69 | - name: Release Github 70 | uses: softprops/action-gh-release@master 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | with: 74 | draft: true 75 | files: "${{ env.out_dir }}/*" 76 | name: ${{ steps.gitversion.outputs.fullSemVer }} 77 | tag_name: ${{ steps.gitversion.outputs.fullSemVer }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij 2 | *.iml 3 | .idea 4 | 5 | # npm 6 | node_modules 7 | package-lock.json 8 | 9 | # build 10 | /dist -------------------------------------------------------------------------------- /GitVersion.yml: -------------------------------------------------------------------------------- 1 | next-version: 0.4.0 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # obsidian-temple 2 | 3 | ![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/garyng/obsidian-temple?label=release&style=for-the-badge) 4 | 5 | A plugin for templating in Obsidian, powered by [Nunjucks](https://mozilla.github.io/nunjucks/). 6 | 7 | ## Configuration 8 | 9 | Set the directory that contains the templates to be used inside Settings: 10 | 11 | ![](docs/settings.png) 12 | 13 | ## Usages 14 | 15 | You can insert a new template by clicking on the button in the sidebar 16 | 17 | ![](docs/sidebar.png) 18 | 19 | or via the Command Palette 20 | 21 | ![](docs/command-palette.png) 22 | 23 | You will be prompted to choose a template if there are multiple defined 24 | 25 | ![](docs/templates-prompt.png) 26 | 27 | ## Templating 28 | 29 | Since `obsidian-temple` uses `nunjucks` under-the-hood, you can use everything supported by `nunjucks`. Check the [official Nunjucks documentation](https://mozilla.github.io/nunjucks/templating.html) on how to write `nunjucks` template. 30 | 31 | ### Example: Populating `aliases` based on filename with Zettelkasten ID 32 | 33 | ```njk 34 | --- 35 | uid: "{{ zettel.uid }}" 36 | aliases: ["{{ zettel.title }}"] 37 | tags: [] 38 | --- 39 | ``` 40 | 41 | If the filename is `20201224030406 title.md`, then the output of the template will be: 42 | 43 | ``` 44 | --- 45 | uid: "20201224030406" 46 | aliases: ["title"] 47 | tags: [] 48 | --- 49 | ``` 50 | 51 | It also works if you have the `uid` as a suffix in the filename, eg: `title 20201224030406.md`. 52 | 53 | `zettel` is just one of the objects that are provided by `obsidian-temple`, see [Providers](#providers) for more. 54 | 55 | ## Providers 56 | 57 | `obsidian-temple` currently includes a few providers that can provide the [`context` objects](https://mozilla.github.io/nunjucks/api.html#renderstring) for `nunjucks`: 58 | - `file` 59 | - `zettel` 60 | - `datetime` 61 | - `clipboard` 62 | 63 | Check their respective documentation at [PROVIDERS DOCUMENTATION](#providers-documentation). You can easily add more providers, see [Adding new provider](#adding-new-provider). 64 | 65 | ## Adding new provider 66 | 67 | You need to: 68 | 69 | 1. create a new context class, `T` 70 | 1. implements `ITempleProvider` 71 | 1. register the provider on load 72 | 73 | For example, for the `datetime` provider: 74 | 75 | 1. the context class is [`DateTimeContext`](https://github.com/garyng/obsidian-temple/blob/57bc5738dbf35df5403947be769f9f8b2694ddaa/src/providers/DateTimeContext.ts) 76 | 1. the provider class is [`DateTimeTempleProvider`](https://github.com/garyng/obsidian-temple/blob/57bc5738dbf35df5403947be769f9f8b2694ddaa/src/providers/DateTimeTempleProvider.ts) 77 | 1. the registration is at [`main.ts`](https://github.com/garyng/obsidian-temple/blob/57bc5738dbf35df5403947be769f9f8b2694ddaa/src/main.ts#L27) 78 | 79 | ## Alternatives 80 | 81 | - [`SilentVoid13/Templater`](https://github.com/SilentVoid13/Templater) 82 | 83 | --- 84 | 85 | # PROVIDERS DOCUMENTATION 86 | 87 | This documentation is best viewed inside Obsidian, which can be generated by activating Command Palette > then select `Obsidian Temple: Insert documentation of all providers`. 88 | 89 | --- 90 | 91 | # `file` 92 | 93 | Exposes Obsidian's internal [`TFile`](https://github.com/obsidianmd/obsidian-api/blob/d10f2f6efc0d0d7c9bf96cd435ef376b18fbd6d8/obsidian.d.ts#L2206) structure for templating. 94 | 95 | ## Usages 96 | 97 | ``` 98 | path: {{ file.path }} 99 | name: {{ file.name }} 100 | basename: {{ file.basename }} 101 | extension: {{ file.extension }} 102 | ``` 103 | 104 | outputs: 105 | 106 | ``` 107 | path: Untitled 20210103181939.md 108 | name: Untitled 20210103181939.md 109 | basename: Untitled 20210103181939 110 | extension: md 111 | ``` 112 | 113 | --- 114 | 115 | # `datetime` 116 | 117 | Returns the current date and time as Luxon [`DateTime`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/e5e63b56d6bb52a95cc5e7cfadc5d1bec3023f14/types/luxon/index.d.ts#L151). 118 | 119 | ## Usage 120 | 121 | ``` 122 | now: {{ datetime.now }} 123 | 124 | day: {{ datetime.now.day }} 125 | month: {{ datetime.now.month }} 126 | year: {{ datetime.now.year }} 127 | 128 | hour: {{ datetime.now.hour }} 129 | minute: {{ datetime.now.minute }} 130 | second: {{ datetime.now.second }} 131 | ``` 132 | 133 | outputs: 134 | 135 | ``` 136 | now: 2021-01-03T22:21:36.585+08:00 137 | 138 | day: 3 139 | month: 1 140 | year: 2021 141 | 142 | hour: 22 143 | minute: 21 144 | second: 36 145 | ``` 146 | 147 | 148 | ## Formatting with `dateFormat` filter 149 | 150 | `dateFormat` uses [Luxon](https://moment.github.io/luxon/index.html) under-the-hood for date formatting. For example: 151 | 152 | ``` 153 | now: {{ datetime.now | dateFormat("ffff") }} 154 | ``` 155 | 156 | outputs: 157 | 158 | ``` 159 | now: Sunday, January 3, 2021, 10:21 PM Singapore Standard Time 160 | ``` 161 | 162 | See [Luxon's documentation](https://moment.github.io/luxon/docs/manual/formatting.html#table-of-tokens) for a complete list of formatting tokens that can be used. 163 | 164 | # Settings 165 | 166 | You can override the default locale and timezone under Settings. 167 | 168 | --- 169 | 170 | # `zettel` 171 | 172 | Extracts uid and title from notes that have the Zettelkasten ID. 173 | 174 | ## Usages 175 | 176 | Given a file named `20201224030406 title.md`, the following template 177 | 178 | ``` 179 | uid: {{ zettel.uid }} 180 | title: {{ zettel.title }} 181 | ``` 182 | 183 | outputs: 184 | 185 | ``` 186 | uid: 20201224030406 187 | title: title.md 188 | ``` 189 | 190 | Works even if the `uid` is used as a suffix, eg. `title 20201224030406.md`. 191 | 192 | ## Settings 193 | 194 | You can override the extraction regex under Settings. 195 | 196 | --- 197 | 198 | # `clipboard` 199 | Extracts data from your system clipboard. Uses [sindresorhus/clipboardy](https://github.com/sindresorhus/clipboardy). 200 | 201 | ## Usages 202 | 203 | ``` 204 | text: {{ clipboard.text }} 205 | ``` 206 | 207 | outputs: 208 | 209 | ``` 210 | text: content 211 | ``` 212 | -------------------------------------------------------------------------------- /docs/command-palette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garyng/obsidian-temple/cc9d4701e4d7b12d034e502ff8a95640b75c5707/docs/command-palette.png -------------------------------------------------------------------------------- /docs/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garyng/obsidian-temple/cc9d4701e4d7b12d034e502ff8a95640b75c5707/docs/settings.png -------------------------------------------------------------------------------- /docs/sidebar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garyng/obsidian-temple/cc9d4701e4d7b12d034e502ff8a95640b75c5707/docs/sidebar.png -------------------------------------------------------------------------------- /docs/templates-prompt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garyng/obsidian-temple/cc9d4701e4d7b12d034e502ff8a95640b75c5707/docs/templates-prompt.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-temple", 3 | "name": "Obsidian Temple", 4 | "version": "0.4.0", 5 | "minAppVersion": "0.9.22", 6 | "description": "A plugin for templating in Obsidian, powered by Nunjucks.", 7 | "author": "GaryNg", 8 | "authorUrl": "https://github.com/garyng/obsidian-temple", 9 | "isDesktopOnly": false 10 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-sample-plugin", 3 | "version": "0.9.7", 4 | "description": "This is a sample plugin for Obsidian (https://obsidian.md)", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "rollup --config rollup.config.js -w", 8 | "build": "rollup --config rollup.config.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "@rollup/plugin-commonjs": "^15.1.0", 15 | "@rollup/plugin-node-resolve": "^9.0.0", 16 | "@rollup/plugin-typescript": "^6.0.0", 17 | "@types/lodash": "^4.14.165", 18 | "@types/luxon": "^1.25.0", 19 | "@types/node": "^14.14.2", 20 | "@types/nunjucks": "^3.1.3", 21 | "@typescript-eslint/eslint-plugin": "^4.11.1", 22 | "@typescript-eslint/parser": "^4.11.1", 23 | "eslint": "^7.17.0", 24 | "eslint-config-standard": "^16.0.2", 25 | "eslint-plugin-import": "^2.22.1", 26 | "eslint-plugin-node": "^11.1.0", 27 | "eslint-plugin-promise": "^4.2.1", 28 | "obsidian": "https://github.com/obsidianmd/obsidian-api/tarball/master", 29 | "rollup": "^2.32.1", 30 | "rollup-plugin-copy": "^3.3.0", 31 | "rollup-plugin-filesize": "^9.1.0", 32 | "rollup-plugin-terser": "^7.0.2", 33 | "tslib": "^2.0.3", 34 | "typescript": "^4.0.3" 35 | }, 36 | "dependencies": { 37 | "clipboardy": "^2.3.0", 38 | "lodash": "^4.17.20", 39 | "luxon": "^1.25.0", 40 | "nunjucks": "^3.2.2", 41 | "reflect-metadata": "^0.1.13", 42 | "tsyringe": "^4.4.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /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 | import copy from "rollup-plugin-copy"; 5 | import { terser } from "rollup-plugin-terser"; 6 | import filesize from 'rollup-plugin-filesize'; 7 | 8 | let notesDir = "notes/.obsidian/plugins/obsidian-temple/"; 9 | let distDir = "dist/"; 10 | let outputConfig = { 11 | sourcemap: "inline", 12 | format: "cjs", 13 | exports: "default", 14 | }; 15 | 16 | export default { 17 | input: "src/main.ts", 18 | output: [ 19 | { 20 | dir: distDir, 21 | plugins: [terser()], 22 | ...outputConfig, 23 | }, 24 | { 25 | dir: notesDir, 26 | ...outputConfig, 27 | }, 28 | ], 29 | external: ["obsidian"], 30 | plugins: [ 31 | typescript(), 32 | nodeResolve({ browser: true }), 33 | commonjs(), 34 | filesize({ 35 | showGzippedSize: false 36 | }), 37 | copy({ 38 | copyOnce: false, 39 | targets: [ 40 | { src: "src/styles.css", dest: [distDir, notesDir] }, 41 | { src: "manifest.json", dest: [distDir, notesDir] }, 42 | ], 43 | }), 44 | ], 45 | }; 46 | -------------------------------------------------------------------------------- /src/ObsidianService.ts: -------------------------------------------------------------------------------- 1 | import { MarkdownSourceView, MarkdownView, Notice, Plugin_2, TFile, TFolder, Vault } from 'obsidian'; 2 | import { inject, injectable } from 'tsyringe'; 3 | import { TempleSettings } from './settings/TempleSettings'; 4 | import { Symbols } from './Symbols'; 5 | import { TempleFuzzySuggestModal } from './TempleFuzzySuggestModal'; 6 | import { TempleService } from './TempleService'; 7 | 8 | @injectable() 9 | export class ObsidianService { 10 | private _prompt: TempleFuzzySuggestModal; 11 | 12 | constructor(@inject(Symbols.Plugin) private _obs: Plugin_2, private _temple: TempleService, @inject(Symbols.TempleSettings) private _settings: TempleSettings) { 13 | this._prompt = new TempleFuzzySuggestModal(_obs.app, this); 14 | } 15 | 16 | /** 17 | * Ask user to select a template if there are multiple defined. 18 | */ 19 | public async promptTemplate(): Promise { 20 | const templates = this.getTemplatePaths(); 21 | if (templates.length > 1) { 22 | this._prompt.open(); 23 | } else { 24 | await this.insertTemplate(templates[0]); 25 | } 26 | } 27 | 28 | /** 29 | * Get the paths of all templates defined in the template directory. 30 | */ 31 | public getTemplatePaths(): string[] { 32 | const templates: string[] = []; 33 | const dir = this._obs.app.vault.getAbstractFileByPath(this._settings.templatesDir); 34 | if (dir instanceof TFolder) { 35 | Vault.recurseChildren(dir, file => { 36 | if (file instanceof TFile) { 37 | templates.push(file.path); 38 | } 39 | }); 40 | } 41 | 42 | return templates; 43 | } 44 | 45 | /** 46 | * Render and insert the selected template. 47 | */ 48 | public async insertTemplate(path: string): Promise { 49 | const template = await this.readFile(path); 50 | const rendered = await this._temple.render(template); 51 | 52 | this.insertAtCursor(rendered); 53 | } 54 | 55 | private insertAtCursor(text: string): void { 56 | const view = this._obs.app.workspace.getActiveViewOfType(MarkdownView); 57 | if (view && view.currentMode instanceof MarkdownSourceView) { 58 | const editor = view.sourceMode.cmEditor; 59 | const doc = editor.getDoc(); 60 | doc.replaceSelection(text); 61 | } 62 | } 63 | 64 | /** 65 | * Insert the documentation of all providers. 66 | */ 67 | public async insertDocs(): Promise { 68 | const doc = await this._temple.renderDoc(); 69 | this.insertAtCursor(doc); 70 | } 71 | 72 | public async readFile(path: string): Promise { 73 | const file = this._obs.app.vault.getAbstractFileByPath(path); 74 | if (file instanceof TFile) { 75 | return await this._obs.app.vault.read(file); 76 | } else { 77 | throw new Error(`Unable to read '${file?.path}'`); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Symbols.ts: -------------------------------------------------------------------------------- 1 | export const Symbols = { 2 | TempleSettings: Symbol.for('TempleSettings'), 3 | ITempleProvider: Symbol.for('ITempleProvider'), 4 | Plugin: Symbol.for('Plugin_2') 5 | } -------------------------------------------------------------------------------- /src/TempleFuzzySuggestModal.ts: -------------------------------------------------------------------------------- 1 | import { App, FuzzySuggestModal } from 'obsidian'; 2 | import { ObsidianService } from './ObsidianService'; 3 | 4 | export class TempleFuzzySuggestModal extends FuzzySuggestModal { 5 | 6 | constructor(app: App, private _obs: ObsidianService) { 7 | super(app); 8 | } 9 | 10 | getItems(): string[] { 11 | return this._obs.getTemplatePaths(); 12 | } 13 | getItemText(item: string): string { 14 | return item; 15 | } 16 | onChooseItem(item: string, evt: MouseEvent | KeyboardEvent): void { 17 | this._obs.insertTemplate(item); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/TempleService.ts: -------------------------------------------------------------------------------- 1 | import * as njk from 'nunjucks'; 2 | import * as _ from 'lodash'; 3 | import { ITempleProvider } from './providers/ITempleProvider'; 4 | import { TempleContext } from './providers/TempleContext'; 5 | import { injectable, injectAll } from 'tsyringe'; 6 | import { Symbols } from './Symbols'; 7 | import { DateTime } from 'luxon'; 8 | import { DateTimeFilters } from './providers/DateTimeFilters'; 9 | import { EOL } from 'os'; 10 | import { inspect } from 'util'; 11 | 12 | export interface IAggregatedContext { 13 | [name: string]: TempleContext; 14 | } 15 | 16 | @injectable() 17 | export class TempleService { 18 | private _env: njk.Environment; 19 | constructor(private _dateTimeFilters: DateTimeFilters, @injectAll(Symbols.ITempleProvider) private _providers: ITempleProvider[]) { 20 | this._env = new njk.Environment(); 21 | this.installFilters(); 22 | } 23 | 24 | private installFilters() { 25 | 26 | // seems like this won't properly capture the variable 27 | // this._env.addFilter('dateFormat', this._dateTimeFilters.format); 28 | 29 | this._env.addFilter('dateFormat', (input: DateTime, format: string) => { 30 | return this._dateTimeFilters.format(input, format); 31 | }); 32 | 33 | this._env.addFilter('inspect', (input: any, depth: null) => { 34 | return inspect(input, undefined, depth); 35 | }) 36 | } 37 | 38 | async resolve(): Promise { 39 | const contexts = (await Promise.all(this._providers 40 | .map(async provider => ({ name: provider.name, value: await provider.provide() })))) 41 | .filter(c => c.value != null) 42 | .map(c => ({ [c.name]: c.value })); 43 | return Object.assign({}, ...contexts); 44 | } 45 | 46 | async render(template: string, aggregated: IAggregatedContext | null = null): Promise { 47 | aggregated ??= await this.resolve(); 48 | // only take the context from each TempleContext for nunjucks 49 | const context = _.mapValues(aggregated, c => c.context); 50 | return this._env.renderString(template, context); 51 | } 52 | 53 | async renderDoc(): Promise { 54 | 55 | const doc = (await Promise.all(this._providers 56 | .map(async provider => { 57 | const doc = await provider.docs(); 58 | return this._env.renderString(doc.template, ({ 59 | [provider.name]: doc.context.context 60 | })); 61 | }))) 62 | .join(`${EOL}---${EOL}`); 63 | 64 | return doc; 65 | } 66 | } -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const ICON = ''; -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { addIcon, App, MarkdownSourceView, MarkdownView, Plugin, PluginManifest, Plugin_2, Workspace } from 'obsidian'; 3 | import { ICON } from './constants'; 4 | import { FileInfoTempleProvider } from './providers/FileInfoTempleProvider'; 5 | import { DateTimeTempleProvider } from './providers/DateTimeTempleProvider'; 6 | import { ZettelTempleProvider } from './providers/ZettelTempleProvider'; 7 | import { TempleService } from './TempleService'; 8 | import { DateTimeFilters } from './providers/DateTimeFilters'; 9 | import { TempleSettingsTab } from './settings/TempleSettingsTab'; 10 | import { ObsidianService } from './ObsidianService'; 11 | import { TempleSettings } from './settings/TempleSettings'; 12 | import { TempleSettingsProvider } from './settings/TempleSettingsProvider'; 13 | import { ClipboardTempleProvider } from './providers/ClipboardTempleProvider'; 14 | import { ITempleProvider } from './providers/ITempleProvider'; 15 | import { container } from 'tsyringe'; 16 | import { Symbols } from './Symbols'; 17 | import { DateTimeProvider } from './providers/DateTimeProvider'; 18 | 19 | export default class TemplePlugin extends Plugin { 20 | constructor(app: App, pluginManifest: PluginManifest) { 21 | super(app, pluginManifest); 22 | } 23 | 24 | async onload(): Promise { 25 | 26 | container.registerInstance(Symbols.Plugin, this); 27 | container.registerSingleton(TempleSettingsProvider, TempleSettingsProvider); 28 | 29 | const settingsProvider = container.resolve(TempleSettingsProvider); 30 | await settingsProvider.load(); 31 | 32 | container.registerInstance(App, this.app); 33 | container.registerInstance(Workspace, this.app.workspace); 34 | 35 | container.registerInstance(Symbols.TempleSettings, settingsProvider.value); 36 | container.registerSingleton(TempleSettingsTab, TempleSettingsTab); 37 | 38 | container.registerSingleton(DateTimeProvider); 39 | container.registerSingleton(DateTimeFilters); 40 | 41 | container.registerSingleton(TempleService, TempleService); 42 | container.registerSingleton>(Symbols.ITempleProvider, FileInfoTempleProvider); 43 | container.registerSingleton>(Symbols.ITempleProvider, DateTimeTempleProvider); 44 | container.registerSingleton>(Symbols.ITempleProvider, ZettelTempleProvider); 45 | container.registerSingleton>(Symbols.ITempleProvider, ClipboardTempleProvider); 46 | 47 | const obs = container.resolve(ObsidianService); 48 | 49 | addIcon('temple', ICON); 50 | 51 | this.addSettingTab(container.resolve(TempleSettingsTab)); 52 | this.addRibbonIcon('temple', 'Temple', async () => { 53 | await obs.promptTemplate(); 54 | }); 55 | this.addCommand({ 56 | id: 'obsidian-temple-insert', 57 | name: 'Insert template', 58 | callback: async () => { 59 | await obs.promptTemplate(); 60 | } 61 | }); 62 | 63 | this.addCommand({ 64 | id: 'obsidian-temple-insert-doc', 65 | name: 'Insert documentation of all providers', 66 | callback: async () => { 67 | await obs.insertDocs(); 68 | } 69 | }); 70 | } 71 | } -------------------------------------------------------------------------------- /src/providers/ClipboardContext.ts: -------------------------------------------------------------------------------- 1 | 2 | export class ClipboardContext { 3 | constructor(public text: string) { } 4 | } -------------------------------------------------------------------------------- /src/providers/ClipboardTempleProvider.ts: -------------------------------------------------------------------------------- 1 | import { read } from 'clipboardy'; 2 | import { ITempleProvider } from './ITempleProvider'; 3 | import { TempleContext } from './TempleContext'; 4 | import { ClipboardContext } from './ClipboardContext'; 5 | import { injectable } from 'tsyringe'; 6 | import { TempleDocsContext } from './TempleDocsContext'; 7 | 8 | @injectable() 9 | export class ClipboardTempleProvider implements ITempleProvider { 10 | name = 'clipboard'; 11 | 12 | async docs(): Promise> { 13 | return { 14 | context: await this.provide(), 15 | template: ` 16 | # \`clipboard\` 17 | Extracts data from your system clipboard. Uses [sindresorhus/clipboardy](https://github.com/sindresorhus/clipboardy). 18 | 19 | ## Usages 20 | 21 | {% raw %}\`\`\` 22 | text: {{ clipboard.text }} 23 | \`\`\`{% endraw %} 24 | 25 | outputs: 26 | 27 | \`\`\` 28 | text: {{ clipboard.text }} 29 | \`\`\` 30 | `, 31 | } 32 | } 33 | 34 | async provide(): Promise> { 35 | const text = await read(); 36 | return new TempleContext(new ClipboardContext( 37 | text 38 | )); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/providers/DateTimeContext.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon'; 2 | 3 | export class DateTimeContext { 4 | constructor(public now: DateTime) { } 5 | } 6 | -------------------------------------------------------------------------------- /src/providers/DateTimeFilters.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'tsyringe'; 2 | import { DateTime } from 'luxon'; 3 | import { DateTimeProvider } from './DateTimeProvider'; 4 | 5 | @injectable() 6 | export class DateTimeFilters { 7 | 8 | constructor(private _datetime: DateTimeProvider) { 9 | } 10 | 11 | public format(input: DateTime, format: string): string { 12 | return this._datetime.apply(input).toFormat(format); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/providers/DateTimeProvider.ts: -------------------------------------------------------------------------------- 1 | import { TempleSettings } from '../settings/TempleSettings'; 2 | import { inject, injectable } from 'tsyringe'; 3 | import { Symbols } from '../Symbols'; 4 | import { DateTime } from 'luxon'; 5 | 6 | 7 | @injectable() 8 | export class DateTimeProvider { 9 | constructor(@inject(Symbols.TempleSettings) private _settings: TempleSettings) { } 10 | 11 | public now(): DateTime { 12 | return this.apply(DateTime.local()); 13 | } 14 | 15 | /** 16 | * Apply locale and timezone settings 17 | */ 18 | public apply(dt: DateTime): DateTime { 19 | if (this._settings.datetime.locale) { 20 | dt = dt.setLocale(this._settings.datetime.locale); 21 | } 22 | if (this._settings.datetime.timezone) { 23 | dt = dt.setZone(this._settings.datetime.timezone); 24 | } 25 | return dt; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/providers/DateTimeTempleProvider.ts: -------------------------------------------------------------------------------- 1 | import { ITempleProvider } from './ITempleProvider'; 2 | import { DateTimeContext } from './DateTimeContext'; 3 | import { TempleContext } from './TempleContext'; 4 | import { injectable } from 'tsyringe'; 5 | import { DateTimeProvider } from './DateTimeProvider'; 6 | import { TempleDocsContext } from './TempleDocsContext'; 7 | 8 | @injectable() 9 | export class DateTimeTempleProvider implements ITempleProvider { 10 | name = 'datetime'; 11 | 12 | constructor(private _datetime: DateTimeProvider) { } 13 | 14 | async docs(): Promise> { 15 | return { 16 | context: await this.provide(), 17 | template: ` 18 | # \`datetime\` 19 | 20 | Returns the current date and time as Luxon [\`DateTime\`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/e5e63b56d6bb52a95cc5e7cfadc5d1bec3023f14/types/luxon/index.d.ts#L151). 21 | 22 | ## Usage 23 | 24 | {% raw %}\`\`\` 25 | now: {{ datetime.now }} 26 | 27 | day: {{ datetime.now.day }} 28 | month: {{ datetime.now.month }} 29 | year: {{ datetime.now.year }} 30 | 31 | hour: {{ datetime.now.hour }} 32 | minute: {{ datetime.now.minute }} 33 | second: {{ datetime.now.second }} 34 | \`\`\`{% endraw %} 35 | 36 | outputs: 37 | 38 | \`\`\` 39 | now: {{ datetime.now }} 40 | 41 | day: {{ datetime.now.day }} 42 | month: {{ datetime.now.month }} 43 | year: {{ datetime.now.year }} 44 | 45 | hour: {{ datetime.now.hour }} 46 | minute: {{ datetime.now.minute }} 47 | second: {{ datetime.now.second }} 48 | \`\`\` 49 | 50 | 51 | ## Formatting with \`dateFormat\` filter 52 | 53 | \`dateFormat\` uses [Luxon](https://moment.github.io/luxon/index.html) under-the-hood for date formatting. For example: 54 | 55 | {% raw %}\`\`\` 56 | now: {{ datetime.now | dateFormat("ffff") }} 57 | \`\`\`{% endraw %} 58 | 59 | outputs: 60 | 61 | \`\`\` 62 | now: {{ datetime.now | dateFormat("ffff") }} 63 | \`\`\` 64 | 65 | See [Luxon's documentation](https://moment.github.io/luxon/docs/manual/formatting.html#table-of-tokens) for a complete list of formatting tokens that can be used. 66 | 67 | # Settings 68 | 69 | You can override the default locale and timezone under Settings. 70 | ` 71 | } 72 | } 73 | 74 | async provide(): Promise> { 75 | return new TempleContext(new DateTimeContext(this._datetime.now())); 76 | } 77 | } -------------------------------------------------------------------------------- /src/providers/FileInfoContext.ts: -------------------------------------------------------------------------------- 1 | import { TFile } from 'obsidian'; 2 | 3 | export class FileInfoContext extends TFile { 4 | } 5 | -------------------------------------------------------------------------------- /src/providers/FileInfoTempleProvider.ts: -------------------------------------------------------------------------------- 1 | import { Workspace } from 'obsidian'; 2 | import { ITempleProvider } from './ITempleProvider'; 3 | import { FileInfoContext } from './FileInfoContext'; 4 | import { TempleContext } from './TempleContext'; 5 | import { injectable } from 'tsyringe'; 6 | import { TempleDocsContext } from './TempleDocsContext'; 7 | 8 | @injectable() 9 | export class FileInfoTempleProvider implements ITempleProvider { 10 | name = 'file'; 11 | 12 | constructor(private _workspace: Workspace) { 13 | } 14 | 15 | async docs(): Promise> { 16 | return { 17 | context: await this.provide(), 18 | template: ` 19 | # \`file\` 20 | 21 | Exposes Obsidian's internal [\`TFile\`](https://github.com/obsidianmd/obsidian-api/blob/d10f2f6efc0d0d7c9bf96cd435ef376b18fbd6d8/obsidian.d.ts#L2206) structure for templating. 22 | 23 | ## Usages 24 | 25 | {% raw %}\`\`\` 26 | path: {{ file.path }} 27 | name: {{ file.name }} 28 | basename: {{ file.basename }} 29 | extension: {{ file.extension }} 30 | \`\`\`{% endraw %} 31 | 32 | outputs: 33 | 34 | \`\`\` 35 | path: {{ file.path }} 36 | name: {{ file.name }} 37 | basename: {{ file.basename }} 38 | extension: {{ file.extension }} 39 | \`\`\` 40 | `, 41 | } 42 | } 43 | 44 | async provide(): Promise> { 45 | const file = this._workspace.getActiveFile(); 46 | if (file == null) 47 | return null; 48 | return new TempleContext(file); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/providers/ITempleProvider.ts: -------------------------------------------------------------------------------- 1 | import { TempleContext } from "./TempleContext"; 2 | import { TempleDocsContext } from "./TempleDocsContext"; 3 | 4 | export interface ITempleProvider { 5 | name: string; 6 | 7 | docs(): Promise>; 8 | provide(): Promise> | null; 9 | } 10 | -------------------------------------------------------------------------------- /src/providers/TempleContext.ts: -------------------------------------------------------------------------------- 1 | export class TempleContext { 2 | // todo: include additional metadata 3 | constructor(public context: T, public metadata: any = null) { 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/providers/TempleDocsContext.ts: -------------------------------------------------------------------------------- 1 | import { TempleContext } from './TempleContext'; 2 | 3 | export interface TempleDocsContext { 4 | context: TempleContext; 5 | template: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/providers/ZettelContext.ts: -------------------------------------------------------------------------------- 1 | export class ZettelContext { 2 | constructor(public uid: string, public title: string) { } 3 | } -------------------------------------------------------------------------------- /src/providers/ZettelTempleProvider.ts: -------------------------------------------------------------------------------- 1 | import { Workspace } from 'obsidian'; 2 | import { TempleSettings } from 'src/settings/TempleSettings'; 3 | import { Symbols } from 'src/Symbols'; 4 | import { inject, injectable } from 'tsyringe'; 5 | import { ITempleProvider } from './ITempleProvider'; 6 | import { TempleContext } from './TempleContext'; 7 | import { TempleDocsContext } from './TempleDocsContext'; 8 | import { ZettelContext } from './ZettelContext'; 9 | 10 | @injectable() 11 | export class ZettelTempleProvider implements ITempleProvider { 12 | name = 'zettel'; 13 | 14 | constructor(private _workspace: Workspace, @inject(Symbols.TempleSettings) private _settings: TempleSettings) { 15 | 16 | } 17 | 18 | async docs(): Promise> { 19 | return { 20 | context: new TempleContext(this.extract('20201224030406 title.md')), 21 | template: ` 22 | # \`zettel\` 23 | 24 | Extracts uid and title from notes that have the Zettelkasten ID. 25 | 26 | ## Usages 27 | 28 | Given a file named \`20201224030406 title.md\`, the following template 29 | 30 | {% raw %}\`\`\` 31 | uid: {{ zettel.uid }} 32 | title: {{ zettel.title }} 33 | \`\`\`{% endraw %} 34 | 35 | outputs: 36 | 37 | \`\`\` 38 | uid: {{ zettel.uid }} 39 | title: {{ zettel.title }} 40 | \`\`\` 41 | 42 | Works even if the \`uid\` is used as a suffix, eg. \`title 20201224030406.md\`. 43 | 44 | ## Settings 45 | 46 | You can override the extraction regex under Settings. 47 | ` 48 | } 49 | } 50 | 51 | async provide(): Promise> { 52 | const file = this._workspace.getActiveFile(); 53 | if (file == null) 54 | return null; 55 | const context = this.extract(file.basename); 56 | return new TempleContext(context); 57 | } 58 | 59 | extract(name: string): ZettelContext { 60 | let context = this.extractPrefix(name) 61 | ?? this.extractSuffix(name); 62 | 63 | if (this._settings.zettel.regex) { 64 | // override if a custom regex is set 65 | context = this.extractCustom(name); 66 | } 67 | return context; 68 | } 69 | 70 | /** 71 | * use custom regex from settings for extraction 72 | */ 73 | extractCustom(name: string): ZettelContext { 74 | const regex = new RegExp(this._settings.zettel.regex, 'gm') 75 | return this.extractRegex(name, regex); 76 | } 77 | 78 | /** 79 | * 80 | */ 81 | extractPrefix(name: string): ZettelContext { 82 | return this.extractRegex(name, /(?<uid>^\d+)(\s(?<title>.*$))?/gm); 83 | } 84 | 85 | /** 86 | * <title> <uid> 87 | */ 88 | extractSuffix(name: string): ZettelContext { 89 | return this.extractRegex(name, /((?<title>^.*)\s)?(?<uid>\d+$)/gm); 90 | } 91 | 92 | extractRegex(name: string, regex: RegExp): ZettelContext { 93 | const matches = regex.exec(name); 94 | 95 | if (matches?.groups == null) return null; 96 | const { groups: { uid, title } } = matches; 97 | 98 | return new ZettelContext(uid, title); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/settings/TempleSettings.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_SETTINGS: TempleSettings = { 2 | templatesDir: '/_templates', 3 | zettel: { 4 | regex: '' 5 | }, 6 | datetime: { 7 | locale: '', 8 | timezone: '' 9 | } 10 | } 11 | 12 | export interface TempleSettings { 13 | templatesDir: string; 14 | zettel: { 15 | regex: string 16 | }; 17 | datetime: { 18 | locale: string, 19 | timezone: string 20 | } 21 | } -------------------------------------------------------------------------------- /src/settings/TempleSettingsProvider.ts: -------------------------------------------------------------------------------- 1 | import { Plugin_2 } from 'obsidian'; 2 | import { Symbols } from 'src/Symbols'; 3 | import { inject, injectable } from 'tsyringe'; 4 | import { TempleSettings, DEFAULT_SETTINGS } from './TempleSettings'; 5 | 6 | @injectable() 7 | export class TempleSettingsProvider { 8 | public value: TempleSettings; 9 | 10 | constructor(@inject(Symbols.Plugin) private _plugin: Plugin_2) { 11 | } 12 | 13 | public async load(): Promise<void> { 14 | this.value = Object.assign(DEFAULT_SETTINGS, await this._plugin.loadData()); 15 | 16 | } 17 | 18 | public async save(): Promise<void> { 19 | await this._plugin.saveData(this.value); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/settings/TempleSettingsTab.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon'; 2 | import { App, PluginSettingTab, Plugin_2, Setting } from 'obsidian'; 3 | import { Symbols } from 'src/Symbols'; 4 | import { inject, injectable } from 'tsyringe'; 5 | import { TempleSettingsProvider } from './TempleSettingsProvider'; 6 | 7 | @injectable() 8 | export class TempleSettingsTab extends PluginSettingTab { 9 | 10 | constructor(app: App, @inject(Symbols.Plugin) private _plugin: Plugin_2, private _settings: TempleSettingsProvider) { 11 | super(app, _plugin); 12 | } 13 | 14 | display(): void { 15 | const { containerEl: e } = this; 16 | 17 | e.empty(); 18 | 19 | e.createEl('h1', { text: this._plugin.manifest.id }); 20 | e.createEl('p', { text: this._plugin.manifest.description }); 21 | e.createEl('br'); 22 | 23 | this.createSection(e, 'common'); 24 | 25 | new Setting(e) 26 | .setName('Templates directory location') 27 | .setDesc('Directory that stores nunjucks templates.') 28 | .addText(path => path 29 | .setPlaceholder('Example: /_templates') 30 | .setValue(this._settings.value.templatesDir) 31 | .onChange(async (value) => { 32 | // trim / and \ from both ends 33 | value = value.replace(/^(\/|\\)+|(\/|\\)+$/g, ''); 34 | this._settings.value.templatesDir = value; 35 | await this._settings.save(); 36 | })); 37 | 38 | this.createSection(e, 'zettel'); 39 | 40 | new Setting(e) 41 | .setName('Override extraction regex') 42 | .setDesc('Override the regex for extracting UID and title from filename. Regex must return capture groups named "uid" and "title". For example: (?<uid>^\\d+)(\\s(?<title>.*$))?') 43 | .addText(regex => regex 44 | .setValue(this._settings.value.zettel.regex) 45 | .onChange(async (value) => { 46 | this._settings.value.zettel.regex = value; 47 | await this._settings.save(); 48 | })); 49 | 50 | e.createEl('h3', { text: 'datetime' }); 51 | 52 | new Setting(e) 53 | .setName('Override timezone') 54 | .setDesc(`Defaults to system timezone ("${DateTime.local().zoneName}").`) 55 | .addText(tz => tz 56 | .setValue(this._settings.value.datetime.timezone) 57 | .onChange(async (value) => { 58 | this._settings.value.datetime.timezone = value; 59 | await this._settings.save(); 60 | })); 61 | 62 | new Setting(e) 63 | .setName('Override locale') 64 | .setDesc(`Defaults to system local ("${DateTime.local().locale}").`) 65 | .addText(tz => tz 66 | .setValue(this._settings.value.datetime.locale) 67 | .onChange(async (value) => { 68 | this._settings.value.datetime.locale = value; 69 | await this._settings.save(); 70 | })); 71 | 72 | } 73 | 74 | 75 | private createSection(e: HTMLElement, title: string) { 76 | e.createEl('h3', { text: title }); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* empty for now */ -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "es5", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "lib": [ 13 | "dom", 14 | "es5", 15 | "es6", 16 | "scripthost", 17 | "es2015" 18 | ], 19 | "experimentalDecorators": true, 20 | "emitDecoratorMetadata": true 21 | }, 22 | "include": [ 23 | "**/*.ts" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "0.1.0": "0.9.22" 3 | } 4 | --------------------------------------------------------------------------------