obsidian-semantic

Log | Files | Refs | README

main.ts (6605B)


      1 import { SemanticsSettings } from 'SemanticsSettings';
      2 import { App, Editor, MarkdownView, Modal, Notice, Plugin, SuggestModal, TFile, TFolder, Vault, requestUrl, RequestUrlParam } from 'obsidian';
      3 import * as path from 'path';
      4 import { SemanticApi } from 'SemanticApi';
      5 import { sanitize } from 'sanitize-filename-ts';
      6 
      7 interface SemanticSettings {
      8 	papersPath: string;
      9 	apiKey?: string;
     10 }
     11 
     12 const DEFAULT_SETTINGS: SemanticSettings = {
     13 	papersPath: 'papers',
     14 	apiKey: undefined
     15 }
     16 
     17 class SemInfos {
     18 	bibtex: string;
     19 	id?: string;
     20 
     21 	constructor(bibtex: string, id?: string) {
     22 		this.bibtex = bibtex;
     23 		this.id = id;
     24 	}
     25 
     26 	static async fromFile(app: App, file: TFile): Promise<SemInfos> {
     27 		var id: string | undefined = undefined;
     28 		await app.fileManager.processFrontMatter(file, (frontmatter) => {
     29 			id = frontmatter["semid"];
     30 		});
     31 		var content = await app.vault.cachedRead(file);
     32 		var bibtex = "";
     33 		var inblock = false;
     34 
     35 		// Extracts the first bibtex block of the file
     36 		for (const line of content.split(/\r?\n/)) {
     37 			if (line.trim() == '```bibtex' && !inblock) {
     38 				inblock = true;
     39 			} else if (line.trim() == '```' && inblock) {
     40 				// Finished the bibtex block
     41 				break;
     42 			} else if (inblock) {
     43 				bibtex += line + "\n";
     44 			}
     45 		}
     46 
     47 		return new SemInfos(bibtex, id);
     48 	}
     49 }
     50 
     51 class Author {
     52 	authorId: string;
     53 	name: string;
     54 }
     55 
     56 class Paper {
     57 	paperId: string;
     58 	title: string;
     59 	authors: Author[];
     60 	year: string;
     61 	externalIds?: { DBLP?: string };
     62 	citationStyles?: { bibtex: string };
     63 	citations?: Paper[];
     64 	references?: Paper[];
     65 
     66 	static fields(): string {
     67 		return "title,authors,year,paperId";
     68 	}
     69 
     70 	static fieldsLong(): string {
     71 		return Paper.fields() + ",externalIds,citationStyles,citations,references"
     72 	}
     73 
     74 	static async get(api: SemanticApi, id: string, long?: boolean, key?: string): Promise<Paper> {
     75 		return api.request(`paper/${id.trim()}`, {
     76 			"fields": long ? Paper.fields() : Paper.fieldsLong()
     77 		}).json;
     78 	}
     79 
     80 	static async getBulk(api: SemanticApi, ids: string[], long?: boolean, key?: string): Promise<Paper[]> {
     81 		return api.request("paper/batch", {
     82 			"fields": long ? Paper.fields() : Paper.fieldsLong(),
     83 		}, {
     84 			method: "POST",
     85 			contentType: "application/json",
     86 			body: JSON.stringify({ids: ids}),
     87 		}).json;
     88 	}
     89 }
     90 
     91 export default class SemanticIntegration extends Plugin {
     92 	settings: SemanticSettings;
     93 	apiAccess: SemanticApi;
     94 
     95 	openModal() {
     96 		new SemanticModal(this.app, this.settings, this.apiAccess).open();
     97 	}
     98 
     99 	getPapersFolder(): TFolder {
    100 		var mfolder = this.app.vault.getAbstractFileByPath(this.settings.papersPath);
    101 		if (mfolder instanceof TFolder) {
    102 			return mfolder;
    103 		} else {
    104 			throw new Error("Papers folder is not a folder");
    105 		}
    106 	}
    107 
    108 	async onload() {
    109 		await this.loadSettings();
    110 
    111 		this.addRibbonIcon('book-plus', 'Add a new reference', (evt: MouseEvent) => {
    112 			this.openModal();
    113 		});
    114 
    115 		// this.addCommand({
    116 		// 	id: 'get-semantic-infos',
    117 		// 	name: 'Get semantic informations',
    118 		// 	callback: () => {
    119 		// 		this.getNoteInfos();
    120 		// 	}
    121 		// })
    122 
    123 		this.addCommand({
    124 			id: 'open-semantic-modal',
    125 			name: 'Open Semantic Scholar search',
    126 			callback: () => {
    127 				this.openModal();
    128 			}
    129 		});
    130 
    131 		this.addCommand({
    132 			id: "extract-bibtex",
    133 			name: 'Extract bibliography to bibtex',
    134 			callback: async () => {
    135 				var allbibtex: string = "";
    136 
    137 				var folder = this.getPapersFolder();
    138 				for (const mfile of folder.children) {
    139 					if (mfile instanceof TFile) {
    140 						var infos = await SemInfos.fromFile(this.app, mfile);
    141 						allbibtex += infos.bibtex;
    142 					}
    143 				}
    144 				var oldfile = this.app.vault.getFileByPath("biblio.bib")
    145 				if (oldfile != null) {
    146 					await this.app.vault.modify(oldfile, allbibtex);
    147 				} else {
    148 					await this.app.vault.create("biblio.bib", allbibtex);
    149 				}
    150 			}
    151 		});
    152 
    153 		this.addCommand({
    154 			id: 'search-papers',
    155 			name: 'Search list of papers',
    156 			callback: async () => {
    157 				var folder = this.getPapersFolder();
    158 
    159 				var ids = [];
    160 				for (const file of folder.children) {
    161 					if (file instanceof TFile) {
    162 						var infos = await SemInfos.fromFile(this.app, file);
    163 						if (infos.id != undefined) {
    164 							ids.push(infos.id);
    165 						}
    166 					}
    167 				}
    168 
    169 				// TODO: fuzzy modal
    170 				console.log(await Paper.getBulk(this.apiAccess, ids));
    171 			}
    172 		})
    173 
    174 		this.addSettingTab(new SemanticsSettings(this.app, this));
    175 	}
    176 
    177 	onunload() {
    178 	}
    179 
    180 	async loadSettings() {
    181 		this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
    182 		this.apiAccess = new SemanticApi(this.settings.apiKey)
    183 	}
    184 
    185 	async saveSettings() {
    186 		await this.saveData(this.settings);
    187 	}
    188 
    189 	async getNoteInfos() {
    190 		var file = this.app.workspace.getActiveFile();
    191 		if (file == null) {
    192 			return;
    193 		}
    194 
    195 		var infos = await SemInfos.fromFile(this.app, file);
    196 		if (infos.id != undefined) {
    197 			console.log(await Paper.get(this.apiAccess, infos.id, true));
    198 		}
    199 	}
    200 }
    201 
    202 class SemanticModal extends SuggestModal<Paper> {
    203 	options: SemanticSettings;
    204 	apiAccess: SemanticApi;
    205 
    206 	constructor(app: App, options: SemanticSettings, api: SemanticApi) {
    207 		super(app);
    208 		this.options = options;
    209 		this.apiAccess = api;
    210 	}
    211 
    212 	async getSuggestions(query: string): Promise<Paper[]> {
    213 		if (query == "") {
    214 			return [];
    215 		}
    216 
    217 		var result: Paper[] = (await this.apiAccess.debouncedRequest("paper/search", {
    218 			"query": query,
    219 			"fields": Paper.fields(),
    220 			"limit": String(10)
    221 		})).json.data;
    222 
    223 		return result;
    224 	}
    225 
    226 	renderSuggestion(value: Paper, el: HTMLElement) {
    227 		el.createEl("div", { text: value.title });
    228 
    229 		var authors = "";
    230 		if (value.authors.length <= 2) {
    231 			authors = value.authors.map(auth => auth.name).join("");
    232 		} else {
    233 			authors = `${value.authors[0].name} et al.`
    234 		}
    235 
    236 		el.createEl("small", { text: `${value.year} - ${authors}` });
    237 	}
    238 
    239 	async onChooseSuggestion(item: Paper, evt: MouseEvent | KeyboardEvent) {
    240 		var final = await Paper.get(this.apiAccess, item.paperId);
    241 
    242 		var fname = path.join(this.options.papersPath, sanitize(final.title)) + ".md";
    243 
    244 		if (final.citationStyles == undefined || final.externalIds == undefined) {
    245 			throw new Error("Missing fields in paper");
    246 		}
    247 
    248 		// Generate the body of the note
    249 		var body = "";
    250 		var unparsedbib = final.citationStyles.bibtex;
    251 		if (final.externalIds.DBLP != undefined) {
    252 			var result = await requestUrl(`https://dblp.org/rec/${final.externalIds.DBLP}.bib?param=0`).text;
    253 			unparsedbib = result.trim();
    254 		}
    255 
    256 		var body = `\`\`\`bibtex
    257 ${unparsedbib}
    258 \`\`\`
    259 `
    260 		var file = await this.app.vault.create(fname, body);
    261 		this.app.fileManager.processFrontMatter(file, (frontmatter) => {
    262 			frontmatter["semid"] = final.paperId;
    263 		})
    264 		await this.app.workspace.openLinkText(fname, fname);
    265 	}
    266 }