├── LICENSE
├── README.md
├── chrome.manifest
├── chrome
└── content
│ ├── addPaper.js
│ ├── addPaperDialog.html
│ ├── options.html
│ ├── options.js
│ ├── overlay.xul
│ ├── references.html
│ └── semanticZotero.js
└── install.rdf
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Tomáš Daniš
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Semantic Zotero Plugin
2 | Semantic Zotero integrates Zotero with Semantic Scholar to fetch and display references related to a selected paper. You can then add them to your library directly from Zotero along with the full-text PDF. For now, the visuals are pretty barebones, I will update when I have time.
3 |
4 | 
5 |
6 | ## Usage
7 |
8 | ### Show References:
9 |
10 | To fetch references for a given paper, right click on it, select "Semantic Zotero" and then "Show references"
11 | 
12 |
13 | ### Configure options
14 |
15 | In the Zotero Menu bar, choosing Tools -> Semantic Zotero Options allows you to configure options such as custom Semantic Scholar API key
16 |
17 | ## Installation
18 |
19 | 1. Download the .xpi file from one of the releases
20 | 2. In Zotero menu bar, select Tools -> Add-ons -> Settings icon on upper right -> Install add-on from file
21 |
--------------------------------------------------------------------------------
/chrome.manifest:
--------------------------------------------------------------------------------
1 | content semanticZotero chrome/content/
2 | overlay chrome://zotero/content/zoteroPane.xul chrome://semanticZotero/content/overlay.xul
3 |
4 |
--------------------------------------------------------------------------------
/chrome/content/addPaper.js:
--------------------------------------------------------------------------------
1 | async function populateDialog() {
2 | const subcollectionsDiv = document.getElementById("subcollections");
3 | const tagsDiv = document.getElementById("tags");
4 |
5 | // Fetch subcollections and tags from Zotero
6 | var zp = Zotero.getActiveZoteroPane();
7 | const subcollections = await Zotero.Collections.getByLibrary(zp.getSelectedLibraryID());
8 | const tags = await Zotero.Tags.getAll();
9 |
10 | // Populate subcollections
11 | for (let subcollection of subcollections) {
12 | const checkbox = document.createElement("input");
13 | checkbox.type = "checkbox";
14 | checkbox.value = subcollection.key;
15 | checkbox.id = "subcollection_" + subcollection.id;
16 |
17 | const label = document.createElement("label");
18 | label.htmlFor = checkbox.key;
19 | label.textContent = subcollection.name;
20 |
21 | subcollectionsDiv.appendChild(checkbox);
22 | subcollectionsDiv.appendChild(label);
23 | subcollectionsDiv.appendChild(document.createElement("br")); // New line
24 | }
25 |
26 | // Populate tags
27 | tags.forEach((tag, index) => {
28 | const checkbox = document.createElement("input");
29 | checkbox.type = "checkbox";
30 | checkbox.value = tag.tag;
31 | checkbox.id = "tag_" + index;
32 |
33 | const label = document.createElement("label");
34 | label.htmlFor = checkbox.id;
35 | label.textContent = tag.tag;
36 |
37 | tagsDiv.appendChild(checkbox);
38 | tagsDiv.appendChild(label);
39 | tagsDiv.appendChild(document.createElement("br")); // New line
40 | });
41 | }
42 |
43 | async function onAddButtonClick() {
44 | const selectedTags = getSelectedTags();
45 | const selectedCollections = getSelectedCollections();
46 |
47 | const item = window.arguments[0];
48 | const reference = window.arguments[1];
49 |
50 | document.getElementById("statusMessage").style.display = "block";
51 |
52 | try {
53 | await addToCollection(reference, item, selectedCollections, selectedTags);
54 | window.opener.SemanticZotero.handleReferenceAdded(reference);
55 | } catch(error) {
56 | console.error("Failed to add item:", error);
57 | document.getElementById("statusMessage").innerText = error;
58 | }
59 |
60 | window.close();
61 | }
62 |
63 | function getSelectedTags() {
64 | const tagsDiv = document.getElementById("tags");
65 | const checkboxes = tagsDiv.querySelectorAll("input[type='checkbox']:checked");
66 | const selectedTags = Array.from(checkboxes).map(cb => cb.value);
67 | return selectedTags;
68 | }
69 |
70 | function getSelectedCollections() {
71 | const collectionsDiv = document.getElementById("subcollections");
72 | const checkboxes = collectionsDiv.querySelectorAll("input[type='checkbox']:checked");
73 | const selectedCollections = Array.from(checkboxes).map(cb => cb.value);
74 | return selectedCollections;
75 | }
76 |
77 | async function addToCollection(reference, item, selectedCollections, selectedTags) {
78 | var newItem = new Zotero.Item('preprint');
79 | var arxivID = reference.externalIds ? reference.externalIds["ArXiv"] : null;
80 | var pdfUrl = undefined;
81 | if(arxivID) {
82 | pdfUrl = "https://arxiv.org/pdf/" + arxivID + '.pdf';
83 | } else if(reference.isOpenAccess) {
84 | pdfUrl = reference.openAccessPdf.url;
85 | }
86 | // Set fields for the item using the provided reference data
87 | newItem.setField('title', reference.title);
88 | newItem.setField('date', reference.publicationDate || reference.year);
89 | newItem.setField('abstractNote', reference.abstract);
90 | if (arxivID) {
91 | newItem.setField('repository', "arXiv");
92 | newItem.setField('archiveID', "arXiv:" + arxivID);
93 | }
94 | newItem.setField('url', reference.url);
95 |
96 | // Set authors
97 | if (reference.authors && reference.authors.length) {
98 | let creators = reference.authors.map(author => ({ firstName: author.name.split(' ').slice(0, -1).join(' '), lastName: author.name.split(' ').slice(-1).join(' '), creatorType: "author" }));
99 | newItem.setCreators(creators);
100 | }
101 |
102 | // Once the paper is added, add tags
103 | for (let tag of selectedTags) {
104 | newItem.addTag(tag);
105 | }
106 |
107 | newItem.setCollections(selectedCollections);
108 |
109 | // Save the new item to the database
110 | await newItem.saveTx();
111 |
112 | //Make items related
113 | let relateItems = Zotero.Prefs.get('SemanticZotero.relateItems') === undefined ? true : Zotero.Prefs.get('SemanticZotero.relateItems');
114 | if (relateItems) {
115 | item.addRelatedItem(newItem);
116 | await item.saveTx();
117 |
118 | newItem.addRelatedItem(item);
119 | await newItem.saveTx();
120 | }
121 |
122 | // Attach the PDF if available
123 | if (pdfUrl) {
124 | var zp = Zotero.getActiveZoteroPane();
125 | var libraryID = zp.getSelectedLibraryID();
126 | await Zotero.Attachments.importFromURL({
127 | url: pdfUrl,
128 | parentItemID: newItem.id,
129 | contentType: 'application/pdf',
130 | libraryID: libraryID
131 | });
132 | }
133 | return newItem;
134 | }
135 |
136 | document.getElementById("addButton").addEventListener("click", onAddButtonClick);
137 |
138 |
139 | window.onload = function() {
140 | populateDialog();
141 | };
142 |
--------------------------------------------------------------------------------
/chrome/content/addPaperDialog.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Add Paper
5 |
6 |
7 |
8 |
Select Subcollections
9 |
10 |
11 |
12 |
Select Tags
13 |
14 |
15 |
16 |
Adding item and downloading PDF... Do not close this window.
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/chrome/content/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Semantic Zotero Options
6 |
19 |
20 |
21 |
22 |
23 |
27 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/chrome/content/options.js:
--------------------------------------------------------------------------------
1 | var SemanticZoteroOptions = {
2 | saveOptions: function() {
3 | let apiKey = document.getElementById('apiKey').value;
4 | let relateItems = document.getElementById('relateItemsCheckbox').checked;
5 |
6 | Zotero.Prefs.set('SemanticZotero.apiKey', apiKey);
7 | Zotero.Prefs.set('SemanticZotero.relateItems', relateItems);
8 |
9 | window.close();
10 | },
11 |
12 | cancel: function() {
13 | window.close();
14 | },
15 |
16 | loadOptions: function() {
17 | let apiKey = Zotero.Prefs.get('SemanticZotero.apiKey') || '';
18 | let relateItems = Zotero.Prefs.get('SemanticZotero.relateItems') === undefined ? true : Zotero.Prefs.get('SemanticZotero.relateItems');
19 |
20 | document.getElementById('apiKey').value = apiKey;
21 | document.getElementById('relateItemsCheckbox').checked = relateItems;
22 | }
23 | }
24 |
25 | window.onload = SemanticZoteroOptions.loadOptions;
26 |
--------------------------------------------------------------------------------
/chrome/content/overlay.xul:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
14 |
25 |
--------------------------------------------------------------------------------
/chrome/content/references.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | References
5 |
87 |
88 |
89 |
90 |
91 |
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/chrome/content/semanticZotero.js:
--------------------------------------------------------------------------------
1 | var SemanticZotero = {
2 | APIError: class extends Error {
3 | constructor(statusCode, statusMessage) {
4 | super(`Status: ${statusCode}, Message: ${statusMessage}`);
5 | this.statusCode = statusCode;
6 | this.statusMessage = statusMessage;
7 | }
8 | },
9 |
10 | determineItemId: function(item) {
11 | let id;
12 |
13 | id = this.getItemIdFromUrl(item);
14 | if (id) return id;
15 |
16 | id = this.getItemIdFromArxiv(item);
17 | if (id) return id;
18 |
19 | return this.getItemIdFromDOI(item);
20 | },
21 |
22 | getItemIdFromUrl: function(item) {
23 | const url = item.getField('url');
24 | const validUrls = ["semanticscholar.org", "arxiv.org", "aclweb.org", "acm.org", "biorxiv.org"];
25 | if (url && validUrls.some(v => url.includes(v))) {
26 | return "URL:" + url;
27 | }
28 | return null;
29 | },
30 |
31 | getItemIdFromArxiv: function(item) {
32 | if (item.getField('repository').toLowerCase() === "arxiv" && item.getField('archiveID')) {
33 | return item.getField('archiveID');
34 | }
35 | return null;
36 | },
37 |
38 | getItemIdFromDOI: function(item) {
39 | let doi = item.getField('DOI');
40 | return doi ? "DOI:" + doi : null;
41 | },
42 |
43 | getS2ApiRequestHeaders: function() {
44 | let apiKey = Zotero.Prefs.get('SemanticZotero.apiKey');
45 | if (apiKey) {
46 | return { 'x-api-key': apiKey }
47 | } else {
48 | return {}
49 | }
50 | },
51 |
52 | getItemIdFromTitleSearch: async function(item) {
53 | const title = item.getField('title');
54 | const results = await this.searchSemanticScholarByTitle(title);
55 | if (results[0] && results[0].title.toLowerCase() === title.toLowerCase()) {
56 | return results[0].paperId;
57 | }
58 | return null;
59 | },
60 |
61 | searchSemanticScholarByTitle: async function(title) {
62 | const apiEndpoint = `https://api.semanticscholar.org/graph/v1/paper/search?query=${encodeURIComponent(title)}&limit=1`;
63 | let response = await fetch(apiEndpoint, {
64 | method: 'GET',
65 | headers: this.getS2ApiRequestHeaders()
66 | });
67 |
68 | if (!response.ok) {
69 | throw new this.APIError(response.status, response.statusText);
70 | }
71 |
72 | let data = await response.json();
73 | return data.data;
74 | },
75 |
76 | showReferences: async function() {
77 | let width = screen.width * 0.75;
78 | let height = screen.height * 0.75;
79 | var win = window.open("chrome://semanticZotero/content/references.html", "references", `chrome,centerscreen,resizable,scrollbars,width=${width},height=${height}`);
80 | this.refWindow = win;
81 |
82 | if (ZoteroPane.canEdit()) {
83 | let items = ZoteroPane.getSelectedItems();
84 | if (items.length > 0) {
85 | let item = items[0];
86 | let id = this.determineItemId(item);
87 | let references = null;
88 |
89 | try {
90 | references = await this.fetchReferences(id);
91 | } catch (error) {
92 | if (error instanceof this.APIError) {
93 | if (error.statusCode == 404) {
94 | //If something goes wrong with id from metadata, fall back to search by title
95 | try {
96 | id = await this.getItemIdFromTitleSearch(item);
97 | if (id) {
98 | references = await this.fetchReferences(id);
99 | } else {
100 | alert("Couldn't find the selected paper on SemanticScholar.");
101 | }
102 | } catch (nestedError) {
103 | alert("Unknown internal error occured.");
104 | console.error("Failed to fetch references: " + nestedError.message);
105 | }
106 | } else if(error.statusCode == 403) {
107 | alert("Provided Semantic Scholar key is invalid. If you do not have a key, you can leave the option empty");
108 | console.error(error);
109 | }
110 | } else {
111 | console.error("Unknown error fetching references: " + error.message);
112 | alert("Unknown internal error occured.");
113 | }
114 | }
115 | if (references) {
116 | await this.populateReferences(win, item, references);
117 | } else {
118 | win.close();
119 | return;
120 | }
121 | }
122 | }
123 | },
124 |
125 | fetchReferences: async function(id) {
126 | var apiUrl = "https://api.semanticscholar.org/graph/v1/paper/" + id + "/references";
127 | apiUrl += "?limit=1000&fields=title,publicationDate,year,abstract,url,externalIds,authors,isOpenAccess,openAccessPdf,citationCount,contexts";
128 |
129 | let response = await fetch(apiUrl, {
130 | method: 'GET',
131 | headers: this.getS2ApiRequestHeaders()
132 | });
133 |
134 | if (!response.ok) {
135 | throw new this.APIError(response.status, response.statusText);
136 | }
137 |
138 | let data = await response.json();
139 | return data["data"].map((item) => {
140 | item["citedPaper"].contexts = item.contexts;
141 | return item["citedPaper"];
142 | });
143 | },
144 |
145 | buildLibraryMap: async function() {
146 | var libraryTitleMap = {};
147 | var items = await Zotero.Items.getAll(ZoteroPane.getSelectedLibraryID());
148 | for (let item of items) {
149 | if (item.isRegularItem() && !item.isCollection()) {
150 | let title = item.getField('title').toLowerCase();
151 | libraryTitleMap[title] = true;
152 | }
153 | }
154 | return libraryTitleMap;
155 | },
156 |
157 | populateReferences: async function(win, item, references) {
158 | var rootDiv = win.document.getElementById("rootDiv");
159 |
160 | while (rootDiv.firstChild) {
161 | rootDiv.removeChild(rootDiv.firstChild);
162 | }
163 |
164 | var libraryTitleMap = await this.buildLibraryMap();
165 |
166 | for(let reference of references) {
167 | let isInCollection = libraryTitleMap[reference.title.toLowerCase()] ? true : false;
168 |
169 | // Create an HTML details element for each reference
170 | let referenceDetails = win.document.createElement("details");
171 | referenceDetails.className = "reference";
172 | referenceDetails.setAttribute("id", "reference-det-" + reference.paperId);
173 |
174 | let summary = win.document.createElement("summary");
175 |
176 | // Title
177 | let titleDiv = win.document.createElement("div");
178 | titleDiv.className = "summary-content reference-title";
179 | titleDiv.textContent = reference.title;
180 | summary.appendChild(titleDiv);
181 |
182 | // First Author
183 | let firstAuthorDiv = win.document.createElement("div");
184 | firstAuthorDiv.className = "summary-content reference-first-author";
185 | if (reference.authors && reference.authors.length > 0) {
186 | firstAuthorDiv.textContent = reference.authors[0].name;
187 | if (reference.authors.length > 1) {
188 | firstAuthorDiv.textContent += " et al.";
189 | }
190 | }
191 | summary.appendChild(firstAuthorDiv);
192 |
193 | // Publication Year
194 | let pubYearDiv = win.document.createElement("div");
195 | pubYearDiv.className = "summary-content reference-year";
196 | if (reference.year) {
197 | pubYearDiv.textContent = reference.year;
198 | } else {
199 | pubYearDiv.textContent = "-";
200 | }
201 | summary.appendChild(pubYearDiv);
202 |
203 | // PDF available
204 | let pdfAvailableDiv = win.document.createElement("div");
205 | pdfAvailableDiv.className = "summary-content reference-pdf";
206 | var arxivID = reference.externalIds ? reference.externalIds["ArXiv"] : null;
207 | var pdfUrl = undefined;
208 | if(arxivID) {
209 | pdfUrl = "https://arxiv.org/pdf/" + arxivID + '.pdf';
210 | } else if(reference.isOpenAccess && reference.openAccessPdf) {
211 | pdfUrl = reference.openAccessPdf.url;
212 | }
213 | if (pdfUrl) {
214 | pdfAvailableDiv.textContent = "Yes";
215 | } else {
216 | pdfAvailableDiv.textContent = "No";
217 | }
218 | summary.appendChild(pdfAvailableDiv);
219 |
220 | // Citation count
221 | let citationCountDiv = win.document.createElement("div");
222 | citationCountDiv.className = "summary-content citation-count";
223 | citationCountDiv.textContent = reference.citationCount;
224 | summary.appendChild(citationCountDiv);
225 |
226 |
227 | //Action
228 | let actionDiv = win.document.createElement("div");
229 | actionDiv.className = "summary-content action-box";
230 |
231 | if (reference.paperId === null) {
232 | actionDiv.textContent = "Not available";
233 | summary.style.opacity = "0.5";
234 | summary.style.pointerEvents = "none";
235 | } else if (!isInCollection) {
236 | let addButton = win.document.createElement("button");
237 | addButton.textContent = "Add";
238 | addButton.addEventListener('click', (event) => {
239 | event.stopPropagation();
240 | this.showAddPaperDialog(item, reference)
241 | });
242 | actionDiv.appendChild(addButton);
243 | } else {
244 | let statusSpan = win.document.createElement("span");
245 | statusSpan.textContent = "Already in Collection";
246 | actionDiv.appendChild(statusSpan);
247 | }
248 |
249 | summary.appendChild(actionDiv);
250 |
251 | referenceDetails.appendChild(summary);
252 |
253 | // Details when clicked
254 | let detailsDiv = win.document.createElement("div");
255 | detailsDiv.className = "reference-details";
256 |
257 | // Authors in detail
258 | let authorsDiv = win.document.createElement("div");
259 | if (reference.authors && reference.authors.length > 0) {
260 | let displayedAuthors = reference.authors.slice(0, 5).map(a => a.name).join(', ');
261 | if (reference.authors.length > 5) {
262 | displayedAuthors += ', et al.';
263 | }
264 | authorsDiv.textContent = `Authors: ${displayedAuthors}`;
265 | } else {
266 | authorsDiv.textContent = "Authors: Not available";
267 | }
268 | detailsDiv.appendChild(authorsDiv);
269 |
270 | // Abstract
271 | let abstractDiv = win.document.createElement("div");
272 |
273 | let abstractTitle = win.document.createElement("h4");
274 | abstractTitle.textContent = "Abstract:";
275 | abstractDiv.appendChild(abstractTitle);
276 |
277 | let abstractContent = win.document.createElement("p");
278 | abstractContent.textContent = reference.abstract;
279 | abstractDiv.appendChild(abstractContent);
280 |
281 | detailsDiv.appendChild(abstractDiv);
282 |
283 | // Citation contexts
284 | let citationContextsDiv = win.document.createElement("div");
285 | citationContextsDiv.className = "citation-contexts";
286 |
287 | let citationContextsTitle = win.document.createElement("h4");
288 | citationContextsTitle.textContent = "Citation Contexts:";
289 | citationContextsDiv.appendChild(citationContextsTitle);
290 |
291 | if (reference.contexts && reference.contexts.length > 0) {
292 | let citationContextsList = win.document.createElement("ul");
293 |
294 | for (let context of reference.contexts) {
295 | let listItem = win.document.createElement("li");
296 | listItem.textContent = context;
297 | citationContextsList.appendChild(listItem);
298 | }
299 |
300 | citationContextsDiv.appendChild(citationContextsList);
301 | } else {
302 | let notAvailableText = win.document.createElement("p");
303 | notAvailableText.textContent = "Not available";
304 | citationContextsDiv.appendChild(notAvailableText);
305 | }
306 |
307 | detailsDiv.appendChild(citationContextsDiv);
308 |
309 | referenceDetails.appendChild(detailsDiv);
310 | rootDiv.appendChild(referenceDetails);
311 | }
312 | },
313 |
314 | showAddPaperDialog: function(item, reference) {
315 | const features = "width=500,height=400,scrollbars=yes,resizable=yes";
316 | const win = window.openDialog("chrome://semanticZotero/content/addPaperDialog.html", "Add Paper", features, item, reference);
317 | },
318 |
319 | handleReferenceAdded: function(reference) {
320 | // Locate the corresponding reference in the view
321 | const referenceElement = this.refWindow.document.getElementById("reference-det-" + reference.paperId);
322 | if (referenceElement) {
323 | const addButton = referenceElement.querySelector("button");
324 | if (addButton) {
325 | addButton.textContent = "Added to collection";
326 | addButton.disabled = true;
327 | }
328 | }
329 | }
330 | };
--------------------------------------------------------------------------------
/install.rdf:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 | tomasdanis26@gmail.com
6 | 0.2
7 | 2
8 | Semantic Zotero
9 | Retrieves references for Zotero items using Semantic Scholar API and lets you add them to your library.
10 | Tomáš Daniš
11 |
12 |
13 | zotero@chnm.gmu.edu
14 | 6.0
15 | 6.*
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------