├── 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 |
--------------------------------------------------------------------------------