From 6018bd89a5ca3f26cbba912285c17fd1a0d9d2e3 Mon Sep 17 00:00:00 2001 From: JK Date: Mon, 18 May 2026 14:21:03 +0200 Subject: [PATCH] feat: add Zotero cleanup feature to remove duplicate annotations from active note --- src/features/settings/settings.ts | 6 ++ src/features/settings/tab.ts | 15 ++++ src/features/zotero-cleanup/index.ts | 118 +++++++++++++++++++++++++++ src/main.ts | 9 ++ 4 files changed, 148 insertions(+) create mode 100644 src/features/zotero-cleanup/index.ts diff --git a/src/features/settings/settings.ts b/src/features/settings/settings.ts index 707e007..9ec2623 100644 --- a/src/features/settings/settings.ts +++ b/src/features/settings/settings.ts @@ -60,6 +60,9 @@ export interface PluginSettings { debug: boolean; enabledCss: boolean; concurrency: string; + + // Zotero Cleanup + zoteroCleanDirectories: string; } export const DEFAULT_SETTINGS: Required = { @@ -116,4 +119,7 @@ export const DEFAULT_SETTINGS: Required = { debug: false, enabledCss: false, concurrency: "5", + + // Zotero Cleanup + zoteroCleanDirectories: "", }; diff --git a/src/features/settings/tab.ts b/src/features/settings/tab.ts index 787c1e5..6cae806 100644 --- a/src/features/settings/tab.ts +++ b/src/features/settings/tab.ts @@ -273,6 +273,21 @@ export class MathSettingTab extends PluginSettingTab { }); }); + containerEl.createEl("h2", { text: "Zotero Cleanup" }); + + new Setting(containerEl) + .setName("Directories to search") + .setDesc("Comma-separated list of directories to search recursively for Zotero annotations (e.g. 'Zotero,Notes/Readings').") + .addTextArea((textArea) => { + textArea + .setValue(this.plugin.settings.zoteroCleanDirectories) + .onChange(async (value) => { + this.plugin.settings.zoteroCleanDirectories = value; + await this.plugin.saveSettings(); + }); + textArea.inputEl.setAttr("rows", 3); + }); + new Setting(containerEl).setName("Debug").setHeading(); new Setting(containerEl) diff --git a/src/features/zotero-cleanup/index.ts b/src/features/zotero-cleanup/index.ts new file mode 100644 index 0000000..ce633b1 --- /dev/null +++ b/src/features/zotero-cleanup/index.ts @@ -0,0 +1,118 @@ +import { MarkdownView, Notice } from 'obsidian'; +import LatexReferencer from '../../main'; + +export const processZoteroCleanup = async (plugin: LatexReferencer, activeView: MarkdownView) => { + const activeFile = activeView.file; + if (!activeFile) { + new Notice("No active file to process."); + return; + } + + const dirsSetting = plugin.settings.zoteroCleanDirectories; + if (!dirsSetting || dirsSetting.trim().length === 0) { + new Notice("No Zotero cleanup directories configured in settings."); + return; + } + + const directories = dirsSetting + .split(',') + .map(d => d.trim()) + .filter(d => d.length > 0) + .map(d => d.replace(/^\/+|\/+$/g, '')); // remove leading/trailing slashes + + if (directories.length === 0) { + new Notice("No valid Zotero cleanup directories configured."); + return; + } + + const app = plugin.app; + const activeContent = activeView.editor.getValue(); + const urlRegex = /zotero:\/\/[^\s\)<>"]+/g; + + const activeUrlsMatches = activeContent.match(urlRegex); + if (!activeUrlsMatches || activeUrlsMatches.length === 0) { + new Notice("No Zotero URLs found in the active note."); + return; + } + + const activeUrls = Array.from(new Set(activeUrlsMatches)); + + // Find all markdown files that match the directories, excluding the active file + const otherFiles = app.vault.getMarkdownFiles().filter(file => { + if (file.path === activeFile.path) return false; + return directories.some(dir => file.path === dir || file.path.startsWith(dir + '/')); + }); + + if (otherFiles.length === 0) { + new Notice("No other markdown files found in the specified directories."); + return; + } + + const existingUrls = new Set(); + const notice = new Notice("Scanning directories for duplicate Zotero annotations...", 0); + + try { + for (const file of otherFiles) { + const text = await app.vault.cachedRead(file); + for (const url of activeUrls) { + if (!existingUrls.has(url) && text.includes(url)) { + existingUrls.add(url); + } + } + if (existingUrls.size === activeUrls.length) break; + } + } finally { + notice.hide(); + } + + if (existingUrls.size === 0) { + new Notice("No duplicate Zotero annotations found."); + return; + } + + const lines = activeContent.split('\n'); + const blocks: string[][] = []; + let currentBlock: string[] = []; + + // Group lines into blocks. A new block starts when a line contains ' l.includes(' { + processZoteroCleanup(this, view); + } + }); + this.addCommand({ id: 'fix-callout-equations', name: 'Fix callout equations in active note',