diff --git a/package-lock.json b/package-lock.json index babf934..ef440a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { - "name": "obsidian-sample-plugin", - "version": "1.0.0", + "name": "note_uid_generator", + "version": "Obsidian plugin to generate uid for your notes", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "obsidian-sample-plugin", - "version": "1.0.0", + "name": "note_uid_generator", + "version": "Obsidian plugin to generate uid for your notes", "license": "MIT", "dependencies": { + "nanoid": "^3.3.7", "uuid": "^11.1.0" }, "devDependencies": { @@ -1913,6 +1914,24 @@ "dev": true, "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", diff --git a/package.json b/package.json index 6325e40..34c6466 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "typescript": "4.7.4" }, "dependencies": { + "nanoid": "^3.3.7", "uuid": "^11.1.0" } } diff --git a/src/commands.ts b/src/commands.ts index 1a2207b..c1786e9 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -9,7 +9,7 @@ export async function handleGenerateUpdateUid(plugin: UIDGenerator, overwrite: t const view = plugin.app.workspace.getActiveViewOfType(MarkdownView); if (view?.file) { const file = view.file; - const newUid = uidUtils.generateUID(); + const newUid = uidUtils.generateUID(plugin); const setResult = await uidUtils.setUID(plugin, file, newUid, overwrite); // Use util if (setResult) { @@ -36,7 +36,7 @@ export async function handleCreateUidIfMissing(plugin: UIDGenerator): Promise { @@ -395,7 +395,7 @@ export async function handleAddMissingUidsInScope(plugin: UIDGenerator): Promise } // --- Generate and Set UID --- - const newUid = uidUtils.generateUID(); + const newUid = uidUtils.generateUID(plugin); try { const success = await uidUtils.setUID(plugin, file, newUid, false); // Do not overwrite if (success) { diff --git a/src/settings.ts b/src/settings.ts index 8ba3790..72db8d3 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -8,6 +8,10 @@ import { FolderExclusionModal } from './ui/FolderExclusionModal'; export interface UIDGeneratorSettings { uidKey: string; autoGenerateUid: boolean; + uidGenerator: 'uuid' | 'nanoid'; + nanoidLength: number; + nanoidAlphabet: string; + nanoidSeparators: Array<{ char: string; position: number }>; autoGenerationScope: 'vault' | 'folder'; autoGenerationFolder: string; autoGenerationExclusions: string[]; @@ -20,6 +24,10 @@ export interface UIDGeneratorSettings { export const DEFAULT_SETTINGS: UIDGeneratorSettings = { uidKey: 'uid', autoGenerateUid: false, + uidGenerator: 'uuid', + nanoidLength: 21, + nanoidAlphabet: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', + nanoidSeparators: [], autoGenerationScope: 'vault', autoGenerationFolder: '', autoGenerationExclusions: [], @@ -60,6 +68,104 @@ export class UIDSettingTab extends PluginSettingTab { this.display(); // Re-render to potentially update descriptions elsewhere })); + // --- UID Generator Type --- + new Setting(containerEl).setName('UID generator type').setHeading(); + + new Setting(containerEl) + .setName('Generator algorithm') + .setDesc('Choose between UUID (standard v4) or NanoID (customizable).') + .addDropdown(dropdown => dropdown + .addOption('uuid', 'UUID') + .addOption('nanoid', 'NanoID') + .setValue(this.plugin.settings.uidGenerator) + .onChange(async (value: 'uuid' | 'nanoid') => { + this.plugin.settings.uidGenerator = value; + await this.plugin.saveSettings(); + this.display(); + })); + + if (this.plugin.settings.uidGenerator === 'nanoid') { + new Setting(containerEl) + .setName('NanoID length') + .setDesc('Length of the generated ID (excluding injected characters).') + .addText(text => text + .setValue(String(this.plugin.settings.nanoidLength)) + .onChange(async (value) => { + const num = parseInt(value); + if (!isNaN(num) && num > 0) { + this.plugin.settings.nanoidLength = num; + await this.plugin.saveSettings(); + } + })); + + new Setting(containerEl) + .setName('NanoID alphabet') + .setDesc('Custom characters to use for ID generation.') + .addTextArea(text => text + .setValue(this.plugin.settings.nanoidAlphabet) + .onChange(async (value) => { + if (value.length > 0) { + this.plugin.settings.nanoidAlphabet = value; + await this.plugin.saveSettings(); + } + })); + + // --- NanoID Separator Groups --- + containerEl.createEl('h3', { text: 'NanoID Separator Groups' }); + containerEl.createEl('p', { text: 'Define characters to inject into the NanoID at specific positions. Position is calculated from the raw Base ID.' }).addClass('setting-item-description'); + + this.plugin.settings.nanoidSeparators.forEach((separator, index) => { + const groupContainer = containerEl.createEl('div', { cls: 'uid-separator-group-container' }); + + new Setting(groupContainer) + .setName(`Separator Group ${index + 1}`) + .setHeading() + .addButton(button => button + .setButtonText('Remove') + .setWarning() + .setTooltip('Remove this separator group') + .onClick(async () => { + this.plugin.settings.nanoidSeparators.splice(index, 1); + await this.plugin.saveSettings(); + this.display(); + })); + + new Setting(groupContainer) + .setName(`Inject character`) + .setDesc('Character to inject (e.g., "+").') + .addText(text => text + .setValue(separator.char) + .onChange(async (value) => { + this.plugin.settings.nanoidSeparators[index].char = value; + await this.plugin.saveSettings(); + })); + + new Setting(groupContainer) + .setName(`Injection position`) + .setDesc('Index to inject the character. Negative numbers count from the end.') + .addText(text => text + .setValue(String(separator.position)) + .onChange(async (value) => { + const num = parseInt(value); + if (!isNaN(num)) { + this.plugin.settings.nanoidSeparators[index].position = num; + await this.plugin.saveSettings(); + } + })); + }); + + new Setting(containerEl) + .setName('Add new separator group') + .setDesc('Adds a new group for injecting a character at a specific position.') + .addButton(button => button + .setButtonText('Add Group') + .setCta() + .onClick(async () => { + this.plugin.settings.nanoidSeparators.push({ char: '', position: -2 }); + await this.plugin.saveSettings(); + this.display(); + })); + } // --- Automatic UID Generation --- new Setting(containerEl).setName('Automatic uid generation').setHeading(); @@ -130,21 +236,21 @@ export class UIDSettingTab extends PluginSettingTab { .setName(`Generate missing ${this.plugin.settings.uidKey}s now`) .setDesc(`Manually scan notes based on the current 'Generation scope' and 'Excluded folders' settings above. Add a ${this.plugin.settings.uidKey} to any applicable notes that don't already have one. This may take time for large vaults.`) .addButton(button => button - .setButtonText("Generate missing uids") - .setTooltip("Scan and add missing uids respecting scope/exclusions") + .setButtonText('Generate missing uids') + .setTooltip('Scan and add missing uids respecting scope/exclusions') .onClick(async () => { button.setDisabled(true); // Disable button during processing - button.setButtonText("Processing..."); + button.setButtonText('Processing...'); try { await this.plugin.triggerAddMissingUidsInScope(); } catch (e) { // Catch potential errors from the trigger function itself - console.error("[UIDGenerator] Error triggering bulk uid generation:", e); - new Notice("Failed to start bulk generation. See console.", 5000); + console.error('[UIDGenerator] Error triggering bulk uid generation:', e); + new Notice('Failed to start bulk generation. See console.', 5000); } finally { // Re-enable button regardless of success/failure button.setDisabled(false); - button.setButtonText("Generate missing uids"); + button.setButtonText('Generate missing uids'); } })); @@ -203,7 +309,7 @@ export class UIDSettingTab extends PluginSettingTab { 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."); + new Notice('Please specify a folder path in the \'Folder to clear uids from\' setting above first.'); return; // Prevent proceeding without a path } @@ -223,18 +329,18 @@ export class UIDSettingTab extends PluginSettingTab { 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); + 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); + 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 + // 4. Re-render the settings tab display to reflect any changes this.display(); } }).open(); })); } -} +} diff --git a/src/uidUtils.ts b/src/uidUtils.ts index 92516f2..896ac99 100644 --- a/src/uidUtils.ts +++ b/src/uidUtils.ts @@ -1,11 +1,46 @@ import { TFile, Notice } from 'obsidian'; import { v4 as uuidv4 } from 'uuid'; +import { customAlphabet } from 'nanoid'; import UIDGenerator from './main'; /** * Generates a unique ID using the UUID v4 standard. + * @param plugin The UIDGenerator plugin instance (for settings). */ -export function generateUID(): string { +export function generateUID(plugin: UIDGenerator): string { + if (plugin.settings.uidGenerator === 'nanoid') { + const alphabet = plugin.settings.nanoidAlphabet; + const length = plugin.settings.nanoidLength; + const nanoid = customAlphabet(alphabet, length); + let id = nanoid(); + + const separators = plugin.settings.nanoidSeparators; + + if (separators && separators.length > 0) { + // Create a mutable array of insertions, normalizing positions + const insertions = separators + .filter(s => s.char && s.char.length > 0) // Only process non-empty chars + .map(s => { + let actualPosition = s.position; + // Normalize negative positions relative to the *initial* nanoid length + if (actualPosition < 0) { + actualPosition = length + actualPosition; // e.g., length=21, pos=-2 -> 19 + } + // Ensure position is within bounds [0, length] + actualPosition = Math.max(0, Math.min(length, actualPosition)); + return { char: s.char, position: actualPosition }; + }) + .sort((a, b) => b.position - a.position); // Sort descending by position + + // Apply insertions from right to left to avoid index shifting issues + for (const insertion of insertions) { + const { char, position } = insertion; + id = id.slice(0, position) + char + id.slice(position); + } + } + + return id; + } return uuidv4(); } @@ -24,7 +59,7 @@ export function getUIDFromFile(plugin: UIDGenerator, file: TFile | null): string return null; } const uidValue = frontmatter[plugin.settings.uidKey]; - // Ensure it's treated as a string, even if stored as number in YAML + // 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); @@ -33,7 +68,7 @@ export function getUIDFromFile(plugin: UIDGenerator, file: TFile | null): string } /** - * Sets or updates the UID property in a file's frontmatter. + * 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. @@ -68,7 +103,7 @@ export async function setUID(plugin: UIDGenerator, file: TFile, uid: string, ove frontmatter[key] = uid; uidWasSetOrOverwritten = true; } else { - // If overwrite is true but value is the same, we didn't *really* change it + // If overwrite is true but value is the same, we didn\'t *really* change it uidWasSetOrOverwritten = false; } @@ -93,7 +128,7 @@ export async function setUID(plugin: UIDGenerator, file: TFile, uid: string, ove } /** - * Removes the UID property from a file's frontmatter, respecting the custom key. + * 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 - True if a UID was found and removed, false otherwise.