mirror of
https://github.com/netajam/obsidian_note_uid_generator.git
synced 2026-07-22 05:45:32 +00:00
V 1.0.0
This commit is contained in:
parent
44056d29a0
commit
85f274f314
15 changed files with 3648 additions and 211 deletions
170
README.md
170
README.md
|
|
@ -1,94 +1,124 @@
|
|||
# Obsidian Sample Plugin
|
||||
# UID Generator Plugin for Obsidian
|
||||
|
||||
This is a sample plugin for Obsidian (https://obsidian.md).
|
||||
## Overview
|
||||
|
||||
This project uses TypeScript to provide type checking and documentation.
|
||||
The repo depends on the latest plugin API (obsidian.d.ts) in TypeScript Definition format, which contains TSDoc comments describing what it does.
|
||||
The UID Generator plugin for Obsidian provides tools to create and manage unique identifiers (UIDs) for your notes directly within their frontmatter metadata. It allows for manual and automatic UID generation, customization of the metadata key and copy formats, and bulk operations within folders. This helps in creating stable, unique references for your notes, useful for linking, scripting, or external systems.
|
||||
|
||||
This sample plugin demonstrates some of the basic functionality the plugin API can do.
|
||||
- Adds a ribbon icon, which shows a Notice when clicked.
|
||||
- Adds a command "Open Sample Modal" which opens a Modal.
|
||||
- Adds a plugin setting tab to the settings page.
|
||||
- Registers a global click event and output 'click' to the console.
|
||||
- Registers a global interval which logs 'setInterval' to the console.
|
||||
## Features
|
||||
|
||||
## First time developing plugins?
|
||||
* **Generate/Update UID:** Manually generate a new UID for the current note, optionally overwriting any existing UID under the configured key.
|
||||
* **Create UID If Missing:** Manually generate a UID for the current note *only* if one doesn't already exist.
|
||||
* **Remove UID:** Manually remove the UID from the current note's frontmatter.
|
||||
* **Copy UID:** Copy the UID of the current note to the clipboard.
|
||||
* **Copy title + UID:** Copy the title and UID of the current note (or multiple selected notes) to the clipboard, using a customizable format.
|
||||
* **Automatic UID Generation:** Automatically add a UID to notes upon creation or opening if they lack one.
|
||||
* Configurable scope (entire vault or specific folder).
|
||||
* Ability to exclude specific folders.
|
||||
* Never overwrites existing UIDs during automatic generation.
|
||||
* **Clear UIDs in Folder:** Remove all UIDs (using the configured key) from notes within a specified folder and its subfolders, with confirmation.
|
||||
* **Customizable UID Key:** Choose the frontmatter key name for your UIDs (e.g., `uid`, `id`, `noteId`).
|
||||
* **Customizable Copy Format:** Define templates for how title/UID information is copied.
|
||||
* **Ribbon Icon:** Quick access to "Create UID if missing" for the current note.
|
||||
* **Context Menu Actions:**
|
||||
* Right-click a folder: Copy titles+UIDs for all notes inside.
|
||||
* Right-click a single note: Copy Title+UID.
|
||||
* Right-click multiple selected notes: Copy titles+UIDs for the selection (uses undocumented `files-menu` event).
|
||||
* **Folder Path Suggestions:** Autocomplete suggestions for folder paths in settings.
|
||||
|
||||
Quick starting guide for new plugin devs:
|
||||
## Installation
|
||||
|
||||
- Check if [someone already developed a plugin for what you want](https://obsidian.md/plugins)! There might be an existing plugin similar enough that you can partner up with.
|
||||
- Make a copy of this repo as a template with the "Use this template" button (login to GitHub if you don't see it).
|
||||
- Clone your repo to a local development folder. For convenience, you can place this folder in your `.obsidian/plugins/your-plugin-name` folder.
|
||||
- Install NodeJS, then run `npm i` in the command line under your repo folder.
|
||||
- Run `npm run dev` to compile your plugin from `main.ts` to `main.js`.
|
||||
- Make changes to `main.ts` (or create new `.ts` files). Those changes should be automatically compiled into `main.js`.
|
||||
- Reload Obsidian to load the new version of your plugin.
|
||||
- Enable plugin in settings window.
|
||||
- For updates to the Obsidian API run `npm update` in the command line under your repo folder.
|
||||
1. Download the plugin files (`main.js`, `manifest.json`, `styles.css`) from the latest release on the GitHub repository (or build them yourself).
|
||||
2. Create a new folder named `obsidian-note-uid-generator` inside your Obsidian vault's plugins folder (`YourVault/.obsidian/plugins/`).
|
||||
3. Copy the downloaded `main.js`, `manifest.json`, and `styles.css` files into the `obsidian-note-uid-generator` folder.
|
||||
4. Restart Obsidian or reload plugins.
|
||||
5. Go to Obsidian Settings -> Community plugins.
|
||||
6. Find "UID Generator" in the list of installed plugins and enable it.
|
||||
|
||||
## Releasing new releases
|
||||
## Usage
|
||||
|
||||
- Update your `manifest.json` with your new version number, such as `1.0.1`, and the minimum Obsidian version required for your latest release.
|
||||
- Update your `versions.json` file with `"new-plugin-version": "minimum-obsidian-version"` so older versions of Obsidian can download an older version of your plugin that's compatible.
|
||||
- Create new GitHub release using your new version number as the "Tag version". Use the exact version number, don't include a prefix `v`. See here for an example: https://github.com/obsidianmd/obsidian-sample-plugin/releases
|
||||
- Upload the files `manifest.json`, `main.js`, `styles.css` as binary attachments. Note: The manifest.json file must be in two places, first the root path of your repository and also in the release.
|
||||
- Publish the release.
|
||||
### Commands
|
||||
|
||||
> You can simplify the version bump process by running `npm version patch`, `npm version minor` or `npm version major` after updating `minAppVersion` manually in `manifest.json`.
|
||||
> The command will bump version in `manifest.json` and `package.json`, and add the entry for the new version to `versions.json`
|
||||
The plugin provides the following commands accessible from the command palette (Ctrl+P or Cmd+P):
|
||||
|
||||
## Adding your plugin to the community plugin list
|
||||
* **`UID Generator: Generate/Update [YourKeyName] (Overwrites)`:** Creates a new UID for the current note. If a UID already exists under the configured key name (`[YourKeyName]`), it will be **replaced**.
|
||||
* **`UID Generator: Create [YourKeyName] if missing`:** Creates a new UID for the current note **only if** one doesn't already exist under the configured key name.
|
||||
* **`UID Generator: Remove [YourKeyName] from current note`:** Deletes the UID key and value from the current note's frontmatter.
|
||||
* **`UID Generator: Copy [YourKeyName] of current note`:** Copies the UID value to the clipboard. (Only available if the current note has a UID).
|
||||
* **`UID Generator: Copy title + [YourKeyName]`:** Copies the current note's title and UID using the configured format. (Always available if a note is open).
|
||||
* **`UID Generator: Copy titles+[YourKeyName]s for selected files`:** Copies the titles and UIDs for all *Markdown* files currently selected in the file explorer, using the configured format. (Only available when the file explorer is active and has Markdown files selected).
|
||||
|
||||
- Check the [plugin guidelines](https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines).
|
||||
- Publish an initial version.
|
||||
- Make sure you have a `README.md` file in the root of your repo.
|
||||
- Make a pull request at https://github.com/obsidianmd/obsidian-releases to add your plugin.
|
||||
### Ribbon Icon
|
||||
|
||||
## How to use
|
||||
* A ribbon icon (looks like a scan/search symbol) is added to the left sidebar.
|
||||
* Clicking this icon performs the **`Create [YourKeyName] if missing`** action on the currently active note.
|
||||
|
||||
- Clone this repo.
|
||||
- Make sure your NodeJS is at least v16 (`node --version`).
|
||||
- `npm i` or `yarn` to install dependencies.
|
||||
- `npm run dev` to start compilation in watch mode.
|
||||
### Context Menus
|
||||
|
||||
## Manually installing the plugin
|
||||
* **Right-clicking a Folder** in the file explorer: Provides an option `Copy titles+[YourKeyName]s from "[FolderName]"`. This copies the titles and UIDs of all Markdown notes within that folder (and subfolders) according to your format settings.
|
||||
* **Right-clicking a Single Markdown File:** Provides an option `Copy Title+[YourKeyName]`. This copies the title and UID for that specific file.
|
||||
* **Right-clicking multiple selected files:** Provides an option `Copy titles+[YourKeyName]s for X selected`. This copies the titles and UIDs for all selected Markdown files.
|
||||
* **Note:** This specific multi-file context menu relies on an *undocumented* Obsidian event (`files-menu`). While functional, it could potentially break in future Obsidian updates. The Command Palette option provides a more stable alternative for multi-file selection.
|
||||
|
||||
- Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`.
|
||||
### Settings
|
||||
|
||||
## Improve code quality with eslint (optional)
|
||||
- [ESLint](https://eslint.org/) is a tool that analyzes your code to quickly find problems. You can run ESLint against your plugin to find common bugs and ways to improve your code.
|
||||
- To use eslint with this project, make sure to install eslint from terminal:
|
||||
- `npm install -g eslint`
|
||||
- To use eslint to analyze this project use this command:
|
||||
- `eslint main.ts`
|
||||
- eslint will then create a report with suggestions for code improvement by file and line number.
|
||||
- If your source code is in a folder, such as `src`, you can use eslint with this command to analyze all files in that folder:
|
||||
- `eslint .\src\`
|
||||
Access the plugin settings from Obsidian Settings -> Community Plugins -> UID Generator:
|
||||
|
||||
## Funding URL
|
||||
* **General:**
|
||||
* **UID Metadata Key:** Set the frontmatter key name used for storing UIDs (default: `uid`). Avoid spaces.
|
||||
* **Automatic UID Generation:**
|
||||
* **Enable Automatic UID Generation:** Toggle the automatic creation of UIDs on/off.
|
||||
* **Generation Scope:** Choose `Entire Vault` or `Specific Folder`.
|
||||
* **Target Folder for Auto-Generation:** (Visible if Scope is 'Specific Folder') Enter the path to the folder where auto-generation should occur. Uses folder path suggestions.
|
||||
* **Excluded Folders:** Click "Manage Exclusions" to open a modal where you can search, add, or remove folders that should be ignored by automatic generation. The current list is displayed below the button.
|
||||
* **Copy Format:**
|
||||
* **Format (UID exists):** Define the template for copied text when a UID is present. Use placeholders `{title}`, `{uid}`, `{uidKey}`. (Default: `{title} - {uidKey}: {uid}`)
|
||||
* **Format (UID missing):** Define the template for copied text when a UID is missing. Use placeholders `{title}`, `{uidKey}`. (Default: `{title} - No {uidKey}`)
|
||||
* **Manual UID Clearing:**
|
||||
* **Folder to clear UIDs from:** Specify the vault path for the bulk removal action. Uses folder path suggestions.
|
||||
* **Clear UIDs Now:** Button to initiate the removal process for the specified folder.
|
||||
* **Warning:** This is irreversible.
|
||||
* A confirmation modal will appear.
|
||||
* If automatic UID generation is enabled, it will be temporarily disabled as a safety measure when you confirm the deletion. You will be notified and can re-enable it afterwards.
|
||||
|
||||
You can include funding URLs where people who use your plugin can financially support it.
|
||||
## Example Usage
|
||||
|
||||
The simple way is to set the `fundingUrl` field to your link in your `manifest.json` file:
|
||||
* **Ensure a Note Has a Unique ID:** Open the note, open the command palette, run `UID Generator: Create uid if missing`.
|
||||
* **Link Using UID:** Open a note, run `UID Generator: Copy uid`, paste the UID into another note's link or alias.
|
||||
* **Auto-Assign IDs to New Notes in Inbox:** Enable Automatic Generation, set Scope to 'Specific Folder', set Target Folder to `Inbox`.
|
||||
* **Clean Up Old IDs:** Set 'Folder to clear UIDs from' to `Archives/Old Project`, click 'Clear UIDs Now', confirm.
|
||||
* **Get List of Project Notes with IDs:** Right-click the `Projects/Current Project` folder, select `Copy titles+uids from "Current Project"`.
|
||||
|
||||
```json
|
||||
{
|
||||
"fundingUrl": "https://buymeacoffee.com"
|
||||
}
|
||||
```
|
||||
## Development
|
||||
|
||||
If you have multiple URLs, you can also do:
|
||||
For developers interested in contributing:
|
||||
|
||||
```json
|
||||
{
|
||||
"fundingUrl": {
|
||||
"Buy Me a Coffee": "https://buymeacoffee.com",
|
||||
"GitHub Sponsor": "https://github.com/sponsors",
|
||||
"Patreon": "https://www.patreon.com/"
|
||||
}
|
||||
}
|
||||
```
|
||||
### Setup
|
||||
|
||||
## API Documentation
|
||||
1. Clone the GitHub repository to your local machine.
|
||||
2. Navigate to the repository directory.
|
||||
3. Install dependencies: `npm install` (or `yarn install`).
|
||||
|
||||
See https://github.com/obsidianmd/obsidian-api
|
||||
### Build
|
||||
|
||||
* To compile the TypeScript code to JavaScript (`main.js`), run: `npm run build` (or `yarn build`).
|
||||
* For development, you can often use a watch command like `npm run dev` (if configured in `package.json`) to automatically rebuild on changes.
|
||||
|
||||
### Code Structure
|
||||
|
||||
* `src/main.ts`: Main plugin class (`UIDGenerator`), `onload`, `onunload`, registration logic.
|
||||
* `src/settings.ts`: Settings interface (`UIDGeneratorSettings`), defaults (`DEFAULT_SETTINGS`), and the settings tab UI class (`UIDSettingTab`).
|
||||
* `src/commands.ts`: Contains handler functions for commands, context menu actions, and event listeners.
|
||||
* `src/uidUtils.ts`: Core utility functions for generating, getting, setting, removing, and formatting UIDs.
|
||||
* `src/ui/`: Contains UI component classes:
|
||||
* `FolderSuggest.ts`
|
||||
* `ConfirmationModal.ts`
|
||||
* `FolderExclusionModal.ts`
|
||||
* `src/obsidian.d.ts`: Contains TypeScript declarations for undocumented Obsidian APIs used (like `files-menu`).
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions, issues, and feature requests are welcome! Please feel free to check the [issues page]([link-to-your-issues-page](https://github.com/Netajam/obsidian_note_uid_generator/issues)) or submit a pull request on the [GitHub repository]([link-to-your-repo](https://github.com/Netajam/obsidian_note_uid_generator)).
|
||||
|
||||
## License
|
||||
|
||||
This plugin is licensed under the MIT License. See the LICENSE file for details.
|
||||
|
|
@ -15,7 +15,7 @@ const context = await esbuild.context({
|
|||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["main.ts"],
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
|
|
|
|||
134
main.ts
134
main.ts
|
|
@ -1,134 +0,0 @@
|
|||
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
|
||||
|
||||
// Remember to rename these classes and interfaces!
|
||||
|
||||
interface MyPluginSettings {
|
||||
mySetting: string;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: MyPluginSettings = {
|
||||
mySetting: 'default'
|
||||
}
|
||||
|
||||
export default class MyPlugin extends Plugin {
|
||||
settings: MyPluginSettings;
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
||||
// This creates an icon in the left ribbon.
|
||||
const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => {
|
||||
// Called when the user clicks the icon.
|
||||
new Notice('This is a notice!');
|
||||
});
|
||||
// Perform additional things with the ribbon
|
||||
ribbonIconEl.addClass('my-plugin-ribbon-class');
|
||||
|
||||
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
|
||||
const statusBarItemEl = this.addStatusBarItem();
|
||||
statusBarItemEl.setText('Status Bar Text');
|
||||
|
||||
// This adds a simple command that can be triggered anywhere
|
||||
this.addCommand({
|
||||
id: 'open-sample-modal-simple',
|
||||
name: 'Open sample modal (simple)',
|
||||
callback: () => {
|
||||
new SampleModal(this.app).open();
|
||||
}
|
||||
});
|
||||
// This adds an editor command that can perform some operation on the current editor instance
|
||||
this.addCommand({
|
||||
id: 'sample-editor-command',
|
||||
name: 'Sample editor command',
|
||||
editorCallback: (editor: Editor, view: MarkdownView) => {
|
||||
console.log(editor.getSelection());
|
||||
editor.replaceSelection('Sample Editor Command');
|
||||
}
|
||||
});
|
||||
// This adds a complex command that can check whether the current state of the app allows execution of the command
|
||||
this.addCommand({
|
||||
id: 'open-sample-modal-complex',
|
||||
name: 'Open sample modal (complex)',
|
||||
checkCallback: (checking: boolean) => {
|
||||
// Conditions to check
|
||||
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (markdownView) {
|
||||
// If checking is true, we're simply "checking" if the command can be run.
|
||||
// If checking is false, then we want to actually perform the operation.
|
||||
if (!checking) {
|
||||
new SampleModal(this.app).open();
|
||||
}
|
||||
|
||||
// This command will only show up in Command Palette when the check function returns true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// This adds a settings tab so the user can configure various aspects of the plugin
|
||||
this.addSettingTab(new SampleSettingTab(this.app, this));
|
||||
|
||||
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
|
||||
// Using this function will automatically remove the event listener when this plugin is disabled.
|
||||
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
|
||||
console.log('click', evt);
|
||||
});
|
||||
|
||||
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
|
||||
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
|
||||
}
|
||||
|
||||
onunload() {
|
||||
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
}
|
||||
|
||||
class SampleModal extends Modal {
|
||||
constructor(app: App) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const {contentEl} = this;
|
||||
contentEl.setText('Woah!');
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const {contentEl} = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
class SampleSettingTab extends PluginSettingTab {
|
||||
plugin: MyPlugin;
|
||||
|
||||
constructor(app: App, plugin: MyPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const {containerEl} = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Setting #1')
|
||||
.setDesc('It\'s a secret')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter your secret')
|
||||
.setValue(this.plugin.settings.mySetting)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.mySetting = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"id": "sample-plugin",
|
||||
"name": "Sample Plugin",
|
||||
"id": "note_uid_generator",
|
||||
"name": "Note UID Generator",
|
||||
"version": "1.0.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Demonstrates some of the capabilities of the Obsidian API.",
|
||||
"author": "Obsidian",
|
||||
"authorUrl": "https://obsidian.md",
|
||||
"fundingUrl": "https://obsidian.md/pricing",
|
||||
"description": "Automatically or manually generates Unique IDs (UIDs) for notes and registers them in metadata (frontmatter).",
|
||||
"author": "Valentin Pelletier",
|
||||
"authorUrl": "https://github.com/Netajam",
|
||||
"fundingUrl": "https://github.com/sponsors/Netajam",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
|
|
|
|||
2413
package-lock.json
generated
Normal file
2413
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -20,5 +20,8 @@
|
|||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": "^11.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
34
release.yml
Normal file
34
release.yml
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
name: Release Obsidian plugin
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18.x"
|
||||
|
||||
- name: Build plugin
|
||||
run: |
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
- name: Create release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
tag="${GITHUB_REF#refs/tags/}"
|
||||
|
||||
gh release create "$tag" \
|
||||
--title="$tag" \
|
||||
--draft \
|
||||
main.js manifest.json styles.css
|
||||
333
src/commands.ts
Normal file
333
src/commands.ts
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
import { Editor, MarkdownView, TFile, TFolder, Notice, normalizePath, WorkspaceLeaf } from 'obsidian';
|
||||
import UIDGenerator from './main';
|
||||
import * as uidUtils from './uidUtils';
|
||||
|
||||
// --- Command/Action Implementations ---
|
||||
|
||||
/** Logic for Generate/Update Command (explicit overwrite) */
|
||||
export async function handleGenerateUpdateUid(plugin: UIDGenerator, overwrite: boolean = true): Promise<void> {
|
||||
const view = plugin.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (view?.file) {
|
||||
const file = view.file;
|
||||
const newUid = uidUtils.generateUID();
|
||||
const setResult = await uidUtils.setUID(plugin, file, newUid, overwrite); // Use util
|
||||
|
||||
if (setResult) {
|
||||
new Notice(`${plugin.settings.uidKey} ${overwrite ? 'updated/set' : 'set'} for ${file.basename}`);
|
||||
} else if (!overwrite && uidUtils.getUIDFromFile(plugin, file)) {
|
||||
// Notice if UID exists and overwrite was false (setUID handles its own console logs)
|
||||
new Notice(`Note ${file.basename} already has a ${plugin.settings.uidKey}. Use "Generate/Update" command to overwrite.`);
|
||||
}
|
||||
// If setResult is false and UID didn't exist, setUID likely showed an error notice
|
||||
} else {
|
||||
new Notice("No active Markdown file selected.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Logic for Ribbon Icon & "Create if Missing" Command (NO overwrite) */
|
||||
export async function handleCreateUidIfMissing(plugin: UIDGenerator): Promise<void> {
|
||||
const view = plugin.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (view?.file) {
|
||||
const file = view.file;
|
||||
const existingUid = uidUtils.getUIDFromFile(plugin, file); // Use util
|
||||
|
||||
if (existingUid) {
|
||||
new Notice(`Note ${file.basename} already has a ${plugin.settings.uidKey}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const newUid = uidUtils.generateUID();
|
||||
const setResult = await uidUtils.setUID(plugin, file, newUid, false); // Use util, NO overwrite
|
||||
|
||||
if (setResult) {
|
||||
new Notice(`${plugin.settings.uidKey} created for ${file.basename}`);
|
||||
}
|
||||
// If setResult is false here, it likely means setUID encountered an error (notice shown there)
|
||||
} else {
|
||||
new Notice("No active Markdown file selected.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Logic for Remove UID Command */
|
||||
export async function handleRemoveUid(plugin: UIDGenerator): Promise<void> {
|
||||
const view = plugin.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
const file = view?.file;
|
||||
if (file) {
|
||||
const removed = await uidUtils.removeUID(plugin, file);
|
||||
if (removed) {
|
||||
new Notice(`${plugin.settings.uidKey} removed from ${file.basename}`);
|
||||
} else {
|
||||
// Check if it exists *before* attempting removal (removeUID handles error notices)
|
||||
const exists = uidUtils.getUIDFromFile(plugin, file);
|
||||
if (!exists) {
|
||||
new Notice(`No ${plugin.settings.uidKey} found on ${file.basename}.`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
new Notice("No active Markdown file to remove UID from.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Logic for Copy UID Command */
|
||||
export function handleCopyUid(plugin: UIDGenerator): void {
|
||||
const view = plugin.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
const file = view?.file;
|
||||
if (!file) {
|
||||
new Notice("No active file."); // Should not happen due to checkCallback
|
||||
return;
|
||||
}
|
||||
const uid = uidUtils.getUIDFromFile(plugin, file); // CheckCallback ensures UID exists
|
||||
if (uid) {
|
||||
navigator.clipboard.writeText(uid)
|
||||
.then(() => new Notice(`${plugin.settings.uidKey} copied: ${uid}`))
|
||||
.catch(err => {
|
||||
console.error("[UIDGenerator] Error copying UID:", err);
|
||||
new Notice("Error copying UID to clipboard.", 5000);
|
||||
});
|
||||
} else {
|
||||
// Should not happen if checkCallback works correctly
|
||||
new Notice(`No ${plugin.settings.uidKey} found to copy.`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Logic for Copy Title+UID Command (from Command Palette or single file context menu) */
|
||||
export function handleCopyTitleUid(plugin: UIDGenerator, specificFile?: TFile): void {
|
||||
let fileToProcess: TFile | null = null;
|
||||
|
||||
if (specificFile) {
|
||||
fileToProcess = specificFile;
|
||||
} else {
|
||||
const view = plugin.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
fileToProcess = view?.file ?? null;
|
||||
}
|
||||
|
||||
if (!fileToProcess) {
|
||||
new Notice("No active/specified file found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const uid = uidUtils.getUIDFromFile(plugin, fileToProcess);
|
||||
const title = fileToProcess.basename;
|
||||
|
||||
let textToCopy: string;
|
||||
if (uid) {
|
||||
textToCopy = uidUtils.formatCopyString(plugin, plugin.settings.copyFormatString, title, uid);
|
||||
} else {
|
||||
textToCopy = uidUtils.formatCopyString(plugin, plugin.settings.copyFormatStringMissingUid, title, null);
|
||||
}
|
||||
navigator.clipboard.writeText(textToCopy)
|
||||
.then(() => new Notice(`Copied: ${textToCopy}`))
|
||||
.catch(err => {
|
||||
console.error("[UIDGenerator] Error copying title + UID:", err);
|
||||
new Notice("Error copying to clipboard.", 5000);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/** Logic for Folder Context Menu Action */
|
||||
export async function handleCopytitlesAndUidsFromFolder(plugin: UIDGenerator, folder: TFolder): Promise<void> {
|
||||
console.log(`[UIDGenerator] Copying titles+${plugin.settings.uidKey}s for folder: ${folder.path}`);
|
||||
const markdownFiles = plugin.app.vault.getMarkdownFiles();
|
||||
const filesInFolder: TFile[] = [];
|
||||
const targetPath = folder.path;
|
||||
|
||||
for (const file of markdownFiles) {
|
||||
let isInside = (targetPath === '/') ? file.path !== '/' : file.path.startsWith(targetPath + '/');
|
||||
if (isInside && !filesInFolder.some(f => f.path === file.path)) {
|
||||
filesInFolder.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
if (filesInFolder.length === 0) {
|
||||
new Notice(`No markdown notes found in "${folder.name}".`);
|
||||
return;
|
||||
}
|
||||
|
||||
let outputLines: string[] = [];
|
||||
let filesWithUidCount = 0;
|
||||
const formatExists = plugin.settings.copyFormatString;
|
||||
const formatMissing = plugin.settings.copyFormatStringMissingUid;
|
||||
|
||||
for (const file of filesInFolder) {
|
||||
const title = file.basename;
|
||||
const uid = uidUtils.getUIDFromFile(plugin, file);
|
||||
if (uid) {
|
||||
outputLines.push(uidUtils.formatCopyString(plugin, formatExists, title, uid));
|
||||
filesWithUidCount++;
|
||||
} else {
|
||||
outputLines.push(uidUtils.formatCopyString(plugin, formatMissing, title, null));
|
||||
}
|
||||
}
|
||||
|
||||
const outputString = outputLines.join('\n');
|
||||
try {
|
||||
await navigator.clipboard.writeText(outputString);
|
||||
new Notice(`Copied ${outputLines.length} items from "${folder.name}" using format.`);
|
||||
} catch (err) {
|
||||
console.error(`[UIDGenerator] Error copying folder items to clipboard:`, err);
|
||||
new Notice("Failed to copy to clipboard. See console.", 5000);
|
||||
}
|
||||
}
|
||||
|
||||
/** Logic for File Context Menu Action (Single File) */
|
||||
export async function handleCopyTitleAndUidForFile(plugin: UIDGenerator, file: TFile): Promise<void> {
|
||||
|
||||
handleCopyTitleUid(plugin, file);
|
||||
}
|
||||
|
||||
/** Logic for Context Menu: Copy titles+UIDs for multiple selected files */
|
||||
export async function handleCopytitlesAndUidsForMultipleFiles(plugin: UIDGenerator, files: TFile[]): Promise<void> {
|
||||
if (!files || files.length === 0) {
|
||||
new Notice("No Markdown files found in selection.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[UIDGenerator] Copying titles+${plugin.settings.uidKey}s for ${files.length} selected files via context menu.`);
|
||||
|
||||
let outputLines: string[] = [];
|
||||
let filesWithUidCount = 0;
|
||||
const formatExists = plugin.settings.copyFormatString;
|
||||
const formatMissing = plugin.settings.copyFormatStringMissingUid;
|
||||
|
||||
// Iterate through the provided array of TFiles
|
||||
for (const file of files) {
|
||||
const title = file.basename;
|
||||
const uid = uidUtils.getUIDFromFile(plugin, file);
|
||||
if (uid) {
|
||||
outputLines.push(uidUtils.formatCopyString(plugin, formatExists, title, uid));
|
||||
filesWithUidCount++;
|
||||
} else {
|
||||
outputLines.push(uidUtils.formatCopyString(plugin, formatMissing, title, null));
|
||||
}
|
||||
}
|
||||
|
||||
const outputString = outputLines.join('\n');
|
||||
try {
|
||||
await navigator.clipboard.writeText(outputString);
|
||||
new Notice(`Copied ${outputLines.length} selected items using format.`);
|
||||
} catch (err) {
|
||||
console.error(`[UIDGenerator] Error copying selected items (files-menu) to clipboard:`, err);
|
||||
new Notice("Failed to copy selected items to clipboard. See console.", 5000);
|
||||
}
|
||||
}
|
||||
|
||||
/** Logic for Command Palette: Copy titles+UIDs for selected files in File Explorer */
|
||||
export async function handleCopytitlesAndUidsForSelection(plugin: UIDGenerator): Promise<void> {
|
||||
// Find the active File Explorer leaf/view
|
||||
const fileExplorerLeaf = plugin.app.workspace.getLeavesOfType('file-explorer')
|
||||
.find(leaf => (leaf.view as any).selectedFiles && plugin.app.workspace.activeLeaf === leaf);
|
||||
|
||||
if (!fileExplorerLeaf || !(fileExplorerLeaf.view as any).selectedFiles) {
|
||||
new Notice("No active file explorer with selected files found.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Access selected files
|
||||
const selectedPaths: string[] = (fileExplorerLeaf.view as any).selectedFiles || [];
|
||||
|
||||
if (selectedPaths.length === 0) {
|
||||
new Notice("No files selected in the file explorer.");
|
||||
return;
|
||||
}
|
||||
|
||||
const filesToProcess: TFile[] = [];
|
||||
for (const path of selectedPaths) {
|
||||
const file = plugin.app.vault.getAbstractFileByPath(path);
|
||||
if (file instanceof TFile && file.extension === 'md') {
|
||||
filesToProcess.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
if (filesToProcess.length === 0) {
|
||||
new Notice("No *Markdown* files selected.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Delegate to the same logic used by the context menu
|
||||
await handleCopytitlesAndUidsForMultipleFiles(plugin, filesToProcess);
|
||||
}
|
||||
|
||||
|
||||
/** Logic for Clearing UIDs in Folder Action (called from settings) */
|
||||
export async function handleClearUIDsInFolder(plugin: UIDGenerator, folderPath: string): Promise<void> {
|
||||
const normalizedFolderPath = normalizePath(folderPath);
|
||||
if (!normalizedFolderPath) { new Notice("Folder path empty."); return; }
|
||||
|
||||
const folder = plugin.app.vault.getAbstractFileByPath(normalizedFolderPath);
|
||||
if (!folder || !(folder instanceof TFolder)) { new Notice(`Folder not found or path is not a folder: ${folderPath}`); return; }
|
||||
|
||||
console.log(`[UIDGenerator] Starting UID clearing process for folder: ${folder.path} using key "${plugin.settings.uidKey}"`);
|
||||
new Notice(`Clearing ${plugin.settings.uidKey}s in "${folder.name}"... This may take a moment.`);
|
||||
|
||||
const markdownFiles = plugin.app.vault.getMarkdownFiles();
|
||||
const filesToProcess: TFile[] = [];
|
||||
const targetPath = folder.path;
|
||||
|
||||
for (const file of markdownFiles) {
|
||||
let isInside = (targetPath === '/') ? file.path !== '/' : file.path.startsWith(targetPath + '/');
|
||||
if (isInside && !filesToProcess.some(f => f.path === file.path)) {
|
||||
filesToProcess.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
if (filesToProcess.length === 0) {
|
||||
new Notice(`No markdown files found within "${folder.name}".`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[UIDGenerator] Found ${filesToProcess.length} markdown files to process.`);
|
||||
let clearedCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const file of filesToProcess) {
|
||||
try {
|
||||
const removed = await uidUtils.removeUID(plugin, file);
|
||||
if (removed) clearedCount++;
|
||||
} catch (err) {
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
let message = `UID clearing complete for "${folder.name}". Removed ${clearedCount} ${plugin.settings.uidKey}s.`;
|
||||
if (errorCount > 0) {
|
||||
message += ` Encountered ${errorCount} errors (check console).`;
|
||||
}
|
||||
new Notice(message, 10000);
|
||||
console.log(`[UIDGenerator] UID clearing finished. Removed: ${clearedCount}, Errors: ${errorCount}`);
|
||||
}
|
||||
|
||||
|
||||
/** Handler for file create/open events for auto-generation */
|
||||
export async function handleAutoGenerateUid(plugin: UIDGenerator, file: TFile | null): Promise<void> {
|
||||
if (!plugin.settings.autoGenerateUid || !file || !(file instanceof TFile) || file.extension !== 'md') {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedPath = normalizePath(file.path);
|
||||
|
||||
// Check exclusions
|
||||
if (plugin.settings.autoGenerationExclusions.some(ex => {
|
||||
const normEx = normalizePath(ex.trim());
|
||||
return normEx && (normalizedPath.startsWith(normEx + '/') || normalizedPath === normEx);
|
||||
})) {
|
||||
return; // Excluded
|
||||
}
|
||||
|
||||
// Check scope
|
||||
if (plugin.settings.autoGenerationScope === 'folder') {
|
||||
const normScope = normalizePath(plugin.settings.autoGenerationFolder.trim());
|
||||
if (!normScope || !(normalizedPath.startsWith(normScope + '/') || file.parent?.path === normScope)) {
|
||||
return; // Outside scope
|
||||
}
|
||||
}
|
||||
|
||||
// Check if UID exists
|
||||
if (uidUtils.getUIDFromFile(plugin, file)) {
|
||||
return; // Skip silently
|
||||
}
|
||||
|
||||
// Generate and set
|
||||
console.log(`[UIDGenerator] Auto-generating ${plugin.settings.uidKey} for: ${file.path}`);
|
||||
const newUid = uidUtils.generateUID();
|
||||
await uidUtils.setUID(plugin, file, newUid, false);
|
||||
}
|
||||
191
src/main.ts
Normal file
191
src/main.ts
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import {
|
||||
App, Editor, MarkdownView, Notice, Plugin, TFile, TFolder,
|
||||
debounce, Menu, TAbstractFile, WorkspaceLeaf
|
||||
} from 'obsidian';
|
||||
import { UIDGeneratorSettings, DEFAULT_SETTINGS, UIDSettingTab } from './settings';
|
||||
import * as commands from './commands';
|
||||
import * as uidUtils from './uidUtils';
|
||||
|
||||
export default class UIDGenerator extends Plugin {
|
||||
settings: UIDGeneratorSettings;
|
||||
|
||||
async onload() {
|
||||
console.log('Loading UID Generator Plugin v7 (files-menu)');
|
||||
await this.loadSettings();
|
||||
|
||||
// --- Ribbon Icon ---
|
||||
this.addRibbonIcon('fingerprint', `Create ${this.settings.uidKey} if missing`, () => {
|
||||
commands.handleCreateUidIfMissing(this);
|
||||
});
|
||||
|
||||
// --- Event Listeners ---
|
||||
const debouncedFileHandler = debounce(commands.handleAutoGenerateUid.bind(null, this), 500, true);
|
||||
|
||||
this.registerEvent(this.app.vault.on('create', (file) => {
|
||||
if (file instanceof TFile && file.extension === 'md') {
|
||||
debouncedFileHandler(file);
|
||||
}
|
||||
}));
|
||||
this.registerEvent(this.app.workspace.on('file-open', (file) => {
|
||||
if (file instanceof TFile && file.extension === 'md') {
|
||||
const activeLeaf = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (activeLeaf && activeLeaf.file === file) {
|
||||
debouncedFileHandler(file);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
// --- Commands ---
|
||||
this.addCommand({
|
||||
id: 'generate-update-uid',
|
||||
name: `Generate/Update ${this.settings.uidKey} (Overwrites)`,
|
||||
editorCallback: (editor: Editor, view: MarkdownView) => {
|
||||
commands.handleGenerateUpdateUid(this, true);
|
||||
}
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'create-uid-if-missing',
|
||||
name: `Create ${this.settings.uidKey} if missing`,
|
||||
editorCallback: (editor: Editor, view: MarkdownView) => {
|
||||
commands.handleCreateUidIfMissing(this);
|
||||
}
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'remove-uid',
|
||||
name: `Remove ${this.settings.uidKey} from current note`,
|
||||
editorCallback: (editor: Editor, view: MarkdownView) => {
|
||||
commands.handleRemoveUid(this);
|
||||
}
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'copy-uid',
|
||||
name: `Copy ${this.settings.uidKey} of current note`,
|
||||
editorCheckCallback: (checking: boolean, editor: Editor, view: MarkdownView) => {
|
||||
const file = view.file;
|
||||
if (!file) return false;
|
||||
const uid = uidUtils.getUIDFromFile(this, file); // Check using util
|
||||
if (!uid) return false;
|
||||
|
||||
if (!checking) {
|
||||
commands.handleCopyUid(this);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'copy-title-uid',
|
||||
name: `Copy title + ${this.settings.uidKey}`,
|
||||
editorCheckCallback: (checking: boolean, editor: Editor, view: MarkdownView) => {
|
||||
const file = view.file;
|
||||
if (!file) return false; // Only enable if file exists
|
||||
|
||||
if (!checking) {
|
||||
// Pass the specific file from the editor view
|
||||
commands.handleCopyTitleUid(this, file);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Command Palette command for multi-selection
|
||||
this.addCommand({
|
||||
id: 'copy-title-uid-for-selection',
|
||||
name: `Copy titles+${this.settings.uidKey}s for selected files`,
|
||||
checkCallback: (checking: boolean): boolean | void => {
|
||||
const fileExplorerLeaf = this.app.workspace.getLeavesOfType('file-explorer')
|
||||
?.find(leaf => (leaf.view as any).selectedFiles && this.app.workspace.activeLeaf === leaf);
|
||||
|
||||
let markdownSelected = false;
|
||||
if (fileExplorerLeaf) {
|
||||
const selectedPaths: string[] = (fileExplorerLeaf.view as any).selectedFiles || [];
|
||||
markdownSelected = selectedPaths.some(path => {
|
||||
const file = this.app.vault.getAbstractFileByPath(path);
|
||||
return file instanceof TFile && file.extension === 'md';
|
||||
});
|
||||
}
|
||||
|
||||
const canRun = !!fileExplorerLeaf && markdownSelected;
|
||||
|
||||
if (canRun) {
|
||||
if (!checking) {
|
||||
commands.handleCopytitlesAndUidsForSelection(this);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// --- Context Menus ---
|
||||
|
||||
// SINGLE Item Context Menu
|
||||
this.registerEvent(this.app.workspace.on('file-menu', (menu: Menu, fileOrFolder: TAbstractFile, source: string, leaf?: WorkspaceLeaf) => {
|
||||
if (fileOrFolder instanceof TFolder) {
|
||||
menu.addItem((item) => {
|
||||
item
|
||||
.setTitle(`Copy titles+${this.settings.uidKey}s from "${fileOrFolder.name}"`)
|
||||
.setIcon('copy')
|
||||
.onClick(() => commands.handleCopytitlesAndUidsFromFolder(this, fileOrFolder));
|
||||
});
|
||||
} else if (fileOrFolder instanceof TFile && fileOrFolder.extension === 'md') {
|
||||
menu.addItem((item) => {
|
||||
item
|
||||
.setTitle(`Copy Title+${this.settings.uidKey}`) // Use specific handler for single file
|
||||
.setIcon('copy')
|
||||
.onClick(() => commands.handleCopyTitleAndUidForFile(this, fileOrFolder));
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
// MULTIPLE Item Context Menu (Undocumented Event)
|
||||
this.registerEvent(this.app.workspace.on('files-menu', (menu: Menu, files: TAbstractFile[], source: string, leaf?: WorkspaceLeaf) => {
|
||||
if (source !== 'file-explorer-context-menu') { return; }
|
||||
|
||||
const markdownFiles = files.filter((file): file is TFile => file instanceof TFile && file.extension === 'md');
|
||||
|
||||
if (markdownFiles.length > 0) {
|
||||
menu.addItem((item) => {
|
||||
item
|
||||
.setTitle(`Copy titles+${this.settings.uidKey}s for ${markdownFiles.length} selected`)
|
||||
.setIcon('copy')
|
||||
// Pass the filtered array of TFiles to the handler
|
||||
.onClick(() => commands.handleCopytitlesAndUidsForMultipleFiles(this, markdownFiles));
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
// --- Settings Tab ---
|
||||
this.addSettingTab(new UIDSettingTab(this.app, this));
|
||||
}
|
||||
|
||||
onunload() {
|
||||
console.log('Unloading UID Generator Plugin v7 (files-menu)');
|
||||
}
|
||||
|
||||
// --- Settings Management ---
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
if (!Array.isArray(this.settings.autoGenerationExclusions)) {
|
||||
this.settings.autoGenerationExclusions = [];
|
||||
}
|
||||
this.settings.copyFormatString = this.settings.copyFormatString || DEFAULT_SETTINGS.copyFormatString;
|
||||
this.settings.copyFormatStringMissingUid = this.settings.copyFormatStringMissingUid || DEFAULT_SETTINGS.copyFormatStringMissingUid;
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
|
||||
}
|
||||
|
||||
// --- Public method needed by Settings Tab ---
|
||||
async clearUIDsInFolder(folderPath: string): Promise<void> {
|
||||
await commands.handleClearUIDsInFolder(this, folderPath);
|
||||
}
|
||||
}
|
||||
232
src/settings.ts
Normal file
232
src/settings.ts
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
import { App, PluginSettingTab, Setting, normalizePath, Notice } from 'obsidian';
|
||||
import UIDGenerator from './main';
|
||||
import { FolderSuggest } from './ui/FolderSuggest';
|
||||
import { ConfirmationModal } from './ui/ConfirmationModal';
|
||||
import { FolderExclusionModal } from './ui/FolderExclusionModal';
|
||||
|
||||
// --- Settings Interface ---
|
||||
export interface UIDGeneratorSettings {
|
||||
uidKey: string;
|
||||
autoGenerateUid: boolean;
|
||||
autoGenerationScope: 'vault' | 'folder';
|
||||
autoGenerationFolder: string;
|
||||
autoGenerationExclusions: string[];
|
||||
folderToClear: string;
|
||||
copyFormatString: string;
|
||||
copyFormatStringMissingUid: string;
|
||||
}
|
||||
|
||||
// --- Default Settings ---
|
||||
export const DEFAULT_SETTINGS: UIDGeneratorSettings = {
|
||||
uidKey: 'uid',
|
||||
autoGenerateUid: false,
|
||||
autoGenerationScope: 'vault',
|
||||
autoGenerationFolder: '',
|
||||
autoGenerationExclusions: [],
|
||||
folderToClear: '',
|
||||
copyFormatString: '{title} - {uidKey}: {uid}',
|
||||
copyFormatStringMissingUid: '{title} - No {uidKey}',
|
||||
}
|
||||
|
||||
// --- Settings Tab Class ---
|
||||
export class UIDSettingTab extends PluginSettingTab {
|
||||
plugin: UIDGenerator;
|
||||
|
||||
constructor(app: App, plugin: UIDGenerator) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
// This method re-renders the settings tab content
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl('h2', { text: 'UID Generator Settings' });
|
||||
|
||||
// --- General Settings ---
|
||||
containerEl.createEl('h3', { text: 'General' });
|
||||
new Setting(containerEl)
|
||||
.setName('UID Metadata Key')
|
||||
.setDesc('The name of the key for the UID in frontmatter (e.g., "uid", "id"). No spaces.')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Default: uid')
|
||||
.setValue(this.plugin.settings.uidKey)
|
||||
.onChange(async (value) => {
|
||||
const cleanedValue = value.trim().replace(/\s+/g, '');
|
||||
this.plugin.settings.uidKey = cleanedValue || DEFAULT_SETTINGS.uidKey;
|
||||
if(value !== this.plugin.settings.uidKey) {
|
||||
// Update input value if it was cleaned (e.g., spaces removed)
|
||||
text.setValue(this.plugin.settings.uidKey);
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
this.display(); // Re-render to potentially update descriptions elsewhere
|
||||
}));
|
||||
|
||||
|
||||
// --- Automatic UID Generation ---
|
||||
containerEl.createEl('h3', { text: 'Automatic UID Generation' });
|
||||
containerEl.createEl('p', { text: `Automatically add a ${this.plugin.settings.uidKey} to notes when they are created or opened, if they don't already have one.` }).addClass('setting-item-description');
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Enable Automatic UID Generation')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.autoGenerateUid)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoGenerateUid = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.display(); // Re-render to show/hide dependent settings
|
||||
}));
|
||||
|
||||
// Only display scope/exclusion settings if auto-generation is enabled
|
||||
if (this.plugin.settings.autoGenerateUid) {
|
||||
new Setting(containerEl)
|
||||
.setName('Generation Scope')
|
||||
.addDropdown(dropdown => dropdown
|
||||
.addOption('vault', 'Entire Vault')
|
||||
.addOption('folder', 'Specific Folder')
|
||||
.setValue(this.plugin.settings.autoGenerationScope)
|
||||
.onChange(async (value: 'vault' | 'folder') => {
|
||||
this.plugin.settings.autoGenerationScope = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
}));
|
||||
|
||||
// Display folder input only if scope is 'folder'
|
||||
if (this.plugin.settings.autoGenerationScope === 'folder') {
|
||||
new Setting(containerEl)
|
||||
.setName('Target Folder for Auto-Generation')
|
||||
.setDesc('Generate UIDs only for notes in this folder (and subfolders).')
|
||||
.addText(text => {
|
||||
new FolderSuggest(this.app, text.inputEl);
|
||||
text.setPlaceholder('Example: Notes/Inbox')
|
||||
.setValue(this.plugin.settings.autoGenerationFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoGenerationFolder = normalizePath(value.trim());
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --- Excluded Folders Setting with Modal Button ---
|
||||
new Setting(containerEl)
|
||||
.setName('Excluded Folders')
|
||||
.setDesc('Folders excluded from automatic UID generation. Click button to manage.')
|
||||
.addButton(button => button
|
||||
.setButtonText('Manage Exclusions')
|
||||
.onClick(() => {
|
||||
new FolderExclusionModal(this.app, this.plugin, () => this.display()).open();
|
||||
}));
|
||||
|
||||
// Display the current list of excluded folders (read-only in this view)
|
||||
const exclusionListEl = containerEl.createEl('ul', { cls: 'uid-exclusion-list' });
|
||||
if (this.plugin.settings.autoGenerationExclusions.length > 0) {
|
||||
// Sort before displaying for consistency
|
||||
const sortedExclusions = [...this.plugin.settings.autoGenerationExclusions].sort();
|
||||
sortedExclusions.forEach(folderPath => {
|
||||
exclusionListEl.createEl('li', { text: folderPath });
|
||||
});
|
||||
} else {
|
||||
// Display message if no folders are excluded
|
||||
exclusionListEl.createEl('li', { text: 'No folders excluded.' });
|
||||
}
|
||||
// Basic styling for the list
|
||||
exclusionListEl.style.marginTop = '5px';
|
||||
exclusionListEl.style.marginBottom = '15px';
|
||||
exclusionListEl.style.paddingLeft = '20px';
|
||||
exclusionListEl.style.listStyle = 'none'; // Or 'disc', 'circle' etc.
|
||||
|
||||
}
|
||||
|
||||
|
||||
// --- Copy Format Settings ---
|
||||
containerEl.createEl('h3', { text: 'Copy Format' });
|
||||
containerEl.createEl('p', { text: 'Define the format for copied text using placeholders: {title}, {uid}, {uidKey}.' }).addClass('setting-item-description');
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Format (UID exists)')
|
||||
.setDesc('Format string used when copying title and UID, and the UID exists.')
|
||||
.addText(text => text
|
||||
.setPlaceholder(DEFAULT_SETTINGS.copyFormatString)
|
||||
.setValue(this.plugin.settings.copyFormatString)
|
||||
.onChange(async (value) => {
|
||||
// Use default if value is empty, otherwise use the provided value
|
||||
this.plugin.settings.copyFormatString = value || DEFAULT_SETTINGS.copyFormatString;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Format (UID missing)')
|
||||
.setDesc('Format string used when copying, but the note has no UID.')
|
||||
.addText(text => text
|
||||
.setPlaceholder(DEFAULT_SETTINGS.copyFormatStringMissingUid)
|
||||
.setValue(this.plugin.settings.copyFormatStringMissingUid)
|
||||
.onChange(async (value) => {
|
||||
// Use default if value is empty, otherwise use the provided value
|
||||
this.plugin.settings.copyFormatStringMissingUid = value || DEFAULT_SETTINGS.copyFormatStringMissingUid;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
|
||||
// --- Manual UID Clearing ---
|
||||
containerEl.createEl('h3', { text: 'Manual UID Clearing' });
|
||||
new Setting(containerEl)
|
||||
.setName('Folder to clear UIDs from')
|
||||
.setDesc('Specify the vault path to remove UIDs from notes within.')
|
||||
.addText(text => {
|
||||
new FolderSuggest(this.app, text.inputEl);
|
||||
text.setPlaceholder('Example: Folder/Subfolder')
|
||||
.setValue(this.plugin.settings.folderToClear)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.folderToClear = normalizePath(value.trim());
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Clear UIDs in Folder')
|
||||
.setDesc(`WARNING: This permanently removes the "${this.plugin.settings.uidKey}" metadata from notes in the specified folder/subfolders and temporarily disables auto-generation.`)
|
||||
.addButton(button => button
|
||||
.setButtonText('Clear UIDs Now')
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
const folderPath = this.plugin.settings.folderToClear;
|
||||
const uidKey = this.plugin.settings.uidKey;
|
||||
// Basic validation for the folder path input
|
||||
if (!folderPath || folderPath.trim() === '') {
|
||||
new Notice("Please specify a folder path in the 'Folder to clear UIDs from' setting above first.");
|
||||
return; // Prevent proceeding without a path
|
||||
}
|
||||
|
||||
// Open the confirmation modal, passing the necessary info and the confirmation callback
|
||||
new ConfirmationModal(this.app, folderPath, uidKey, async () => {
|
||||
// --- This code runs ONLY if the user clicks "Confirm" in the modal ---
|
||||
let autoGenWasOn = false;
|
||||
// 1. Check if auto-generation is enabled and disable it temporarily
|
||||
if (this.plugin.settings.autoGenerateUid) {
|
||||
autoGenWasOn = true;
|
||||
this.plugin.settings.autoGenerateUid = false;
|
||||
await this.plugin.saveSettings();
|
||||
console.log("[UIDGenerator] Automatic UID generation temporarily disabled due to manual clear action.");
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. Call the main plugin method to perform the clearing
|
||||
await this.plugin.clearUIDsInFolder(folderPath);
|
||||
} catch (err) {
|
||||
// Catch potential errors during the clearing process
|
||||
console.error("[UIDGenerator] Error during bulk UID clearing process:", err);
|
||||
new Notice("An unexpected error occurred during UID clearing. Check console.", 5000);
|
||||
} finally {
|
||||
// 3. This block runs whether the clearing succeeded or failed
|
||||
if (autoGenWasOn) {
|
||||
// Notify the user that auto-gen was turned off
|
||||
new Notice("Automatic UID generation was disabled. You can re-enable it in settings if desired.", 8000);
|
||||
}
|
||||
// 4. Re-render the settings tab display to reflect any changes
|
||||
this.display();
|
||||
}
|
||||
}).open();
|
||||
}));
|
||||
}
|
||||
}
|
||||
17
src/typings/obsidian.d.ts
vendored
Normal file
17
src/typings/obsidian.d.ts
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// src/obsidian.d.ts
|
||||
import 'obsidian';
|
||||
|
||||
declare module 'obsidian' {
|
||||
interface Workspace {
|
||||
on(
|
||||
name: 'files-menu',
|
||||
callback: (
|
||||
menu: Menu,
|
||||
files: TAbstractFile[],
|
||||
source: string,
|
||||
leaf?: WorkspaceLeaf
|
||||
) => any,
|
||||
ctx?: any
|
||||
): EventRef;
|
||||
}
|
||||
}
|
||||
52
src/ui/ConfirmationModal.ts
Normal file
52
src/ui/ConfirmationModal.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { App, Modal, Setting } from 'obsidian';
|
||||
|
||||
// Confirmation Modal
|
||||
export class ConfirmationModal extends Modal {
|
||||
folderPath: string;
|
||||
uidKey: string;
|
||||
onConfirm: () => void; // Callback function when user confirms
|
||||
|
||||
constructor(app: App, folderPath: string, uidKey: string, onConfirm: () => void) {
|
||||
super(app);
|
||||
this.folderPath = folderPath;
|
||||
this.uidKey = uidKey;
|
||||
this.onConfirm = onConfirm;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
contentEl.createEl('h2', { text: 'Confirm UID Deletion' });
|
||||
contentEl.createEl('p', { text: `Are you sure you want to remove the '${this.uidKey}' metadata property from all notes within the folder "${this.folderPath}" and its subfolders?` });
|
||||
contentEl.createEl('p', { text: `This action cannot be undone.` }).addClass('mod-warning');
|
||||
|
||||
const buttonContainer = contentEl.createDiv({ cls: 'modal-button-container' });
|
||||
|
||||
// Confirm Button
|
||||
new Setting(buttonContainer)
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText('Confirm Deletion')
|
||||
.setWarning()
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
this.close();
|
||||
this.onConfirm();
|
||||
}));
|
||||
|
||||
// Cancel Button
|
||||
new Setting(buttonContainer)
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText('Cancel')
|
||||
.onClick(() => {
|
||||
this.close();
|
||||
}));
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
98
src/ui/FolderExclusionModal.ts
Normal file
98
src/ui/FolderExclusionModal.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { App, Modal, TFolder, Setting, debounce } from 'obsidian';
|
||||
import UIDGenerator from '../main';
|
||||
|
||||
export class FolderExclusionModal extends Modal {
|
||||
plugin: UIDGenerator;
|
||||
allFolders: TFolder[];
|
||||
suggestionsEl: HTMLElement;
|
||||
inputEl: HTMLInputElement;
|
||||
onSettingsChanged: () => void; // Callback function
|
||||
|
||||
constructor(app: App, plugin: UIDGenerator, onSettingsChanged: () => void) { // Add callback to constructor
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.allFolders = this.getAllFolders();
|
||||
this.onSettingsChanged = onSettingsChanged; // Store the callback
|
||||
}
|
||||
|
||||
getAllFolders(): TFolder[] {
|
||||
return this.app.vault.getAllLoadedFiles()
|
||||
.filter((f): f is TFolder => f instanceof TFolder)
|
||||
.sort((a, b) => a.path.localeCompare(b.path));
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.addClass('uid-folder-exclusion-modal');
|
||||
|
||||
contentEl.createEl('h2', { text: 'Manage Excluded Folders' });
|
||||
contentEl.createEl('p', { text: 'Add or remove folders from the automatic UID generation exclusion list.' });
|
||||
|
||||
// Search Input
|
||||
this.inputEl = contentEl.createEl('input', { type: 'text', placeholder: 'Search folders...' });
|
||||
this.inputEl.addClass('uid-search-input');
|
||||
this.inputEl.addEventListener('input', debounce(() => this.renderSuggestions(this.inputEl.value), 150, true));
|
||||
|
||||
// Results Container
|
||||
this.suggestionsEl = contentEl.createDiv('uid-suggestion-container');
|
||||
|
||||
this.renderSuggestions('');
|
||||
}
|
||||
|
||||
renderSuggestions(searchTerm: string) {
|
||||
this.suggestionsEl.empty();
|
||||
const lowerSearch = searchTerm.toLowerCase().trim();
|
||||
|
||||
const filteredFolders = lowerSearch === ''
|
||||
? this.allFolders
|
||||
: this.allFolders.filter(folder => folder.path.toLowerCase().includes(lowerSearch));
|
||||
|
||||
if (filteredFolders.length === 0) {
|
||||
this.suggestionsEl.createDiv({text: "No matching folders found.", cls: "uid-no-results"});
|
||||
return;
|
||||
}
|
||||
|
||||
filteredFolders.forEach(folder => {
|
||||
const isExcluded = this.plugin.settings.autoGenerationExclusions.includes(folder.path);
|
||||
const settingItem = this.suggestionsEl.createDiv('setting-item');
|
||||
const infoDiv = settingItem.createDiv('setting-item-info');
|
||||
infoDiv.createDiv({ text: folder.path, cls: 'setting-item-name' });
|
||||
const controlDiv = settingItem.createDiv('setting-item-control');
|
||||
const button = controlDiv.createEl('button');
|
||||
|
||||
if (isExcluded) {
|
||||
button.setText('Remove');
|
||||
button.addClass('mod-warning');
|
||||
button.onclick = () => this.removeExclusion(folder);
|
||||
} else {
|
||||
button.setText('Add');
|
||||
button.addClass('mod-cta');
|
||||
button.onclick = () => this.addExclusion(folder);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async addExclusion(folder: TFolder) {
|
||||
if (!this.plugin.settings.autoGenerationExclusions.includes(folder.path)) {
|
||||
this.plugin.settings.autoGenerationExclusions.push(folder.path);
|
||||
this.plugin.settings.autoGenerationExclusions.sort();
|
||||
await this.plugin.saveSettings();
|
||||
this.renderSuggestions(this.inputEl.value);
|
||||
this.onSettingsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
async removeExclusion(folder: TFolder) {
|
||||
this.plugin.settings.autoGenerationExclusions = this.plugin.settings.autoGenerationExclusions.filter(p => p !== folder.path);
|
||||
await this.plugin.saveSettings();
|
||||
this.renderSuggestions(this.inputEl.value);
|
||||
this.onSettingsChanged();
|
||||
}
|
||||
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
28
src/ui/FolderSuggest.ts
Normal file
28
src/ui/FolderSuggest.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { App, TFolder, AbstractInputSuggest } from 'obsidian';
|
||||
|
||||
export class FolderSuggest extends AbstractInputSuggest<TFolder> {
|
||||
constructor(app: App, private inputEl: HTMLInputElement) {
|
||||
super(app, inputEl);
|
||||
}
|
||||
|
||||
getSuggestions(query: string): TFolder[] {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
const folders = this.app.vault.getAllLoadedFiles()
|
||||
.filter((file): file is TFolder =>
|
||||
file instanceof TFolder &&
|
||||
file.path.toLowerCase().contains(lowerCaseQuery)
|
||||
);
|
||||
folders.sort((a, b) => a.path.localeCompare(b.path));
|
||||
return folders;
|
||||
}
|
||||
|
||||
renderSuggestion(folder: TFolder, el: HTMLElement): void {
|
||||
el.setText(folder.path);
|
||||
}
|
||||
|
||||
selectSuggestion(folder: TFolder): void {
|
||||
this.inputEl.value = folder.path;
|
||||
this.inputEl.trigger("input");
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
140
src/uidUtils.ts
Normal file
140
src/uidUtils.ts
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import { App, TFile, Notice, FrontMatterCache } from 'obsidian';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import UIDGenerator from './main';
|
||||
|
||||
/**
|
||||
* Generates a unique ID using the UUID v4 standard.
|
||||
*/
|
||||
export function generateUID(): string {
|
||||
return uuidv4();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the UID for a given file, respecting the custom key setting.
|
||||
* @param plugin The UIDGenerator plugin instance.
|
||||
* @param file The TFile to check.
|
||||
* @returns The UID string or null if not found or on error.
|
||||
*/
|
||||
export function getUIDFromFile(plugin: UIDGenerator, file: TFile | null): string | null {
|
||||
if (!file) return null;
|
||||
try {
|
||||
const cache = plugin.app.metadataCache.getFileCache(file);
|
||||
const frontmatter = cache?.frontmatter;
|
||||
if (!frontmatter) {
|
||||
return null;
|
||||
}
|
||||
const uidValue = frontmatter[plugin.settings.uidKey];
|
||||
// Ensure it's treated as a string, even if stored as number in YAML
|
||||
return typeof uidValue === 'string' || typeof uidValue === 'number' ? String(uidValue) : null;
|
||||
} catch (error) {
|
||||
console.error(`[UIDGenerator] Error reading metadata for ${file.path}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or updates the UID property in a file's frontmatter.
|
||||
* @param plugin The UIDGenerator plugin instance.
|
||||
* @param file The TFile to modify.
|
||||
* @param uid The UID string to set.
|
||||
* @param overwrite If true, will overwrite an existing UID. If false (default), will only set if missing.
|
||||
* @returns Promise<boolean> - True if UID was set/modified, False otherwise.
|
||||
*/
|
||||
export async function setUID(plugin: UIDGenerator, file: TFile, uid: string, overwrite: boolean = false): Promise<boolean> {
|
||||
if (!file || !(file instanceof TFile) || file.extension !== 'md') {
|
||||
return false;
|
||||
}
|
||||
if (!uid) {
|
||||
console.warn(`[UIDGenerator] Attempted to set an empty UID for ${file.path}. Aborting.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
let uidWasSetOrOverwritten = false;
|
||||
const key = plugin.settings.uidKey;
|
||||
let initialUidExists = false; // Keep track if UID existed before processing
|
||||
|
||||
try {
|
||||
await plugin.app.fileManager.processFrontMatter(file, (frontmatter) => {
|
||||
const currentUid = frontmatter[key];
|
||||
initialUidExists = currentUid !== undefined && currentUid !== null && currentUid !== ''; // Check before potential modification
|
||||
|
||||
if (initialUidExists && !overwrite) {
|
||||
console.log(`[UIDGenerator] ${key} already exists for ${file.path}, not overwriting.`);
|
||||
uidWasSetOrOverwritten = false; // Explicitly false
|
||||
return; // Exit processor
|
||||
}
|
||||
|
||||
// Only set the flag if the value actually changes or is newly set
|
||||
if (frontmatter[key] !== uid) {
|
||||
frontmatter[key] = uid;
|
||||
uidWasSetOrOverwritten = true;
|
||||
} else {
|
||||
// If overwrite is true but value is the same, we didn't *really* change it
|
||||
uidWasSetOrOverwritten = false;
|
||||
}
|
||||
|
||||
|
||||
// Optional cleanup
|
||||
if (key !== 'uid' && frontmatter.hasOwnProperty('uid')) delete frontmatter.uid;
|
||||
if (key !== 'Uid' && frontmatter.hasOwnProperty('Uid')) delete frontmatter.Uid;
|
||||
if (key !== 'UID' && frontmatter.hasOwnProperty('UID')) delete frontmatter.UID;
|
||||
});
|
||||
|
||||
if (uidWasSetOrOverwritten) {
|
||||
// Determine action based on initial state and overwrite flag
|
||||
const action = initialUidExists && overwrite ? 'Overwrote' : 'Set';
|
||||
console.log(`[UIDGenerator] ${action} ${key} for ${file.path}.`);
|
||||
}
|
||||
return uidWasSetOrOverwritten;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[UIDGenerator] Error processing frontmatter for ${file.path} during setUID:`, error);
|
||||
new Notice(`Error setting ${key}. Check console.`, 5000);
|
||||
return false; // Indicate failure
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the UID property from a file's frontmatter, respecting the custom key.
|
||||
* @param plugin The UIDGenerator plugin instance.
|
||||
* @param file The TFile to modify.
|
||||
* @returns Promise<boolean> - True if a UID was found and removed, false otherwise.
|
||||
*/
|
||||
export async function removeUID(plugin: UIDGenerator, file: TFile): Promise<boolean> {
|
||||
if (!file || !(file instanceof TFile)) return false;
|
||||
let uidWasPresent = false;
|
||||
const key = plugin.settings.uidKey;
|
||||
try {
|
||||
await plugin.app.fileManager.processFrontMatter(file, (frontmatter) => {
|
||||
if (frontmatter.hasOwnProperty(key)) {
|
||||
uidWasPresent = true;
|
||||
delete frontmatter[key];
|
||||
}
|
||||
});
|
||||
if (uidWasPresent) {
|
||||
console.log(`[UIDGenerator] Removed key "${key}" from ${file.path}`);
|
||||
}
|
||||
return uidWasPresent;
|
||||
} catch (error) {
|
||||
console.error(`[UIDGenerator] Error processing frontmatter for ${file.path} during removal:`, error);
|
||||
new Notice(`Error removing ${key}. Check console.`, 5000);
|
||||
return false; // Indicate failure
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a string based on template and available data.
|
||||
* @param plugin The UIDGenerator plugin instance (needed for settings).
|
||||
* @param formatString The template string with placeholders {title}, {uid}, {uidKey}.
|
||||
* @param title The note title.
|
||||
* @param uid The actual UID value (or null/undefined if missing).
|
||||
* @returns The formatted string.
|
||||
*/
|
||||
export function formatCopyString(plugin: UIDGenerator, formatString: string, title: string, uid: string | null | undefined): string {
|
||||
let result = formatString;
|
||||
const uidKey = plugin.settings.uidKey;
|
||||
result = result.replace(/{title}/g, title || '');
|
||||
result = result.replace(/{uidKey}/g, uidKey || '');
|
||||
result = result.replace(/{uid}/g, uid || '');
|
||||
return result;
|
||||
}
|
||||
Loading…
Reference in a new issue