Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bba622ee5 | ||
|
|
aabb25c733 | ||
|
|
eba9a2c113 | ||
|
|
d8dc81dcff | ||
|
|
28bcf2d874 | ||
|
|
85b152f570 | ||
|
|
effe0bbd96 | ||
|
|
a1037ae3a0 |
@@ -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}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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})`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
@@ -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
@@ -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
@@ -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)})`);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user