Compare commits

8 Commits
13 changed files with 665 additions and 234 deletions
+55
View File
@@ -0,0 +1,55 @@
import { App, FuzzyMatch, FuzzySuggestModal } from 'obsidian';
import { ZoteroClient } from "./ZoteroClient";
import { loadAnnotations, renderZoteroAnnotation } from "./Utils"
import { Settings } from "./Settings";
const ZOTERO_BASE_URL = "zotero://open-pdf/library/items/";
export class AnnotationModal extends FuzzySuggestModal<ZoteroItem> {
private client: ZoteroClient;
private annotations: ZoteroItem[] = [];
private onChoose: (item: string) => void;
private settings: Settings;
constructor(
app: App,
settings: Settings,
onChoose: (item: string) => void,
) {
super(app);
this.settings = settings;
this.client = new ZoteroClient(this.settings.port, this.settings.style);
this.setPlaceholder("Loading...");
this.onChoose = onChoose;
}
async onOpen(){
this.annotations = await loadAnnotations(this.client);
super.onOpen();
}
getItems(): ZoteroItem[] {
return this.annotations;
}
getItemText(item: ZoteroItem): string {
return `${item.annotationText} ${item.parentItem[this.settings.search]}`;
}
renderSuggestion(match: FuzzyMatch<ZoteroItem>, el: HTMLElement) {
renderZoteroAnnotation(match, el, this.settings.search);
}
async onChooseItem(item: ZoteroItem) {
let citation = item.parentItem.citation ?? "";
if (this.settings.link) {
citation = `[${citation}](${ZOTERO_BASE_URL}${item.attachmentItemKey}?annotation=${item.key})`;
}
this.onChoose(`"${item.annotationText}" ${citation}`);
}
}
+68
View File
@@ -0,0 +1,68 @@
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 CiteModal extends FuzzySuggestModal<ZoteroItem> {
private onChoose: (reference: string) => void;
private client: ZoteroClient;
private items: ZoteroItem[] = [];
private settings: Settings;
private mode: string;
constructor(
app: App,
settings: Settings,
mode: string,
onChoose: (reference: string) => void,
) {
super(app);
this.setPlaceholder("Loading...");
this.settings = settings;
this.client = new ZoteroClient(this.settings.port, this.settings.style);
this.mode = mode;
this.onChoose = onChoose;
}
async onOpen(){
this.items = await loadItems(this, this.client);
super.onOpen();
}
getItems(): ZoteroItem[] {
return this.items;
}
getItemText(item: ZoteroItem): string {
return `${item.title} ${item[this.settings.search]}`;
}
renderSuggestion(match: FuzzyMatch<ZoteroItem>, el: HTMLElement) {
renderZoteroItem(match, el, this.settings.search);
}
onChooseItem(item: ZoteroItem) {
let output = "";
if (this.mode === "citation"){
output = item.citation;
} else if (this.mode === "bibliography"){
output = item.bibliography;
} else {
throw new Error(`Unknown mode: ${this.mode}`);
}
if (!this.settings.link) {
this.onChoose(output);
} else {
this.onChoose(`[${output}](${ZOTERO_BASE_URL}${item.key})`);
}
}
}
+49
View File
@@ -0,0 +1,49 @@
import { shell } from "electron";
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,
settings: Settings,
) {
super(app);
this.settings = settings;
this.client = new ZoteroClient(this.settings.port);
this.setPlaceholder("Loading...");
}
async onOpen(){
this.items = await loadItems(this, this.client);
super.onOpen();
}
getItems(): ZoteroItem[] {
return this.items;
}
getItemText(item: ZoteroItem): string {
return `${item.title} ${item[this.settings.search]}`;
}
renderSuggestion(match: FuzzyMatch<ZoteroItem>, el: HTMLElement) {
renderZoteroItem(match, el, this.settings.search);
}
onChooseItem(item: ZoteroItem) {
if (!item.key) return;
const uri = `${ZOTERO_BASE_URL}${item.key}`;
shell.openExternal(uri);
}
}
+25 -23
View File
@@ -1,19 +1,7 @@
import {App, PluginSettingTab, Setting } from "obsidian";
import Cite from "./main";
import { ZoteroFields } from "./ZoteroFields";
export interface CiteSettings {
portSetting: string;
styleSetting: string;
linkSetting: boolean;
}
export const DEFAULT_SETTINGS: CiteSettings = {
portSetting: '23119',
styleSetting: 'apa',
linkSetting: true
}
export class CiteSettingTab extends PluginSettingTab {
export class SettingTab extends PluginSettingTab {
plugin: Cite;
constructor(app: App, plugin: Cite) {
@@ -31,9 +19,9 @@ export class CiteSettingTab 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();
}));
@@ -42,24 +30,38 @@ export class CiteSettingTab 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();
}));
new Setting(containerEl)
.setName('Add link to library')
.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: "citation",
}
+97
View File
@@ -0,0 +1,97 @@
import { renderResults, Notice } from "obsidian";
export function renderZoteroItem(
match: FuzzyMatch<ZoteroItem>,
el: HTMLElement,
property: string
) {
const { item } = match;
const titleEl = el.createDiv();
renderResults(titleEl, item.title, match.match);
const subtitleEl = el.createEl("small");
const offset = -(item.title.length + 1);
renderResults(
subtitleEl,
`${item[property]}`,
match.match,
offset
);
}
export function renderZoteroAnnotation(
match: FuzzyMatch<ZoteroItem>,
el: HTMLElement,
property: string
) {
const { item } = match;
const titleEl = el.createDiv();
renderResults(titleEl, item.annotationText, match.match);
const subtitleEl = el.createEl("small");
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(
modal: FuzzySuggestModal<ZoteroItem>,
client: ZoteroClient,
): Promise<ZoteroItem[]> {
try {
const items = await client.getItemsTop();
modal.setPlaceholder("Search Zotero...");
modal.inputEl.disabled = false;
modal.inputEl.focus();
modal.updateSuggestions();
return items;
} catch (error) {
new Notice(
"Failed to load citations/bibliographies from Zotero."
);
console.error(error);
modal.close();
return [];
}
}
export async function loadAnnotations(
client: ZoteroClient,
): Promise<ZoteroItem[]> {
try {
const annotations = await client.getAnnotations();
return annotations;
} catch (error) {
new Notice(
"Failed to load annotations from Zotero."
);
console.error(error);
return [];
}
}
+135
View File
@@ -0,0 +1,135 @@
import { Notice, requestUrl } from 'obsidian';
import { ZoteroItem } from './ZoteroItem';
import type { ZoteroResponse } from './ZoteroResponse';
export class ZoteroClient {
private userID: number;
private baseURL: string;
private style?: string;
private params: URLSearchParams;
private itemCache = new Map<string, Promise<ZoteroResponse>>();
constructor(port: string);
constructor(port: string, style: string);
constructor(port: string, style?: string) {
this.userID = 0;
this.baseURL = `http://127.0.0.1:${port}/api/users/${this.userID}`;
this.style = style;
this.params = new URLSearchParams();
this.params.set("format", "json");
}
private async request<T>(url: string): Promise<T> {
console.log(url);
const response = await requestUrl({
url,
method: "GET",
throw: false,
headers: {
"Zotero-API-Version": "3",
"Accept": "application/json",
"User-Agent": "curl/8.0.0",
},
});
if (response.status !== 200) {
new Notice(`Zotero error ${response.status}: ${response.text}`);
throw new Error(`Zotero request failed: ${response.status} ${response.text}`);
}
if (response.json == null) {
new Notice(`Invalid JSON response from Zotero: ${response.text}`);
throw new Error(`Invalid JSON response from Zotero: ${response.text}`);
}
return response.json as T;
}
async getItems(): Promise<ZoteroItem[]> {
if (this.style) {
this.params.set("style", this.style);
}
this.params.set("include", "citation,bib,data");
const url = `${this.baseURL}/items?${this.params.toString()}`;
const items = await this.request<ZoteroResponse[]>(url);
return items.map(item => ZoteroItem.fromResponse(item));
}
async getItemsTop(): Promise<ZoteroItem[]> {
if (this.style) {
this.params.set("style", this.style);
}
this.params.set("include", "citation,bib,data");
const url = `${this.baseURL}/items/top?${this.params.toString()}`;
const items = await this.request<ZoteroResponse[]>(url);
return items.map(item => ZoteroItem.fromResponse(item));
}
private async getItemByKey(itemKey: string, include: string = "data"): Promise<ZoteroResponse> {
const cacheKey = `${itemKey}:${include}:${this.style ?? ""}`;
if (this.itemCache.has(cacheKey)) {
return this.itemCache.get(cacheKey)!;
}
const params = new URLSearchParams();
params.set("format", "json");
params.set("include", include);
if (this.style && include.includes("citation")) {
params.set("style", this.style);
}
const url = `${this.baseURL}/items/${itemKey}?${params.toString()}`;
const promise = this.request<ZoteroResponse>(url);
this.itemCache.set(cacheKey, promise);
return promise;
}
private async resolveParentItem(annotation: ZoteroItem): Promise<void> {
if (!annotation.attachmentItemKey) return;
const attachmentOrParent = await this.getItemByKey(annotation.attachmentItemKey, "citation,bib,data");
const parentItemKey = attachmentOrParent.data?.parentItem;
if (!parentItemKey) {
annotation.parentItemKey = attachmentOrParent.key;
const fullItem = await this.getItemByKey(attachmentOrParent.key, "citation,bib,data");
annotation.parentItem = ZoteroItem.fromResponse(fullItem);
return;
}
annotation.parentItemKey = parentItemKey;
const fullParentItem = await this.getItemByKey(parentItemKey, "citation,bib,data");
annotation.parentItem = ZoteroItem.fromResponse(fullParentItem);
}
async getAnnotations(): Promise<ZoteroItem[]> {
this.params.set("include", "data");
this.params.set("itemType", "annotation");
const url = `${this.baseURL}/items?${this.params.toString()}`;
const responses = await this.request<ZoteroResponse[]>(url);
this.params.delete("itemType");
const annotations = responses
.map(annotation => ZoteroItem.fromResponse(annotation))
.filter(a => a.annotationText && a.annotationText.trim() !== "");
for (const annotation of annotations) {
try {
await this.resolveParentItem(annotation);
} catch (error) {
console.error(`Failed to resolve parent item for annotation ${annotation.key}`, error);
}
}
return annotations;
}
}
+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",
]
+115
View File
@@ -0,0 +1,115 @@
import type { ZoteroResponse } from "./ZoteroResponse";
export class ZoteroItem {
constructor(
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 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(
creators?: Array<{
firstName?: string;
lastName?: string;
name?: string;
}>
): string {
return creators
?.map(c => c.name ?? [c.firstName, c.lastName].filter(Boolean).join(" "))
.filter(Boolean)
.join(", ") ?? "";
}
private static html2text(html: string): string {
const div = document.createElement("div");
div.innerHTML = html;
return div.textContent?.trim() ?? "";
}
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 "";
}
}
}
+38
View File
@@ -0,0 +1,38 @@
export type ZoteroResponse = {
key: string;
version: number;
links?: Record<string, unknown>;
meta?: Record<string, unknown>;
citation?: string;
bib?: string;
data?: {
itemType?: 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;
};
};
-77
View File
@@ -1,77 +0,0 @@
import { Notice, requestUrl } from 'obsidian';
import Cite from "./main";
type ZoteroItemResponse = {
key: string;
version: number;
links?: Record<string, unknown>;
meta?: Record<string, unknown>;
citation?: string;
bib?: string;
data?: {
title?: string;
date?: string;
creators?: Array<{
firstName?: string;
lastName?: string;
name?: string;
}>;
};
};
export class CiteReferenceClient {
private baseURL: string;
private userID: number;
constructor(port: string){
this.baseURL = `http://127.0.0.1:${port}/api`;
this.userID = 0;
}
private async request<T>(url: string): Promise<T> {
const response = await requestUrl({
url,
method: "GET",
throw: false,
headers: {
"Zotero-API-Version": "3",
"Accept": "application/json",
"User-Agent": "curl/8.0.0",
},
});
if (response.status !== 200) {
new Notice(`Zotero error ${response.status}: ${response.text}`);
throw new Error(`Zotero request failed: ${response.status} ${response.text}`);
}
if (response.json == null) {
new Notice(`Invalid JSON response from Zotero: ${response.text}`);
throw new Error(`Invalid JSON response from Zotero: ${response.text}`);
}
return response.json as T;
}
async getItemReference(itemKey: string): Promise<string> {
const url = `${this.baseURL}/users/${this.userID}/items/${itemKey}?format=json&include=citation`
const item = await this.request<ZoteroItemResponse>(url);
if (!item.citation) {
throw new Error(`No citation returned for item ${itemKey}`);
}
return item.citation;
}
async getAllItems(): Promise<ZoteroItemResponse[]> {
const url = `${this.baseURL}/users/${this.userID}/items?format=json&include=citation,data&style=apa`
return this.request<ZoteroItemResponse[]>(url);
}
}
+39 -22
View File
@@ -1,42 +1,59 @@
import { App, Editor, MarkdownView, Modal, Plugin, Notice } from 'obsidian';
import { DEFAULT_SETTINGS, CiteSettings, CiteSettingTab } from "./settings";
import { CiteReferenceModal } from "./modals"
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";
export default class Cite extends Plugin {
settings: CiteSettings;
settings: Settings;
async onload() {
await this.loadSettings();
this.addRibbonIcon('scroll-text', 'cite', (evt: MouseEvent) => {
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
this.addSettingTab(new SettingTab(this.app, this));
if (!view) {
new Notice("No active Markdown view");
console.log("No active Markdown view");
return;
}
const editor = view.editor;
new CiteReferenceModal(this.app, this.settings.portSetting, this.settings.linkSetting, (citation: string) => {
editor.replaceSelection(citation);
}).open();
this.addCommand({
id: 'open-command',
name: 'open',
callback: () => {
new OpenModal(this.app, this.settings).open();
},
});
this.addCommand({
id: 'citation-modal',
name: 'reference',
id: 'citation-command',
name: 'insert citation',
editorCallback: (editor: Editor, view: MarkdownView) => {
const selection = editor.getSelection();
new CiteReferenceModal(this.app, this.settings.portSetting, this.settings.linkSetting, (citation: string) => {
new CiteModal(this.app, this.settings, "citation",(citation: string) => {
editor.replaceSelection(citation);
}).open();
}
});
this.addSettingTab(new CiteSettingTab(this.app, this));
this.addCommand({
id: 'bibliograpy-command',
name: 'insert bibliography',
editorCallback: (editor: Editor, view: MarkdownView) => {
const selection = editor.getSelection();
new CiteModal(this.app, this.settings, "bibliography", (bibliography: string) => {
editor.replaceSelection(bibliography);
}).open();
}
});
this.addCommand({
id: 'annotation-command',
name: 'insert annotations',
editorCallback: (editor: Editor, view: MarkdownView) => {
const selection = editor.getSelection();
new AnnotationModal(this.app, this.settings, (annotations: string) => {
editor.replaceSelection(annotations);
}).open();
}
});
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
@@ -46,7 +63,7 @@ export default class Cite extends Plugin {
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData() as Partial<CiteSettings>);
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData() as Partial<Settings>);
}
async saveSettings() {
-112
View File
@@ -1,112 +0,0 @@
import { App, FuzzyMatch, FuzzySuggestModal, renderResults, Notice } from 'obsidian';
import {CiteReferenceClient, ZoteroItemResponse} from "./clients"
export class CiteReferenceModal extends FuzzySuggestModal<ZoteroItemResponse> {
private onChoose: (citation: string) => void;
private client: CiteReferenceClient;
private citations: ZoteroItemResponse[] = [];
private zoteroBaseUrl: string;
private createLink: boolean;
constructor(
app: App,
port: string,
createLink: boolean,
onChoose: (citation: string) => void,
) {
super(app);
this.client = new CiteReferenceClient(port);
this.onChoose = onChoose;
this.setPlaceholder("Loading citations...");
this.zoteroBaseUrl = "zotero://select/library/items/";
this.createLink = createLink;
}
async onOpen(){
super.onOpen();
try {
const items = await this.client.getAllItems();
this.citations = items.filter(
(item): item is ZoteroItemResponse & { citation: string } => !!item.citation
);
this.setPlaceholder("Search citations...");
this.inputEl.disabled = false;
this.inputEl.focus();
this.updateSuggestions();
} catch (error) {
new Notice("Failed to load citations from Zotero. Maybe you forgot to turn on or configure Zotero.");
console.error(error);
this.close();
}
}
private getTitle(item: ZoteroItemResponse): string {
return item.data?.title ?? "Untitled";
}
private getYear(item: ZoteroItemResponse): string {
const date = item.data?.date;
if (!date) return "n.d.";
const match = date.match(/\b\d{4}\b/);
return match ? match[0] : "n.d.";
}
private getAuthor(item: ZoteroItemResponse): string {
const creators = item.data?.creators;
if (!creators || creators.length === 0) return "Unknown author";
const first = creators[0];
if (first.name) return first.name;
if (first.firstName && first.lastName) return `${first.firstName} ${first.lastName}`;
if (first.lastName) return first.lastName;
return "Unknown author";
}
private getKey(item: ZoteroItemResponse): string {
return item.key ?? "No Key";
}
getItems(): ZoteroItemResponse[] {
return this.citations;
}
getItemText(item: ZoteroItemResponse): string {
return `${this.getTitle(item)} ${this.getAuthor(item)} ${this.getYear(item)}`;
}
renderSuggestion(match: FuzzyMatch<ZoteroItemResponse>, el: HTMLElement) {
const { item } = match;
const title = this.getTitle(item);
const author = this.getAuthor(item);
const year = this.getYear(item);
const titleEl = el.createDiv();
renderResults(titleEl, title, match.match);
const subtitleEl = el.createEl("small");
const offset = -(title.length + 1);
renderResults(subtitleEl, `${author}, ${year}`, match.match, offset);
}
onChooseItem(item: ZoteroItemResponse) {
if (!item.citation) {
throw new Error(`Missing citation for item ${item.key}`);
return;
}
if (!this.createLink) {
this.onChoose(item.citation);
} else {
this.onChoose(`[${item.citation}](${this.zoteroBaseUrl}${this.getKey(item)})`);
}
}
}