first commit

This commit is contained in:
huaqo
2026-04-22 12:31:20 +02:00
commit 4436c88dcc
17 changed files with 5683 additions and 0 deletions
+10
View File
@@ -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
View File
@@ -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
View File
+1
View File
@@ -0,0 +1 @@
tag-version-prefix=""
+49
View File
@@ -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();
}
+34
View File
@@ -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",
]),
);
+10
View File
@@ -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
}
+5160
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -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"
}
}
+77
View File
@@ -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
View File
@@ -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
View File
@@ -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)})`);
}
}
}
+65
View File
@@ -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();
})
);
}
}
+8
View File
@@ -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.
*/
+30
View 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"
]
}
+17
View File
@@ -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'));
}
+3
View File
@@ -0,0 +1,3 @@
{
"1.0.0": "0.15.0"
}