├── prefs.js ├── .gitignore ├── bin └── build ├── manifest.json ├── preferences.xhtml ├── locale └── en-US │ └── citation-counts.ftl ├── bootstrap.js ├── preferences.js ├── README.md ├── LICENSE └── zoterocitationcounts.js /prefs.js: -------------------------------------------------------------------------------- 1 | pref("extensions.citationcounts.autoretrieve", "none"); 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | *.xpi 3 | 4 | .DS_Store 5 | .AppleDouble 6 | .LSOverride -------------------------------------------------------------------------------- /bin/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd .. 4 | 5 | version='2.0' 6 | 7 | rm -f zoterocitationcountsmanager-${version}.xpi 8 | zip -r zoterocitationcountsmanager-${version}.xpi locale/* manifest.json bootstrap.js preferences.js preferences.xhtml prefs.js zoterocitationcounts.js 9 | 10 | # To release a new version: 11 | # - increase version number in all files (not just here) 12 | # - run this script to create a new .xpi file 13 | # - commit and push to Github 14 | # - make a release on Github, and manually upload the new .xpi file -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Zotero Citation Counts Manager", 4 | "version": "2.0", 5 | "description": "Enhanced Citation Counts Manager for Zotero 7", 6 | "homepage_url": "https://github.com/FrLars21/ZoteroCitationCountsManager", 7 | "applications": { 8 | "zotero": { 9 | "id": "frlars21@github.com", 10 | "update_url": "https://www.zotero.org/download/plugins/make-it-red/updates.json", 11 | "strict_min_version": "6.999", 12 | "strict_max_version": "7.0.*" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /preferences.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /locale/en-US/citation-counts.ftl: -------------------------------------------------------------------------------- 1 | ## For the custom column that the plugin registers 2 | citationcounts-column-title = Citation count 3 | 4 | ## For the "Item" contextmenu, where citation counts can be manually retrieved for the selected items. 5 | citationcounts-itemmenu-retrieve-title = 6 | .label = Get citation count 7 | citationcounts-itemmenu-retrieve-api = 8 | .label = Get { $api } citation count 9 | 10 | ## For the ProgressWindow, showing citation counts retrieval operation status 11 | citationcounts-progresswindow-headline = Getting { $api } citation counts. 12 | citationcounts-progresswindow-finished-headline = Finished getting { $api } citation counts. 13 | citationcounts-progresswindow-error-no-doi = No DOI field exists on the item. 14 | citationcounts-progresswindow-error-no-arxiv = No arXiv id found on the item. 15 | citationcounts-progresswindow-error-no-doi-or-arxiv = No DOI / arXiv ID found on the item. 16 | citationcounts-progresswindow-error-bad-api-response = Problem accesing the { $api } API. 17 | citationcounts-progresswindow-error-no-citation-count = { $api } doesn't have a citation count for this item. 18 | 19 | ## For the "Tools" menu, where the "autoretrieve" preference can be set. 20 | citationcounts-menutools-autoretrieve-title = 21 | .label = Get citation counts for new items? 22 | citationcounts-menutools-autoretrieve-api = 23 | .label = { $api } 24 | citationcounts-menutools-autoretrieve-api-none = 25 | .label = No 26 | 27 | ## For the plugins "Preferences" pane. 28 | citationcounts-preference-pane-label = Citation Counts 29 | citationcounts-preferences-pane-autoretrieve-title = Get citation counts for new items? 30 | citationcounts-preferences-pane-autoretrieve-api = 31 | .label = { $api } 32 | citationcounts-preferences-pane-autoretrieve-api-none = 33 | .label = No 34 | 35 | ## Misc 36 | citationcounts-internal-error = Internal error -------------------------------------------------------------------------------- /bootstrap.js: -------------------------------------------------------------------------------- 1 | let ZoteroCitationCounts, itemObserver; 2 | 3 | async function startup({ id, version, rootURI }) { 4 | Services.scriptloader.loadSubScript(rootURI + "zoterocitationcounts.js"); 5 | 6 | ZoteroCitationCounts.init({ id, version, rootURI }); 7 | ZoteroCitationCounts.addToAllWindows(); 8 | 9 | Zotero.PreferencePanes.register({ 10 | pluginID: id, 11 | label: await ZoteroCitationCounts.l10n.formatValue( 12 | "citationcounts-preference-pane-label" 13 | ), 14 | image: ZoteroCitationCounts.icon("edit-list-order", false), 15 | src: "preferences.xhtml", 16 | scripts: ["preferences.js"], 17 | }); 18 | 19 | await Zotero.ItemTreeManager.registerColumns({ 20 | dataKey: "citationcounts", 21 | label: await ZoteroCitationCounts.l10n.formatValue( 22 | "citationcounts-column-title" 23 | ), 24 | pluginID: id, 25 | dataProvider: (item) => ZoteroCitationCounts.getCitationCount(item), 26 | }); 27 | 28 | itemObserver = Zotero.Notifier.registerObserver( 29 | { 30 | notify: function (event, type, ids, extraData) { 31 | if (event == "add") { 32 | const pref = ZoteroCitationCounts.getPref("autoretrieve"); 33 | if (pref === "none") return; 34 | 35 | const api = ZoteroCitationCounts.APIs.find((api) => api.key === pref); 36 | if (!api) return; 37 | 38 | ZoteroCitationCounts.updateItems(Zotero.Items.get(ids), api); 39 | } 40 | }, 41 | }, 42 | ["item"] 43 | ); 44 | } 45 | 46 | function onMainWindowLoad({ window }) { 47 | ZoteroCitationCounts.addToWindow(window); 48 | } 49 | 50 | function onMainWindowUnload({ window }) { 51 | ZoteroCitationCounts.removeFromWindow(window); 52 | } 53 | 54 | function shutdown() { 55 | ZoteroCitationCounts.removeFromAllWindows(); 56 | Zotero.Notifier.unregisterObserver(itemObserver); 57 | ZoteroCitationCounts = undefined; 58 | } 59 | -------------------------------------------------------------------------------- /preferences.js: -------------------------------------------------------------------------------- 1 | ZoteroCitationCounts_Prefs = { 2 | /** 3 | * @TODO reference ZoteroCitationCounts.APIs directly. 4 | */ 5 | APIs: [ 6 | { 7 | key: "crossref", 8 | name: "Crossref", 9 | }, 10 | { 11 | key: "inspire", 12 | name: "INSPIRE-HEP", 13 | }, 14 | { 15 | key: "semanticscholar", 16 | name: "Semantic Scholar", 17 | }, 18 | ], 19 | 20 | init: function () { 21 | this.APIs.concat({ key: "none" }).forEach((api) => { 22 | const label = 23 | api.key === "none" 24 | ? { 25 | "data-l10n-id": 26 | "citationcounts-preferences-pane-autoretrieve-api-none", 27 | } 28 | : { 29 | "data-l10n-id": 30 | "citationcounts-preferences-pane-autoretrieve-api", 31 | "data-l10n-args": `{"api": "${api.name}"}`, 32 | }; 33 | 34 | this._injectXULElement( 35 | document, 36 | "radio", 37 | `citationcounts-preferences-pane-autoretrieve-radio-${api.key}`, 38 | { 39 | ...label, 40 | value: api.key, 41 | }, 42 | "citationcounts-preference-pane-autoretrieve-radiogroup" 43 | ); 44 | }); 45 | }, 46 | 47 | /** 48 | * @TODO reference ZoteroCitationCounts._injectXULElement directly. 49 | */ 50 | _injectXULElement: function ( 51 | document, 52 | elementType, 53 | elementID, 54 | elementAttributes, 55 | parentID, 56 | eventListeners 57 | ) { 58 | const element = document.createXULElement(elementType); 59 | element.id = elementID; 60 | 61 | Object.entries(elementAttributes || {}) 62 | .filter(([_, value]) => value !== null && value !== undefined) 63 | .forEach(([key, value]) => element.setAttribute(key, value)); 64 | 65 | Object.entries(eventListeners || {}).forEach(([eventType, listener]) => { 66 | element.addEventListener(eventType, listener); 67 | }); 68 | 69 | document.getElementById(parentID).appendChild(element); 70 | 71 | return element; 72 | }, 73 | }; 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zotero 7 Citation Counts Manager Enhaned 2 | 3 | - [GitHub](https://github.com/FrLars21/ZoteroCitationCountsManager): Source 4 | code repository 5 | 6 | This is an add-on for [Zotero](https://www.zotero.org), a research source management tool. The add-on can auto-fetch citation counts for journal articles using various APIs, including [Crossref](https://www.crossref.org), [INSPIRE-HEP](https://inspirehep.net), and [Semantic Scholar](https://www.semanticscholar.org). [Google Scholar](https://scholar.google.com) is not supported because automated access is against its terms of service. 7 | 8 | Please report any bugs, questions, or feature requests in the Github repository. 9 | 10 | ## Features 11 | 12 | - Autoretrieve citation counts when a new item is added to your Zotero library. 13 | - Retrieve citation counts manually by right-clicking on one or more items in your Zotero library. 14 | - Works with the following APIs: [Crossref](https://www.crossref.org), [INSPIRE-HEP](https://inspirehep.net) and [Semantic Scholar](https://www.semanticscholar.org). 15 | - _NEW:_ The plugin is compatible with **Zotero 7** (Zotero 6 is **NOT** supported!). 16 | - _NEW:_ The plugin registers a custom column ("Citation Counts") in your Zotero library so that items can be **ordered by citation count**. 17 | - _NEW:_ Improved _citation count retrieval operation_ status reporting, including item-specific error messages for those items where a citation count couldn't be retrieved. 18 | - _NEW:_ Concurrent citation count retrieval operations is now possible. Especially important for the autoretrieve feature. 19 | - _NEW:_ Fluent is used for localizing, while the locale file has been simplified and now cover the whole plugin. You are welcome to submit translations as a PR. 20 | - _NEW:_ The whole codebade has been refactored with a focus on easy maintenance, especially for the supported citation count APIs. 21 | 22 | ## Acknowledgements 23 | 24 | This plugin is a refactored and enhanced version of Erik Schnetter's [Zotero Citations Counts Manager](https://github.com/eschnett/zotero-citationcounts) for Zotero 7. Code for that extension was based on [Zotero DOI Manager](https://github.com/bwiernik/zotero-shortdoi), which is based in part on [Zotero Google Scholar Citations](https://github.com/beloglazov/zotero-scholar-citations) by Anton Beloglazov. 25 | Boilerplate for this plugin was based on Zotero's sample plugin for v7 [Make-It-Red](https://github.com/zotero/make-it-red). 26 | 27 | ## Installing 28 | 29 | - Download the add-on (the .xpi file) from the latest release: https://github.com/FrLars21/ZoteroCitationCountsManager/releases 30 | - To download the .xpi file, right click it and select 'Save link as' 31 | - Run Zotero (version 7.x) 32 | - Go to `Tools -> Add-ons` 33 | - `Install Add-on From File` 34 | - Choose the file `zoterocitationcountsmanager-2.0.0.xpi` 35 | - Restart Zotero 36 | 37 | ## License 38 | 39 | Distributed under the Mozilla Public License (MPL) Version 2.0. 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /zoterocitationcounts.js: -------------------------------------------------------------------------------- 1 | ZoteroCitationCounts = { 2 | _initialized: false, 3 | 4 | pluginID: null, 5 | pluginVersion: null, 6 | rootURI: null, 7 | 8 | l10n: null, 9 | APIs: [], 10 | 11 | /** 12 | * Track injected XULelements for removal upon mainWindowUnload. 13 | */ 14 | _addedElementIDs: [], 15 | 16 | _log(msg) { 17 | Zotero.debug("Zotero Citation Counts: " + msg); 18 | }, 19 | 20 | init: function ({ id, version, rootURI }) { 21 | if (this._initialized) return; 22 | 23 | this.pluginID = id; 24 | this.pluginVersion = version; 25 | this.rootURI = rootURI; 26 | 27 | this.l10n = new Localization(["citation-counts.ftl"]); 28 | 29 | /** 30 | * To add a new API: 31 | * ----------------- 32 | * (1) Create a urlBuilder method on the ZoteroCitationCounts object. Args: urlencoded *id* and *idtype* ("doi" or "arxiv"). Return: URL for API request. 33 | * 34 | * (2) Create a responseCallback method on the ZoteroCitationCounts object. Args: *response* from api call. Return: citation count number. 35 | * 36 | * (3) Register the API here, and specify whether it works with doi, arxiv id or both. 37 | * 38 | * (4) for now, you also need to register the APIs key and name in "preferences.js" (important that they match the keys and names from below). 39 | */ 40 | this.APIs = [ 41 | { 42 | key: "crossref", 43 | name: "Crossref", 44 | useDoi: true, 45 | useArxiv: false, 46 | methods: { 47 | urlBuilder: this._crossrefUrl, 48 | responseCallback: this._crossrefCallback, 49 | }, 50 | }, 51 | { 52 | key: "inspire", 53 | name: "INSPIRE-HEP", 54 | useDoi: true, 55 | useArxiv: true, 56 | methods: { 57 | urlBuilder: this._inspireUrl, 58 | responseCallback: this._inspireCallback, 59 | }, 60 | }, 61 | { 62 | key: "semanticscholar", 63 | name: "Semantic Scholar", 64 | useDoi: true, 65 | useArxiv: true, 66 | methods: { 67 | urlBuilder: this._semanticScholarUrl, 68 | responseCallback: this._semanticScholarCallback, 69 | }, 70 | }, 71 | ]; 72 | 73 | this._initialized = true; 74 | }, 75 | 76 | getCitationCount: function (item) { 77 | const extraFieldLines = (item.getField("extra") || "") 78 | .split("\n") 79 | .filter((line) => /^Citations:|^\d+ citations/i.test(line)); 80 | 81 | return extraFieldLines[0]?.match(/^\d+/) || "-"; 82 | }, 83 | 84 | getPref: function (pref) { 85 | return Zotero.Prefs.get("extensions.citationcounts." + pref, true); 86 | }, 87 | 88 | setPref: function (pref, value) { 89 | return Zotero.Prefs.set("extensions.citationcounts." + pref, value, true); 90 | }, 91 | 92 | icon: function (iconName, hiDPI) { 93 | return `chrome://zotero/skin/${iconName}${ 94 | hiDPI ? (Zotero.hiDPI ? "@2x" : "") : "" 95 | }.png`; 96 | }, 97 | 98 | ///////////////////////////////////////////// 99 | // UI related stuff // 100 | //////////////////////////////////////////// 101 | 102 | /** 103 | * Create XULElement, set it's attributes, inject accordingly to the DOM & save a reference for later removal. 104 | * 105 | * @param {Document} document - "Document"-interface to be operated on. 106 | * @param {String} elementType - XULElement type (e.g. "menu", "popupmenu" etc.) 107 | * @param {String} elementID - The elements *unique* ID attribute. 108 | * @param {Object} elementAttributes - An object of key-value pairs that represent the DOM element attributes. 109 | * @param {String} parentID - The *unique* ID attribute of the element's parent element. 110 | * @param {Object} eventListeners - An object where keys are event types (e.g., 'command') and values are corresponding event handler functions. 111 | * 112 | * @returns {MozXULElement} - A reference to the injected XULElement. 113 | */ 114 | _injectXULElement: function ( 115 | document, 116 | elementType, 117 | elementID, 118 | elementAttributes, 119 | parentID, 120 | eventListeners 121 | ) { 122 | const element = document.createXULElement(elementType); 123 | element.id = elementID; 124 | 125 | Object.entries(elementAttributes || {}) 126 | .filter(([_, value]) => value !== null && value !== undefined) 127 | .forEach(([key, value]) => element.setAttribute(key, value)); 128 | 129 | Object.entries(eventListeners || {}).forEach(([eventType, listener]) => { 130 | element.addEventListener(eventType, listener); 131 | }); 132 | 133 | document.getElementById(parentID).appendChild(element); 134 | this._storeAddedElement(element); 135 | 136 | return element; 137 | }, 138 | 139 | _storeAddedElement: function (elem) { 140 | if (!elem.id) { 141 | throw new Error("Element must have an id."); 142 | } 143 | 144 | this._addedElementIDs.push(elem.id); 145 | }, 146 | 147 | /** 148 | * Create a submenu to Zotero's "Tools"-menu, from which the plugin specific "autoretrieve" preference can be set. 149 | */ 150 | _createToolsMenu: function (document) { 151 | const menu = this._injectXULElement( 152 | document, 153 | "menu", 154 | "menu_Tools-citationcounts-menu", 155 | { "data-l10n-id": "citationcounts-menutools-autoretrieve-title" }, 156 | "menu_ToolsPopup" 157 | ); 158 | 159 | const menupopup = this._injectXULElement( 160 | document, 161 | "menupopup", 162 | "menu_Tools-citationcounts-menu-popup", 163 | {}, 164 | menu.id, 165 | { 166 | popupshowing: () => { 167 | this.APIs.concat({ key: "none" }).forEach((api) => { 168 | document 169 | .getElementById(`menu_Tools-citationcounts-menu-popup-${api.key}`) 170 | .setAttribute( 171 | "checked", 172 | Boolean(this.getPref("autoretrieve") === api.key) 173 | ); 174 | }); 175 | }, 176 | } 177 | ); 178 | 179 | this.APIs.concat({ key: "none" }).forEach((api) => { 180 | const label = 181 | api.key === "none" 182 | ? { "data-l10n-id": "citationcounts-menutools-autoretrieve-api-none" } 183 | : { 184 | "data-l10n-id": "citationcounts-menutools-autoretrieve-api", 185 | "data-l10n-args": `{"api": "${api.name}"}`, 186 | }; 187 | 188 | this._injectXULElement( 189 | document, 190 | "menuitem", 191 | `menu_Tools-citationcounts-menu-popup-${api.key}`, 192 | { 193 | ...label, 194 | type: "checkbox", 195 | }, 196 | menupopup.id, 197 | { command: () => this.setPref("autoretrieve", api.key) } 198 | ); 199 | }); 200 | }, 201 | 202 | /** 203 | * Create a submenu to Zotero's "Item"-context menu, from which citation counts for selected items can be manually retrieved. 204 | */ 205 | _createItemMenu: function (document) { 206 | const menu = this._injectXULElement( 207 | document, 208 | "menu", 209 | "zotero-itemmenu-citationcounts-menu", 210 | { 211 | "data-l10n-id": "citationcounts-itemmenu-retrieve-title", 212 | class: "menu-iconic", 213 | }, 214 | "zotero-itemmenu" 215 | ); 216 | 217 | const menupopup = this._injectXULElement( 218 | document, 219 | "menupopup", 220 | "zotero-itemmenu-citationcounts-menupopup", 221 | {}, 222 | menu.id 223 | ); 224 | 225 | this.APIs.forEach((api) => { 226 | this._injectXULElement( 227 | document, 228 | "menuitem", 229 | `zotero-itemmenu-citationcounts-${api.key}`, 230 | { 231 | "data-l10n-id": "citationcounts-itemmenu-retrieve-api", 232 | "data-l10n-args": `{"api": "${api.name}"}`, 233 | }, 234 | menupopup.id, 235 | { 236 | command: () => 237 | this.updateItems( 238 | Zotero.getActiveZoteroPane().getSelectedItems(), 239 | api 240 | ), 241 | } 242 | ); 243 | }); 244 | }, 245 | 246 | /** 247 | * Inject plugin specific DOM elements in a DOM window. 248 | */ 249 | addToWindow: function (window) { 250 | window.MozXULElement.insertFTLIfNeeded("citation-counts.ftl"); 251 | 252 | this._createToolsMenu(window.document); 253 | this._createItemMenu(window.document); 254 | }, 255 | 256 | /** 257 | * Inject plugin specific DOM elements into all Zotero windows. 258 | */ 259 | addToAllWindows: function () { 260 | const windows = Zotero.getMainWindows(); 261 | 262 | for (let window of windows) { 263 | if (!window.ZoteroPane) continue; 264 | this.addToWindow(window); 265 | } 266 | }, 267 | 268 | /** 269 | * Remove plugin specific DOM elements from a DOM window. 270 | */ 271 | removeFromWindow: function (window) { 272 | const document = window.document; 273 | 274 | for (let id of this._addedElementIDs) { 275 | document.getElementById(id)?.remove(); 276 | } 277 | 278 | document.querySelector('[href="citation-counts.ftl"]').remove(); 279 | }, 280 | 281 | /** 282 | * Remove plugin specific DOM elements from all Zotero windows. 283 | */ 284 | removeFromAllWindows: function () { 285 | const windows = Zotero.getMainWindows(); 286 | 287 | for (let window of windows) { 288 | if (!window.ZoteroPane) continue; 289 | this.removeFromWindow(window); 290 | } 291 | }, 292 | 293 | ////////////////////////////////////////////////////////// 294 | // Update citation count operation stuff // 295 | ///////////////////////////////////////////////////////// 296 | 297 | /** 298 | * Start citation count retrieval operation 299 | */ 300 | updateItems: async function (itemsRaw, api) { 301 | const items = itemsRaw.filter((item) => !item.isFeedItem); 302 | if (!items.length) return; 303 | 304 | const progressWindow = new Zotero.ProgressWindow(); 305 | progressWindow.changeHeadline( 306 | await this.l10n.formatValue("citationcounts-progresswindow-headline", { 307 | api: api.name, 308 | }), 309 | this.icon("toolbar-advanced-search") 310 | ); 311 | 312 | const progressWindowItems = []; 313 | const itemTitles = items.map((item) => item.getField("title")); 314 | itemTitles.forEach((title) => { 315 | progressWindowItems.push( 316 | new progressWindow.ItemProgress(this.icon("spinner-16px"), title) 317 | ); 318 | }); 319 | 320 | progressWindow.show(); 321 | 322 | this._updateItem(0, items, api, progressWindow, progressWindowItems); 323 | }, 324 | 325 | /** 326 | * Updates citation counts recursively for a list of items. 327 | * 328 | * @param currentItemIndex - Index of currently updating Item. Zero-based. 329 | * @param items - List of all Items to be updated in this operation. 330 | * @param api - API to be used to retrieve *items* citation counts. 331 | * @param progressWindow - ProgressWindow associated with this operation. 332 | * @param progressWindowItems - List of references to each Zotero.ItemProgress in *progressWindow*. 333 | */ 334 | _updateItem: async function ( 335 | currentItemIndex, 336 | items, 337 | api, 338 | progressWindow, 339 | progressWindowItems 340 | ) { 341 | // Check if operation is done 342 | if (currentItemIndex >= items.length) { 343 | const headlineFinished = await this.l10n.formatValue( 344 | "citationcounts-progresswindow-finished-headline", 345 | { api: api.name } 346 | ); 347 | progressWindow.changeHeadline(headlineFinished); 348 | progressWindow.startCloseTimer(5000); 349 | return; 350 | } 351 | 352 | const item = items[currentItemIndex]; 353 | const pwItem = progressWindowItems[currentItemIndex]; 354 | 355 | try { 356 | const [count, source] = await this._retrieveCitationCount( 357 | item, 358 | api.name, 359 | api.useDoi, 360 | api.useArxiv, 361 | api.methods.urlBuilder, 362 | api.methods.responseCallback 363 | ); 364 | 365 | this._setCitationCount(item, source, count); 366 | 367 | pwItem.setIcon(this.icon("tick")); 368 | pwItem.setProgress(100); 369 | } catch (error) { 370 | pwItem.setError(); 371 | new progressWindow.ItemProgress( 372 | this.icon("bullet_yellow"), 373 | await this.l10n.formatValue(error.message, { api: api.name }), 374 | pwItem 375 | ); 376 | } 377 | 378 | this._updateItem( 379 | currentItemIndex + 1, 380 | items, 381 | api, 382 | progressWindow, 383 | progressWindowItems 384 | ); 385 | }, 386 | 387 | /** 388 | * Insert the retrieve citation count into the Items "extra" field. 389 | * Ref: https://www.zotero.org/support/kb/item_types_and_fields#citing_fields_from_extra 390 | */ 391 | _setCitationCount: function (item, source, count) { 392 | const pattern = /^Citations \(${source}\):|^\d+ citations \(${source}\)/i; 393 | const extraFieldLines = (item.getField("extra") || "") 394 | .split("\n") 395 | .filter((line) => !pattern.test(line)); 396 | 397 | const today = new Date().toISOString().split("T")[0]; 398 | extraFieldLines.unshift(`${count} citations (${source}) [${today}]`); 399 | 400 | item.setField("extra", extraFieldLines.join("\n")); 401 | item.saveTx(); 402 | }, 403 | 404 | /** 405 | * Get the value of an items DOI field. 406 | * @TODO make more robust, e.g. try to extract DOI from url/extra field as well. 407 | */ 408 | _getDoi: function (item) { 409 | const doi = item.getField("DOI"); 410 | if (!doi) { 411 | throw new Error("citationcounts-progresswindow-error-no-doi"); 412 | } 413 | 414 | return encodeURIComponent(doi); 415 | }, 416 | 417 | /** 418 | * Get the value of an items arXiv field. 419 | * @TODO make more robust, e.g. try to extract arxiv id from extra field as well. 420 | */ 421 | _getArxiv: function (item) { 422 | const itemURL = item.getField("url"); 423 | const arxivMatch = 424 | /(?:arxiv.org[/]abs[/]|arXiv:)([a-z.-]+[/]\d+|\d+[.]\d+)/i.exec(itemURL); 425 | 426 | if (!arxivMatch) { 427 | throw new Error("citationcounts-progresswindow-error-no-arxiv"); 428 | } 429 | 430 | return encodeURIComponent(arxivMatch[1]); 431 | }, 432 | 433 | /** 434 | * Send a request to a specified url, handle response with specified callback, and return a validated integer. 435 | */ 436 | _sendRequest: async function (url, callback) { 437 | const response = await fetch(url) 438 | .then((response) => response.json()) 439 | .catch(() => { 440 | throw new Error("citationcounts-progresswindow-error-bad-api-response"); 441 | }); 442 | 443 | try { 444 | const count = parseInt(await callback(response)); 445 | 446 | if (!(Number.isInteger(count) && count >= 0)) { 447 | // throw generic error since catch bloc will convert it. 448 | throw new Error(); 449 | } 450 | 451 | return count; 452 | } catch (error) { 453 | throw new Error("citationcounts-progresswindow-error-no-citation-count"); 454 | } 455 | }, 456 | 457 | _retrieveCitationCount: async function ( 458 | item, 459 | apiName, 460 | useDoi, 461 | useArxiv, 462 | urlFunction, 463 | requestCallback 464 | ) { 465 | let errorMessage = ""; 466 | let doiField, 467 | arxivField = false; 468 | 469 | if (useDoi) { 470 | try { 471 | doiField = this._getDoi(item); 472 | 473 | const count = await this._sendRequest( 474 | urlFunction(doiField, "doi"), 475 | requestCallback 476 | ); 477 | 478 | return [count, `${apiName}/DOI`]; 479 | } catch (error) { 480 | errorMessage = error.message; 481 | } 482 | 483 | // if arxiv is not used, throw errors picked up along the way now. 484 | if (!useArxiv) { 485 | throw new Error(errorMessage); 486 | } 487 | } 488 | 489 | // If doi is not used for this api or if it is, but was unsuccessfull, and arxiv is used. 490 | if (useArxiv) { 491 | // save the error message from the doi operation. 492 | const doiErrorMessage = errorMessage; 493 | 494 | try { 495 | arxivField = this._getArxiv(item); 496 | 497 | const count = await this._sendRequest( 498 | urlFunction(arxivField, "arxiv"), 499 | requestCallback 500 | ); 501 | 502 | return [count, `${apiName}/arXiv`]; 503 | } catch (error) { 504 | errorMessage = error.message; 505 | } 506 | 507 | // if both no doi and no arxiv id on item 508 | if (useDoi && !doiField && !arxivField) { 509 | throw new Error("citationcounts-progresswindow-error-no-doi-or-arxiv"); 510 | } 511 | 512 | // show proper error from unsuccessfull doi operation 513 | if (useDoi && !arxivField && doiErrorMessage) { 514 | throw new Error(doiErrorMessage); 515 | } 516 | 517 | // throw the last error incurred. 518 | throw new Error(errorMessage); 519 | } 520 | 521 | //if none is used, it is an internal error. 522 | throw new Error("citationcounts-internal-error"); 523 | }, 524 | 525 | ///////////////////////////////////////////// 526 | // API specific stuff // 527 | //////////////////////////////////////////// 528 | 529 | _crossrefUrl: function (id, type) { 530 | return `https://api.crossref.org/works/${id}/transform/application/vnd.citationstyles.csl+json`; 531 | }, 532 | 533 | _crossrefCallback: function (response) { 534 | return response["is-referenced-by-count"]; 535 | }, 536 | 537 | _inspireUrl: function (id, type) { 538 | return `https://inspirehep.net/api/${type}/${id}`; 539 | }, 540 | 541 | _inspireCallback: function (response) { 542 | return response["metadata"]["citation_count"]; 543 | }, 544 | 545 | _semanticScholarUrl: function (id, type) { 546 | const prefix = type === "doi" ? "" : "arXiv:"; 547 | return `https://api.semanticscholar.org/graph/v1/paper/${prefix}${id}?fields=citationCount`; 548 | }, 549 | 550 | // The callback can be async if we want. 551 | _semanticScholarCallback: async function (response) { 552 | count = response["citationCount"]; 553 | 554 | // throttle Semantic Scholar so we don't reach limit. 555 | await new Promise((r) => setTimeout(r, 3000)); 556 | return count; 557 | }, 558 | }; 559 | --------------------------------------------------------------------------------