first commit
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
tab_width = 4
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
# vscode
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# Intellij
|
||||||
|
*.iml
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# npm
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Don't include the compiled main.js file in the repo.
|
||||||
|
# They should be uploaded to GitHub releases instead.
|
||||||
|
main.js
|
||||||
|
|
||||||
|
# Exclude sourcemaps
|
||||||
|
*.map
|
||||||
|
|
||||||
|
# obsidian
|
||||||
|
data.json
|
||||||
|
|
||||||
|
# Exclude macOS Finder (System Explorer) View States
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import esbuild from "esbuild";
|
||||||
|
import process from "process";
|
||||||
|
import { builtinModules } from 'node:module';
|
||||||
|
|
||||||
|
const banner =
|
||||||
|
`/*
|
||||||
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||||
|
if you want to view the source, please visit the github repository of this plugin
|
||||||
|
*/
|
||||||
|
`;
|
||||||
|
|
||||||
|
const prod = (process.argv[2] === "production");
|
||||||
|
|
||||||
|
const context = await esbuild.context({
|
||||||
|
banner: {
|
||||||
|
js: banner,
|
||||||
|
},
|
||||||
|
entryPoints: ["src/main.ts"],
|
||||||
|
bundle: true,
|
||||||
|
external: [
|
||||||
|
"obsidian",
|
||||||
|
"electron",
|
||||||
|
"@codemirror/autocomplete",
|
||||||
|
"@codemirror/collab",
|
||||||
|
"@codemirror/commands",
|
||||||
|
"@codemirror/language",
|
||||||
|
"@codemirror/lint",
|
||||||
|
"@codemirror/search",
|
||||||
|
"@codemirror/state",
|
||||||
|
"@codemirror/view",
|
||||||
|
"@lezer/common",
|
||||||
|
"@lezer/highlight",
|
||||||
|
"@lezer/lr",
|
||||||
|
...builtinModules],
|
||||||
|
format: "cjs",
|
||||||
|
target: "es2018",
|
||||||
|
logLevel: "info",
|
||||||
|
sourcemap: prod ? false : "inline",
|
||||||
|
treeShaking: true,
|
||||||
|
outfile: "main.js",
|
||||||
|
minify: prod,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (prod) {
|
||||||
|
await context.rebuild();
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
await context.watch();
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
import obsidianmd from "eslint-plugin-obsidianmd";
|
||||||
|
import globals from "globals";
|
||||||
|
import { globalIgnores } from "eslint/config";
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
projectService: {
|
||||||
|
allowDefaultProject: [
|
||||||
|
'eslint.config.js',
|
||||||
|
'manifest.json'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
extraFileExtensions: ['.json']
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...obsidianmd.configs.recommended,
|
||||||
|
globalIgnores([
|
||||||
|
"node_modules",
|
||||||
|
"dist",
|
||||||
|
"esbuild.config.mjs",
|
||||||
|
"eslint.config.js",
|
||||||
|
"version-bump.mjs",
|
||||||
|
"versions.json",
|
||||||
|
"main.js",
|
||||||
|
]),
|
||||||
|
);
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"id": "cite",
|
||||||
|
"name": "cite",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"minAppVersion": "0.15.0",
|
||||||
|
"description": "Zotero local API integration by huaqo",
|
||||||
|
"author": "Obsidian",
|
||||||
|
"authorUrl": "https://huaqo.github.io/",
|
||||||
|
"isDesktopOnly": false
|
||||||
|
}
|
||||||
Generated
+5160
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "obsidian-sample-plugin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
|
||||||
|
"main": "main.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "node esbuild.config.mjs",
|
||||||
|
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||||
|
"version": "node version-bump.mjs && git add manifest.json versions.json",
|
||||||
|
"lint": "eslint ."
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"license": "0-BSD",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^16.11.6",
|
||||||
|
"esbuild": "0.25.5",
|
||||||
|
"eslint-plugin-obsidianmd": "0.1.9",
|
||||||
|
"globals": "14.0.0",
|
||||||
|
"tslib": "2.4.0",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"typescript-eslint": "8.35.1",
|
||||||
|
"@eslint/js": "9.30.1",
|
||||||
|
"jiti": "2.6.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"obsidian": "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
import { App, Editor, MarkdownView, Modal, Plugin, Notice } from 'obsidian';
|
||||||
|
import { DEFAULT_SETTINGS, CiteSettings, CiteSettingTab } from "./settings";
|
||||||
|
import { CiteReferenceModal } from "./modals"
|
||||||
|
|
||||||
|
export default class Cite extends Plugin {
|
||||||
|
settings: CiteSettings;
|
||||||
|
|
||||||
|
async onload() {
|
||||||
|
|
||||||
|
await this.loadSettings();
|
||||||
|
|
||||||
|
this.addRibbonIcon('scroll-text', 'cite', (evt: MouseEvent) => {
|
||||||
|
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||||
|
|
||||||
|
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: 'citation-modal',
|
||||||
|
name: 'reference',
|
||||||
|
editorCallback: (editor: Editor, view: MarkdownView) => {
|
||||||
|
const selection = editor.getSelection();
|
||||||
|
new CiteReferenceModal(this.app, this.settings.portSetting, this.settings.linkSetting, (citation: string) => {
|
||||||
|
editor.replaceSelection(citation);
|
||||||
|
}).open();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addSettingTab(new CiteSettingTab(this.app, this));
|
||||||
|
|
||||||
|
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onunload() {
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadSettings() {
|
||||||
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData() as Partial<CiteSettings>);
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSettings() {
|
||||||
|
await this.saveData(this.settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+112
@@ -0,0 +1,112 @@
|
|||||||
|
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)})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import {App, PluginSettingTab, Setting } from "obsidian";
|
||||||
|
import Cite from "./main";
|
||||||
|
|
||||||
|
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 {
|
||||||
|
plugin: Cite;
|
||||||
|
|
||||||
|
constructor(app: App, plugin: Cite) {
|
||||||
|
super(app, plugin);
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
display(): void {
|
||||||
|
const {containerEl} = this;
|
||||||
|
|
||||||
|
containerEl.empty();
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Port')
|
||||||
|
.setDesc('http://localhost:<PORT>/api/')
|
||||||
|
.addText(text => text
|
||||||
|
.setPlaceholder('Enter a Port')
|
||||||
|
.setValue(this.plugin.settings.portSetting)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.portSetting = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
}));
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Style')
|
||||||
|
.setDesc('https://www.zotero.org/styles?q=<STYLE>')
|
||||||
|
.addText(text => text
|
||||||
|
.setPlaceholder('Enter a Style')
|
||||||
|
.setValue(this.plugin.settings.styleSetting)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.styleSetting = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
}));
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Add link to library')
|
||||||
|
.setDesc('zotero://select/library/items/...')
|
||||||
|
.addToggle(toggle => toggle
|
||||||
|
.setValue(this.plugin.settings.linkSetting)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.linkSetting = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
this.display();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
This CSS file will be included with your plugin, and
|
||||||
|
available in the app when your plugin is enabled.
|
||||||
|
|
||||||
|
If your plugin does not need CSS, delete this file.
|
||||||
|
|
||||||
|
*/
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "src",
|
||||||
|
"inlineSourceMap": true,
|
||||||
|
"inlineSources": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"target": "ES6",
|
||||||
|
"allowJs": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"importHelpers": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictBindCallApply": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"useUnknownInCatchVariables": true,
|
||||||
|
"lib": [
|
||||||
|
"DOM",
|
||||||
|
"ES5",
|
||||||
|
"ES6",
|
||||||
|
"ES7"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { readFileSync, writeFileSync } from "fs";
|
||||||
|
|
||||||
|
const targetVersion = process.env.npm_package_version;
|
||||||
|
|
||||||
|
// read minAppVersion from manifest.json and bump version to target version
|
||||||
|
const manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
|
||||||
|
const { minAppVersion } = manifest;
|
||||||
|
manifest.version = targetVersion;
|
||||||
|
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
|
||||||
|
|
||||||
|
// update versions.json with target version and minAppVersion from manifest.json
|
||||||
|
// but only if the target version is not already in versions.json
|
||||||
|
const versions = JSON.parse(readFileSync('versions.json', 'utf8'));
|
||||||
|
if (!Object.values(versions).includes(minAppVersion)) {
|
||||||
|
versions[targetVersion] = minAppVersion;
|
||||||
|
writeFileSync('versions.json', JSON.stringify(versions, null, '\t'));
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"1.0.0": "0.15.0"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user