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