Last active
April 13, 2026 16:21
-
-
Save kherge/ea5e1f38c24c001ece409fba6305a7e2 to your computer and use it in GitHub Desktop.
A collection of utilities as a custom user script for the Templater plugin in Obsidian.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * The base class for the find utility classes. | |
| * | |
| * When a tag is provided for inclusion or exclusion, the tag expression | |
| * is converted into a regular expression. The tag expression supports the | |
| * wildcard character `*`, which matches anything after the string. For | |
| * example, the tag expression `project/*` would match any tag that starts | |
| * with `#project/`, such as `#project/alpha` or `#project/beta`. | |
| */ | |
| class FindBase { | |
| /** | |
| * The tags to exclude. | |
| */ | |
| _excludedTags = []; | |
| /** | |
| * The tags to include. | |
| */ | |
| _includedTags = []; | |
| /** | |
| * Excludes a tag from the search. | |
| * | |
| * @param {string} tag The tag expression. | |
| * | |
| * @returns {FindMarkdownFiles} For method chaining. | |
| */ | |
| excludeTag(tag) { | |
| this._excludedTags.push(this.#toRegExp(tag)); | |
| return this; | |
| } | |
| /** | |
| * Exclude multiple tags from the search. | |
| * | |
| * @param {string[]} tags The tags to exclude. | |
| * | |
| * @returns {FindMarkdownFiles} For method chaining. | |
| */ | |
| excludeTags(...tags) { | |
| tags.flat().forEach(tag => this.excludeTag(tag)); | |
| return this; | |
| } | |
| /** | |
| * Includes a tag from the search. | |
| * | |
| * @param {string} tag The tag expression. | |
| * | |
| * @returns {FindMarkdownFiles} For method chaining. | |
| */ | |
| includeTag(tag) { | |
| this._includedTags.push(this.#toRegExp(tag)); | |
| return this; | |
| } | |
| /** | |
| * Include multiple tags from the search. | |
| * | |
| * @param {string[]} tags The tags to include. | |
| * | |
| * @returns {FindMarkdownFiles} For method chaining. | |
| */ | |
| includeTags(...tags) { | |
| tags.flat().forEach(tag => this.includeTag(tag)); | |
| return this; | |
| } | |
| /** | |
| * Checks if one or more tags have been excluded. | |
| * | |
| * @param {string[]} tags The tags to check. | |
| * | |
| * @return {boolean} If the file is excluded. | |
| */ | |
| _isExcludedByTag(...tags) { | |
| if (this._excludedTags.length > 0) { | |
| return tags | |
| .some(tag => this | |
| ._excludedTags | |
| .some(expression => expression.test(tag))); | |
| } | |
| return false; | |
| } | |
| /** | |
| * Checks if one or more tags have been included. | |
| * | |
| * @param {string[]} tags The tags to check. | |
| * | |
| * @return {boolean} If the file is included. | |
| */ | |
| _isIncludedByTag(...tags) { | |
| if (this._includedTags.length > 0) { | |
| return tags | |
| .some(tag => this | |
| ._includedTags | |
| .some(expression => expression.test(tag))); | |
| } | |
| return true; | |
| } | |
| /** | |
| * Converts a tag expression into a regular expression. | |
| * | |
| * @param {string} tag The tag expression to convert. | |
| * | |
| * @returns {RegExp} The converted regular expression. | |
| */ | |
| #toRegExp(tag) { | |
| const expression = RegExp | |
| .escape(tag) | |
| .replace(/^\x23/, '') | |
| .replace(/\\\*/, '.*'); | |
| return new RegExp(`^${expression}$`); | |
| } | |
| } | |
| /** | |
| * A utility class for finding Markdown files. | |
| */ | |
| class FindMarkdownFiles extends FindBase { | |
| /** | |
| * The folders to exclude. | |
| */ | |
| #excludedFolders = []; | |
| /** | |
| * The folders to include. | |
| */ | |
| #includedFolders = []; | |
| /** | |
| * Excludes a folder from the search. | |
| * | |
| * Any file found will be excluded if its folder matches the given value. | |
| * This is not a prefix check, this is using the complete folder path for | |
| * comparison (TFile#parent.path). | |
| * | |
| * @param {string} folder The folder to exclude. | |
| * | |
| * @returns {FindMarkdownFiles} For method chaining. | |
| */ | |
| excludeFolder(folder) { | |
| this.#excludedFolders.push(folder); | |
| return this; | |
| } | |
| /** | |
| * Searches the vault for Markdown files based on the given criteria. | |
| * | |
| * @returns {TFile[]} The found files. | |
| */ | |
| getFiles() { | |
| const files = []; | |
| for (const file of app.vault.getMarkdownFiles()) { | |
| if (this.#isExcludedByFolder(file) || !this.#isIncludedByFolder(file)) { | |
| continue; | |
| } | |
| if (this._includedTags.length > 0 || this._excludedTags.length > 0) { | |
| const tags = this.#getTags(file); | |
| if (this._isExcludedByTag(...tags) || !this._isIncludedByTag(...tags)) { | |
| continue; | |
| } | |
| } | |
| files.push(file); | |
| } | |
| return files; | |
| } | |
| /** | |
| * Includes a folder in the search. | |
| * | |
| * Any file found will be included if its folder matches the given value. | |
| * This is not a prefix check, this is using the complete folder path for | |
| * comparison (TFile#parent.path). | |
| * | |
| * @param {string} folder The folder to include. | |
| * | |
| * @returns {FindMarkdownFiles} For method chaining. | |
| */ | |
| includeFolder(folder) { | |
| this.#includedFolders.push(folder); | |
| return this; | |
| } | |
| /** | |
| * Returns all tags for a file. | |
| * | |
| * This will return the tags from both the front matter and the contents | |
| * of the file. For consistency, the tags from the contents will have the | |
| * `#` prefix removed to match the format used in the front matter. | |
| * | |
| * @param {TFile} file The file with tags. | |
| * | |
| * @return {string[]} The tags for the file. | |
| */ | |
| #getTags(file) { | |
| const cache = app.metadataCache.getFileCache(file); | |
| const tags = new Set(); | |
| cache | |
| ?.tags | |
| ?.map(tag => tags.add(tag.tag.replace(/^\x23/, ''))); | |
| cache | |
| ?.frontmatter | |
| ?.tags | |
| ?.map(tag => tags.add(tag)); | |
| return Array.from(tags); | |
| } | |
| /** | |
| * Checks if a file has been excluded based on its folder. | |
| * | |
| * @param {TFile} file The file to check. | |
| * | |
| * @return {boolean} If the file is excluded. | |
| */ | |
| #isExcludedByFolder(file) { | |
| if (this.#excludedFolders.length > 0) { | |
| return this.#excludedFolders.includes(file.parent.path); | |
| } | |
| return false; | |
| } | |
| /** | |
| * Checks if a file has been included based on its folder. | |
| * | |
| * @param {TFile} file The file to check. | |
| * | |
| * @return {boolean} If the file is included. | |
| */ | |
| #isIncludedByFolder(file) { | |
| if (this.#includedFolders.length > 0) { | |
| return this.#includedFolders.includes(file.parent.path); | |
| } | |
| return true; | |
| } | |
| } | |
| /** | |
| * A utility class for prompting for one or more Markdown files. | |
| */ | |
| class ChooseMarkdownFiles extends FindMarkdownFiles { | |
| /** | |
| * The properties to use for display text. | |
| */ | |
| #displayProperties = []; | |
| /** | |
| * The maximum number of files allowed. | |
| */ | |
| #max = Infinity; | |
| /** | |
| * The minimum number of files required. | |
| */ | |
| #min = 0; | |
| /** | |
| * The prompt text to use for the file selection. | |
| */ | |
| #prompt = "Choose a file"; | |
| /** | |
| * The Templater instance. | |
| */ | |
| #templater; | |
| /** | |
| * Initializes the builder. | |
| * | |
| * @param {Templater} templater The Templater instance. | |
| */ | |
| constructor(templater) { | |
| super(); | |
| this.#templater = templater; | |
| } | |
| /** | |
| * Adds a property to use for the display text of the file options. | |
| * | |
| * The property is retrieved from the file's front matter and used as the | |
| * text to display in the file selection modal. If multiple properties are | |
| * added, the first property with a value will be used. If no properties | |
| * are added or if none of the properties have a value, the file name will | |
| * be used. | |
| * | |
| * @param {string} property The property name. | |
| * | |
| * @returns {ChooseMarkdownFiles} For method chaining. | |
| */ | |
| displayProperty(property) { | |
| this.#displayProperties.push(property); | |
| return this; | |
| } | |
| /** | |
| * Prompts the user to select one or more files. | |
| * | |
| * @return {Promise<TFile[]>} The selected files. | |
| */ | |
| async getFiles() { | |
| const chosen = []; | |
| let files = this.#getFilesWithDisplayText(); | |
| for (let i = 0; i < this.#max && files.length > 0; i++) { | |
| const file = await this.#promptForFile(files, chosen.length); | |
| if (file) { | |
| chosen.push(file); | |
| files.splice(files.indexOf(file), 1); | |
| } else { | |
| break; | |
| } | |
| } | |
| return chosen; | |
| } | |
| /** | |
| * Sets the maximum number of files that can be selected. | |
| * | |
| * @param {number} count The maximum number of files. | |
| * | |
| * @returns {ChooseMarkdownFiles} For method chaining. | |
| */ | |
| max(count) { | |
| this.#max = count; | |
| return this; | |
| } | |
| /** | |
| * Sets the minimum number of files that must be selected. | |
| * | |
| * @param {number} count The minimum number of files. | |
| * | |
| * @returns {ChooseMarkdownFiles} For method chaining. | |
| */ | |
| min(count) { | |
| this.#min = count; | |
| return this; | |
| } | |
| /** | |
| * Sets the text for the prompt. | |
| * | |
| * @param {string} prompt The prompt text. | |
| * | |
| * @returns {ChooseMarkdownFiles} For method chaining. | |
| */ | |
| prompt(prompt) { | |
| this.#prompt = prompt; | |
| return this; | |
| } | |
| /** | |
| * Searches the vault for Markdown files based on the given criteria. | |
| * | |
| * This function also sets the display text in the prompt for each | |
| * file based on the display properties that have been configured. | |
| * | |
| * @returns {TFile[]} The found files. | |
| */ | |
| #getFilesWithDisplayText() { | |
| return super | |
| .getFiles() | |
| .map(file => { | |
| this.#setDisplayText(file); | |
| return file; | |
| }); | |
| } | |
| /** | |
| * Generates the prompt text to display. | |
| * | |
| * @param {number} count The number of files selected. | |
| * | |
| * @returns {string} The prompt to display. | |
| */ | |
| #getPrompt(count) { | |
| let prompt = this.#prompt; | |
| if (this.#max !== Infinity && count < this.#max) { | |
| prompt = `[${count + 1}/${this.#max}] ${prompt}`; | |
| } | |
| if (this.#min === 0 || count >= this.#min) { | |
| prompt += " (Press ESC to finish)"; | |
| } | |
| return prompt; | |
| } | |
| /** | |
| * Prompts the user to select a file. | |
| * | |
| * @param {TFile[]} files The files to choose from. | |
| * @param {number} count The number of files selected. | |
| * | |
| * @returns {Promise<TFile|null>} The selected file, if any. | |
| */ | |
| async #promptForFile(files, count) { | |
| return await this.#templater.system.suggester( | |
| file => file.displayText ?? file.path, | |
| files, | |
| !(count >= this.#min), | |
| this.#getPrompt(count) | |
| ); | |
| } | |
| /** | |
| * Sets the display text for a file in a prompt. | |
| * | |
| * @param {TFile} file The file to set. | |
| */ | |
| #setDisplayText(file) { | |
| const cache = app.metadataCache.getFileCache(file); | |
| for (const property of this.#displayProperties) { | |
| const value = cache?.frontmatter?.[property]; | |
| if (Array.isArray(value)) { | |
| file.displayText = value[0]; | |
| break | |
| } else if (typeof value === 'string') { | |
| file.displayText = value; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * A utility class for finding tags. | |
| */ | |
| class FindTags extends FindBase { | |
| /** | |
| * Searches the vault for tags based on the given criteria. | |
| * | |
| * @returns {string[]} The found tags. | |
| */ | |
| getTags() { | |
| return Object | |
| .keys(app.metadataCache.getTags()) | |
| .map(tag => tag.replace(/^\x23/, '')) | |
| .filter(tag => !this._isExcludedByTag(tag)) | |
| .filter(tag => this._isIncludedByTag(tag)); | |
| } | |
| } | |
| /** | |
| * A utility class for prompting for one or more tags. | |
| */ | |
| class ChooseTags extends FindTags { | |
| /** | |
| * The maximum number of tags allowed. | |
| */ | |
| #max = Infinity; | |
| /** | |
| * The minimum number of tags required. | |
| */ | |
| #min = 0; | |
| /** | |
| * The prompt text to use for the tag selection. | |
| */ | |
| #prompt = "Choose a tag"; | |
| /** | |
| * The Templater instance. | |
| */ | |
| #templater; | |
| /** | |
| * Initializes the builder. | |
| * | |
| * @param {Templater} templater The Templater instance. | |
| */ | |
| constructor(templater) { | |
| super(); | |
| this.#templater = templater; | |
| } | |
| /** | |
| * Prompts the user to select one or more tags. | |
| * | |
| * @return {Promise<string[]>} The selected tags. | |
| */ | |
| async getTags() { | |
| const chosen = []; | |
| let tags = super.getTags(); | |
| tags.sort(); | |
| for (let i = 0; i < this.#max && tags.length > 0; i++) { | |
| const tag = await this.#promptForTag(tags, chosen.length); | |
| if (tag) { | |
| chosen.push(tag); | |
| tags.splice(tags.indexOf(tag), 1); | |
| } else { | |
| break; | |
| } | |
| } | |
| return chosen; | |
| } | |
| /** | |
| * Sets the maximum number of tags that can be selected. | |
| * | |
| * @param {number} count The maximum number of tags. | |
| * | |
| * @returns {ChooseTags} For method chaining. | |
| */ | |
| max(count) { | |
| this.#max = count; | |
| return this; | |
| } | |
| /** | |
| * Sets the minimum number of tags that must be selected. | |
| * | |
| * @param {number} count The minimum number of tags. | |
| * | |
| * @returns {ChooseTags} For method chaining. | |
| */ | |
| min(count) { | |
| this.#min = count; | |
| return this; | |
| } | |
| /** | |
| * Sets the text for the prompt. | |
| * | |
| * @param {string} prompt The prompt text. | |
| * | |
| * @returns {ChooseTags} For method chaining. | |
| */ | |
| prompt(prompt) { | |
| this.#prompt = prompt; | |
| return this; | |
| } | |
| /** | |
| * Generates the prompt text to display. | |
| * | |
| * @param {number} count The number of files selected. | |
| * | |
| * @returns {string} The prompt to display. | |
| */ | |
| #getPrompt(count) { | |
| let prompt = this.#prompt; | |
| if (this.#max !== Infinity && count < this.#max) { | |
| prompt = `[${count + 1}/${this.#max}] ${prompt}`; | |
| } | |
| if (this.#min === 0 || count >= this.#min) { | |
| prompt += " (Press ESC to finish)"; | |
| } | |
| return prompt; | |
| } | |
| /** | |
| * Prompts the user to select a tag. | |
| * | |
| * @param {string[]} tags The tags to choose from. | |
| * @param {number} count The number of tags selected. | |
| * | |
| * @returns {Promise<string|null>} The selected tag, if any. | |
| */ | |
| async #promptForTag(tags, count) { | |
| return await this.#templater.system.suggester( | |
| tag => tag, | |
| tags, | |
| !(count >= this.#min), | |
| this.#getPrompt(count) | |
| ); | |
| } | |
| } | |
| module.exports = { | |
| /** | |
| * Renders a file as a Wikilink. | |
| * | |
| * @param {TFile} file The file to render. | |
| * | |
| * @returns {string} The rendered Wikilink. | |
| */ | |
| asLink(file) { | |
| if (Array.isArray(file)) { | |
| file = file[0]; | |
| } | |
| if (!file) { | |
| return ''; | |
| } | |
| const link = ['[[', file.basename]; | |
| if (file.displayText) { | |
| link.push('|', file.displayText); | |
| } | |
| link.push(']]'); | |
| return link.join(''); | |
| }, | |
| /** | |
| * Prompts the user to select one or more Markdown files. | |
| * | |
| * A new builder instance is returned that will allow a search for vault | |
| * Markdown files to be performed based on various criteria. The search | |
| * is not performed until the getFiles() method is called on the builder. | |
| * | |
| * @param {Templater} templater The Templater instance. | |
| * | |
| * @returns {ChooseMarkdownFiles} The new builder. | |
| */ | |
| chooseMarkdownFiles(templater) { | |
| return new ChooseMarkdownFiles(templater); | |
| }, | |
| /** | |
| * Prompts the user to select one or more tags. | |
| * | |
| * A new builder instance is returned that will allow a search for vault | |
| * tags to be performed based on various criteria. The search is not | |
| * performed until the getTags() method is called on the builder. | |
| * | |
| * @param {Templater} templater The Templater instance. | |
| * | |
| * @returns {ChooseTags} The new builder. | |
| */ | |
| chooseTags(templater) { | |
| return new ChooseTags(templater); | |
| }, | |
| /** | |
| * Finds Markdown files based on the given criteria. | |
| * | |
| * A new builder instance is returned that will allow a search for vault | |
| * Markdown files to be performed based on various criteria. The search | |
| * is not performed until the getFiles() method is called on the builder. | |
| * | |
| * @returns {FindMarkdownFiles} The new builder. | |
| */ | |
| findMarkdownFiles() { | |
| return new FindMarkdownFiles(); | |
| }, | |
| /** | |
| * Finds tags based on the given criteria. | |
| * | |
| * A new builder instance is returned that will allow a search for vault | |
| * tags to be performed based on various criteria. The search is not | |
| * performed until the getTags() method is called on the builder. | |
| * | |
| * @returns {FindTags} The new builder. | |
| */ | |
| findTags() { | |
| return new FindTags(); | |
| }, | |
| /** | |
| * Flattens multiple values into a single array, removing duplicates. | |
| * | |
| * @param {...(any[]|any)} values The values to flatten. | |
| * | |
| * @returns {any[]} The flattened array. | |
| */ | |
| flatArray(...values) { | |
| const merged = new Set(); | |
| for (const value of values) { | |
| if (Array.isArray(value)) { | |
| value.flat().forEach(item => merged.add(item)); | |
| } else { | |
| merged.add(value); | |
| } | |
| } | |
| return Array.from(merged); | |
| }, | |
| /** | |
| * Requires the user to respond to at least the first prompt. | |
| * | |
| * @param {Templater} tp The Templater instance. | |
| * @param {string} required The required prompt to display. | |
| * @param {...string} optional The optional prompts to display. | |
| * | |
| * @returns {string[]} The responses. | |
| */ | |
| async requireFirstInput(tp, required, ...optional) { | |
| const responses = [await this.requireInput(tp, required)]; | |
| for (const prompt of optional) { | |
| const response = (await tp.system.prompt(prompt))?.trim(); | |
| if (response) { | |
| responses.push(response); | |
| } | |
| } | |
| return responses; | |
| }, | |
| /** | |
| * Requires the user to respond to a prompt. | |
| * | |
| * @param {Templater} tp The Templater instance. | |
| * @param {string} prompt The prompt to display. | |
| * | |
| * @returns {string} The response. | |
| */ | |
| async requireInput(tp, prompt) { | |
| const response = await tp.system.prompt(prompt, null, true); | |
| return response?.trim(); | |
| }, | |
| /** | |
| * Requires the user to respond to multiple prompts. | |
| * | |
| * @param {Templater} tp The Templater instance. | |
| * @param {...string} prompts The prompts to display. | |
| * | |
| * @returns {string[]} The responses. | |
| */ | |
| async requireInputs(tp, ...prompts) { | |
| const responses = []; | |
| for (const prompt of prompts) { | |
| responses.push(await this.requireInput(tp, prompt)); | |
| } | |
| return responses; | |
| }, | |
| /** | |
| * Serializes the given JavaScript value into a YAML string. | |
| * | |
| * If the value is undefined, an empty string is returned. This allows for | |
| * optional values to be easily handled in templates without rendering the | |
| * string "undefined". If a prefix is provided, it will be prepended to the | |
| * YAML string. | |
| * | |
| * @param {Templater} tp The Templater instance. | |
| * @param {any} value The value to serialize. | |
| * @param {string} prefix The string to prepend. | |
| * | |
| * @returns {string} The serialized YAML string. | |
| */ | |
| toYaml(tp, value, prefix = "") { | |
| if (value === "" || value === undefined) { | |
| return ''; | |
| } | |
| return prefix + tp.obsidian.stringifyYaml(value).trim(); | |
| }, | |
| /** | |
| * Validates the given value using a regular expression. | |
| * | |
| * @param {string} value The value to validate. | |
| * @param {RegExp|string} regex The validation expression. | |
| * @param {string} [message] The error message. | |
| * | |
| * @return {string} The value that was validated. | |
| */ | |
| validate(tp, value, regex, message = "The value is not valid.") { | |
| if (!value.match(regex)) { | |
| new tp.obsidian.Notice(message); | |
| throw new Error(message); | |
| } | |
| return value; | |
| }, | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment