feat: add insert annotation
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
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(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
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,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static formatCreators(
|
||||||
|
creators?: Array<{
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
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(" & ");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static htmlToPlainText(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(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
export type ZoteroResponse = {
|
||||||
|
key: string;
|
||||||
|
version: number;
|
||||||
|
links?: Record<string, unknown>;
|
||||||
|
meta?: Record<string, unknown>;
|
||||||
|
citation?: string;
|
||||||
|
bib?: string;
|
||||||
|
data?: {
|
||||||
|
title?: string;
|
||||||
|
date?: string;
|
||||||
|
itemType?: string;
|
||||||
|
parentItem?: string;
|
||||||
|
annotationText?: string;
|
||||||
|
annotationComment?: string;
|
||||||
|
annotationColor?: string;
|
||||||
|
pageLabel?: string;
|
||||||
|
creators?: Array<{
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
name?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
+80
-33
@@ -1,29 +1,14 @@
|
|||||||
import { Notice, requestUrl } from 'obsidian';
|
import { Notice, requestUrl } from 'obsidian';
|
||||||
|
import { ZoteroItem } from './ZoteroItem';
|
||||||
type ZoteroItemResponse = {
|
import { ZoteroAnnotation } from './ZoteroAnnotation';
|
||||||
key: string;
|
import type { ZoteroResponse } from './ZoteroResponse';
|
||||||
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 ZoteroClient {
|
export class ZoteroClient {
|
||||||
|
|
||||||
private userID: number;
|
private userID: number;
|
||||||
private baseURL: string;
|
private baseURL: string;
|
||||||
private style?: string;
|
private style?: string;
|
||||||
private baseParams: URLSearchParams;
|
private params: URLSearchParams;
|
||||||
|
|
||||||
constructor(port: string);
|
constructor(port: string);
|
||||||
constructor(port: string, style: string);
|
constructor(port: string, style: string);
|
||||||
@@ -31,15 +16,12 @@ export class ZoteroClient {
|
|||||||
this.userID = 0;
|
this.userID = 0;
|
||||||
this.baseURL = `http://127.0.0.1:${port}/api/users/${this.userID}`;
|
this.baseURL = `http://127.0.0.1:${port}/api/users/${this.userID}`;
|
||||||
this.style = style;
|
this.style = style;
|
||||||
this.baseParams = new URLSearchParams();
|
this.params = new URLSearchParams();
|
||||||
if (this.style) {
|
this.params.set("format", "json");
|
||||||
this.baseParams.set("style", this.style);
|
|
||||||
}
|
|
||||||
this.baseParams.set("format", "json");
|
|
||||||
this.baseParams.set("include", "citation,bib,data");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async request<T>(url: string): Promise<T> {
|
private async request<T>(url: string): Promise<T> {
|
||||||
|
console.log(url);
|
||||||
const response = await requestUrl({
|
const response = await requestUrl({
|
||||||
url,
|
url,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -51,7 +33,6 @@ export class ZoteroClient {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
new Notice(`Zotero error ${response.status}: ${response.text}`);
|
new Notice(`Zotero error ${response.status}: ${response.text}`);
|
||||||
throw new Error(`Zotero request failed: ${response.status} ${response.text}`);
|
throw new Error(`Zotero request failed: ${response.status} ${response.text}`);
|
||||||
@@ -66,14 +47,80 @@ export class ZoteroClient {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getItems(): Promise<ZoteroItemResponse[]> {
|
async getItems(): Promise<ZoteroItem[]> {
|
||||||
const url = `${this.baseURL}/items?${this.baseParams.toString()}`;
|
if (this.style) {
|
||||||
return this.request<ZoteroItemResponse[]>(url);
|
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<ZoteroItemResponse[]> {
|
async getItemsTop(): Promise<ZoteroItem[]> {
|
||||||
const url = `${this.baseURL}/items/top?${this.baseParams.toString()}`;
|
if (this.style) {
|
||||||
return this.request<ZoteroItemResponse[]>(url);
|
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 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()}`;
|
||||||
|
return await this.request<ZoteroResponse>(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async resolveParentItem(annotation: ZoteroAnnotation): 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<ZoteroAnnotation[]> {
|
||||||
|
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 => ZoteroAnnotation.fromResponse(annotation))
|
||||||
|
.filter(a => a.text && a.text.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
+16
-5
@@ -1,9 +1,9 @@
|
|||||||
import { App, Editor, MarkdownView, Modal, Plugin, Notice } from 'obsidian';
|
import { App, Editor, MarkdownView, Modal, Plugin, Notice } from 'obsidian';
|
||||||
import { DEFAULT_SETTINGS, CiteSettings, CiteSettingTab } from "./settings";
|
import { DEFAULT_SETTINGS, Settings, SettingTab } from "./settings";
|
||||||
import { CiteModal, OpenModal } from "./modals"
|
import { CiteModal, OpenModal, AnnotationModal } from "./modals"
|
||||||
|
|
||||||
export default class Cite extends Plugin {
|
export default class Cite extends Plugin {
|
||||||
settings: CiteSettings;
|
settings: Settings;
|
||||||
|
|
||||||
async onload() {
|
async onload() {
|
||||||
|
|
||||||
@@ -71,7 +71,18 @@ export default class Cite extends Plugin {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.addSettingTab(new CiteSettingTab(this.app, this));
|
this.addCommand({
|
||||||
|
id: 'annotation-command',
|
||||||
|
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) => {
|
||||||
|
editor.replaceSelection(annotations);
|
||||||
|
}).open();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addSettingTab(new SettingTab(this.app, this));
|
||||||
|
|
||||||
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
|
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
|
||||||
|
|
||||||
@@ -81,7 +92,7 @@ export default class Cite extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadSettings() {
|
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() {
|
async saveSettings() {
|
||||||
|
|||||||
+151
-150
@@ -1,12 +1,134 @@
|
|||||||
import { shell } from "electron";
|
import { shell } from "electron";
|
||||||
import { App, FuzzyMatch, FuzzySuggestModal, renderResults, Notice } from 'obsidian';
|
import { App, FuzzyMatch, FuzzySuggestModal, renderResults, Notice } from 'obsidian';
|
||||||
import { ZoteroClient } from "./clients";
|
import { ZoteroClient } from "./clients";
|
||||||
import type { ZoteroItemResponse } from "./clients";
|
import { ZoteroItem } from "./ZoteroItem";
|
||||||
|
import { ZoteroAnnotation } from "./ZoteroAnnotation";
|
||||||
|
|
||||||
export class OpenModal extends FuzzySuggestModal<ZoteroItemResponse> {
|
const ZOTERO_BASE_URL = "zotero://select/library/items/";
|
||||||
|
|
||||||
|
function renderZoteroItem(
|
||||||
|
match: FuzzyMatch<ZoteroItem>,
|
||||||
|
el: HTMLElement
|
||||||
|
) {
|
||||||
|
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.creators}, ${item.year}`,
|
||||||
|
match.match,
|
||||||
|
offset
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderZoteroAnnotation(
|
||||||
|
match: FuzzyMatch<ZoteroAnnotation>,
|
||||||
|
el: HTMLElement
|
||||||
|
) {
|
||||||
|
const { item: annotation } = match;
|
||||||
|
|
||||||
|
const titleEl = el.createDiv();
|
||||||
|
renderResults(titleEl, annotation.text, match.match);
|
||||||
|
|
||||||
|
const subtitleEl = el.createEl("small");
|
||||||
|
const offset = -(annotation.text.length + 1);
|
||||||
|
renderResults(subtitleEl, annotation.parentItem.toStringShort(), match.match, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAnnotations(
|
||||||
|
client: ZoteroClient,
|
||||||
|
): Promise<ZoteroAnnotation[]> {
|
||||||
|
|
||||||
|
try {
|
||||||
|
const annotations = await client.getAnnotations();
|
||||||
|
return annotations;
|
||||||
|
} catch (error) {
|
||||||
|
new Notice(
|
||||||
|
"Failed to load annotations from Zotero."
|
||||||
|
);
|
||||||
|
console.error(error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AnnotationModal extends FuzzySuggestModal<ZoteroAnnotation> {
|
||||||
private client: ZoteroClient;
|
private client: ZoteroClient;
|
||||||
private items: ZoteroItemResponse[] = [];
|
private annotations: ZoteroAnnotation[] = [];
|
||||||
private zoteroBaseUrl;
|
private createLink: boolean;
|
||||||
|
private onChoose: (annotations: string) => void;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
app: App,
|
||||||
|
port: string,
|
||||||
|
createLink: boolean,
|
||||||
|
style: string,
|
||||||
|
onChoose: (annotations: string) => void,
|
||||||
|
) {
|
||||||
|
super(app);
|
||||||
|
this.client = new ZoteroClient(port, style);
|
||||||
|
this.setPlaceholder("Loading...");
|
||||||
|
this.createLink = createLink
|
||||||
|
this.onChoose = onChoose;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onOpen(){
|
||||||
|
this.annotations = await loadAnnotations(this.client);
|
||||||
|
super.onOpen();
|
||||||
|
}
|
||||||
|
|
||||||
|
getItems(): ZoteroAnnotation[] {
|
||||||
|
return this.annotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
getItemText(annotation: ZoteroAnnotation): string {
|
||||||
|
return annotation.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSuggestion(match: FuzzyMatch<ZoteroAnnotation>, el: HTMLElement) {
|
||||||
|
renderZoteroAnnotation(match, el);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onChooseItem(annotation: ZoteroAnnotation) {
|
||||||
|
|
||||||
|
let citation = annotation.parentItem.citation;
|
||||||
|
|
||||||
|
if (this.createLink) {
|
||||||
|
citation = `[${citation}](${ZOTERO_BASE_URL}${annotation.parentItemKey})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onChoose(`"${annotation.text}" ${citation}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OpenModal extends FuzzySuggestModal<ZoteroItem> {
|
||||||
|
private client: ZoteroClient;
|
||||||
|
private items: ZoteroItem[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
app: App,
|
app: App,
|
||||||
@@ -14,93 +136,39 @@ export class OpenModal extends FuzzySuggestModal<ZoteroItemResponse> {
|
|||||||
) {
|
) {
|
||||||
super(app);
|
super(app);
|
||||||
this.client = new ZoteroClient(port);
|
this.client = new ZoteroClient(port);
|
||||||
this.setPlaceholder("Loading references...");
|
this.setPlaceholder("Loading...");
|
||||||
this.zoteroBaseUrl = "zotero://select/library/items/";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onOpen(){
|
async onOpen(){
|
||||||
|
this.items = await loadItems(this, this.client);
|
||||||
super.onOpen();
|
super.onOpen();
|
||||||
|
|
||||||
try {
|
|
||||||
this.items = await this.client.getItemsTop();
|
|
||||||
this.setPlaceholder("Search Zotero...");
|
|
||||||
this.inputEl.disabled = false;
|
|
||||||
this.inputEl.focus();
|
|
||||||
this.updateSuggestions();
|
|
||||||
} catch (error) {
|
|
||||||
new Notice("Failed to load citations/bibliographies from Zotero. Maybe you forgot to turn on or configure Zotero.");
|
|
||||||
console.error(error);
|
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getItems(): ZoteroItem[] {
|
||||||
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 ?? "";
|
|
||||||
}
|
|
||||||
|
|
||||||
getItems(): ZoteroItemResponse[] {
|
|
||||||
return this.items;
|
return this.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemText(item: ZoteroItemResponse): string {
|
getItemText(item: ZoteroItem): string {
|
||||||
return `${this.getTitle(item)} ${this.getAuthor(item)} ${this.getYear(item)}`;
|
return item.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSuggestion(match: FuzzyMatch<ZoteroItemResponse>, el: HTMLElement) {
|
renderSuggestion(match: FuzzyMatch<ZoteroItem>, el: HTMLElement) {
|
||||||
const { item } = match;
|
renderZoteroItem(match, el);
|
||||||
|
|
||||||
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) {
|
onChooseItem(item: ZoteroItem) {
|
||||||
if (!this.getKey(item)) return;
|
if (!item.key) return;
|
||||||
const uri = `${this.zoteroBaseUrl}${this.getKey(item)}`;
|
const uri = `${ZOTERO_BASE_URL}${item.key}`;
|
||||||
shell.openExternal(uri);
|
shell.openExternal(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CiteModal extends FuzzySuggestModal<ZoteroItemResponse> {
|
export class CiteModal extends FuzzySuggestModal<ZoteroItem> {
|
||||||
|
|
||||||
private onChoose: (reference: string) => void;
|
private onChoose: (reference: string) => void;
|
||||||
private client: ZoteroClient;
|
private client: ZoteroClient;
|
||||||
private items: ZoteroItemResponse[] = [];
|
private items: ZoteroItem[] = [];
|
||||||
private zoteroBaseUrl: string;
|
|
||||||
private createLink: boolean;
|
private createLink: boolean;
|
||||||
private mode: string;
|
private mode: string;
|
||||||
|
|
||||||
@@ -113,106 +181,38 @@ export class CiteModal extends FuzzySuggestModal<ZoteroItemResponse> {
|
|||||||
onChoose: (reference: string) => void,
|
onChoose: (reference: string) => void,
|
||||||
) {
|
) {
|
||||||
super(app);
|
super(app);
|
||||||
|
this.setPlaceholder("Loading...");
|
||||||
this.client = new ZoteroClient(port, style);
|
this.client = new ZoteroClient(port, style);
|
||||||
this.onChoose = onChoose;
|
|
||||||
this.setPlaceholder("Loading citations...");
|
|
||||||
this.zoteroBaseUrl = "zotero://select/library/items/";
|
|
||||||
this.createLink = createLink;
|
this.createLink = createLink;
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
|
this.onChoose = onChoose;
|
||||||
}
|
}
|
||||||
|
|
||||||
async onOpen(){
|
async onOpen(){
|
||||||
|
this.items = await loadItems(this, this.client);
|
||||||
super.onOpen();
|
super.onOpen();
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.mode === "citation"){
|
|
||||||
this.items = await this.client.getItemsTop();
|
|
||||||
} else if (this.mode === "bibliography"){
|
|
||||||
this.items = await this.client.getItemsTop();
|
|
||||||
} else {
|
|
||||||
throw new Error(`Unknown mode: ${this.mode}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setPlaceholder("Search Zotero...");
|
|
||||||
this.inputEl.disabled = false;
|
|
||||||
this.inputEl.focus();
|
|
||||||
this.updateSuggestions();
|
|
||||||
} catch (error) {
|
|
||||||
new Notice("Failed to load citations/bibliographies from Zotero. Maybe you forgot to turn on or configure Zotero.");
|
|
||||||
console.error(error);
|
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTitle(item: ZoteroItemResponse): string {
|
getItems(): ZoteroItem[] {
|
||||||
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";
|
|
||||||
}
|
|
||||||
|
|
||||||
private htmlToPlainText(html: string): string {
|
|
||||||
const div = document.createElement("div");
|
|
||||||
div.innerHTML = html;
|
|
||||||
|
|
||||||
return div.textContent?.trim() ?? "";
|
|
||||||
}
|
|
||||||
|
|
||||||
getItems(): ZoteroItemResponse[] {
|
|
||||||
return this.items;
|
return this.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemText(item: ZoteroItemResponse): string {
|
getItemText(item: ZoteroItem): string {
|
||||||
return `${this.getTitle(item)} ${this.getAuthor(item)} ${this.getYear(item)}`;
|
return item.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSuggestion(match: FuzzyMatch<ZoteroItemResponse>, el: HTMLElement) {
|
renderSuggestion(match: FuzzyMatch<ZoteroItem>, el: HTMLElement) {
|
||||||
const { item } = match;
|
renderZoteroItem(match, el);
|
||||||
|
|
||||||
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) {
|
onChooseItem(item: ZoteroItem) {
|
||||||
|
|
||||||
let output = "";
|
let output = "";
|
||||||
|
|
||||||
if (this.mode === "citation"){
|
if (this.mode === "citation"){
|
||||||
const raw = item.citation ?? "";
|
output = item.citation;
|
||||||
output = this.htmlToPlainText(raw);
|
|
||||||
} else if (this.mode === "bibliography"){
|
} else if (this.mode === "bibliography"){
|
||||||
const raw = item.bib ?? "";
|
output = item.bibliography;
|
||||||
output = this.htmlToPlainText(raw);
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unknown mode: ${this.mode}`);
|
throw new Error(`Unknown mode: ${this.mode}`);
|
||||||
}
|
}
|
||||||
@@ -220,8 +220,9 @@ export class CiteModal extends FuzzySuggestModal<ZoteroItemResponse> {
|
|||||||
if (!this.createLink) {
|
if (!this.createLink) {
|
||||||
this.onChoose(output);
|
this.onChoose(output);
|
||||||
} else {
|
} else {
|
||||||
this.onChoose(`[${output}](${this.zoteroBaseUrl}${this.getKey(item)})`);
|
this.onChoose(`[${output}](${ZOTERO_BASE_URL}${item.key})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -1,18 +1,18 @@
|
|||||||
import {App, PluginSettingTab, Setting } from "obsidian";
|
import {App, PluginSettingTab, Setting } from "obsidian";
|
||||||
|
|
||||||
export interface CiteSettings {
|
export interface Settings {
|
||||||
portSetting: string;
|
portSetting: string;
|
||||||
styleSetting: string;
|
styleSetting: string;
|
||||||
linkSetting: boolean;
|
linkSetting: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: CiteSettings = {
|
export const DEFAULT_SETTINGS: Settings = {
|
||||||
portSetting: '23119',
|
portSetting: '23119',
|
||||||
styleSetting: 'apa',
|
styleSetting: 'apa',
|
||||||
linkSetting: true
|
linkSetting: true
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CiteSettingTab extends PluginSettingTab {
|
export class SettingTab extends PluginSettingTab {
|
||||||
plugin: Cite;
|
plugin: Cite;
|
||||||
|
|
||||||
constructor(app: App, plugin: Cite) {
|
constructor(app: App, plugin: Cite) {
|
||||||
|
|||||||
Reference in New Issue
Block a user