├── .github └── FUNDING.yml ├── .gitignore ├── README.md ├── consts.ts ├── defaultSettings.ts ├── interfaces ├── GeneralContentOptions.ts ├── IndexItemStyle.ts ├── ZottelkeeperPluginSettings.ts └── index.ts ├── main.ts ├── manifest.json ├── models ├── index.ts └── sortOrder.ts ├── package.json ├── rollup.config.js ├── styles.css ├── tsconfig.json ├── utils ├── cleanDisallowedFolders.ts ├── getFrontmatter.ts ├── hasFrontmatter.ts ├── index.ts ├── isInSpecificFolder.ts ├── removeFrontmatter.ts ├── updateFrontmatter.ts └── updateIndexContent.ts └── versions.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: akosbalasko 11 | otechie: # Replace with a single Otechie username 12 | custom: buymeacoffee.com/akosbalasko 13 | -------------------------------------------------------------------------------- /.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 | #wakatime 17 | .wakatime-project -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obsidian Zoottelkeeper 2 | 3 | [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/akosbalasko/zoottelkeeper-obsidian-plugin?style=for-the-badge&sort=semver)](https://github.com/akosbalasko/zoottelkeeper-obsidian-plugin/releases/latest) 4 | ![GitHub All Releases](https://img.shields.io/github/downloads/akosbalasko/zoottelkeeper-obsidian-plugin/total?style=for-the-badge) 5 | 6 | ## Changes in v.0.18.0 7 | 8 | - Include/Exclude folders improved: absolute paths are required, independently from its first character (it can be '/', or simply just start with the name of the folder within the root path to be included/excluded ). 9 | - Specific character is introduced: if you type '*' at the end of the folder, it means that it AND its subdirectories(recursively) will be included or excluded 10 | 11 | ### Example: 12 | Assuming that you have the following directories in the root of your vault: 13 | ``` 14 | Notes 15 | Notes/Daily 16 | Articles 17 | Articles/Science 18 | ``` 19 | 20 | If you would like to include Notes, the Daily within and Articles to be included, but exclude Articles/Science you should set 21 | include property to: 22 | ``` 23 | Notes/* 24 | Articles 25 | ``` 26 | and exclude property to: 27 | ``` 28 | Articles/Science 29 | ``` 30 | 31 | 32 | ## What's new in the latest version (v0.17.0) 33 | - Option to set a **template** for index files 34 | - Numbers sorted correctly in index links (bug: https://github.com/akosbalasko/zoottelkeeper-obsidian-plugin/issues/45) 35 | - Frontmatter separator is configurable (by default it's '---' ) 36 | ## What's new in the latest version (v0.16.1) 37 | 38 | Plenty of new features coming requested by you! Namely, from version 0.16.1 you can: 39 | 40 | - **use sections** ('---' lines) within the index files content, they won't be removed during the content update 41 | - **square brackets** are optional in tags 42 | - **embed child index notes** in the preview ('Embed sub-index content in preview' option in the config) 43 | - **select folders to cover or to be excluded** by the plugin 44 | - trigger the **indexing manually** (see 'Generate index now' button in the config) 45 | - **sort** the index links (ascending or descending) (see 'Index links Order' in the config) 46 | - **emojis** can be set to each row, folders and files can be denoted separately (see 'Emojis' section in the config) 47 | 48 | ## What's new (v0.10.0) 49 | **Customizabe index notes**: From now you can customize your index file, not the whole content will be updated, but the exact list to the notes only (and tags metadata if it is set). 50 | 51 | In order to achieve this, I added 2 extra autogenerated texts to separate the note list to be updated from the other part of the index file. They appear in Edit mode only. 52 | 53 | These are: 54 | - 'Zoottelkeeper: Beginning of the autogenerated index file list' and 55 | - 'Zoottelkeeper: End of the autogenerated index file list' 56 | 57 | Please do not remove them. 58 | 59 | ## 1. General Idea 60 | Following the idea of Nick Milo and the [LYT](https://www.linkingyourthinking.com/) -concept (Linking Your Thinking), an amazing way to bring structure to your files, folder and thoughts is by using **Maps of Content** (MOCs). Because even though Obsidian is generally built around the idea of being 'beyond' folder structures, you generally still need to have some sort of system to store all those juicy insights. 61 | 62 | ## 2. How does it work? 63 | ZoottelKeeper watches the followings: 64 | 65 | - _Creation_ of files in rootFolder and any subfolders within 66 | - _Deletion_ of files in rootFolder and any subfolders within 67 | - _Move_ a file among rootFolder to subFolders 68 | - _Move_ a file among subfolders 69 | 70 | ### 2.1 Introduction 71 | So, the idea behind Zoottelkeeper is to help you generate the base form of these maps automatically. It does so by indexing all the files and folders that lay in a folder, thus creating a link from the file to all it's content. 72 | 73 | ![image](https://user-images.githubusercontent.com/46029522/126865703-c3a3d12f-a88f-42d1-806a-415d9e1afa53.png) --> ![image](https://user-images.githubusercontent.com/46029522/126865758-883888d3-8cf1-496a-aa04-58ae6a4c69a6.png) --> ![image](https://user-images.githubusercontent.com/46029522/126865823-84272e62-8f4f-417c-8af1-e624a02963be.png) 74 | 75 | **(1)** shows the current folder structure. The plugin generates an index-file in each folder, showing all files and folders it contains. An example list **(2)** is shown for the main folder, but the subfolders contain a similar file. Each of these index-files is tagged **(3)** based on your preferences. This then results in the graph view with "folders" **(4)** (it's actually the index-files that are connected, but it looks like folders) and their respective files **(5)**. 76 | 77 | ### 2.2 What's actually cool about this? 78 | So far so good, we've seen that before. The actually nice thing is, if I now move *Folder B* into *Folder A* **(6)**, then the index file will automatically update **(7)**, resulting in the desired graph view **(8)**. 79 | 80 | ![image](https://user-images.githubusercontent.com/46029522/126866100-be3717da-cae6-4550-9e52-7719d00e49f7.png) --> ![image](https://user-images.githubusercontent.com/46029522/126866120-b2b8d0b1-2334-4be9-88d8-84bb825705a6.png) --> ![image](https://user-images.githubusercontent.com/46029522/126866136-ba068748-5698-4ca7-aeff-562ab0c435a0.png) 81 | 82 | ### 2.3 Disclaimer and other used plugins 83 | You might have noticed that you can't see the index files in the folders in view **(1)** and **(6)**, that is because I did not add a prefix to the index-file (so it's automatically named like the folder) and I also use the **Folder Note** plugin, just for the fact that it hides files in folders when they are named like the folder and displays them when you click on the folder (which is super nice for the MOC purpose here too). 84 | 85 | 86 | 87 | Please note that manually created folder notes may be overwritten by Zottelkeeper, please set up your template using the **Templater** plugin. 88 | 89 | ### 2.4 TL;DR 90 | Does this plugin replace the need to think about structure? No. But it could relief you of the tedious work that has to happen when you just want to allocate files to a broad category and, what's even bigger, it will relief you of the pain to manually go through all the files and change their "parent-category whenever a topic gets too big or you want to move it somewhere else. Basically all you have to do is save things where they belong and the plugin will map that basic structure out for you. You can then, on top of that, add whatever MOC, index or tag logic you like. 91 | 92 | ## 3. Installation and Settings 93 | Similarly to any other plugins it is downloadable within Obsidian. Then, after enabling it, you will be able to configure Zoottelkeeper in its config interface. 94 | 95 | ![image](https://user-images.githubusercontent.com/46029522/126864195-4a8c7dd6-54ca-435e-a0bf-5a6520083609.png) 96 | 97 | ### 3.1 Choose your List-Style 98 | There is three different types of lists for you to choose from: 99 | - pure Obsidian links, 100 | - list items (dots) 101 | - links with checkboxes 102 | 103 | ### 3.2 Choose your Index Prefix 104 | Depending on your preferences, you can set any prefix to your index-files (or none at all). (Please note that the prefix must be unique, otherwise, normal notes with the same note name might be recognized as index files, and in this cases they will be updated! 105 | 106 | ### 3.3 Enable Meta Tags 107 | You can choose to add YAML Meta Tags to your automatically generated index-files. 108 | 109 | ### 3.4 Set Custom Meta Tags 110 | You can set one or multiple custom Meta Tags. Since they are displayed in the YAML format, you don't need to add a '#'. 111 | If you're setting multiple tags please make sure to separate them with commas. 112 | 113 | ### 3.5 Additional Things 114 | - The file and the folder are no longer listed in the in the index-file. 115 | 116 | --- 117 | 118 | ### 3.6 Templates 119 | 120 | 1. Install templater plugin (https://github.com/SilentVoid13/Templater) 121 | 2. In the Templater's settings page: 122 | 123 | 1. Set a template folder 124 | 2. Create a template file (based on the example below) and assign it to your folder handled by Zottelkeeper 125 | 3. Set Templater to be triggered on file creation 126 | 127 | 3. In Zoottelkeeper's settings page, specify the full path of your template location like 'templates/zoottel_template.md'. The template can be managed by the **Templater** plugin. 128 | 129 | In order to prevent the generalization of the Zoottelkeeper's metadata in the real files created, please use the following template as a base, which puts Zoottelkeepers placeholders only if the filename ends with the parent folder's name (so it's an index file/ folder note): 130 | 131 | ```markdown 132 | --- 133 | 134 | tags: 135 | 136 | --- 137 | 138 | <%* if (tp.file.title.endsWith(tp.file.folder())) { %> 139 | 140 | %% Zoottelkeeper: Beginning of the autogenerated index file list %% 141 | %% Zoottelkeeper: End of the autogenerated index file list %% 142 | 143 | <%* } %> 144 | 145 | WARNING: PLease make sure that the placeholders (lines starting with %% Zottelkeeper) pasted into Obsidian has double spaces before the ending '%%' characters in each line, otherwise they won't be recognized and therefore the whole index file is going to be regenerated removing the custom texts! 146 | 147 | ``` 148 | 149 | ## Release notes 150 | 151 | ## Appreciation and feedbacks 152 | 153 | Any feedbacks or feature requests are welcome, feel free to [create issues on Zoottelkeeper's repository page](https://github.com/akosbalasko/zoottelkeeper-obsidian-plugin/issues/new)! 154 | 155 | 156 | If you like the plugin, please let me know by giving a star to it on github: [![GitHub Repo stars](https://img.shields.io/github/stars/akosbalasko/zoottelkeeper-obsidian-plugin?style=social)](https://github.com/akosbalasko/zoottelkeeper-obsidian-plugin/stargazers) 157 | 158 | or you can Buy Me A Coffee 159 | 160 | 161 | ## Disclaimer 162 | 163 | **As with every plugin, there is risk of data-loss and I don't give any guarantees or take any responsibility.** -------------------------------------------------------------------------------- /consts.ts: -------------------------------------------------------------------------------- 1 | export const ZOOTTELKEEPER_INDEX_LIST_BEGINNING_TEXT='%% Zoottelkeeper: Beginning of the autogenerated index file list %%' 2 | export const ZOOTTELKEEPER_INDEX_LIST_END_TEXT='%% Zoottelkeeper: End of the autogenerated index file list %%' 3 | -------------------------------------------------------------------------------- /defaultSettings.ts: -------------------------------------------------------------------------------- 1 | import { SortOrder } from 'models'; 2 | import { IndexItemStyle, ZoottelkeeperPluginSettings } from './interfaces' 3 | 4 | export const DEFAULT_SETTINGS: ZoottelkeeperPluginSettings = { 5 | indexPrefix: '_Index_of_', 6 | indexItemStyle: IndexItemStyle.PureLink, 7 | indexTagValue: 'MOC', 8 | indexTagBoolean: true, 9 | indexTagSeparator: ', ', 10 | indexTagLabel: 'tags', 11 | cleanPathBoolean: true, 12 | folderEmoji: ':card_index_dividers:', 13 | fileEmoji: ':page_facing_up:', 14 | enableEmojis: false, 15 | foldersExcluded: '', 16 | foldersIncluded: '', 17 | sortOrder: SortOrder.ASC, 18 | addSquareBrackets: true, 19 | embedSubIndex: false, 20 | templateFile: '', 21 | frontMatterSeparator: '---', 22 | }; 23 | -------------------------------------------------------------------------------- /interfaces/GeneralContentOptions.ts: -------------------------------------------------------------------------------- 1 | 2 | import { TAbstractFile, } from 'obsidian'; 3 | 4 | export interface GeneralContentOptions { 5 | items: Array; 6 | initValue: Array; 7 | func: Function; 8 | } -------------------------------------------------------------------------------- /interfaces/IndexItemStyle.ts: -------------------------------------------------------------------------------- 1 | 2 | export enum IndexItemStyle { 3 | List = 'list', 4 | Checkbox = 'checkbox', 5 | PureLink='pureLink', 6 | } 7 | -------------------------------------------------------------------------------- /interfaces/ZottelkeeperPluginSettings.ts: -------------------------------------------------------------------------------- 1 | import { SortOrder } from './../models'; 2 | import { IndexItemStyle } from './IndexItemStyle'; 3 | 4 | export interface ZoottelkeeperPluginSettings { 5 | indexPrefix: string; 6 | indexItemStyle: IndexItemStyle; 7 | indexTagValue: string; 8 | indexTagBoolean: boolean; 9 | indexTagLabel: string; 10 | cleanPathBoolean: boolean; 11 | indexTagSeparator: string; 12 | folderEmoji: string; 13 | fileEmoji: string; 14 | enableEmojis: boolean; 15 | foldersIncluded: string; 16 | foldersExcluded: string; 17 | sortOrder: SortOrder; 18 | addSquareBrackets: boolean; 19 | embedSubIndex: boolean; 20 | templateFile: string; 21 | frontMatterSeparator: string; 22 | [key: string]: any; 23 | 24 | } -------------------------------------------------------------------------------- /interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ZottelkeeperPluginSettings'; 2 | export * from './GeneralContentOptions'; 3 | export * from './IndexItemStyle'; -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { App, Modal, debounce, Plugin, PluginSettingTab, Setting, TFile, TAbstractFile, } from 'obsidian'; 2 | import { IndexItemStyle } from './interfaces/IndexItemStyle'; 3 | import { GeneralContentOptions, ZoottelkeeperPluginSettings } from './interfaces' 4 | import { isInAllowedFolder, isInDisAllowedFolder, updateFrontmatter, updateIndexContent, removeFrontmatter, hasFrontmatter } from './utils' 5 | import { DEFAULT_SETTINGS } from './defaultSettings'; 6 | import * as emoji from 'node-emoji'; 7 | import { SortOrder } from 'models'; 8 | 9 | export default class ZoottelkeeperPlugin extends Plugin { 10 | settings: ZoottelkeeperPluginSettings; 11 | lastVault: Set; 12 | 13 | triggerUpdateIndexFile = debounce( 14 | this.keepTheZooClean.bind(this, false), 15 | 3000, 16 | true 17 | ); 18 | 19 | async onload(): Promise { 20 | await this.loadSettings(); 21 | this.app.workspace.onLayoutReady(async () => { 22 | this.loadVault(); 23 | console.debug( 24 | `Vault in files: ${JSON.stringify( 25 | this.app.vault.getMarkdownFiles().map((f) => f.path) 26 | )}` 27 | ); 28 | }); 29 | this.registerEvent( 30 | this.app.vault.on('create', this.triggerUpdateIndexFile) 31 | ); 32 | this.registerEvent( 33 | this.app.vault.on('delete', this.triggerUpdateIndexFile) 34 | ); 35 | this.registerEvent( 36 | this.app.vault.on('rename', this.triggerUpdateIndexFile) 37 | ); 38 | 39 | this.addSettingTab(new ZoottelkeeperPluginSettingTab(this.app, this)); 40 | } 41 | loadVault() { 42 | this.lastVault = new Set( 43 | this.app.vault.getMarkdownFiles().map((file) => file.path) 44 | ); 45 | } 46 | async keepTheZooClean(triggeredManually?: boolean) { 47 | console.debug('keeping the zoo clean...'); 48 | if (this.lastVault || triggeredManually) { 49 | const vaultFilePathsSet = new Set( 50 | this.app.vault.getMarkdownFiles().map((file) => file.path) 51 | ); 52 | try { 53 | // getting the changed files using symmetric diff 54 | 55 | let changedFiles = new Set([ 56 | ...Array.from(vaultFilePathsSet).filter( 57 | (currentFile) => !this.lastVault.has(currentFile) 58 | ), 59 | ...Array.from(this.lastVault).filter( 60 | (currentVaultFile) => !vaultFilePathsSet.has(currentVaultFile) 61 | ), 62 | ]); 63 | console.debug( 64 | `changedFiles: ${JSON.stringify(Array.from(changedFiles))}` 65 | ); 66 | // getting index files to be updated 67 | const indexFiles2BUpdated = new Set(); 68 | 69 | for (const changedFile of Array.from(changedFiles)) { 70 | const indexFilePath = this.getIndexFilePath(changedFile); 71 | if (indexFilePath 72 | && isInAllowedFolder(this.settings, indexFilePath) 73 | && !isInDisAllowedFolder(this.settings, indexFilePath)) { 74 | indexFiles2BUpdated.add(indexFilePath); 75 | } 76 | 77 | // getting the parents' index notes of each changed file in order to update their links as well (hierarhical backlinks) 78 | const parentIndexFilePath = this.getIndexFilePath( 79 | this.getParentFolder(changedFile) 80 | ); 81 | if (parentIndexFilePath) indexFiles2BUpdated.add(parentIndexFilePath); 82 | } 83 | console.debug( 84 | `Index files to be updated: ${JSON.stringify( 85 | Array.from(indexFiles2BUpdated) 86 | )}` 87 | ); 88 | 89 | await this.removeDisallowedFoldersIndexes(indexFiles2BUpdated); 90 | // update index files 91 | for (const indexFile of Array.from(indexFiles2BUpdated)) { 92 | await this.generateIndexContents(indexFile); 93 | } 94 | await this.cleanDisallowedFolders(); 95 | 96 | } catch (e) {} 97 | } 98 | this.lastVault = new Set( 99 | this.app.vault.getMarkdownFiles().map((file) => file.path) 100 | ); 101 | } 102 | 103 | onunload() { 104 | console.debug('unloading plugin'); 105 | } 106 | 107 | async loadSettings() { 108 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); 109 | } 110 | 111 | async saveSettings() { 112 | await this.saveData(this.settings); 113 | } 114 | 115 | generateIndexContents = async (indexFile: string): Promise => { 116 | const templateFile = this.app.vault.getAbstractFileByPath(this.settings.templateFile); 117 | let currentTemplateContent = ''; 118 | 119 | if (templateFile instanceof TFile){ 120 | currentTemplateContent = await this.app.vault.cachedRead(templateFile); 121 | } 122 | 123 | let indexTFile = 124 | this.app.vault.getAbstractFileByPath(indexFile) || 125 | (await this.app.vault.create(indexFile, currentTemplateContent)); 126 | 127 | if (indexTFile && indexTFile instanceof TFile) 128 | return this.generateIndexContent(indexTFile); 129 | }; 130 | 131 | 132 | 133 | generateGeneralIndexContent = (options: GeneralContentOptions): Array => { 134 | return options.items 135 | .reduce( 136 | (acc, curr) => { 137 | acc.push(options.func(curr.path, this.isFile(curr))); 138 | return acc; 139 | }, options.initValue); 140 | 141 | } 142 | 143 | generateIndexContent = async (indexTFile: TFile): Promise => { 144 | 145 | let indexContent; 146 | // get subFolders 147 | //const subFolders = indexTFile.parent.children.filter(item => !this.isFile(item)); 148 | //const files = indexTFile.parent.children.filter(item => this.isFile(item)); 149 | 150 | const splitItems = indexTFile.parent.children.reduce( 151 | (acc,curr) => { 152 | if (this.isFile(curr)) 153 | acc['files'].push(curr) 154 | else acc['subFolders'].push(curr); 155 | return acc; 156 | }, {subFolders: [], files: []} 157 | ) 158 | 159 | indexContent = this.generateGeneralIndexContent({ 160 | items: splitItems.subFolders, 161 | func: this.generateIndexFolderItem, 162 | initValue: [], 163 | }) 164 | indexContent = this.generateGeneralIndexContent({ 165 | items: splitItems.files.filter(file => file.name !== indexTFile.name ), 166 | func: this.generateIndexItem, 167 | initValue: indexContent, 168 | }) 169 | 170 | try { 171 | if (indexTFile instanceof TFile){ 172 | 173 | let currentContent = await this.app.vault.cachedRead(indexTFile); 174 | if (currentContent === ''){ 175 | const templateFile = this.app.vault.getAbstractFileByPath(this.settings.templateFile); 176 | 177 | if (templateFile instanceof TFile){ 178 | currentContent = await this.app.vault.cachedRead(templateFile); 179 | } 180 | } 181 | const updatedFrontmatter = hasFrontmatter(currentContent, this.settings.frontMatterSeparator) 182 | ? updateFrontmatter(this.settings, currentContent) 183 | : ''; 184 | 185 | currentContent = removeFrontmatter(currentContent, this.settings.frontMatterSeparator); 186 | const updatedIndexContent = updateIndexContent(this.settings.sortOrder, currentContent, indexContent); 187 | await this.app.vault.modify(indexTFile, `${updatedFrontmatter}${updatedIndexContent}`); 188 | } else { 189 | throw new Error('Creation index as folder is not supported'); 190 | } 191 | } catch (e) { 192 | console.warn('Error during deletion/creation of index files', e); 193 | } 194 | }; 195 | setEmojiPrefix = (isFile: boolean): string => { 196 | return this.settings.enableEmojis 197 | ? isFile 198 | ? emoji.get(this.settings.fileEmoji) 199 | : emoji.get(this.settings.folderEmoji) 200 | : ''; 201 | 202 | } 203 | 204 | generateFormattedIndexItem = (path: string, isFile: boolean): string => { 205 | const realFileName = `${path.split('|')[0]}.md`; 206 | const fileAbstrPath = this.app.vault.getAbstractFileByPath(realFileName); 207 | const embedSubIndexCharacter = this.settings.embedSubIndex && this.isIndexFile(fileAbstrPath) ? '!' : ''; 208 | 209 | switch (this.settings.indexItemStyle) { 210 | case IndexItemStyle.PureLink: 211 | return `${this.setEmojiPrefix(isFile)} ${embedSubIndexCharacter}[[${path}]]`; 212 | case IndexItemStyle.List: 213 | return `- ${this.setEmojiPrefix(isFile)} ${embedSubIndexCharacter}[[${path}]]`; 214 | case IndexItemStyle.Checkbox: 215 | return `- [ ] ${this.setEmojiPrefix(isFile)} ${embedSubIndexCharacter}[[${path}]]` 216 | }; 217 | } 218 | 219 | generateIndexItem = (path: string, isFile: boolean): string => { 220 | let internalFormattedIndex; 221 | if (this.settings.cleanPathBoolean) { 222 | const cleanPath = ( path.endsWith(".md")) 223 | ? path.replace(/\.md$/,'') 224 | : path; 225 | const fileName = cleanPath.split("/").pop(); 226 | internalFormattedIndex = `${cleanPath}|${fileName}`; 227 | } 228 | else { 229 | internalFormattedIndex = path; 230 | } 231 | return this.generateFormattedIndexItem(internalFormattedIndex, isFile); 232 | } 233 | 234 | generateIndexFolderItem = (path: string, isFile: boolean): string => { 235 | return this.generateIndexItem(this.getInnerIndexFilePath(path), isFile); 236 | } 237 | 238 | getInnerIndexFilePath = (folderPath: string): string => { 239 | const folderName = this.getFolderName(folderPath); 240 | return `${folderPath}/${this.settings.indexPrefix}${folderName}.md`; 241 | } 242 | getIndexFilePath = (filePath: string): string => { 243 | const fileAbstrPath = this.app.vault.getAbstractFileByPath(filePath); 244 | 245 | if (this.isIndexFile(fileAbstrPath)) return null; 246 | let parentPath = this.getParentFolder(filePath); 247 | 248 | // if its parent does not exits, then its a moved subfolder, so it should not be updated 249 | const parentTFolder = this.app.vault.getAbstractFileByPath(parentPath); 250 | if (parentPath && parentPath !== '') { 251 | if (!parentTFolder) return undefined; 252 | parentPath = `${parentPath}/`; 253 | } 254 | const parentName = this.getParentFolderName(filePath); 255 | 256 | return `${parentPath}${this.settings.indexPrefix}${parentName}.md`; 257 | }; 258 | 259 | removeDisallowedFoldersIndexes = async (indexFiles: Set): Promise => { 260 | for (const folder of this.settings.foldersExcluded.split('\n').map(f=> f.trim())){ 261 | const innerIndex = this.getInnerIndexFilePath(folder); 262 | indexFiles.delete(innerIndex); 263 | } 264 | } 265 | 266 | cleanDisallowedFolders = async (): Promise => { 267 | for (const folder of this.settings.foldersExcluded.split('\n').map(f=> f.trim())){ 268 | const innerIndex = this.getInnerIndexFilePath(folder); 269 | const indexTFile = this.app.vault.getAbstractFileByPath(innerIndex); 270 | await this.app.vault.delete(indexTFile); 271 | } 272 | } 273 | 274 | getParentFolder = (filePath: string): string => { 275 | const fileFolderArray = filePath.split('/'); 276 | fileFolderArray.pop(); 277 | 278 | return fileFolderArray.join('/'); 279 | }; 280 | 281 | getParentFolderName = (filePath: string): string => { 282 | const parentFolder = this.getParentFolder(filePath); 283 | const fileFolderArray = parentFolder.split('/'); 284 | return fileFolderArray[0] !== '' 285 | ? fileFolderArray[fileFolderArray.length - 1] 286 | : this.app.vault.getName(); 287 | }; 288 | 289 | getFolderName = (folderPath: string): string => { 290 | const folderArray = folderPath.split('/'); 291 | return (folderArray[0] !== '') ? folderArray[folderArray.length - 1] : this.app.vault.getName(); 292 | } 293 | 294 | isIndexFile = (item: TAbstractFile): boolean => { 295 | 296 | return this.isFile(item) 297 | && (this.settings.indexPrefix === '' 298 | ? item.name === item.parent.name 299 | : item.name.startsWith(this.settings.indexPrefix)); 300 | } 301 | 302 | isFile = (item: TAbstractFile): boolean => { 303 | return item instanceof TFile; 304 | } 305 | 306 | } 307 | 308 | class ZoottelkeeperPluginModal extends Modal { 309 | constructor(app: App) { 310 | super(app); 311 | } 312 | } 313 | 314 | class ZoottelkeeperPluginSettingTab extends PluginSettingTab { 315 | plugin: ZoottelkeeperPlugin; 316 | 317 | constructor(app: App, plugin: ZoottelkeeperPlugin) { 318 | super(app, plugin); 319 | this.plugin = plugin; 320 | } 321 | 322 | display(): void { 323 | let { containerEl } = this; 324 | 325 | containerEl.empty(); 326 | containerEl.createEl('h2', { text: 'Zoottelkeeper Settings' }); 327 | containerEl.createEl('h3', { text: 'Folder Settings' }); 328 | 329 | new Setting(containerEl) 330 | .setName('Folders included') 331 | .setDesc( 332 | 'Specify the folders to be handled by Zoottelkeeper. They must be absolute paths starting from the root vault, one per line, example: Notes/ Articles/, which will include Notes and Articles folder in the root folder. Empty list means all of the vault will be handled except the excluded folders. \'*\' can be added to the end, to include the folder\'s subdirectories recursively, e.g. Notes/* Articles/' 333 | ) 334 | .addTextArea((text) => 335 | text 336 | .setPlaceholder('') 337 | .setValue(this.plugin.settings.foldersIncluded) 338 | .onChange(async (value) => { 339 | this.plugin.settings.foldersIncluded = value 340 | .replace(/,/g,'\n') 341 | .split('\n') 342 | .map( 343 | folder=> { 344 | const f = folder.trim(); 345 | return f.startsWith('/') 346 | ? f.substring(1) 347 | : f 348 | }) 349 | .join('\n'); 350 | await this.plugin.saveSettings(); 351 | }) 352 | ); 353 | new Setting(containerEl) 354 | .setName('Folders excluded') 355 | .setDesc( 356 | 'Specify the folders NOT to be handled by Zoottelkeeper. They must be absolute paths starting from the root vault, one per line. Example: "Notes/ Articles/ ", it will exclude Notes and Articles folder in the root folder. * can be added to the end, to exclude the folder\'s subdirectories recursively.' 357 | ) 358 | .addTextArea((text) => 359 | text 360 | .setPlaceholder('') 361 | .setValue(this.plugin.settings.foldersExcluded) 362 | .onChange(async (value) => { 363 | this.plugin.settings.foldersExcluded = value 364 | .replace(/,/g,'\n') 365 | .split('\n') 366 | .map( 367 | folder=> { 368 | const f = folder.trim(); 369 | return f.startsWith('/') 370 | ? f.substring(1) 371 | : f 372 | }) 373 | .join('\n');; 374 | await this.plugin.saveSettings(); 375 | }) 376 | ); 377 | new Setting(containerEl) 378 | .setName('Trigger indexing') 379 | .setDesc( 380 | 'By pushing this button you can trigger the indexing on folders match your include/exclude criterias currently set.' 381 | ) 382 | .addButton((btn) => { 383 | btn.setButtonText('Generate index now') 384 | btn.onClick(async () => { 385 | this.plugin.lastVault = new Set(); 386 | await this.plugin.keepTheZooClean(true); 387 | }) 388 | } 389 | ); 390 | 391 | 392 | containerEl.createEl('h3', { text: 'General Settings' }); 393 | new Setting(containerEl) 394 | .setName("Clean Files") 395 | .setDesc( 396 | "This enables you to only show the files without path and '.md' ending in preview mode." 397 | ) 398 | .addToggle((t) => { 399 | t.setValue(this.plugin.settings.cleanPathBoolean); 400 | t.onChange(async (v) => { 401 | this.plugin.settings.cleanPathBoolean = v; 402 | await this.plugin.saveSettings(); 403 | }); 404 | }); 405 | new Setting(containerEl) 406 | .setName('Index links Order') 407 | .setDesc('Select the order of the links to be sorted in the index files.') 408 | .addDropdown(async (dropdown) => { 409 | dropdown.addOption(SortOrder.ASC, 'Ascending'); 410 | dropdown.addOption(SortOrder.DESC, 'Descending'); 411 | 412 | dropdown.setValue(this.plugin.settings.sortOrder); 413 | dropdown.onChange(async (option) => { 414 | this.plugin.settings.sortOrder = option as SortOrder; 415 | await this.plugin.saveSettings(); 416 | }); 417 | }); 418 | 419 | new Setting(containerEl) 420 | .setName('List Style') 421 | .setDesc('Select the style of the index-list.') 422 | .addDropdown(async (dropdown) => { 423 | dropdown.addOption(IndexItemStyle.PureLink, 'Pure Obsidian link'); 424 | dropdown.addOption(IndexItemStyle.List, 'Listed link'); 425 | dropdown.addOption(IndexItemStyle.Checkbox, 'Checkboxed link'); 426 | 427 | dropdown.setValue(this.plugin.settings.indexItemStyle); 428 | dropdown.onChange(async (option) => { 429 | console.debug('Chosen index item style: ' + option); 430 | this.plugin.settings.indexItemStyle = option as IndexItemStyle; 431 | await this.plugin.saveSettings(); 432 | }); 433 | }); 434 | new Setting(containerEl) 435 | .setName('Embed sub-index content in preview') 436 | .setDesc( 437 | "If you enable this, the plugin will embed the sub-index content in preview mode." 438 | ) 439 | .addToggle((t) => { 440 | t.setValue(this.plugin.settings.embedSubIndex); 441 | t.onChange(async (v) => { 442 | this.plugin.settings.embedSubIndex = v; 443 | await this.plugin.saveSettings(); 444 | }); 445 | }); 446 | 447 | // index prefix 448 | new Setting(containerEl) 449 | .setName('Index Prefix') 450 | .setDesc( 451 | 'Per default the file is named after your folder, but you can prefix it here.' 452 | ) 453 | .addText((text) => 454 | text 455 | .setPlaceholder('') 456 | .setValue(this.plugin.settings.indexPrefix) 457 | .onChange(async (value) => { 458 | console.debug('Index prefix: ' + value); 459 | this.plugin.settings.indexPrefix = value; 460 | await this.plugin.saveSettings(); 461 | }) 462 | ); 463 | new Setting(containerEl) 464 | .setName('Template file') 465 | .setDesc( 466 | 'Set your template file\'s absolute path like "templates/zoottel_template.md"' 467 | ) 468 | .addText((text) => 469 | text 470 | .setPlaceholder('') 471 | .setValue(this.plugin.settings.templateFile) 472 | .onChange(async (value) => { 473 | console.debug('Template file: ' + value); 474 | this.plugin.settings.templateFile = value; 475 | await this.plugin.saveSettings(); 476 | }) 477 | ); 478 | new Setting(containerEl) 479 | .setName('Frontmatter separator') 480 | .setDesc('It specifies the separator string generated before and after the frontmatter, by default its ---') 481 | .addText((text) => 482 | text 483 | .setPlaceholder('') 484 | .setValue(this.plugin.settings.frontMatterSeparator) 485 | .onChange(async (value) => { 486 | this.plugin.settings.frontMatterSeparator = value; 487 | await this.plugin.saveSettings(); 488 | }) 489 | ); 490 | containerEl.createEl('h4', { text: 'Meta Tags' }); 491 | 492 | // Enabling Meta Tags 493 | new Setting(containerEl) 494 | .setName('Enable Meta Tags') 495 | .setDesc( 496 | "You can add Meta Tags at the top of your index-file. This is useful when you're using the index files as MOCs." 497 | ) 498 | .addToggle((t) => { 499 | t.setValue(this.plugin.settings.indexTagBoolean); 500 | t.onChange(async (v) => { 501 | this.plugin.settings.indexTagBoolean = v; 502 | await this.plugin.saveSettings(); 503 | }); 504 | }); 505 | 506 | // setting the meta tag value 507 | const metaTagsSetting = new Setting(containerEl) 508 | .setName('Set Meta Tags') 509 | .setDesc( 510 | 'You can add one or multiple tags to your index-files! There is no need to use "#", just use the exact value of the tags\' separator specified below between the tags.' 511 | ) 512 | .addText((text) => 513 | text 514 | .setPlaceholder('moc') 515 | .setValue(this.plugin.settings.indexTagValue) 516 | .onChange(async (value) => { 517 | this.plugin.settings.indexTagValue = value; 518 | await this.plugin.saveSettings(); 519 | }) 520 | ); 521 | new Setting(containerEl) 522 | .setName('Set the tag\'s label in frontmatter') 523 | .setDesc( 524 | 'Please specify the label of the tags in frontmatter (the text before the colon ):' 525 | ) 526 | .addText((text) => 527 | text 528 | .setPlaceholder('tags') 529 | .setValue(this.plugin.settings.indexTagLabel) 530 | .onChange(async (value) => { 531 | this.plugin.settings.indexTagLabel = value; 532 | await this.plugin.saveSettings(); 533 | }) 534 | ); 535 | new Setting(containerEl) 536 | .setName('Set the tag\'s separator in Frontmatter') 537 | .setDesc( 538 | 'Please specify the separator characters that distinguish the tags in Frontmatter:' 539 | ) 540 | .addText((text) => 541 | text 542 | .setPlaceholder(', ') 543 | .setValue(this.plugin.settings.indexTagSeparator) 544 | .onChange(async (value) => { 545 | this.plugin.settings.indexTagSeparator = value; 546 | await this.plugin.saveSettings(); 547 | }) 548 | ); 549 | new Setting(containerEl) 550 | .setName('Add square brackets around each tags') 551 | .setDesc( 552 | "If you enable this, the plugin will put square brackets around the tags set." 553 | ) 554 | .addToggle((t) => { 555 | t.setValue(this.plugin.settings.addSquareBrackets); 556 | t.onChange(async (v) => { 557 | this.plugin.settings.addSquareBrackets = v; 558 | await this.plugin.saveSettings(); 559 | }); 560 | }); 561 | containerEl.createEl('h4', { text: 'Emojis' }); 562 | 563 | // Enabling Meta Tags 564 | new Setting(containerEl) 565 | .setName('Enable Emojis') 566 | .setDesc("You can set an emoji at the beginning of each index item depending on its type (file or folder). If multiple emojis matches, the first one will be stored." 567 | ) 568 | .addToggle((t) => { 569 | t.setValue(this.plugin.settings.enableEmojis); 570 | t.onChange(async (v) => { 571 | this.plugin.settings.enableEmojis = v; 572 | await this.plugin.saveSettings(); 573 | }); 574 | }); 575 | 576 | 577 | let emojiFolderDesc = 'Set an emoji for folders:' 578 | if (this.plugin.settings.folderEmoji){ 579 | const setFolderEmoji = emoji.search(this.plugin.settings.folderEmoji); 580 | emojiFolderDesc = `Matching Options:${setFolderEmoji[0].emoji} (${setFolderEmoji[0].key})`; 581 | } 582 | const emojiForFoldersSetting = new Setting(containerEl) 583 | .setName('Emojis') 584 | .setDesc(emojiFolderDesc) 585 | .addText((text) => 586 | text.setPlaceholder('card_index_dividers') 587 | .setValue(this.plugin.settings.folderEmoji.replace(/:/g, '')) 588 | .onChange(async (value) => { 589 | if (value !== ''){ 590 | const emojiOptions = emoji.search(value); 591 | emojiForFoldersSetting.setDesc(`Matching Options:${emojiOptions.map(emojOp => emojOp.emoji + "("+emojOp.key+")")}`) 592 | if (emojiOptions.length > 0){ 593 | this.plugin.settings.folderEmoji = `:${emojiOptions[0].key}:`; 594 | await this.plugin.saveSettings(); 595 | } 596 | } else { 597 | emojiForFoldersSetting.setDesc( 598 | 'Set an emoji for folders:' 599 | ) 600 | } 601 | })); 602 | let emojiFileDesc = 'Set an emoji for files:' 603 | if (this.plugin.settings.fileEmoji){ 604 | const setFileEmoji = emoji.search(this.plugin.settings.fileEmoji); 605 | emojiFileDesc = `Matching Options:${setFileEmoji[0].emoji} (${setFileEmoji[0].key})`; 606 | } 607 | 608 | const emojiForFilesSetting = new Setting(containerEl) 609 | .setName('Emojis') 610 | .setDesc(emojiFileDesc) 611 | .addText((text) => 612 | text.setPlaceholder('page_facing_up') 613 | .setValue(this.plugin.settings.fileEmoji.replace(/:/g, '')) 614 | .onChange(async (value) => { 615 | if (value !== ''){ 616 | const emojiOptions = emoji.search(value); 617 | emojiForFilesSetting.setDesc(`Matching Options:${emojiOptions.map(emojOp => emojOp.emoji + "("+emojOp.key+")")}`) 618 | if (emojiOptions.length > 0){ 619 | this.plugin.settings.fileEmoji = `:${emojiOptions[0].key}:`; 620 | await this.plugin.saveSettings(); 621 | } 622 | } else { 623 | emojiForFilesSetting.setDesc( 624 | 'Set an emoji for files:' 625 | ) 626 | } 627 | }) 628 | ); 629 | 630 | } 631 | } 632 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "zoottelkeeper-obsidian-plugin", 3 | "name": "Zoottelkeeper Plugin", 4 | "version": "0.18.0", 5 | "minAppVersion": "0.12.1", 6 | "description": "This plugin automatically creates, maintains and tags MOCs for all your folders.", 7 | "author": "Akos Balasko, Micha Brugger", 8 | "authorUrl": "https://github.com/akosbalasko, https://github.com/michabrugger", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sortOrder'; 2 | -------------------------------------------------------------------------------- /models/sortOrder.ts: -------------------------------------------------------------------------------- 1 | export enum SortOrder { 2 | 'ASC'= 'asc', 3 | 'DESC' = 'desc' 4 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zoottelkeeper-obsidian-plugin", 3 | "version": "0.18.0", 4 | "description": "This plugin automatically creates, maintains and tags MOCs for all your folders.", 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 | "zettelkasten", 12 | "obsidian.md", 13 | "obsidian-plugin" 14 | ], 15 | "author": "Akos Balasko, Micha Brugger", 16 | "license": "MIT", 17 | "devDependencies": { 18 | "@rollup/plugin-commonjs": "18.0.0", 19 | "@rollup/plugin-json": "4.1.0", 20 | "@rollup/plugin-node-resolve": "11.2.1", 21 | "@rollup/plugin-typescript": "8.2.1", 22 | "@types/node": "14.14.37", 23 | "@types/node-emoji": "1.8.1", 24 | "obsidian": "0.12.0", 25 | "rollup": "2.32.1", 26 | "tslib": "2.2.0", 27 | "typescript": "4.2.4" 28 | }, 29 | "dependencies": { 30 | "node-emoji": "1.11.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import json from '@rollup/plugin-json'; 3 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 4 | import commonjs from '@rollup/plugin-commonjs'; 5 | 6 | const isProd = (process.env.BUILD === 'production'); 7 | 8 | const banner = 9 | `/* 10 | THIS IS A GENERATED/BUNDLED FILE BY ROLLUP 11 | if you want to view the source visit the plugins github repository 12 | */ 13 | `; 14 | 15 | export default { 16 | input: 'main.ts', 17 | output: { 18 | dir: '.', 19 | sourcemap: 'inline', 20 | sourcemapExcludeSources: isProd, 21 | format: 'cjs', 22 | exports: 'default', 23 | banner, 24 | }, 25 | external: ['obsidian'], 26 | plugins: [ 27 | typescript(), 28 | nodeResolve({browser: true}), 29 | commonjs(), 30 | json(), 31 | ] 32 | }; -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akosbalasko/zoottelkeeper-obsidian-plugin/c09853c57db51bbdacff663b6436aafc422a5fae/styles.css -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "es2018", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "lib": ["dom", "es5", "scripthost", "es2015"] 13 | }, 14 | "include": ["**/*.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /utils/cleanDisallowedFolders.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akosbalasko/zoottelkeeper-obsidian-plugin/c09853c57db51bbdacff663b6436aafc422a5fae/utils/cleanDisallowedFolders.ts -------------------------------------------------------------------------------- /utils/getFrontmatter.ts: -------------------------------------------------------------------------------- 1 | import { hasFrontmatter } from './hasFrontmatter'; 2 | 3 | export const getFrontmatter = (content: string, separator: string): string => { 4 | return hasFrontmatter(content, separator) 5 | ? `${separator}${content.split(separator)[1]}${separator}` 6 | : '' 7 | } -------------------------------------------------------------------------------- /utils/hasFrontmatter.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export const hasFrontmatter = (content: string, separator: string): boolean => { 4 | return (content.trim().startsWith(separator) && content.split(separator).length > 1); 5 | } -------------------------------------------------------------------------------- /utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './updateFrontmatter'; 2 | export * from './updateIndexContent'; 3 | export * from './removeFrontmatter'; 4 | export * from './hasFrontmatter'; 5 | export * from './getFrontmatter'; 6 | export * from './isInSpecificFolder'; -------------------------------------------------------------------------------- /utils/isInSpecificFolder.ts: -------------------------------------------------------------------------------- 1 | import { ZoottelkeeperPluginSettings } from "../interfaces" 2 | 3 | export const isInAllowedFolder = (settings: ZoottelkeeperPluginSettings, indexFilePath: string): boolean => { 4 | return settings.foldersIncluded === '' || isInSpecificFolder(settings, indexFilePath, 'foldersIncluded'); 5 | 6 | } 7 | 8 | export const isInDisAllowedFolder = (settings: ZoottelkeeperPluginSettings, indexFilePath: string): boolean => { 9 | return isInSpecificFolder(settings, indexFilePath, 'foldersExcluded'); 10 | } 11 | 12 | export const isInSpecificFolder = (settings: ZoottelkeeperPluginSettings, indexFilePath: string, folderType: string): boolean => { 13 | 14 | return !!settings[folderType].replace(/,/g,'\n').split('\n').find((folder: any) => { 15 | return folder.endsWith('*') 16 | ? indexFilePath.startsWith(folder.slice(0, -1).trim()) 17 | : indexFilePath.split(folder).length > 1 && !indexFilePath.split(folder)[1].includes('/'); 18 | }) 19 | } 20 | 21 | -------------------------------------------------------------------------------- /utils/removeFrontmatter.ts: -------------------------------------------------------------------------------- 1 | export const removeFrontmatter = (content: string, separator: string): string => { 2 | return (content.startsWith(separator)&& content.split(separator).length > 1) 3 | ? content.split(separator).slice(2).join(separator) 4 | : content 5 | } 6 | -------------------------------------------------------------------------------- /utils/updateFrontmatter.ts: -------------------------------------------------------------------------------- 1 | import { ZoottelkeeperPluginSettings } from '../interfaces'; 2 | import { getFrontmatter } from './getFrontmatter'; 3 | 4 | export const updateFrontmatter = (settings: ZoottelkeeperPluginSettings, currentContent: string): string => { 5 | 6 | 7 | if (!settings.indexTagBoolean) 8 | return getFrontmatter(currentContent, settings.frontMatterSeparator); 9 | 10 | let currentFrontmatterWithoutSep = `${currentContent.split(settings.frontMatterSeparator)[1]}`; 11 | 12 | if (currentFrontmatterWithoutSep === '') 13 | return '' 14 | else { 15 | let tagLine = currentFrontmatterWithoutSep.split('\n').find(elem => elem.split(':')[0]=== settings.indexTagLabel); 16 | if (!tagLine && settings.indexTagValue && settings.indexTagBoolean){ 17 | tagLine = 'tags:'; 18 | currentFrontmatterWithoutSep = `${currentFrontmatterWithoutSep}${tagLine}\n`; 19 | } 20 | 21 | const taglist = tagLine.split(':')[1].trim(); 22 | const indexTags = settings.indexTagSeparator 23 | ? settings.indexTagValue.split(settings.indexTagSeparator) 24 | : [settings.indexTagValue]; 25 | 26 | let updatedTaglist = taglist.replace(/\[|\]/g,'') 27 | for (const indexTag of indexTags) { 28 | if (!taglist.includes(indexTag)) { 29 | updatedTaglist = !settings.indexTagSeparator 30 | || (updatedTaglist.split(settings.indexTagSeparator).length >= 1 31 | && updatedTaglist.split(settings.indexTagSeparator)[0]!== '') 32 | ? `${updatedTaglist}${settings.indexTagSeparator}${indexTag}` 33 | : indexTag; 34 | } 35 | } 36 | if (settings.addSquareBrackets) 37 | updatedTaglist = `[${updatedTaglist}]`; 38 | const updatedTagLine = `tags: ${updatedTaglist}`; 39 | const regex = new RegExp(tagLine.replace(/\[/g,'\\[').replace(/\]/g,'\\]'), 'g'); 40 | 41 | return settings.indexTagBoolean 42 | ? `${settings.frontMatterSeparator}${currentFrontmatterWithoutSep.replace(regex,updatedTagLine )}${settings.frontMatterSeparator}` 43 | : `${settings.frontMatterSeparator}${currentFrontmatterWithoutSep}${settings.frontMatterSeparator}`; 44 | } 45 | } -------------------------------------------------------------------------------- /utils/updateIndexContent.ts: -------------------------------------------------------------------------------- 1 | import { SortOrder } from './../models'; 2 | import { 3 | ZOOTTELKEEPER_INDEX_LIST_BEGINNING_TEXT, 4 | ZOOTTELKEEPER_INDEX_LIST_END_TEXT 5 | } from './../consts' 6 | 7 | export const updateIndexContent = (sortOrder: SortOrder, currentContent: string, indexContent: Array): string => { 8 | 9 | 10 | const intro = currentContent.split(ZOOTTELKEEPER_INDEX_LIST_BEGINNING_TEXT)[0]; 11 | const outro = currentContent.split(ZOOTTELKEEPER_INDEX_LIST_END_TEXT); 12 | 13 | indexContent = indexContent.sort(function (a, b) { 14 | return a.localeCompare(b, undefined, {numeric: true}); 15 | }); 16 | if(sortOrder === SortOrder.DESC) 17 | indexContent.reverse(); 18 | const content = (currentContent === intro || currentContent === outro[0]) 19 | ? `${ZOOTTELKEEPER_INDEX_LIST_BEGINNING_TEXT}\n${indexContent.join('\n')}\n${ZOOTTELKEEPER_INDEX_LIST_END_TEXT}\n` 20 | : `${intro}${ZOOTTELKEEPER_INDEX_LIST_BEGINNING_TEXT}\n${indexContent.join('\n')}\n${ZOOTTELKEEPER_INDEX_LIST_END_TEXT}${outro[1]}` 21 | return content; 22 | 23 | } 24 | 25 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "0.5.2": "0.12.1", 3 | "0.5.3": "0.12.1", 4 | "0.6.0": "0.12.1", 5 | "0.8.0": "0.12.1", 6 | "0.9.0": "0.12.1", 7 | "0.10.0": "0.12.1", 8 | "0.10.1": "0.12.1", 9 | "0.10.2": "0.12.1", 10 | "0.10.3": "0.12.1", 11 | "0.16.0": "0.12.1", 12 | "0.16.1": "0.12.1", 13 | "0.16.2": "0.12.1", 14 | "0.17.0": "0.12.1", 15 | "0.17.1": "0.12.1", 16 | "0.17.2": "0.12.1", 17 | "0.17.3": "0.12.1", 18 | "0.18.0": "0.12.1" 19 | } --------------------------------------------------------------------------------