Add nanoID

This commit is contained in:
stefano-HH 2026-03-18 17:35:05 +01:00
parent 9f619492d8
commit 781a449690
5 changed files with 188 additions and 27 deletions

27
package-lock.json generated
View file

@ -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",

View file

@ -22,6 +22,7 @@
"typescript": "4.7.4"
},
"dependencies": {
"nanoid": "^3.3.7",
"uuid": "^11.1.0"
}
}

View file

@ -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<vo
return;
}
const newUid = uidUtils.generateUID();
const newUid = uidUtils.generateUID(plugin);
const setResult = await uidUtils.setUID(plugin, file, newUid, false); // Use util, NO overwrite
if (setResult) {
@ -234,10 +234,10 @@ export async function handleCopyTitlesAndUidsForSelection(plugin: UIDGenerator):
return;
}
// 3. Get the selected paths
// 3. Get the selected paths
const selectedPaths: string[] = view.selectedFiles;
// 4. Filter for Markdown files
// 4. Filter for Markdown files
const filesToProcess: TFile[] = [];
for (const path of selectedPaths) {
const file = plugin.app.vault.getAbstractFileByPath(path);
@ -332,7 +332,7 @@ export async function handleAutoGenerateUid(plugin: UIDGenerator, file: TFile |
}
// Generate and set
const newUid = uidUtils.generateUID();
const newUid = uidUtils.generateUID(plugin);
await uidUtils.setUID(plugin, file, newUid, false);
}
export async function handleAddMissingUidsInScope(plugin: UIDGenerator): Promise<void> {
@ -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) {

View file

@ -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();
}));
}
}
}

View file

@ -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<boolean> - True if a UID was found and removed, false otherwise.