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