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 }