feat: add Zotero cleanup feature to remove duplicate annotations from active note

This commit is contained in:
JK 2026-05-18 14:21:03 +02:00
parent e1f8b1831b
commit 6018bd89a5
4 changed files with 148 additions and 0 deletions

View file

@ -60,6 +60,9 @@ export interface PluginSettings {
debug: boolean;
enabledCss: boolean;
concurrency: string;
// Zotero Cleanup
zoteroCleanDirectories: string;
}
export const DEFAULT_SETTINGS: Required<PluginSettings> = {
@ -116,4 +119,7 @@ export const DEFAULT_SETTINGS: Required<PluginSettings> = {
debug: false,
enabledCss: false,
concurrency: "5",
// Zotero Cleanup
zoteroCleanDirectories: "",
};

View file

@ -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)

View file

@ -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<string>();
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 '<mark class='
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.includes('<mark class=') && currentBlock.length > 0) {
blocks.push(currentBlock);
currentBlock = [];
}
currentBlock.push(line);
}
if (currentBlock.length > 0) {
blocks.push(currentBlock);
}
const newLines: string[] = [];
for (const block of blocks) {
const blockText = block.join('\n');
let shouldDelete = false;
const isAnnotationBlock = block.some(l => l.includes('<mark class='));
if (isAnnotationBlock) {
for (const url of existingUrls) {
if (blockText.includes(url)) {
shouldDelete = true;
break;
}
}
}
if (!shouldDelete) {
newLines.push(...block);
}
}
const newContent = newLines.join('\n');
if (newContent !== activeContent) {
activeView.editor.setValue(newContent);
new Notice(`Removed ${existingUrls.size} duplicate Zotero annotation(s).`);
} else {
new Notice("No changes made.");
}
};

View file

@ -27,6 +27,7 @@ import { ExportConfigModal } from "./features/export-pdf/modal";
import { checkAndFixCalloutMath } from 'utils/fixer';
import { traverseFolder } from "./features/export-pdf/utils";
import { SnippetManager } from 'features/snippets/manager';
import { processZoteroCleanup } from 'features/zotero-cleanup';
const isDev = process.env.NODE_ENV === "development";
@ -49,6 +50,14 @@ export default class LatexReferencer extends Plugin {
this.addSettingTab(new MathSettingTab(this.app, this));
// Commands
this.addCommand({
id: 'remove-duplicate-zotero-annotations',
name: 'Remove duplicate Zotero annotations',
editorCallback: (editor: Editor, view: MarkdownView) => {
processZoteroCleanup(this, view);
}
});
this.addCommand({
id: 'fix-callout-equations',
name: 'Fix callout equations in active note',