diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..4c889e0
Binary files /dev/null and b/.DS_Store differ
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..1be3de4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,39 @@
+# Zotero 7 Citation Counts Manager Enhaned
+
+- [GitHub](https://github.com/FrLars21/ZoteroCitationCountsManager): Source
+ code repository
+
+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.
+
+Please report any bugs, questions, or feature requests in the Github repository.
+
+## Features
+
+- Autoretrieve citation counts when a new item is added to your Zotero library.
+- Retrieve citation counts manually by right-clicking on one or more items in your Zotero library.
+- Works with the following APIs: [Crossref](https://www.crossref.org), [INSPIRE-HEP](https://inspirehep.net) and [Semantic Scholar](https://www.semanticscholar.org).
+- _NEW:_ The plugin is compatible with **Zotero 7** (Zotero 6 is **NOT** supported!).
+- _NEW:_ The plugin registers a custom column ("Citation Counts") in your Zotero library so that items can be **ordered by citation count**.
+- _NEW:_ Improved _citation count retrieval operation_ status reporting, including item-specific error messages for those items where a citation count couldn't be retrieved.
+- _NEW:_ Concurrent citation count retrieval operations is now possible. Especially important for the autoretrieve feature.
+- _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.
+- _NEW:_ The whole codebade has been refactored with a focus on easy maintenance, especially for the supported citation count APIs.
+
+## Acknowledgements
+
+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.
+Boilerplate for this plugin was based on Zotero's sample plugin for v7 [Make-It-Red](https://github.com/zotero/make-it-red).
+
+## Installing
+
+- Download the add-on (the .xpi file) from the latest release: https://github.com/FrLars21/ZoteroCitationCountsManager/releases
+- To download the .xpi file, right click it and select 'Save link as'
+- Run Zotero (version 7.x)
+- Go to `Tools -> Add-ons`
+- `Install Add-on From File`
+- Choose the file `zoterocitationcountsmanager-2.0.0.xpi`
+- Restart Zotero
+
+## License
+
+Distributed under the Mozilla Public License (MPL) Version 2.0.
diff --git a/bin/.DS_Store b/bin/.DS_Store
new file mode 100644
index 0000000..4ea60df
Binary files /dev/null and b/bin/.DS_Store differ
diff --git a/bin/build b/bin/build
new file mode 100755
index 0000000..615cb4c
--- /dev/null
+++ b/bin/build
@@ -0,0 +1,14 @@
+#!/bin/sh
+
+cd ..
+
+version='2.0'
+
+rm -f zoterocitationcountsmanager-${version}.xpi
+zip -r zoterocitationcountsmanager-${version}.xpi locale/* manifest.json bootstrap.js preferences.js preferences.xhtml prefs.js zoterocitationcounts.js
+
+# To release a new version:
+# - increase version number in all files (not just here)
+# - run this script to create a new .xpi file
+# - commit and push to Github
+# - make a release on Github, and manually upload the new .xpi file
\ No newline at end of file
diff --git a/bootstrap.js b/bootstrap.js
new file mode 100644
index 0000000..4607de9
--- /dev/null
+++ b/bootstrap.js
@@ -0,0 +1,58 @@
+let ZoteroCitationCounts, itemObserver;
+
+async function startup({ id, version, rootURI }) {
+ Services.scriptloader.loadSubScript(rootURI + "zoterocitationcounts.js");
+
+ ZoteroCitationCounts.init({ id, version, rootURI });
+ ZoteroCitationCounts.addToAllWindows();
+
+ Zotero.PreferencePanes.register({
+ pluginID: id,
+ label: await ZoteroCitationCounts.l10n.formatValue(
+ "citationcounts-preference-pane-label"
+ ),
+ image: ZoteroCitationCounts.icon("edit-list-order", false),
+ src: "preferences.xhtml",
+ scripts: ["preferences.js"],
+ });
+
+ await Zotero.ItemTreeManager.registerColumns({
+ dataKey: "citationcounts",
+ label: await ZoteroCitationCounts.l10n.formatValue(
+ "citationcounts-column-title"
+ ),
+ pluginID: id,
+ dataProvider: (item) => ZoteroCitationCounts.getCitationCount(item),
+ });
+
+ itemObserver = Zotero.Notifier.registerObserver(
+ {
+ notify: function (event, type, ids, extraData) {
+ if (event == "add") {
+ const pref = ZoteroCitationCounts.getPref("autoretrieve");
+ if (pref === "none") return;
+
+ const api = ZoteroCitationCounts.APIs.find((api) => api.key === pref);
+ if (!api) return;
+
+ ZoteroCitationCounts.updateItems(Zotero.Items.get(ids), api);
+ }
+ },
+ },
+ ["item"]
+ );
+}
+
+function onMainWindowLoad({ window }) {
+ ZoteroCitationCounts.addToWindow(window);
+}
+
+function onMainWindowUnload({ window }) {
+ ZoteroCitationCounts.removeFromWindow(window);
+}
+
+function shutdown() {
+ ZoteroCitationCounts.removeFromAllWindows();
+ Zotero.Notifier.unregisterObserver(itemObserver);
+ ZoteroCitationCounts = undefined;
+}
diff --git a/locale/.DS_Store b/locale/.DS_Store
new file mode 100644
index 0000000..f54a845
Binary files /dev/null and b/locale/.DS_Store differ
diff --git a/locale/en-US/citation-counts.ftl b/locale/en-US/citation-counts.ftl
new file mode 100644
index 0000000..f084549
--- /dev/null
+++ b/locale/en-US/citation-counts.ftl
@@ -0,0 +1,36 @@
+## For the custom column that the plugin registers
+citationcounts-column-title = Citation count
+
+## For the "Item" contextmenu, where citation counts can be manually retrieved for the selected items.
+citationcounts-itemmenu-retrieve-title =
+ .label = Get citation count
+citationcounts-itemmenu-retrieve-api =
+ .label = Get { $api } citation count
+
+## For the ProgressWindow, showing citation counts retrieval operation status
+citationcounts-progresswindow-headline = Getting { $api } citation counts.
+citationcounts-progresswindow-finished-headline = Finished getting { $api } citation counts.
+citationcounts-progresswindow-error-no-doi = No DOI field exists on the item.
+citationcounts-progresswindow-error-no-arxiv = No arXiv id found on the item.
+citationcounts-progresswindow-error-no-doi-or-arxiv = No DOI / arXiv ID found on the item.
+citationcounts-progresswindow-error-bad-api-response = Problem accesing the { $api } API.
+citationcounts-progresswindow-error-no-citation-count = { $api } doesn't have a citation count for this item.
+
+## For the "Tools" menu, where the "autoretrieve" preference can be set.
+citationcounts-menutools-autoretrieve-title =
+ .label = Get citation counts for new items?
+citationcounts-menutools-autoretrieve-api =
+ .label = { $api }
+citationcounts-menutools-autoretrieve-api-none =
+ .label = No
+
+## For the plugins "Preferences" pane.
+citationcounts-preference-pane-label = Citation Counts
+citationcounts-preferences-pane-autoretrieve-title = Get citation counts for new items?
+citationcounts-preferences-pane-autoretrieve-api =
+ .label = { $api }
+citationcounts-preferences-pane-autoretrieve-api-none =
+ .label = No
+
+## Misc
+citationcounts-internal-error = Internal error
\ No newline at end of file
diff --git a/manifest.json b/manifest.json
new file mode 100644
index 0000000..07da0dd
--- /dev/null
+++ b/manifest.json
@@ -0,0 +1,15 @@
+{
+ "manifest_version": 2,
+ "name": "Zotero Citation Counts Manager",
+ "version": "2.0",
+ "description": "Enhanced Citation Counts Manager for Zotero 7",
+ "homepage_url": "https://github.com/FrLars21/ZoteroCitationCountsManager",
+ "applications": {
+ "zotero": {
+ "id": "frlars21@github.com",
+ "update_url": "https://www.zotero.org/download/plugins/make-it-red/updates.json",
+ "strict_min_version": "6.999",
+ "strict_max_version": "7.0.*"
+ }
+ }
+}
diff --git a/preferences.js b/preferences.js
new file mode 100644
index 0000000..204a91d
--- /dev/null
+++ b/preferences.js
@@ -0,0 +1,73 @@
+ZoteroCitationCounts_Prefs = {
+ /**
+ * @TODO reference ZoteroCitationCounts.APIs directly.
+ */
+ APIs: [
+ {
+ key: "crossref",
+ name: "Crossref",
+ },
+ {
+ key: "inspire",
+ name: "INSPIRE-HEP",
+ },
+ {
+ key: "semanticscholar",
+ name: "Semantic Scholar",
+ },
+ ],
+
+ init: function () {
+ this.APIs.concat({ key: "none" }).forEach((api) => {
+ const label =
+ api.key === "none"
+ ? {
+ "data-l10n-id":
+ "citationcounts-preferences-pane-autoretrieve-api-none",
+ }
+ : {
+ "data-l10n-id":
+ "citationcounts-preferences-pane-autoretrieve-api",
+ "data-l10n-args": `{"api": "${api.name}"}`,
+ };
+
+ this._injectXULElement(
+ document,
+ "radio",
+ `citationcounts-preferences-pane-autoretrieve-radio-${api.key}`,
+ {
+ ...label,
+ value: api.key,
+ },
+ "citationcounts-preference-pane-autoretrieve-radiogroup"
+ );
+ });
+ },
+
+ /**
+ * @TODO reference ZoteroCitationCounts._injectXULElement directly.
+ */
+ _injectXULElement: function (
+ document,
+ elementType,
+ elementID,
+ elementAttributes,
+ parentID,
+ eventListeners
+ ) {
+ const element = document.createXULElement(elementType);
+ element.id = elementID;
+
+ Object.entries(elementAttributes || {})
+ .filter(([_, value]) => value !== null && value !== undefined)
+ .forEach(([key, value]) => element.setAttribute(key, value));
+
+ Object.entries(eventListeners || {}).forEach(([eventType, listener]) => {
+ element.addEventListener(eventType, listener);
+ });
+
+ document.getElementById(parentID).appendChild(element);
+
+ return element;
+ },
+};
diff --git a/preferences.xhtml b/preferences.xhtml
new file mode 100644
index 0000000..9892fea
--- /dev/null
+++ b/preferences.xhtml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/prefs.js b/prefs.js
new file mode 100644
index 0000000..0af35e4
--- /dev/null
+++ b/prefs.js
@@ -0,0 +1 @@
+pref("extensions.citationcounts.autoretrieve", "none");
diff --git a/zoterocitationcounts.js b/zoterocitationcounts.js
new file mode 100644
index 0000000..d81604f
--- /dev/null
+++ b/zoterocitationcounts.js
@@ -0,0 +1,558 @@
+ZoteroCitationCounts = {
+ _initialized: false,
+
+ pluginID: null,
+ pluginVersion: null,
+ rootURI: null,
+
+ l10n: null,
+ APIs: [],
+
+ /**
+ * Track injected XULelements for removal upon mainWindowUnload.
+ */
+ _addedElementIDs: [],
+
+ _log(msg) {
+ Zotero.debug("Zotero Citation Counts: " + msg);
+ },
+
+ init: function ({ id, version, rootURI }) {
+ if (this._initialized) return;
+
+ this.pluginID = id;
+ this.pluginVersion = version;
+ this.rootURI = rootURI;
+
+ this.l10n = new Localization(["citation-counts.ftl"]);
+
+ /**
+ * To add a new API:
+ * -----------------
+ * (1) Create a urlBuilder method on the ZoteroCitationCounts object. Args: urlencoded *id* and *idtype* ("doi" or "arxiv"). Return: URL for API request.
+ *
+ * (2) Create a responseCallback method on the ZoteroCitationCounts object. Args: *response* from api call. Return: citation count number.
+ *
+ * (3) Register the API here, and specify whether it works with doi, arxiv id or both.
+ *
+ * (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).
+ */
+ this.APIs = [
+ {
+ key: "crossref",
+ name: "Crossref",
+ useDoi: true,
+ useArxiv: false,
+ methods: {
+ urlBuilder: this._crossrefUrl,
+ responseCallback: this._crossrefCallback,
+ },
+ },
+ {
+ key: "inspire",
+ name: "INSPIRE-HEP",
+ useDoi: true,
+ useArxiv: true,
+ methods: {
+ urlBuilder: this._inspireUrl,
+ responseCallback: this._inspireCallback,
+ },
+ },
+ {
+ key: "semanticscholar",
+ name: "Semantic Scholar",
+ useDoi: true,
+ useArxiv: true,
+ methods: {
+ urlBuilder: this._semanticScholarUrl,
+ responseCallback: this._semanticScholarCallback,
+ },
+ },
+ ];
+
+ this._initialized = true;
+ },
+
+ getCitationCount: function (item) {
+ const extraFieldLines = (item.getField("extra") || "")
+ .split("\n")
+ .filter((line) => /^Citations:|^\d+ citations/i.test(line));
+
+ return extraFieldLines[0]?.match(/^\d+/) || "-";
+ },
+
+ getPref: function (pref) {
+ return Zotero.Prefs.get("extensions.citationcounts." + pref, true);
+ },
+
+ setPref: function (pref, value) {
+ return Zotero.Prefs.set("extensions.citationcounts." + pref, value, true);
+ },
+
+ icon: function (iconName, hiDPI) {
+ return `chrome://zotero/skin/${iconName}${
+ hiDPI ? (Zotero.hiDPI ? "@2x" : "") : ""
+ }.png`;
+ },
+
+ /////////////////////////////////////////////
+ // UI related stuff //
+ ////////////////////////////////////////////
+
+ /**
+ * Create XULElement, set it's attributes, inject accordingly to the DOM & save a reference for later removal.
+ *
+ * @param {Document} document - "Document"-interface to be operated on.
+ * @param {String} elementType - XULElement type (e.g. "menu", "popupmenu" etc.)
+ * @param {String} elementID - The elements *unique* ID attribute.
+ * @param {Object} elementAttributes - An object of key-value pairs that represent the DOM element attributes.
+ * @param {String} parentID - The *unique* ID attribute of the element's parent element.
+ * @param {Object} eventListeners - An object where keys are event types (e.g., 'command') and values are corresponding event handler functions.
+ *
+ * @returns {MozXULElement} - A reference to the injected XULElement.
+ */
+ _injectXULElement: function (
+ document,
+ elementType,
+ elementID,
+ elementAttributes,
+ parentID,
+ eventListeners
+ ) {
+ const element = document.createXULElement(elementType);
+ element.id = elementID;
+
+ Object.entries(elementAttributes || {})
+ .filter(([_, value]) => value !== null && value !== undefined)
+ .forEach(([key, value]) => element.setAttribute(key, value));
+
+ Object.entries(eventListeners || {}).forEach(([eventType, listener]) => {
+ element.addEventListener(eventType, listener);
+ });
+
+ document.getElementById(parentID).appendChild(element);
+ this._storeAddedElement(element);
+
+ return element;
+ },
+
+ _storeAddedElement: function (elem) {
+ if (!elem.id) {
+ throw new Error("Element must have an id.");
+ }
+
+ this._addedElementIDs.push(elem.id);
+ },
+
+ /**
+ * Create a submenu to Zotero's "Tools"-menu, from which the plugin specific "autoretrieve" preference can be set.
+ */
+ _createToolsMenu: function (document) {
+ const menu = this._injectXULElement(
+ document,
+ "menu",
+ "menu_Tools-citationcounts-menu",
+ { "data-l10n-id": "citationcounts-menutools-autoretrieve-title" },
+ "menu_ToolsPopup"
+ );
+
+ const menupopup = this._injectXULElement(
+ document,
+ "menupopup",
+ "menu_Tools-citationcounts-menu-popup",
+ {},
+ menu.id,
+ {
+ popupshowing: () => {
+ this.APIs.concat({ key: "none" }).forEach((api) => {
+ document
+ .getElementById(`menu_Tools-citationcounts-menu-popup-${api.key}`)
+ .setAttribute(
+ "checked",
+ Boolean(this.getPref("autoretrieve") === api.key)
+ );
+ });
+ },
+ }
+ );
+
+ this.APIs.concat({ key: "none" }).forEach((api) => {
+ const label =
+ api.key === "none"
+ ? { "data-l10n-id": "citationcounts-menutools-autoretrieve-api-none" }
+ : {
+ "data-l10n-id": "citationcounts-menutools-autoretrieve-api",
+ "data-l10n-args": `{"api": "${api.name}"}`,
+ };
+
+ this._injectXULElement(
+ document,
+ "menuitem",
+ `menu_Tools-citationcounts-menu-popup-${api.key}`,
+ {
+ ...label,
+ type: "checkbox",
+ },
+ menupopup.id,
+ { command: () => this.setPref("autoretrieve", api.key) }
+ );
+ });
+ },
+
+ /**
+ * Create a submenu to Zotero's "Item"-context menu, from which citation counts for selected items can be manually retrieved.
+ */
+ _createItemMenu: function (document) {
+ const menu = this._injectXULElement(
+ document,
+ "menu",
+ "zotero-itemmenu-citationcounts-menu",
+ {
+ "data-l10n-id": "citationcounts-itemmenu-retrieve-title",
+ class: "menu-iconic",
+ },
+ "zotero-itemmenu"
+ );
+
+ const menupopup = this._injectXULElement(
+ document,
+ "menupopup",
+ "zotero-itemmenu-citationcounts-menupopup",
+ {},
+ menu.id
+ );
+
+ this.APIs.forEach((api) => {
+ this._injectXULElement(
+ document,
+ "menuitem",
+ `zotero-itemmenu-citationcounts-${api.key}`,
+ {
+ "data-l10n-id": "citationcounts-itemmenu-retrieve-api",
+ "data-l10n-args": `{"api": "${api.name}"}`,
+ },
+ menupopup.id,
+ {
+ command: () =>
+ this.updateItems(
+ Zotero.getActiveZoteroPane().getSelectedItems(),
+ api
+ ),
+ }
+ );
+ });
+ },
+
+ /**
+ * Inject plugin specific DOM elements in a DOM window.
+ */
+ addToWindow: function (window) {
+ window.MozXULElement.insertFTLIfNeeded("citation-counts.ftl");
+
+ this._createToolsMenu(window.document);
+ this._createItemMenu(window.document);
+ },
+
+ /**
+ * Inject plugin specific DOM elements into all Zotero windows.
+ */
+ addToAllWindows: function () {
+ const windows = Zotero.getMainWindows();
+
+ for (let window of windows) {
+ if (!window.ZoteroPane) continue;
+ this.addToWindow(window);
+ }
+ },
+
+ /**
+ * Remove plugin specific DOM elements from a DOM window.
+ */
+ removeFromWindow: function (window) {
+ const document = window.document;
+
+ for (let id of this._addedElementIDs) {
+ document.getElementById(id)?.remove();
+ }
+
+ document.querySelector('[href="citation-counts.ftl"]').remove();
+ },
+
+ /**
+ * Remove plugin specific DOM elements from all Zotero windows.
+ */
+ removeFromAllWindows: function () {
+ const windows = Zotero.getMainWindows();
+
+ for (let window of windows) {
+ if (!window.ZoteroPane) continue;
+ this.removeFromWindow(window);
+ }
+ },
+
+ //////////////////////////////////////////////////////////
+ // Update citation count operation stuff //
+ /////////////////////////////////////////////////////////
+
+ /**
+ * Start citation count retrieval operation
+ */
+ updateItems: async function (itemsRaw, api) {
+ const items = itemsRaw.filter((item) => !item.isFeedItem);
+ if (!items.length) return;
+
+ const progressWindow = new Zotero.ProgressWindow();
+ progressWindow.changeHeadline(
+ await this.l10n.formatValue("citationcounts-progresswindow-headline", {
+ api: api.name,
+ }),
+ this.icon("toolbar-advanced-search")
+ );
+
+ const progressWindowItems = [];
+ const itemTitles = items.map((item) => item.getField("title"));
+ itemTitles.forEach((title) => {
+ progressWindowItems.push(
+ new progressWindow.ItemProgress(this.icon("spinner-16px"), title)
+ );
+ });
+
+ progressWindow.show();
+
+ this._updateItem(0, items, api, progressWindow, progressWindowItems);
+ },
+
+ /**
+ * Updates citation counts recursively for a list of items.
+ *
+ * @param currentItemIndex - Index of currently updating Item. Zero-based.
+ * @param items - List of all Items to be updated in this operation.
+ * @param api - API to be used to retrieve *items* citation counts.
+ * @param progressWindow - ProgressWindow associated with this operation.
+ * @param progressWindowItems - List of references to each Zotero.ItemProgress in *progressWindow*.
+ */
+ _updateItem: async function (
+ currentItemIndex,
+ items,
+ api,
+ progressWindow,
+ progressWindowItems
+ ) {
+ // Check if operation is done
+ if (currentItemIndex >= items.length) {
+ const headlineFinished = await this.l10n.formatValue(
+ "citationcounts-progresswindow-finished-headline",
+ { api: api.name }
+ );
+ progressWindow.changeHeadline(headlineFinished);
+ progressWindow.startCloseTimer(5000);
+ return;
+ }
+
+ const item = items[currentItemIndex];
+ const pwItem = progressWindowItems[currentItemIndex];
+
+ try {
+ const [count, source] = await this._retrieveCitationCount(
+ item,
+ api.name,
+ api.useDoi,
+ api.useArxiv,
+ api.methods.urlBuilder,
+ api.methods.responseCallback
+ );
+
+ this._setCitationCount(item, source, count);
+
+ pwItem.setIcon(this.icon("tick"));
+ pwItem.setProgress(100);
+ } catch (error) {
+ pwItem.setError();
+ new progressWindow.ItemProgress(
+ this.icon("bullet_yellow"),
+ await this.l10n.formatValue(error.message, { api: api.name }),
+ pwItem
+ );
+ }
+
+ this._updateItem(
+ currentItemIndex + 1,
+ items,
+ api,
+ progressWindow,
+ progressWindowItems
+ );
+ },
+
+ /**
+ * Insert the retrieve citation count into the Items "extra" field.
+ * Ref: https://www.zotero.org/support/kb/item_types_and_fields#citing_fields_from_extra
+ */
+ _setCitationCount: function (item, source, count) {
+ const pattern = /^Citations \(${source}\):|^\d+ citations \(${source}\)/i;
+ const extraFieldLines = (item.getField("extra") || "")
+ .split("\n")
+ .filter((line) => !pattern.test(line));
+
+ const today = new Date().toISOString().split("T")[0];
+ extraFieldLines.unshift(`${count} citations (${source}) [${today}]`);
+
+ item.setField("extra", extraFieldLines.join("\n"));
+ item.saveTx();
+ },
+
+ /**
+ * Get the value of an items DOI field.
+ * @TODO make more robust, e.g. try to extract DOI from url/extra field as well.
+ */
+ _getDoi: function (item) {
+ const doi = item.getField("DOI");
+ if (!doi) {
+ throw new Error("citationcounts-progresswindow-error-no-doi");
+ }
+
+ return encodeURIComponent(doi);
+ },
+
+ /**
+ * Get the value of an items arXiv field.
+ * @TODO make more robust, e.g. try to extract arxiv id from extra field as well.
+ */
+ _getArxiv: function (item) {
+ const itemURL = item.getField("url");
+ const arxivMatch =
+ /(?:arxiv.org[/]abs[/]|arXiv:)([a-z.-]+[/]\d+|\d+[.]\d+)/i.exec(itemURL);
+
+ if (!arxivMatch) {
+ throw new Error("citationcounts-progresswindow-error-no-arxiv");
+ }
+
+ return encodeURIComponent(arxivMatch[1]);
+ },
+
+ /**
+ * Send a request to a specified url, handle response with specified callback, and return a validated integer.
+ */
+ _sendRequest: async function (url, callback) {
+ const response = await fetch(url)
+ .then((response) => response.json())
+ .catch(() => {
+ throw new Error("citationcounts-progresswindow-error-bad-api-response");
+ });
+
+ try {
+ const count = parseInt(await callback(response));
+
+ if (!(Number.isInteger(count) && count >= 0)) {
+ // throw generic error since catch bloc will convert it.
+ throw new Error();
+ }
+
+ return count;
+ } catch (error) {
+ throw new Error("citationcounts-progresswindow-error-no-citation-count");
+ }
+ },
+
+ _retrieveCitationCount: async function (
+ item,
+ apiName,
+ useDoi,
+ useArxiv,
+ urlFunction,
+ requestCallback
+ ) {
+ let errorMessage = "";
+ let doiField,
+ arxivField = false;
+
+ if (useDoi) {
+ try {
+ doiField = this._getDoi(item);
+
+ const count = await this._sendRequest(
+ urlFunction(doiField, "doi"),
+ requestCallback
+ );
+
+ return [count, `${apiName}/DOI`];
+ } catch (error) {
+ errorMessage = error.message;
+ }
+
+ // if arxiv is not used, throw errors picked up along the way now.
+ if (!useArxiv) {
+ throw new Error(errorMessage);
+ }
+ }
+
+ // If doi is not used for this api or if it is, but was unsuccessfull, and arxiv is used.
+ if (useArxiv) {
+ // save the error message from the doi operation.
+ const doiErrorMessage = errorMessage;
+
+ try {
+ arxivField = this._getArxiv(item);
+
+ const count = await this._sendRequest(
+ urlFunction(arxivField, "arxiv"),
+ requestCallback
+ );
+
+ return [count, `${apiName}/arXiv`];
+ } catch (error) {
+ errorMessage = error.message;
+ }
+
+ // if both no doi and no arxiv id on item
+ if (useDoi && !doiField && !arxivField) {
+ throw new Error("citationcounts-progresswindow-error-no-doi-or-arxiv");
+ }
+
+ // show proper error from unsuccessfull doi operation
+ if (useDoi && !arxivField && doiErrorMessage) {
+ throw new Error(doiErrorMessage);
+ }
+
+ // throw the last error incurred.
+ throw new Error(errorMessage);
+ }
+
+ //if none is used, it is an internal error.
+ throw new Error("citationcounts-internal-error");
+ },
+
+ /////////////////////////////////////////////
+ // API specific stuff //
+ ////////////////////////////////////////////
+
+ _crossrefUrl: function (id, type) {
+ return `https://api.crossref.org/works/${id}/transform/application/vnd.citationstyles.csl+json`;
+ },
+
+ _crossrefCallback: function (response) {
+ return response["is-referenced-by-count"];
+ },
+
+ _inspireUrl: function (id, type) {
+ return `https://inspirehep.net/api/${type}/${id}`;
+ },
+
+ _inspireCallback: function (response) {
+ return response["metadata"]["citation_count"];
+ },
+
+ _semanticScholarUrl: function (id, type) {
+ const prefix = type === "doi" ? "" : "arXiv:";
+ return `https://api.semanticscholar.org/graph/v1/paper/${prefix}${id}?fields=citationCount`;
+ },
+
+ // The callback can be async if we want.
+ _semanticScholarCallback: async function (response) {
+ count = response["citationCount"];
+
+ // throttle Semantic Scholar so we don't reach limit.
+ await new Promise((r) => setTimeout(r, 3000));
+ return count;
+ },
+};