Repository: akosbalasko/zoottelkeeper-obsidian-plugin
Branch: master
Commit: c09853c57db5
Files: 26
Total size: 40.4 KB
Directory structure:
gitextract_boktdy6x/
├── .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
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: akosbalasko
otechie: # Replace with a single Otechie username
custom: buymeacoffee.com/akosbalasko
================================================
FILE: .gitignore
================================================
# Intellij
*.iml
.idea
# npm
node_modules
package-lock.json
# build
main.js
*.js.map
# obsidian
data.json
#wakatime
.wakatime-project
================================================
FILE: README.md
================================================
# Obsidian Zoottelkeeper
[](https://github.com/akosbalasko/zoottelkeeper-obsidian-plugin/releases/latest)

## Changes in v.0.18.0
- 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 ).
- 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
### Example:
Assuming that you have the following directories in the root of your vault:
```
Notes
Notes/Daily
Articles
Articles/Science
```
If you would like to include Notes, the Daily within and Articles to be included, but exclude Articles/Science you should set
include property to:
```
Notes/*
Articles
```
and exclude property to:
```
Articles/Science
```
## What's new in the latest version (v0.17.0)
- Option to set a **template** for index files
- Numbers sorted correctly in index links (bug: https://github.com/akosbalasko/zoottelkeeper-obsidian-plugin/issues/45)
- Frontmatter separator is configurable (by default it's '---' )
## What's new in the latest version (v0.16.1)
Plenty of new features coming requested by you! Namely, from version 0.16.1 you can:
- **use sections** ('---' lines) within the index files content, they won't be removed during the content update
- **square brackets** are optional in tags
- **embed child index notes** in the preview ('Embed sub-index content in preview' option in the config)
- **select folders to cover or to be excluded** by the plugin
- trigger the **indexing manually** (see 'Generate index now' button in the config)
- **sort** the index links (ascending or descending) (see 'Index links Order' in the config)
- **emojis** can be set to each row, folders and files can be denoted separately (see 'Emojis' section in the config)
## What's new (v0.10.0)
**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).
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.
These are:
- 'Zoottelkeeper: Beginning of the autogenerated index file list' and
- 'Zoottelkeeper: End of the autogenerated index file list'
Please do not remove them.
## 1. General Idea
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.
## 2. How does it work?
ZoottelKeeper watches the followings:
- _Creation_ of files in rootFolder and any subfolders within
- _Deletion_ of files in rootFolder and any subfolders within
- _Move_ a file among rootFolder to subFolders
- _Move_ a file among subfolders
### 2.1 Introduction
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.
 -->  --> 
**(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)**.
### 2.2 What's actually cool about this?
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)**.
 -->  --> 
### 2.3 Disclaimer and other used plugins
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).
Please note that manually created folder notes may be overwritten by Zottelkeeper, please set up your template using the **Templater** plugin.
### 2.4 TL;DR
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.
## 3. Installation and Settings
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.

### 3.1 Choose your List-Style
There is three different types of lists for you to choose from:
- pure Obsidian links,
- list items (dots)
- links with checkboxes
### 3.2 Choose your Index Prefix
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!
### 3.3 Enable Meta Tags
You can choose to add YAML Meta Tags to your automatically generated index-files.
### 3.4 Set Custom Meta Tags
You can set one or multiple custom Meta Tags. Since they are displayed in the YAML format, you don't need to add a '#'.
If you're setting multiple tags please make sure to separate them with commas.
### 3.5 Additional Things
- The file and the folder are no longer listed in the in the index-file.
---
### 3.6 Templates
1. Install templater plugin (https://github.com/SilentVoid13/Templater)
2. In the Templater's settings page:
1. Set a template folder
2. Create a template file (based on the example below) and assign it to your folder handled by Zottelkeeper
3. Set Templater to be triggered on file creation
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.
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):
```markdown
---
tags:
---
<%* if (tp.file.title.endsWith(tp.file.folder())) { %>
%% Zoottelkeeper: Beginning of the autogenerated index file list %%
%% Zoottelkeeper: End of the autogenerated index file list %%
<%* } %>
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!
```
## Release notes
## Appreciation and feedbacks
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)!
If you like the plugin, please let me know by giving a star to it on github: [](https://github.com/akosbalasko/zoottelkeeper-obsidian-plugin/stargazers)
or you can <a href="https://www.buymeacoffee.com/akosbalasko" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-yellow.png" alt="Buy Me A Coffee" height="41" width="174"></a>
## Disclaimer
**As with every plugin, there is risk of data-loss and I don't give any guarantees or take any responsibility.**
================================================
FILE: consts.ts
================================================
export const ZOOTTELKEEPER_INDEX_LIST_BEGINNING_TEXT='%% Zoottelkeeper: Beginning of the autogenerated index file list %%'
export const ZOOTTELKEEPER_INDEX_LIST_END_TEXT='%% Zoottelkeeper: End of the autogenerated index file list %%'
================================================
FILE: defaultSettings.ts
================================================
import { SortOrder } from 'models';
import { IndexItemStyle, ZoottelkeeperPluginSettings } from './interfaces'
export const DEFAULT_SETTINGS: ZoottelkeeperPluginSettings = {
indexPrefix: '_Index_of_',
indexItemStyle: IndexItemStyle.PureLink,
indexTagValue: 'MOC',
indexTagBoolean: true,
indexTagSeparator: ', ',
indexTagLabel: 'tags',
cleanPathBoolean: true,
folderEmoji: ':card_index_dividers:',
fileEmoji: ':page_facing_up:',
enableEmojis: false,
foldersExcluded: '',
foldersIncluded: '',
sortOrder: SortOrder.ASC,
addSquareBrackets: true,
embedSubIndex: false,
templateFile: '',
frontMatterSeparator: '---',
};
================================================
FILE: interfaces/GeneralContentOptions.ts
================================================
import { TAbstractFile, } from 'obsidian';
export interface GeneralContentOptions {
items: Array<TAbstractFile>;
initValue: Array<string>;
func: Function;
}
================================================
FILE: interfaces/IndexItemStyle.ts
================================================
export enum IndexItemStyle {
List = 'list',
Checkbox = 'checkbox',
PureLink='pureLink',
}
================================================
FILE: interfaces/ZottelkeeperPluginSettings.ts
================================================
import { SortOrder } from './../models';
import { IndexItemStyle } from './IndexItemStyle';
export interface ZoottelkeeperPluginSettings {
indexPrefix: string;
indexItemStyle: IndexItemStyle;
indexTagValue: string;
indexTagBoolean: boolean;
indexTagLabel: string;
cleanPathBoolean: boolean;
indexTagSeparator: string;
folderEmoji: string;
fileEmoji: string;
enableEmojis: boolean;
foldersIncluded: string;
foldersExcluded: string;
sortOrder: SortOrder;
addSquareBrackets: boolean;
embedSubIndex: boolean;
templateFile: string;
frontMatterSeparator: string;
[key: string]: any;
}
================================================
FILE: interfaces/index.ts
================================================
export * from './ZottelkeeperPluginSettings';
export * from './GeneralContentOptions';
export * from './IndexItemStyle';
================================================
FILE: main.ts
================================================
import { App, Modal, debounce, Plugin, PluginSettingTab, Setting, TFile, TAbstractFile, } from 'obsidian';
import { IndexItemStyle } from './interfaces/IndexItemStyle';
import { GeneralContentOptions, ZoottelkeeperPluginSettings } from './interfaces'
import { isInAllowedFolder, isInDisAllowedFolder, updateFrontmatter, updateIndexContent, removeFrontmatter, hasFrontmatter } from './utils'
import { DEFAULT_SETTINGS } from './defaultSettings';
import * as emoji from 'node-emoji';
import { SortOrder } from 'models';
export default class ZoottelkeeperPlugin extends Plugin {
settings: ZoottelkeeperPluginSettings;
lastVault: Set<string>;
triggerUpdateIndexFile = debounce(
this.keepTheZooClean.bind(this, false),
3000,
true
);
async onload(): Promise<void> {
await this.loadSettings();
this.app.workspace.onLayoutReady(async () => {
this.loadVault();
console.debug(
`Vault in files: ${JSON.stringify(
this.app.vault.getMarkdownFiles().map((f) => f.path)
)}`
);
});
this.registerEvent(
this.app.vault.on('create', this.triggerUpdateIndexFile)
);
this.registerEvent(
this.app.vault.on('delete', this.triggerUpdateIndexFile)
);
this.registerEvent(
this.app.vault.on('rename', this.triggerUpdateIndexFile)
);
this.addSettingTab(new ZoottelkeeperPluginSettingTab(this.app, this));
}
loadVault() {
this.lastVault = new Set(
this.app.vault.getMarkdownFiles().map((file) => file.path)
);
}
async keepTheZooClean(triggeredManually?: boolean) {
console.debug('keeping the zoo clean...');
if (this.lastVault || triggeredManually) {
const vaultFilePathsSet = new Set(
this.app.vault.getMarkdownFiles().map((file) => file.path)
);
try {
// getting the changed files using symmetric diff
let changedFiles = new Set([
...Array.from(vaultFilePathsSet).filter(
(currentFile) => !this.lastVault.has(currentFile)
),
...Array.from(this.lastVault).filter(
(currentVaultFile) => !vaultFilePathsSet.has(currentVaultFile)
),
]);
console.debug(
`changedFiles: ${JSON.stringify(Array.from(changedFiles))}`
);
// getting index files to be updated
const indexFiles2BUpdated = new Set<string>();
for (const changedFile of Array.from(changedFiles)) {
const indexFilePath = this.getIndexFilePath(changedFile);
if (indexFilePath
&& isInAllowedFolder(this.settings, indexFilePath)
&& !isInDisAllowedFolder(this.settings, indexFilePath)) {
indexFiles2BUpdated.add(indexFilePath);
}
// getting the parents' index notes of each changed file in order to update their links as well (hierarhical backlinks)
const parentIndexFilePath = this.getIndexFilePath(
this.getParentFolder(changedFile)
);
if (parentIndexFilePath) indexFiles2BUpdated.add(parentIndexFilePath);
}
console.debug(
`Index files to be updated: ${JSON.stringify(
Array.from(indexFiles2BUpdated)
)}`
);
await this.removeDisallowedFoldersIndexes(indexFiles2BUpdated);
// update index files
for (const indexFile of Array.from(indexFiles2BUpdated)) {
await this.generateIndexContents(indexFile);
}
await this.cleanDisallowedFolders();
} catch (e) {}
}
this.lastVault = new Set(
this.app.vault.getMarkdownFiles().map((file) => file.path)
);
}
onunload() {
console.debug('unloading plugin');
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
generateIndexContents = async (indexFile: string): Promise<void> => {
const templateFile = this.app.vault.getAbstractFileByPath(this.settings.templateFile);
let currentTemplateContent = '';
if (templateFile instanceof TFile){
currentTemplateContent = await this.app.vault.cachedRead(templateFile);
}
let indexTFile =
this.app.vault.getAbstractFileByPath(indexFile) ||
(await this.app.vault.create(indexFile, currentTemplateContent));
if (indexTFile && indexTFile instanceof TFile)
return this.generateIndexContent(indexTFile);
};
generateGeneralIndexContent = (options: GeneralContentOptions): Array<string> => {
return options.items
.reduce(
(acc, curr) => {
acc.push(options.func(curr.path, this.isFile(curr)));
return acc;
}, options.initValue);
}
generateIndexContent = async (indexTFile: TFile): Promise<void> => {
let indexContent;
// get subFolders
//const subFolders = indexTFile.parent.children.filter(item => !this.isFile(item));
//const files = indexTFile.parent.children.filter(item => this.isFile(item));
const splitItems = indexTFile.parent.children.reduce(
(acc,curr) => {
if (this.isFile(curr))
acc['files'].push(curr)
else acc['subFolders'].push(curr);
return acc;
}, {subFolders: [], files: []}
)
indexContent = this.generateGeneralIndexContent({
items: splitItems.subFolders,
func: this.generateIndexFolderItem,
initValue: [],
})
indexContent = this.generateGeneralIndexContent({
items: splitItems.files.filter(file => file.name !== indexTFile.name ),
func: this.generateIndexItem,
initValue: indexContent,
})
try {
if (indexTFile instanceof TFile){
let currentContent = await this.app.vault.cachedRead(indexTFile);
if (currentContent === ''){
const templateFile = this.app.vault.getAbstractFileByPath(this.settings.templateFile);
if (templateFile instanceof TFile){
currentContent = await this.app.vault.cachedRead(templateFile);
}
}
const updatedFrontmatter = hasFrontmatter(currentContent, this.settings.frontMatterSeparator)
? updateFrontmatter(this.settings, currentContent)
: '';
currentContent = removeFrontmatter(currentContent, this.settings.frontMatterSeparator);
const updatedIndexContent = updateIndexContent(this.settings.sortOrder, currentContent, indexContent);
await this.app.vault.modify(indexTFile, `${updatedFrontmatter}${updatedIndexContent}`);
} else {
throw new Error('Creation index as folder is not supported');
}
} catch (e) {
console.warn('Error during deletion/creation of index files', e);
}
};
setEmojiPrefix = (isFile: boolean): string => {
return this.settings.enableEmojis
? isFile
? emoji.get(this.settings.fileEmoji)
: emoji.get(this.settings.folderEmoji)
: '';
}
generateFormattedIndexItem = (path: string, isFile: boolean): string => {
const realFileName = `${path.split('|')[0]}.md`;
const fileAbstrPath = this.app.vault.getAbstractFileByPath(realFileName);
const embedSubIndexCharacter = this.settings.embedSubIndex && this.isIndexFile(fileAbstrPath) ? '!' : '';
switch (this.settings.indexItemStyle) {
case IndexItemStyle.PureLink:
return `${this.setEmojiPrefix(isFile)} ${embedSubIndexCharacter}[[${path}]]`;
case IndexItemStyle.List:
return `- ${this.setEmojiPrefix(isFile)} ${embedSubIndexCharacter}[[${path}]]`;
case IndexItemStyle.Checkbox:
return `- [ ] ${this.setEmojiPrefix(isFile)} ${embedSubIndexCharacter}[[${path}]]`
};
}
generateIndexItem = (path: string, isFile: boolean): string => {
let internalFormattedIndex;
if (this.settings.cleanPathBoolean) {
const cleanPath = ( path.endsWith(".md"))
? path.replace(/\.md$/,'')
: path;
const fileName = cleanPath.split("/").pop();
internalFormattedIndex = `${cleanPath}|${fileName}`;
}
else {
internalFormattedIndex = path;
}
return this.generateFormattedIndexItem(internalFormattedIndex, isFile);
}
generateIndexFolderItem = (path: string, isFile: boolean): string => {
return this.generateIndexItem(this.getInnerIndexFilePath(path), isFile);
}
getInnerIndexFilePath = (folderPath: string): string => {
const folderName = this.getFolderName(folderPath);
return `${folderPath}/${this.settings.indexPrefix}${folderName}.md`;
}
getIndexFilePath = (filePath: string): string => {
const fileAbstrPath = this.app.vault.getAbstractFileByPath(filePath);
if (this.isIndexFile(fileAbstrPath)) return null;
let parentPath = this.getParentFolder(filePath);
// if its parent does not exits, then its a moved subfolder, so it should not be updated
const parentTFolder = this.app.vault.getAbstractFileByPath(parentPath);
if (parentPath && parentPath !== '') {
if (!parentTFolder) return undefined;
parentPath = `${parentPath}/`;
}
const parentName = this.getParentFolderName(filePath);
return `${parentPath}${this.settings.indexPrefix}${parentName}.md`;
};
removeDisallowedFoldersIndexes = async (indexFiles: Set<string>): Promise<void> => {
for (const folder of this.settings.foldersExcluded.split('\n').map(f=> f.trim())){
const innerIndex = this.getInnerIndexFilePath(folder);
indexFiles.delete(innerIndex);
}
}
cleanDisallowedFolders = async (): Promise<void> => {
for (const folder of this.settings.foldersExcluded.split('\n').map(f=> f.trim())){
const innerIndex = this.getInnerIndexFilePath(folder);
const indexTFile = this.app.vault.getAbstractFileByPath(innerIndex);
await this.app.vault.delete(indexTFile);
}
}
getParentFolder = (filePath: string): string => {
const fileFolderArray = filePath.split('/');
fileFolderArray.pop();
return fileFolderArray.join('/');
};
getParentFolderName = (filePath: string): string => {
const parentFolder = this.getParentFolder(filePath);
const fileFolderArray = parentFolder.split('/');
return fileFolderArray[0] !== ''
? fileFolderArray[fileFolderArray.length - 1]
: this.app.vault.getName();
};
getFolderName = (folderPath: string): string => {
const folderArray = folderPath.split('/');
return (folderArray[0] !== '') ? folderArray[folderArray.length - 1] : this.app.vault.getName();
}
isIndexFile = (item: TAbstractFile): boolean => {
return this.isFile(item)
&& (this.settings.indexPrefix === ''
? item.name === item.parent.name
: item.name.startsWith(this.settings.indexPrefix));
}
isFile = (item: TAbstractFile): boolean => {
return item instanceof TFile;
}
}
class ZoottelkeeperPluginModal extends Modal {
constructor(app: App) {
super(app);
}
}
class ZoottelkeeperPluginSettingTab extends PluginSettingTab {
plugin: ZoottelkeeperPlugin;
constructor(app: App, plugin: ZoottelkeeperPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
let { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { text: 'Zoottelkeeper Settings' });
containerEl.createEl('h3', { text: 'Folder Settings' });
new Setting(containerEl)
.setName('Folders included')
.setDesc(
'Specify the folders to be handled by Zoottelkeeper. They must be absolute paths starting from the root vault, one per line, example: Notes/ <enter> 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/* <enter> Articles/'
)
.addTextArea((text) =>
text
.setPlaceholder('')
.setValue(this.plugin.settings.foldersIncluded)
.onChange(async (value) => {
this.plugin.settings.foldersIncluded = value
.replace(/,/g,'\n')
.split('\n')
.map(
folder=> {
const f = folder.trim();
return f.startsWith('/')
? f.substring(1)
: f
})
.join('\n');
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Folders excluded')
.setDesc(
'Specify the folders NOT to be handled by Zoottelkeeper. They must be absolute paths starting from the root vault, one per line. Example: "Notes/ <enter> 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.'
)
.addTextArea((text) =>
text
.setPlaceholder('')
.setValue(this.plugin.settings.foldersExcluded)
.onChange(async (value) => {
this.plugin.settings.foldersExcluded = value
.replace(/,/g,'\n')
.split('\n')
.map(
folder=> {
const f = folder.trim();
return f.startsWith('/')
? f.substring(1)
: f
})
.join('\n');;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Trigger indexing')
.setDesc(
'By pushing this button you can trigger the indexing on folders match your include/exclude criterias currently set.'
)
.addButton((btn) => {
btn.setButtonText('Generate index now')
btn.onClick(async () => {
this.plugin.lastVault = new Set();
await this.plugin.keepTheZooClean(true);
})
}
);
containerEl.createEl('h3', { text: 'General Settings' });
new Setting(containerEl)
.setName("Clean Files")
.setDesc(
"This enables you to only show the files without path and '.md' ending in preview mode."
)
.addToggle((t) => {
t.setValue(this.plugin.settings.cleanPathBoolean);
t.onChange(async (v) => {
this.plugin.settings.cleanPathBoolean = v;
await this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName('Index links Order')
.setDesc('Select the order of the links to be sorted in the index files.')
.addDropdown(async (dropdown) => {
dropdown.addOption(SortOrder.ASC, 'Ascending');
dropdown.addOption(SortOrder.DESC, 'Descending');
dropdown.setValue(this.plugin.settings.sortOrder);
dropdown.onChange(async (option) => {
this.plugin.settings.sortOrder = option as SortOrder;
await this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName('List Style')
.setDesc('Select the style of the index-list.')
.addDropdown(async (dropdown) => {
dropdown.addOption(IndexItemStyle.PureLink, 'Pure Obsidian link');
dropdown.addOption(IndexItemStyle.List, 'Listed link');
dropdown.addOption(IndexItemStyle.Checkbox, 'Checkboxed link');
dropdown.setValue(this.plugin.settings.indexItemStyle);
dropdown.onChange(async (option) => {
console.debug('Chosen index item style: ' + option);
this.plugin.settings.indexItemStyle = option as IndexItemStyle;
await this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName('Embed sub-index content in preview')
.setDesc(
"If you enable this, the plugin will embed the sub-index content in preview mode."
)
.addToggle((t) => {
t.setValue(this.plugin.settings.embedSubIndex);
t.onChange(async (v) => {
this.plugin.settings.embedSubIndex = v;
await this.plugin.saveSettings();
});
});
// index prefix
new Setting(containerEl)
.setName('Index Prefix')
.setDesc(
'Per default the file is named after your folder, but you can prefix it here.'
)
.addText((text) =>
text
.setPlaceholder('')
.setValue(this.plugin.settings.indexPrefix)
.onChange(async (value) => {
console.debug('Index prefix: ' + value);
this.plugin.settings.indexPrefix = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Template file')
.setDesc(
'Set your template file\'s absolute path like "templates/zoottel_template.md"'
)
.addText((text) =>
text
.setPlaceholder('')
.setValue(this.plugin.settings.templateFile)
.onChange(async (value) => {
console.debug('Template file: ' + value);
this.plugin.settings.templateFile = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Frontmatter separator')
.setDesc('It specifies the separator string generated before and after the frontmatter, by default its ---')
.addText((text) =>
text
.setPlaceholder('')
.setValue(this.plugin.settings.frontMatterSeparator)
.onChange(async (value) => {
this.plugin.settings.frontMatterSeparator = value;
await this.plugin.saveSettings();
})
);
containerEl.createEl('h4', { text: 'Meta Tags' });
// Enabling Meta Tags
new Setting(containerEl)
.setName('Enable Meta Tags')
.setDesc(
"You can add Meta Tags at the top of your index-file. This is useful when you're using the index files as MOCs."
)
.addToggle((t) => {
t.setValue(this.plugin.settings.indexTagBoolean);
t.onChange(async (v) => {
this.plugin.settings.indexTagBoolean = v;
await this.plugin.saveSettings();
});
});
// setting the meta tag value
const metaTagsSetting = new Setting(containerEl)
.setName('Set Meta Tags')
.setDesc(
'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.'
)
.addText((text) =>
text
.setPlaceholder('moc')
.setValue(this.plugin.settings.indexTagValue)
.onChange(async (value) => {
this.plugin.settings.indexTagValue = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Set the tag\'s label in frontmatter')
.setDesc(
'Please specify the label of the tags in frontmatter (the text before the colon ):'
)
.addText((text) =>
text
.setPlaceholder('tags')
.setValue(this.plugin.settings.indexTagLabel)
.onChange(async (value) => {
this.plugin.settings.indexTagLabel = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Set the tag\'s separator in Frontmatter')
.setDesc(
'Please specify the separator characters that distinguish the tags in Frontmatter:'
)
.addText((text) =>
text
.setPlaceholder(', ')
.setValue(this.plugin.settings.indexTagSeparator)
.onChange(async (value) => {
this.plugin.settings.indexTagSeparator = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Add square brackets around each tags')
.setDesc(
"If you enable this, the plugin will put square brackets around the tags set."
)
.addToggle((t) => {
t.setValue(this.plugin.settings.addSquareBrackets);
t.onChange(async (v) => {
this.plugin.settings.addSquareBrackets = v;
await this.plugin.saveSettings();
});
});
containerEl.createEl('h4', { text: 'Emojis' });
// Enabling Meta Tags
new Setting(containerEl)
.setName('Enable Emojis')
.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."
)
.addToggle((t) => {
t.setValue(this.plugin.settings.enableEmojis);
t.onChange(async (v) => {
this.plugin.settings.enableEmojis = v;
await this.plugin.saveSettings();
});
});
let emojiFolderDesc = 'Set an emoji for folders:'
if (this.plugin.settings.folderEmoji){
const setFolderEmoji = emoji.search(this.plugin.settings.folderEmoji);
emojiFolderDesc = `Matching Options:${setFolderEmoji[0].emoji} (${setFolderEmoji[0].key})`;
}
const emojiForFoldersSetting = new Setting(containerEl)
.setName('Emojis')
.setDesc(emojiFolderDesc)
.addText((text) =>
text.setPlaceholder('card_index_dividers')
.setValue(this.plugin.settings.folderEmoji.replace(/:/g, ''))
.onChange(async (value) => {
if (value !== ''){
const emojiOptions = emoji.search(value);
emojiForFoldersSetting.setDesc(`Matching Options:${emojiOptions.map(emojOp => emojOp.emoji + "("+emojOp.key+")")}`)
if (emojiOptions.length > 0){
this.plugin.settings.folderEmoji = `:${emojiOptions[0].key}:`;
await this.plugin.saveSettings();
}
} else {
emojiForFoldersSetting.setDesc(
'Set an emoji for folders:'
)
}
}));
let emojiFileDesc = 'Set an emoji for files:'
if (this.plugin.settings.fileEmoji){
const setFileEmoji = emoji.search(this.plugin.settings.fileEmoji);
emojiFileDesc = `Matching Options:${setFileEmoji[0].emoji} (${setFileEmoji[0].key})`;
}
const emojiForFilesSetting = new Setting(containerEl)
.setName('Emojis')
.setDesc(emojiFileDesc)
.addText((text) =>
text.setPlaceholder('page_facing_up')
.setValue(this.plugin.settings.fileEmoji.replace(/:/g, ''))
.onChange(async (value) => {
if (value !== ''){
const emojiOptions = emoji.search(value);
emojiForFilesSetting.setDesc(`Matching Options:${emojiOptions.map(emojOp => emojOp.emoji + "("+emojOp.key+")")}`)
if (emojiOptions.length > 0){
this.plugin.settings.fileEmoji = `:${emojiOptions[0].key}:`;
await this.plugin.saveSettings();
}
} else {
emojiForFilesSetting.setDesc(
'Set an emoji for files:'
)
}
})
);
}
}
================================================
FILE: manifest.json
================================================
{
"id": "zoottelkeeper-obsidian-plugin",
"name": "Zoottelkeeper Plugin",
"version": "0.18.0",
"minAppVersion": "0.12.1",
"description": "This plugin automatically creates, maintains and tags MOCs for all your folders.",
"author": "Akos Balasko, Micha Brugger",
"authorUrl": "https://github.com/akosbalasko, https://github.com/michabrugger",
"isDesktopOnly": false
}
================================================
FILE: models/index.ts
================================================
export * from './sortOrder';
================================================
FILE: models/sortOrder.ts
================================================
export enum SortOrder {
'ASC'= 'asc',
'DESC' = 'desc'
}
================================================
FILE: package.json
================================================
{
"name": "zoottelkeeper-obsidian-plugin",
"version": "0.18.0",
"description": "This plugin automatically creates, maintains and tags MOCs for all your folders.",
"main": "main.js",
"scripts": {
"dev": "rollup --config rollup.config.js -w",
"build": "rollup --config rollup.config.js --environment BUILD:production"
},
"keywords": [
"zettelkasten",
"obsidian.md",
"obsidian-plugin"
],
"author": "Akos Balasko, Micha Brugger",
"license": "MIT",
"devDependencies": {
"@rollup/plugin-commonjs": "18.0.0",
"@rollup/plugin-json": "4.1.0",
"@rollup/plugin-node-resolve": "11.2.1",
"@rollup/plugin-typescript": "8.2.1",
"@types/node": "14.14.37",
"@types/node-emoji": "1.8.1",
"obsidian": "0.12.0",
"rollup": "2.32.1",
"tslib": "2.2.0",
"typescript": "4.2.4"
},
"dependencies": {
"node-emoji": "1.11.0"
}
}
================================================
FILE: rollup.config.js
================================================
import typescript from '@rollup/plugin-typescript';
import json from '@rollup/plugin-json';
import {nodeResolve} from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
const isProd = (process.env.BUILD === 'production');
const banner =
`/*
THIS IS A GENERATED/BUNDLED FILE BY ROLLUP
if you want to view the source visit the plugins github repository
*/
`;
export default {
input: 'main.ts',
output: {
dir: '.',
sourcemap: 'inline',
sourcemapExcludeSources: isProd,
format: 'cjs',
exports: 'default',
banner,
},
external: ['obsidian'],
plugins: [
typescript(),
nodeResolve({browser: true}),
commonjs(),
json(),
]
};
================================================
FILE: styles.css
================================================
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "es2018",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"lib": ["dom", "es5", "scripthost", "es2015"]
},
"include": ["**/*.ts"]
}
================================================
FILE: utils/cleanDisallowedFolders.ts
================================================
================================================
FILE: utils/getFrontmatter.ts
================================================
import { hasFrontmatter } from './hasFrontmatter';
export const getFrontmatter = (content: string, separator: string): string => {
return hasFrontmatter(content, separator)
? `${separator}${content.split(separator)[1]}${separator}`
: ''
}
================================================
FILE: utils/hasFrontmatter.ts
================================================
export const hasFrontmatter = (content: string, separator: string): boolean => {
return (content.trim().startsWith(separator) && content.split(separator).length > 1);
}
================================================
FILE: utils/index.ts
================================================
export * from './updateFrontmatter';
export * from './updateIndexContent';
export * from './removeFrontmatter';
export * from './hasFrontmatter';
export * from './getFrontmatter';
export * from './isInSpecificFolder';
================================================
FILE: utils/isInSpecificFolder.ts
================================================
import { ZoottelkeeperPluginSettings } from "../interfaces"
export const isInAllowedFolder = (settings: ZoottelkeeperPluginSettings, indexFilePath: string): boolean => {
return settings.foldersIncluded === '' || isInSpecificFolder(settings, indexFilePath, 'foldersIncluded');
}
export const isInDisAllowedFolder = (settings: ZoottelkeeperPluginSettings, indexFilePath: string): boolean => {
return isInSpecificFolder(settings, indexFilePath, 'foldersExcluded');
}
export const isInSpecificFolder = (settings: ZoottelkeeperPluginSettings, indexFilePath: string, folderType: string): boolean => {
return !!settings[folderType].replace(/,/g,'\n').split('\n').find((folder: any) => {
return folder.endsWith('*')
? indexFilePath.startsWith(folder.slice(0, -1).trim())
: indexFilePath.split(folder).length > 1 && !indexFilePath.split(folder)[1].includes('/');
})
}
================================================
FILE: utils/removeFrontmatter.ts
================================================
export const removeFrontmatter = (content: string, separator: string): string => {
return (content.startsWith(separator)&& content.split(separator).length > 1)
? content.split(separator).slice(2).join(separator)
: content
}
================================================
FILE: utils/updateFrontmatter.ts
================================================
import { ZoottelkeeperPluginSettings } from '../interfaces';
import { getFrontmatter } from './getFrontmatter';
export const updateFrontmatter = (settings: ZoottelkeeperPluginSettings, currentContent: string): string => {
if (!settings.indexTagBoolean)
return getFrontmatter(currentContent, settings.frontMatterSeparator);
let currentFrontmatterWithoutSep = `${currentContent.split(settings.frontMatterSeparator)[1]}`;
if (currentFrontmatterWithoutSep === '')
return ''
else {
let tagLine = currentFrontmatterWithoutSep.split('\n').find(elem => elem.split(':')[0]=== settings.indexTagLabel);
if (!tagLine && settings.indexTagValue && settings.indexTagBoolean){
tagLine = 'tags:';
currentFrontmatterWithoutSep = `${currentFrontmatterWithoutSep}${tagLine}\n`;
}
const taglist = tagLine.split(':')[1].trim();
const indexTags = settings.indexTagSeparator
? settings.indexTagValue.split(settings.indexTagSeparator)
: [settings.indexTagValue];
let updatedTaglist = taglist.replace(/\[|\]/g,'')
for (const indexTag of indexTags) {
if (!taglist.includes(indexTag)) {
updatedTaglist = !settings.indexTagSeparator
|| (updatedTaglist.split(settings.indexTagSeparator).length >= 1
&& updatedTaglist.split(settings.indexTagSeparator)[0]!== '')
? `${updatedTaglist}${settings.indexTagSeparator}${indexTag}`
: indexTag;
}
}
if (settings.addSquareBrackets)
updatedTaglist = `[${updatedTaglist}]`;
const updatedTagLine = `tags: ${updatedTaglist}`;
const regex = new RegExp(tagLine.replace(/\[/g,'\\[').replace(/\]/g,'\\]'), 'g');
return settings.indexTagBoolean
? `${settings.frontMatterSeparator}${currentFrontmatterWithoutSep.replace(regex,updatedTagLine )}${settings.frontMatterSeparator}`
: `${settings.frontMatterSeparator}${currentFrontmatterWithoutSep}${settings.frontMatterSeparator}`;
}
}
================================================
FILE: utils/updateIndexContent.ts
================================================
import { SortOrder } from './../models';
import {
ZOOTTELKEEPER_INDEX_LIST_BEGINNING_TEXT,
ZOOTTELKEEPER_INDEX_LIST_END_TEXT
} from './../consts'
export const updateIndexContent = (sortOrder: SortOrder, currentContent: string, indexContent: Array<string>): string => {
const intro = currentContent.split(ZOOTTELKEEPER_INDEX_LIST_BEGINNING_TEXT)[0];
const outro = currentContent.split(ZOOTTELKEEPER_INDEX_LIST_END_TEXT);
indexContent = indexContent.sort(function (a, b) {
return a.localeCompare(b, undefined, {numeric: true});
});
if(sortOrder === SortOrder.DESC)
indexContent.reverse();
const content = (currentContent === intro || currentContent === outro[0])
? `${ZOOTTELKEEPER_INDEX_LIST_BEGINNING_TEXT}\n${indexContent.join('\n')}\n${ZOOTTELKEEPER_INDEX_LIST_END_TEXT}\n`
: `${intro}${ZOOTTELKEEPER_INDEX_LIST_BEGINNING_TEXT}\n${indexContent.join('\n')}\n${ZOOTTELKEEPER_INDEX_LIST_END_TEXT}${outro[1]}`
return content;
}
================================================
FILE: versions.json
================================================
{
"0.5.2": "0.12.1",
"0.5.3": "0.12.1",
"0.6.0": "0.12.1",
"0.8.0": "0.12.1",
"0.9.0": "0.12.1",
"0.10.0": "0.12.1",
"0.10.1": "0.12.1",
"0.10.2": "0.12.1",
"0.10.3": "0.12.1",
"0.16.0": "0.12.1",
"0.16.1": "0.12.1",
"0.16.2": "0.12.1",
"0.17.0": "0.12.1",
"0.17.1": "0.12.1",
"0.17.2": "0.12.1",
"0.17.3": "0.12.1",
"0.18.0": "0.12.1"
}
gitextract_boktdy6x/ ├── .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
SYMBOL INDEX (19 symbols across 7 files)
FILE: consts.ts
constant ZOOTTELKEEPER_INDEX_LIST_BEGINNING_TEXT (line 1) | const ZOOTTELKEEPER_INDEX_LIST_BEGINNING_TEXT='%% Zoottelkeeper: Beginni...
constant ZOOTTELKEEPER_INDEX_LIST_END_TEXT (line 2) | const ZOOTTELKEEPER_INDEX_LIST_END_TEXT='%% Zoottelkeeper: End of the au...
FILE: defaultSettings.ts
constant DEFAULT_SETTINGS (line 4) | const DEFAULT_SETTINGS: ZoottelkeeperPluginSettings = {
FILE: interfaces/GeneralContentOptions.ts
type GeneralContentOptions (line 4) | interface GeneralContentOptions {
FILE: interfaces/IndexItemStyle.ts
type IndexItemStyle (line 2) | enum IndexItemStyle {
FILE: interfaces/ZottelkeeperPluginSettings.ts
type ZoottelkeeperPluginSettings (line 4) | interface ZoottelkeeperPluginSettings {
FILE: main.ts
class ZoottelkeeperPlugin (line 9) | class ZoottelkeeperPlugin extends Plugin {
method onload (line 19) | async onload(): Promise<void> {
method loadVault (line 41) | loadVault() {
method keepTheZooClean (line 46) | async keepTheZooClean(triggeredManually?: boolean) {
method onunload (line 103) | onunload() {
method loadSettings (line 107) | async loadSettings() {
method saveSettings (line 111) | async saveSettings() {
class ZoottelkeeperPluginModal (line 308) | class ZoottelkeeperPluginModal extends Modal {
method constructor (line 309) | constructor(app: App) {
class ZoottelkeeperPluginSettingTab (line 314) | class ZoottelkeeperPluginSettingTab extends PluginSettingTab {
method constructor (line 317) | constructor(app: App, plugin: ZoottelkeeperPlugin) {
method display (line 322) | display(): void {
FILE: models/sortOrder.ts
type SortOrder (line 1) | enum SortOrder {
Condensed preview — 26 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (47K chars).
[
{
"path": ".github/FUNDING.yml",
"chars": 637,
"preview": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [u"
},
{
"path": ".gitignore",
"chars": 153,
"preview": "# Intellij\r\n*.iml\r\n.idea\r\n\r\n# npm\r\nnode_modules\r\npackage-lock.json\r\n\r\n# build\r\nmain.js\r\n*.js.map\r\n\r\n# obsidian\r\ndata.jso"
},
{
"path": "README.md",
"chars": 9875,
"preview": "# Obsidian Zoottelkeeper\r\n\r\n[: "
},
{
"path": "utils/hasFrontmatter.ts",
"chars": 174,
"preview": "\n\nexport const hasFrontmatter = (content: string, separator: string): boolean => {\n return (content.trim().startsWith"
},
{
"path": "utils/index.ts",
"chars": 217,
"preview": "export * from './updateFrontmatter';\nexport * from './updateIndexContent';\nexport * from './removeFrontmatter';\nexport *"
},
{
"path": "utils/isInSpecificFolder.ts",
"chars": 918,
"preview": "import { ZoottelkeeperPluginSettings } from \"../interfaces\"\n\nexport const isInAllowedFolder = (settings: ZoottelkeeperPl"
},
{
"path": "utils/removeFrontmatter.ts",
"chars": 244,
"preview": "export const removeFrontmatter = (content: string, separator: string): string => {\n return (content.startsWith(separa"
},
{
"path": "utils/updateFrontmatter.ts",
"chars": 2133,
"preview": "import { ZoottelkeeperPluginSettings } from '../interfaces';\nimport { getFrontmatter } from './getFrontmatter'; \n\nexport"
},
{
"path": "utils/updateIndexContent.ts",
"chars": 1003,
"preview": "import { SortOrder } from './../models';\nimport {\n ZOOTTELKEEPER_INDEX_LIST_BEGINNING_TEXT,\n ZOOTTELKEEPER_INDEX_L"
},
{
"path": "versions.json",
"chars": 354,
"preview": "{\n\t\"0.5.2\": \"0.12.1\",\n\t\"0.5.3\": \"0.12.1\",\n\t\"0.6.0\": \"0.12.1\",\n\t\"0.8.0\": \"0.12.1\",\n\t\"0.9.0\": \"0.12.1\",\n\t\"0.10.0\": \"0.12.1"
}
]
About this extraction
This page contains the full source code of the akosbalasko/zoottelkeeper-obsidian-plugin GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 26 files (40.4 KB), approximately 11.2k tokens, and a symbol index with 19 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.