feat: add secondary search parameter

This commit is contained in:
huaqo
2026-04-24 19:31:38 +02:00
parent d8dc81dcff
commit eba9a2c113
12 changed files with 253 additions and 162 deletions
+19 -22
View File
@@ -1,27 +1,25 @@
import { App, FuzzyMatch, FuzzySuggestModal } from 'obsidian'; import { App, FuzzyMatch, FuzzySuggestModal } from 'obsidian';
import { ZoteroClient } from "./ZoteroClient"; import { ZoteroClient } from "./ZoteroClient";
import { ZoteroAnnotation } from "./ZoteroAnnotation";
import { loadAnnotations, renderZoteroAnnotation } from "./Utils" import { loadAnnotations, renderZoteroAnnotation } from "./Utils"
import { Settings } from "./Settings";
const ZOTERO_BASE_URL = "zotero://select/library/items/"; const ZOTERO_BASE_URL = "zotero://select/library/items/";
export class AnnotationModal extends FuzzySuggestModal<ZoteroAnnotation> { export class AnnotationModal extends FuzzySuggestModal<ZoteroItem> {
private client: ZoteroClient; private client: ZoteroClient;
private annotations: ZoteroAnnotation[] = []; private annotations: ZoteroItem[] = [];
private createLink: boolean; private onChoose: (item: string) => void;
private onChoose: (annotations: string) => void; private settings: Settings;
constructor( constructor(
app: App, app: App,
port: string, settings: Settings,
createLink: boolean, onChoose: (item: string) => void,
style: string,
onChoose: (annotations: string) => void,
) { ) {
super(app); super(app);
this.client = new ZoteroClient(port, style); this.settings = settings;
this.client = new ZoteroClient(this.settings.port, this.settings.style);
this.setPlaceholder("Loading..."); this.setPlaceholder("Loading...");
this.createLink = createLink
this.onChoose = onChoose; this.onChoose = onChoose;
} }
@@ -30,28 +28,27 @@ export class AnnotationModal extends FuzzySuggestModal<ZoteroAnnotation> {
super.onOpen(); super.onOpen();
} }
getItems(): ZoteroAnnotation[] { getItems(): ZoteroItem[] {
return this.annotations; return this.annotations;
} }
getItemText(item: ZoteroItem): string {
getItemText(annotation: ZoteroAnnotation): string { return `${item.annotationText} ${item.parentItem[this.settings.search]}`;
return annotation.toString();
} }
renderSuggestion(match: FuzzyMatch<ZoteroAnnotation>, el: HTMLElement) { renderSuggestion(match: FuzzyMatch<ZoteroItem>, el: HTMLElement) {
renderZoteroAnnotation(match, el); renderZoteroAnnotation(match, el, this.settings.search);
} }
async onChooseItem(annotation: ZoteroAnnotation) { async onChooseItem(item: ZoteroItem) {
let citation = annotation.parentItem.citation; let citation = item.parentItem.citation ?? "";
if (this.createLink) { if (this.settings.link) {
citation = `[${citation}](${ZOTERO_BASE_URL}${annotation.parentItemKey})`; citation = `[${citation}](${ZOTERO_BASE_URL}${item.parentItemKey})`;
} }
this.onChoose(`"${annotation.text}" ${citation}`); this.onChoose(`"${item.annotationText}" ${citation}`);
} }
} }
+8 -9
View File
@@ -2,6 +2,7 @@ import { App, FuzzyMatch, FuzzySuggestModal } from 'obsidian';
import { ZoteroClient } from "./ZoteroClient"; import { ZoteroClient } from "./ZoteroClient";
import { ZoteroItem } from "./ZoteroItem"; import { ZoteroItem } from "./ZoteroItem";
import { loadItems, renderZoteroItem } from "./Utils" import { loadItems, renderZoteroItem } from "./Utils"
import { Settings } from "./Settings";
const ZOTERO_BASE_URL = "zotero://select/library/items/"; const ZOTERO_BASE_URL = "zotero://select/library/items/";
@@ -10,21 +11,19 @@ export class CiteModal extends FuzzySuggestModal<ZoteroItem> {
private onChoose: (reference: string) => void; private onChoose: (reference: string) => void;
private client: ZoteroClient; private client: ZoteroClient;
private items: ZoteroItem[] = []; private items: ZoteroItem[] = [];
private createLink: boolean; private settings: Settings;
private mode: string; private mode: string;
constructor( constructor(
app: App, app: App,
port: string, settings: Settings,
createLink: boolean,
style: string,
mode: string, mode: string,
onChoose: (reference: string) => void, onChoose: (reference: string) => void,
) { ) {
super(app); super(app);
this.setPlaceholder("Loading..."); this.setPlaceholder("Loading...");
this.client = new ZoteroClient(port, style); this.settings = settings;
this.createLink = createLink; this.client = new ZoteroClient(this.settings.port, this.settings.style);
this.mode = mode; this.mode = mode;
this.onChoose = onChoose; this.onChoose = onChoose;
} }
@@ -39,11 +38,11 @@ export class CiteModal extends FuzzySuggestModal<ZoteroItem> {
} }
getItemText(item: ZoteroItem): string { getItemText(item: ZoteroItem): string {
return item.toString(); return `${item.title} ${item[this.settings.search]}`;
} }
renderSuggestion(match: FuzzyMatch<ZoteroItem>, el: HTMLElement) { renderSuggestion(match: FuzzyMatch<ZoteroItem>, el: HTMLElement) {
renderZoteroItem(match, el); renderZoteroItem(match, el, this.settings.search);
} }
onChooseItem(item: ZoteroItem) { onChooseItem(item: ZoteroItem) {
@@ -58,7 +57,7 @@ export class CiteModal extends FuzzySuggestModal<ZoteroItem> {
throw new Error(`Unknown mode: ${this.mode}`); throw new Error(`Unknown mode: ${this.mode}`);
} }
if (!this.createLink) { if (!this.settings.link) {
this.onChoose(output); this.onChoose(output);
} else { } else {
this.onChoose(`[${output}](${ZOTERO_BASE_URL}${item.key})`); this.onChoose(`[${output}](${ZOTERO_BASE_URL}${item.key})`);
+7 -4
View File
@@ -3,19 +3,22 @@ import { App, FuzzyMatch, FuzzySuggestModal } from 'obsidian';
import { ZoteroClient } from "./ZoteroClient"; import { ZoteroClient } from "./ZoteroClient";
import { ZoteroItem } from "./ZoteroItem"; import { ZoteroItem } from "./ZoteroItem";
import { loadItems, renderZoteroItem } from "./Utils" import { loadItems, renderZoteroItem } from "./Utils"
import { Settings } from "./Settings";
const ZOTERO_BASE_URL = "zotero://select/library/items/"; const ZOTERO_BASE_URL = "zotero://select/library/items/";
export class OpenModal extends FuzzySuggestModal<ZoteroItem> { export class OpenModal extends FuzzySuggestModal<ZoteroItem> {
private client: ZoteroClient; private client: ZoteroClient;
private items: ZoteroItem[] = []; private items: ZoteroItem[] = [];
private settings: Settings
constructor( constructor(
app: App, app: App,
port: string, settings: Settings,
) { ) {
super(app); super(app);
this.client = new ZoteroClient(port); this.settings = settings;
this.client = new ZoteroClient(this.settings.port);
this.setPlaceholder("Loading..."); this.setPlaceholder("Loading...");
} }
@@ -29,11 +32,11 @@ export class OpenModal extends FuzzySuggestModal<ZoteroItem> {
} }
getItemText(item: ZoteroItem): string { getItemText(item: ZoteroItem): string {
return item.toString(); return `${item.title} ${item[this.settings.search]}`;
} }
renderSuggestion(match: FuzzyMatch<ZoteroItem>, el: HTMLElement) { renderSuggestion(match: FuzzyMatch<ZoteroItem>, el: HTMLElement) {
renderZoteroItem(match, el); renderZoteroItem(match, el, this.settings.search);
} }
onChooseItem(item: ZoteroItem) { onChooseItem(item: ZoteroItem) {
+23 -20
View File
@@ -1,16 +1,5 @@
import {App, PluginSettingTab, Setting } from "obsidian"; import {App, PluginSettingTab, Setting } from "obsidian";
import { ZoteroFields } from "./ZoteroFields";
export interface Settings {
portSetting: string;
styleSetting: string;
linkSetting: boolean;
}
export const DEFAULT_SETTINGS: Settings = {
portSetting: '23119',
styleSetting: 'apa',
linkSetting: true
}
export class SettingTab extends PluginSettingTab { export class SettingTab extends PluginSettingTab {
plugin: Cite; plugin: Cite;
@@ -30,9 +19,9 @@ export class SettingTab extends PluginSettingTab {
.setDesc('http://localhost:<PORT>/api/') .setDesc('http://localhost:<PORT>/api/')
.addText(text => text .addText(text => text
.setPlaceholder('Enter a Port') .setPlaceholder('Enter a Port')
.setValue(this.plugin.settings.portSetting) .setValue(this.plugin.settings.port)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.portSetting = value; this.plugin.settings.port = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
})); }));
@@ -41,9 +30,9 @@ export class SettingTab extends PluginSettingTab {
.setDesc('https://www.zotero.org/styles?q=<STYLE>') .setDesc('https://www.zotero.org/styles?q=<STYLE>')
.addText(text => text .addText(text => text
.setPlaceholder('Enter a Style') .setPlaceholder('Enter a Style')
.setValue(this.plugin.settings.styleSetting) .setValue(this.plugin.settings.style)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.styleSetting = value; this.plugin.settings.style = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
})); }));
@@ -51,14 +40,28 @@ export class SettingTab extends PluginSettingTab {
.setName('URI') .setName('URI')
.setDesc('zotero://select/library/items/...') .setDesc('zotero://select/library/items/...')
.addToggle(toggle => toggle .addToggle(toggle => toggle
.setValue(this.plugin.settings.linkSetting) .setValue(this.plugin.settings.link)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.linkSetting = value; this.plugin.settings.link = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
this.display(); this.display();
}) }));
);
new Setting(containerEl)
.setName('Search')
.setDesc('secondary search field')
.addDropdown(dropdown => {
ZoteroFields.forEach (opt => {
dropdown.addOption(opt, opt);
});
dropdown
.setValue(this.plugin.settings.search)
.onChange(async (value) => {
this.plugin.settings.search = value;
await this.plugin.saveSettings();
});
});
} }
} }
+14
View File
@@ -0,0 +1,14 @@
export interface Settings {
port: string;
style: string;
link: boolean;
search: string;
}
export const DEFAULT_SETTINGS: Settings = {
port: '23119',
style: 'apa',
link: true,
search: "creators",
}
+35 -9
View File
@@ -2,7 +2,8 @@ import { renderResults, Notice } from "obsidian";
export function renderZoteroItem( export function renderZoteroItem(
match: FuzzyMatch<ZoteroItem>, match: FuzzyMatch<ZoteroItem>,
el: HTMLElement el: HTMLElement,
property: string
) { ) {
const { item } = match; const { item } = match;
@@ -13,24 +14,49 @@ export function renderZoteroItem(
const offset = -(item.title.length + 1); const offset = -(item.title.length + 1);
renderResults( renderResults(
subtitleEl, subtitleEl,
`${item.creators}, ${item.year}`, `${item[property]}`,
match.match, match.match,
offset offset
); );
} }
export function renderZoteroAnnotation( export function renderZoteroAnnotation(
match: FuzzyMatch<ZoteroAnnotation>, match: FuzzyMatch<ZoteroItem>,
el: HTMLElement el: HTMLElement,
property: string
) { ) {
const { item: annotation } = match; const { item } = match;
const titleEl = el.createDiv(); const titleEl = el.createDiv();
renderResults(titleEl, annotation.text, match.match); renderResults(titleEl, item.annotationText, match.match);
const subtitleEl = el.createEl("small"); const subtitleEl = el.createEl("small");
const offset = -(annotation.text.length + 1); const offset = -(item.annotationText.length + 1);
renderResults(subtitleEl, annotation.parentItem.toStringShort(), match.match, offset);
const annotationProperties = [
"attachmentItemKey",
"parentItemKey",
"parentItem",
"annotationText",
"annotationComment",
"annotationColor",
"pageLabel",
]
let output = "";
if (annotationProperties.includes(property)){
output = `${item[property]}`;
} else {
output = `${item.parentItem[property]}`;
};
renderResults(
subtitleEl,
output,
match.match,
offset
);
} }
export async function loadItems( export async function loadItems(
@@ -56,7 +82,7 @@ export async function loadItems(
export async function loadAnnotations( export async function loadAnnotations(
client: ZoteroClient, client: ZoteroClient,
): Promise<ZoteroAnnotation[]> { ): Promise<ZoteroItem[]> {
try { try {
const annotations = await client.getAnnotations(); const annotations = await client.getAnnotations();
-47
View File
@@ -1,47 +0,0 @@
import type { ZoteroResponse } from "./ZoteroResponse";
import { ZoteroItem } from "./ZoteroItem";
export class ZoteroAnnotation {
constructor(
public key: string,
public attachmentItemKey: string,
public parentItemKey: string,
public parentItem?: ZoteroItem,
public text: string = "",
public comment: string = "",
public color: string = "",
public pageLabel: string = "",
){}
static fromResponse(item: ZoteroResponse): ZoteroAnnotation {
return new ZoteroAnnotation(
item.key ?? "",
item.data?.parentItem ?? "",
"",
undefined,
item.data?.annotationText ?? "",
item.data?.annotationComment ?? "",
item.data?.annotationColor ?? "",
item.data?.pageLabel ?? "",
);
}
toString(): string {
return [
this.key,
this.text,
this.comment,
this.color,
this.pageLabel,
this.parentItem?.title,
this.parentItem?.creators,
this.parentItem?.year,
this.parentItemKey,
this.attachmentItemKey,
]
.filter(Boolean)
.join(" ");
}
}
+4 -5
View File
@@ -1,6 +1,5 @@
import { Notice, requestUrl } from 'obsidian'; import { Notice, requestUrl } from 'obsidian';
import { ZoteroItem } from './ZoteroItem'; import { ZoteroItem } from './ZoteroItem';
import { ZoteroAnnotation } from './ZoteroAnnotation';
import type { ZoteroResponse } from './ZoteroResponse'; import type { ZoteroResponse } from './ZoteroResponse';
export class ZoteroClient { export class ZoteroClient {
@@ -90,7 +89,7 @@ export class ZoteroClient {
return promise; return promise;
} }
private async resolveParentItem(annotation: ZoteroAnnotation): Promise<void> { private async resolveParentItem(annotation: ZoteroItem): Promise<void> {
if (!annotation.attachmentItemKey) return; if (!annotation.attachmentItemKey) return;
const attachmentOrParent = await this.getItemByKey(annotation.attachmentItemKey, "citation,bib,data"); const attachmentOrParent = await this.getItemByKey(annotation.attachmentItemKey, "citation,bib,data");
@@ -109,7 +108,7 @@ export class ZoteroClient {
annotation.parentItem = ZoteroItem.fromResponse(fullParentItem); annotation.parentItem = ZoteroItem.fromResponse(fullParentItem);
} }
async getAnnotations(): Promise<ZoteroAnnotation[]> { async getAnnotations(): Promise<ZoteroItem[]> {
this.params.set("include", "data"); this.params.set("include", "data");
this.params.set("itemType", "annotation"); this.params.set("itemType", "annotation");
@@ -119,8 +118,8 @@ export class ZoteroClient {
this.params.delete("itemType"); this.params.delete("itemType");
const annotations = responses const annotations = responses
.map(annotation => ZoteroAnnotation.fromResponse(annotation)) .map(annotation => ZoteroItem.fromResponse(annotation))
.filter(a => a.text && a.text.trim() !== ""); .filter(a => a.annotationText && a.annotationText.trim() !== "");
for (const annotation of annotations) { for (const annotation of annotations) {
try { try {
+30
View File
@@ -0,0 +1,30 @@
export const ZoteroFields =
[
"key",
"citation",
"bibliography",
"itemType",
"title",
"abstractNote",
"date",
"shortTitle",
"language",
"libraryCatalog",
"publisher",
"place",
"ISBN",
"numPages",
"creators",
"tags",
"collections",
"relations",
"dateAdded",
"dateModified",
"attachmentItemKey",
"parentItemKey",
"parentItem",
"annotationText",
"annotationComment",
"annotationColor",
"pageLabel",
]
+84 -33
View File
@@ -2,24 +2,83 @@ import type { ZoteroResponse } from "./ZoteroResponse";
export class ZoteroItem { export class ZoteroItem {
constructor( constructor(
public title: string,
public year: string,
public creators: string,
public key: string, public key: string,
public citation: string, public citation: string,
public bibliography: string, public bibliography: string,
public itemType: string,
public title: string,
public abstractNote: string,
public date: string,
public shortTitle: string,
public language: string,
public libraryCatalog: string,
public publisher: string,
public place: string,
public ISBN: string,
public numPages: string,
public creators: string,
public tags: string,
public collections: string,
public relations: string,
public dateAdded: string,
public dateModified: string,
public attachmentItemKey: string,
public parentItemKey: string,
public parentItem?: ZoteroItem,
public annotationText: string,
public annotationComment: string,
public annotationColor: string,
public pageLabel: string,
) {} ) {}
static fromResponse(item: ZoteroResponse): ZoteroItem { static fromResponse(item: ZoteroResponse): ZoteroItem {
const title = item.data?.title ?? "Untitled"; const data = item.data;
const date = item.data?.date;
const yearMatch = date?.match(/\b\d{4}\b/); return new ZoteroItem(
const year = yearMatch ? yearMatch[0] : "n.d."; item.key ?? "",
const creators = this.formatCreators(item.data?.creators); this.html2text(item.citation ?? ""),
const key = item.key ?? "No Key"; this.html2text(item.bib ?? ""),
const citation = this.htmlToPlainText(item.citation ?? ""); data?.itemType ?? "",
const bibliography = this.htmlToPlainText(item.bib ?? ""); data?.title ?? "",
return new ZoteroItem(title, year, creators, key, citation, bibliography); data?.abstractNote ?? "",
data?.date ?? "",
data?.shortTitle ?? "",
data?.language ?? "",
data?.libraryCatalog ?? "",
data?.publisher ?? "",
data?.place ?? "",
data?.ISBN ?? "",
data?.numPages ?? "",
this.formatCreators(data?.creators),
this.formatTags(data?.tags),
this.array2text(data?.collections),
this.object2text(data?.relations),
data?.dateAdded ?? "",
data?.dateModified ?? "",
data?.parentItem ?? "",
"",
undefined,
data?.annotationText ?? "",
data?.annotationComment ?? "",
this.hex2color(data?.annotationColor),
data?.pageLabel ?? "",
);
}
private static array2text(values?: unknown[]): string {
return values?.map(String).join(", ") ?? "";
}
private static object2text(value?: Record<string, unknown>): string {
if (!value) return "";
return Object.values(value).flat().map(String).join(", ");
}
private static formatTags(tags?: Array<{ tag?: string }>): string {
return tags
?.map(t => t.tag ?? "")
.filter(Boolean)
.join(", ") ?? "";
} }
private static formatCreators( private static formatCreators(
@@ -29,35 +88,27 @@ export class ZoteroItem {
name?: string; name?: string;
}> }>
): string { ): string {
if (!creators || creators.length === 0) return "Unknown author"; return creators
?.map(c => c.name ?? [c.firstName, c.lastName].filter(Boolean).join(" "))
const names = creators.map(c => { .filter(Boolean)
if (c.name) return c.name; .join(", ") ?? "";
if (c.firstName && c.lastName) return `${c.firstName} ${c.lastName}`;
if (c.lastName) return c.lastName;
return "Unknown";
});
return names.join(" & ");
} }
private static htmlToPlainText(html: string): string { private static html2text(html: string): string {
const div = document.createElement("div"); const div = document.createElement("div");
div.innerHTML = html; div.innerHTML = html;
return div.textContent?.trim() ?? ""; return div.textContent?.trim() ?? "";
} }
toString(): string { private static hex2color(hex?: string): string {
return [this.title, this.year, this.creators, this.key, this.citation, this.bibliography] switch (hex?.toLowerCase()) {
.filter(Boolean) case "#a28ae5": return "Purple";
.join(" "); case "#ff6666": return "Red";
} case "#009980": return "Teal";
case "#ffd400": return "Yellow";
toStringShort(): string { default: return "";
return [this.creators, this.year] }
.filter(Boolean)
.join(", ");
} }
} }
+22 -7
View File
@@ -6,18 +6,33 @@ export type ZoteroResponse = {
citation?: string; citation?: string;
bib?: string; bib?: string;
data?: { data?: {
title?: string;
date?: string;
itemType?: string; itemType?: string;
parentItem?: string; title?: string;
annotationText?: string; abstractNote?: string;
annotationComment?: string; date?: string;
annotationColor?: string; shortTitle?: string;
pageLabel?: string; language?: string;
libraryCatalog?: string;
publisher?: string;
place?: string;
ISBN?: string;
numPages?: string;
creators?: Array<{ creators?: Array<{
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
name?: string; name?: string;
}>; }>;
tags?: Array<{
tag?: string;
}>;
collections?: Array<string>;
relations?: Array<any>;
dateAdded?: string;
dateModified?: string;
parentItem?: string;
annotationText?: string;
annotationComment?: string;
annotationColor?: string;
pageLabel?: string;
}; };
}; };
+7 -6
View File
@@ -1,5 +1,6 @@
import { App, Editor, MarkdownView, Modal, Plugin, Notice } from 'obsidian'; import { App, Editor, MarkdownView, Modal, Plugin } from 'obsidian';
import { DEFAULT_SETTINGS, Settings, SettingTab } from "./settings"; import { DEFAULT_SETTINGS, Settings } from "./Settings";
import { SettingTab } from "./SettingTab";
import { CiteModal } from "./CiteModal"; import { CiteModal } from "./CiteModal";
import { OpenModal } from "./OpenModal"; import { OpenModal } from "./OpenModal";
import { AnnotationModal } from "./AnnotationModal"; import { AnnotationModal } from "./AnnotationModal";
@@ -17,7 +18,7 @@ export default class Cite extends Plugin {
id: 'open-command', id: 'open-command',
name: 'open', name: 'open',
callback: () => { callback: () => {
new OpenModal(this.app, this.settings.portSetting).open(); new OpenModal(this.app, this.settings).open();
}, },
}); });
@@ -26,7 +27,7 @@ export default class Cite extends Plugin {
name: 'insert citation', name: 'insert citation',
editorCallback: (editor: Editor, view: MarkdownView) => { editorCallback: (editor: Editor, view: MarkdownView) => {
const selection = editor.getSelection(); const selection = editor.getSelection();
new CiteModal(this.app, this.settings.portSetting, this.settings.linkSetting, this.settings.styleSetting, "citation",(citation: string) => { new CiteModal(this.app, this.settings, "citation",(citation: string) => {
editor.replaceSelection(citation); editor.replaceSelection(citation);
}).open(); }).open();
} }
@@ -37,7 +38,7 @@ export default class Cite extends Plugin {
name: 'insert bibliography', name: 'insert bibliography',
editorCallback: (editor: Editor, view: MarkdownView) => { editorCallback: (editor: Editor, view: MarkdownView) => {
const selection = editor.getSelection(); const selection = editor.getSelection();
new CiteModal(this.app, this.settings.portSetting, this.settings.linkSetting, this.settings.styleSetting, "bibliography", (bibliography: string) => { new CiteModal(this.app, this.settings, "bibliography", (bibliography: string) => {
editor.replaceSelection(bibliography); editor.replaceSelection(bibliography);
}).open(); }).open();
} }
@@ -48,7 +49,7 @@ export default class Cite extends Plugin {
name: 'insert annotations', name: 'insert annotations',
editorCallback: (editor: Editor, view: MarkdownView) => { editorCallback: (editor: Editor, view: MarkdownView) => {
const selection = editor.getSelection(); const selection = editor.getSelection();
new AnnotationModal(this.app, this.settings.portSetting, this.settings.linkSetting, this.settings.styleSetting, (annotations: string) => { new AnnotationModal(this.app, this.settings, (annotations: string) => {
editor.replaceSelection(annotations); editor.replaceSelection(annotations);
}).open(); }).open();
} }