diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c0aac20 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Intellij +*.iml +.idea + +# npm +node_modules +package-lock.json + +# build +main.js +*.js.map \ No newline at end of file diff --git a/README.md b/README.md index 1014c54..f54c0dd 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,27 @@ -# concatenate - A plugin for Obsidian.md to allow you to put the contents of sections together in one file +## Obsidian Concatenate Plugin + +### Manual Installation + +- Download the latest `main.js, mainfest.json and styles.css` from releases. +- Create a new folder named 'obsidian-concatenate' +- Place the three files in the folder (although honestly styles.css doesn’t matter) +- Place the folder in your .obsidian/plugins directory +- Reload plugins +- Activate the "Concatenate" plugin + +### How to use the plugin + +1. Navigate to plugin settings. +2. Assign a value for which header you want to concatenate the contents of. (i.e. `## Reflections` or `### Meeting Logs`) +3. Assign a folder you want to limit the concatenation to (i.e. `Calendar/2021/January`) +4. Use the command palette (`ctrl+p` on windows by default) to `Concatenate Headings`. +5. View, rename, and/or move the outputted file, which will be created in your vault’s root directory with a name like “Concatenated_Note-Timestamp.” There will be a popup in the top right hand corner telling you the filename. + +### Disclaimer + +This is my _very first code project of any kind_. A few months ago I had never done any programming more complex than simple lua scripts for video games. Please install at your own risk, and understand that updates & bugfixes will be slow and awkward unless someone is willing to submit a pull request. + +### Credits + +Enormous thanks to `@pjeby`, `@mrjackphil`, and all the [Obsidian.md moderators](https://help.obsidian.md/Obsidian/Credits) for their encouragement, support, code tips, refactoring help, handholding with git, and more. + diff --git a/buildandload.bat b/buildandload.bat new file mode 100644 index 0000000..b73117a --- /dev/null +++ b/buildandload.bat @@ -0,0 +1,10 @@ +@echo off +start /wait "building plugin" cmd /c npm run build +rem npm run build +echo errorlevel after npm run build is %errorlevel% +if not errorlevel 0 exit + +rem If we get this far then the .js file got created +echo copy /Y main.js D:\js_projs\sample_plugin\test-vault\.obsidian\plugins\sample-plugin +copy /Y main.js D:\js_projs\sample_plugin\test-vault\.obsidian\plugins\sample-plugin +echo errorlevel after copy is %errorlevel% \ No newline at end of file diff --git a/main.ts b/main.ts new file mode 100644 index 0000000..463bf0c --- /dev/null +++ b/main.ts @@ -0,0 +1,153 @@ +import { + App, HeadingCache, MetadataCache, Notice, + Plugin, + PluginSettingTab, + Setting, + TFile, +} from 'obsidian'; + +interface MyPluginSettings { + concatHeader: string; + pathIncluded: string; +} + +const DEFAULT_SETTINGS: MyPluginSettings = { + concatHeader: '## Reading Log', + pathIncluded: '' +} +async function asyncForEach(array:TFile[], callback: (markFile: TFile) => void) { + for (let index = 0; index < array.length; index++) { + await callback(array[index]); + } +} + +export default class MyPlugin extends Plugin { + settings: MyPluginSettings; + + async performConcatenation() { + let markdownFiles = this.app.vault.getMarkdownFiles(); + if (this.settings.pathIncluded) { + markdownFiles = markdownFiles.filter(e => e.path.startsWith(this.settings.pathIncluded)) + } + + if (!markdownFiles.length) { + new Notice("Can't find files which fit the filter") + return + } + + let content = Array(); + let timenow = Date.now(); + // Await lets you block further execution until this thing is done. It must be paired with async. + + const level = this.settings.concatHeader.split(' ').filter(e => /^#/.test(e))[0]?.split('#').length - 1 || 0 + const title = this.settings.concatHeader.split(' ').slice(1).join(' ') + + await asyncForEach(markdownFiles, async (markFile: TFile) => { + const metadata = this.app.metadataCache.getFileCache(markFile) + + type HeadingWithOffset = [HeadingCache, { sectionEndLine: number }] + + const matchedHeadings: HeadingWithOffset[] = metadata.headings + ?.filter(h => h.level >= level) + .map((h, i, arr) => { + return [ + h, + { sectionEndLine: arr[i + 1]?.position.start.line || undefined } + ] as HeadingWithOffset + }) + .filter(h => h[0].level === level && h[0].heading === title) + || [] + + if (matchedHeadings.length) { + const fileContent = await this.app.vault.read(markFile) + content.push( + matchedHeadings.map(h => { + return fileContent + .split('\n') + .slice(h[0].position.start.line + 1, h[1].sectionEndLine) + .join('\n') + }).join('\n') + ) + } + }); + + const finalizedContents = content.join('\n') + + await this.app.vault.create("Concatenated_Note-" + timenow + ".md", finalizedContents); + new Notice('File: "' + 'Concatenated_Note-' + timenow + '.md' + '" created.'); + } + + async onload() { + console.log('loading plugin'); + + await this.loadSettings(); + + this.addCommand({ + id: 'concatenate-headings', + name: 'Concatenate Headings', + checkCallback: (checking: boolean) => { + let leaf = this.app.workspace.activeLeaf; + if (leaf) { + if (!checking) { + this.performConcatenation(); + } + return true; + } + return false; + }, + callback: () => this.performConcatenation(), + }); + + this.addSettingTab(new SampleSettingTab(this.app, this)); + } + + onunload() { + console.log('unloading plugin'); + } + + async loadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + + async saveSettings() { + await this.saveData(this.settings); + } +} + +class SampleSettingTab extends PluginSettingTab { + plugin: MyPlugin; + + constructor(app: App, plugin: MyPlugin) { + super(app, plugin); + this.plugin = plugin; + } + + display(): void { + let {containerEl} = this; + + containerEl.empty(); + + containerEl.createEl('h2', {text: 'Settings for my awesome plugin.'}); + + new Setting(containerEl) + .setName('Heading Title') + .setDesc('Type the heading (including the hashtags) you would like to concatenate') + .addText(text => text + .setValue(this.plugin.settings.concatHeader) + .onChange(async (value) => { + this.plugin.settings.concatHeader = value; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Folder in which search for') + .setDesc('Specify the folder where plugin will search headers') + .addText(text => text + .setPlaceholder('path/folder/deep') + .setValue(this.plugin.settings.pathIncluded) + .onChange(async (value) => { + this.plugin.settings.pathIncluded = value; + await this.plugin.saveSettings(); + })); + } +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..54c50a8 --- /dev/null +++ b/manifest.json @@ -0,0 +1,10 @@ +{ + "id": "obsidian-concatenate", + "name": "Concatenate", + "version": "0.0.1", + "minAppVersion": "0.0.1", + "description": "This plugin will allow you to put the contents of sections together in one file", + "author": "Eleanor Konik", + "authorUrl": "https://github.com/eleanorkonik/-concatenate", + "isDesktopOnly": false +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c0573df --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "obsidian-sample-plugin", + "version": "0.9.7", + "description": "This is a sample plugin for Obsidian (https://obsidian.md)", + "main": "main.js", + "scripts": { + "dev": "rollup --config rollup.config.js -w", + "build": "rollup --config rollup.config.js" + }, + "keywords": [], + "author": "", + "license": "MIT", + "devDependencies": { + "@rollup/plugin-commonjs": "^15.1.0", + "@rollup/plugin-node-resolve": "^9.0.0", + "@rollup/plugin-typescript": "^6.0.0", + "@types/node": "^14.14.2", + "obsidian": "https://github.com/obsidianmd/obsidian-api/tarball/master", + "rollup": "^2.32.1", + "tslib": "^2.0.3", + "typescript": "^4.0.3" + } +} diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..0c67484 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,19 @@ +import typescript from '@rollup/plugin-typescript'; +import {nodeResolve} from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; + +export default { + input: 'main.ts', + output: { + dir: '.', + sourcemap: 'inline', + format: 'cjs', + exports: 'default' + }, + external: ['obsidian'], + plugins: [ + typescript(), + nodeResolve({browser: true}), + commonjs(), + ] +}; \ No newline at end of file diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..e69de29 diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..38c0fdb --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "inlineSourceMap": true, + "inlineSources": true, + "module": "ESNext", + "target": "es6", + "allowJs": true, + "noImplicitAny": true, + "moduleResolution": "node", + "importHelpers": true, + "lib": [ + "dom", + "es5", + "scripthost", + "es2015" + ], + "noEmitOnError": true, + }, + "include": [ + "**/*.ts" + ] +} diff --git a/versions.json b/versions.json new file mode 100644 index 0000000..be42d9f --- /dev/null +++ b/versions.json @@ -0,0 +1,4 @@ +{ + "1.0.1": "0.9.12", + "1.0.0": "0.9.7" +}