From e72da8c7c0a37cbea6d2d3cdb118374578713e5a Mon Sep 17 00:00:00 2001 From: Emiliano Heyns Date: Sat, 30 Jan 2021 22:43:23 +0100 Subject: [PATCH] partial eslintification --- .eslintignore | 2 + .eslintrc.js | 58 ++-- .ignore | 1 + content/ErrorReport.ts | 64 +++-- content/ExportOptions.ts | 9 +- content/FirstRun.ts | 2 +- content/ItemPane.ts | 7 +- content/Preferences.ts | 42 ++- content/TestSupport.ts | 42 ++- content/ZoteroPane.ts | 40 +-- content/arXiv.ts | 2 +- content/auto-export.ts | 53 ++-- content/aux-scanner.ts | 43 ++- content/better-bibtex.ts | 209 ++++++++------ content/case.ts | 10 +- content/cayw.ts | 108 ++++--- content/cayw/formatter.ts | 43 +-- content/collection.ts | 10 +- content/dateparser.ts | 67 +++-- content/db/cache.ts | 22 +- content/db/loki.ts | 29 +- content/db/main.ts | 8 +- content/db/store.ts | 74 +++-- content/db/upgrade-extra.ts | 26 +- content/db/upgrade.ts | 10 +- content/db/zotero.ts | 3 +- content/escape.ts | 7 +- content/events.ts | 8 +- content/extra.ts | 27 +- content/flash.ts | 6 +- content/get-items-async.ts | 8 +- content/issn.ts | 10 +- content/journal-abbrev.ts | 20 +- content/json-rpc.ts | 48 +++- content/key-manager.ts | 118 ++++---- content/key-manager/formatter.ts | 270 ++++++++++-------- content/key-manager/kuroshiro.ts | 8 +- content/library.ts | 6 +- content/logger.ts | 23 +- content/markupparser.ts | 116 ++++---- content/monkey-patch.ts | 15 +- content/path-search.ts | 21 +- content/prefs.ts | 15 +- content/pull-export.ts | 44 +-- content/qr-check.ts | 2 +- content/scheduler.ts | 19 +- content/serializer.ts | 24 +- content/sleep.ts | 2 +- content/stringify.ts | 7 +- content/tex-studio.ts | 12 +- content/translators.ts | 69 +++-- content/typings/bbt.d.ts | 14 + minitests/bibertool.ts | 1 + minitests/text2latex.ts | 2 +- schema/hashes.json | 4 +- setup/citeproc.ts | 1 + setup/key-formatter-methods.ts | 22 +- setup/loaders/bibertool.ts | 23 +- setup/loaders/inline-ts.ts | 2 +- setup/loaders/trace.ts | 9 +- setup/plugins/log-used.ts | 9 +- setup/preferences.py | 2 +- setup/shims/assert.js | 3 + setup/templates/items/items.ts.mako | 17 +- .../templates/items/serialized-item.d.ts.mako | 41 ++- .../templates/preferences/preferences.ts.mako | 34 ++- setup/translators.py | 10 - translators/Better BibLaTeX.ts | 66 +++-- .../Better BibTeX Citation Key Quick Copy.ts | 36 ++- translators/Better BibTeX.ts | 145 ++++++---- translators/Better CSL JSON.ts | 9 +- translators/Better CSL YAML.ts | 11 +- translators/BetterBibTeX JSON.ts | 26 +- translators/Citation graph.ts | 26 +- translators/Collected notes.ts | 24 +- translators/bibtex/datefield.ts | 35 ++- translators/bibtex/exporter.ts | 16 +- translators/bibtex/jabref.ts | 16 +- translators/bibtex/postfix.ts | 14 +- translators/bibtex/reference.ts | 243 +++++++++------- translators/bibtex/unicode_translator.ts | 90 +++--- translators/csl/csl.ts | 23 +- translators/lib/normalize.ts | 38 ++- translators/lib/translator.ts | 122 ++++---- translators/typings/exporter.d.ts | 18 ++ translators/typings/worker.d.ts | 4 +- translators/worker/zotero.ts | 42 ++- 87 files changed, 1862 insertions(+), 1225 deletions(-) create mode 100644 .eslintignore create mode 100644 content/typings/bbt.d.ts create mode 100644 minitests/bibertool.ts create mode 100644 setup/shims/assert.js create mode 100644 translators/typings/exporter.d.ts diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000..8170577a9c --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +node_modules +*.d.ts diff --git a/.eslintrc.js b/.eslintrc.js index e63fe92474..c90dc1285d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,27 +1,31 @@ -module.exports = { - "env": { - "browser": true, - "es6": true, - "node": true - }, - "extends": [ - "prettier", - "prettier/@typescript-eslint", - "zotero-plugin", - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "tsconfig.json", - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint" - ], - "rules": { - "@typescript-eslint/consistent-type-definitions": "off", - "@typescript-eslint/member-ordering": "off", - "max-classes-per-file": "off", - "no-console": "off", - "no-new-func": "off" - } -}; +const config = require('zotero-plugin/.eslintrc') + +config.rules['@typescript-eslint/consistent-type-definitions'] = 'off' +config.rules['@typescript-eslint/member-ordering'] = 'off' +config.rules['max-classes-per-file'] = 'off' +config.rules['no-console'] = 'error' +config.rules['no-new-func'] = 'off' +config.rules['no-underscore-dangle'] = [ 'error', { "allowAfterThis": true } ] + +config.rules['@typescript-eslint/no-unsafe-member-access'] = 'off' +config.rules['@typescript-eslint/no-unsafe-call'] = 'off' +config.rules['@typescript-eslint/prefer-regexp-exec'] = 'off' +config.rules['@typescript-eslint/no-implied-eval'] = 'off' +config.rules['@typescript-eslint/no-unsafe-assignment'] = 'off' +config.rules['@typescript-eslint/restrict-template-expressions'] = 'off' + +config.rules['@typescript-eslint/ban-ts-comment'] = 'warn' +config.rules['@typescript-eslint/member-delimiter-style'] = [ 'error', { + multiline: { delimiter: 'none', requireLast: false }, + singleline: { delimiter: 'comma', requireLast: false }, +}] +config.rules['@typescript-eslint/no-unused-vars'] = [ 'error', { "argsIgnorePattern": "^_" } ] + +config.ignorePatterns = [ + 'webpack.config.ts', + 'util/*.ts', + 'minitests/*.ts', + 'content/minitests/*.ts', +] + +module.exports = config diff --git a/.ignore b/.ignore index 1ec38e7e88..8b6ba4432a 100644 --- a/.ignore +++ b/.ignore @@ -1,2 +1,3 @@ citeproc-js test +site/themes diff --git a/content/ErrorReport.ts b/content/ErrorReport.ts index 184a06dc70..f58e369381 100644 --- a/content/ErrorReport.ts +++ b/content/ErrorReport.ts @@ -39,10 +39,10 @@ export = new class ErrorReport { private params: any private errorlog: { - info: string, - errors: string, - debug: string | string[], - references?: string, + info: string + errors: string + debug: string | string[] + references?: string db?: string } @@ -72,7 +72,8 @@ export = new class ErrorReport { document.getElementById('better-bibtex-report-id').value = this.key document.getElementById('better-bibtex-report-result').hidden = false - } catch (err) { + } + catch (err) { const ps = Components.classes['@mozilla.org/embedcomp/prompt-service;1'].getService(Components.interfaces.nsIPromptService) ps.alert(null, Zotero.getString('general.error'), `${err} (${this.key}, references: ${!!this.errorlog.references})`) if (wizard.rewind) wizard.rewind() @@ -110,18 +111,21 @@ export = new class ErrorReport { private async latest() { try { const latest = PACKAGE.xpi.releaseURL.replace('https://github.com/', 'https://api.github.com/repos/').replace(/\/releases\/.*/, '/releases/latest') + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return JSON.parse(await (await fetch(latest, { method: 'GET', cache: 'no-cache', redirect: 'follow' })).text()).tag_name.replace('v', '') - } catch (err) { + } + catch (err) { return null } } - private async ping(region) { + private async ping(region: string) { await fetch(`http://s3.${region}.amazonaws.com${s3.region[region].tld || ''}/ping`, { method: 'GET', cache: 'no-cache', redirect: 'follow', }) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { region, ...s3.region[region] } } @@ -163,39 +167,40 @@ export = new class ErrorReport { const show_latest = document.getElementById('better-bibtex-report-latest') if (current === latest) { show_latest.hidden = true - } else { + } + else { show_latest.value = Zotero.BetterBibTeX.getString('ErrorReport.better-bibtex.latest', { version: latest || '' }) show_latest.hidden = false } try { - const region = await Zotero.Promise.any(Object.keys(s3.region).map(this.ping)) + const region = await Zotero.Promise.any(Object.keys(s3.region).map(this.ping.bind(this))) this.bucket = `http://${s3.bucket}-${region.short}.s3-${region.region}.amazonaws.com${region.tld || ''}` this.key = `${Zotero.Utilities.generateObjectKey()}-${region.short}` continueButton.disabled = false continueButton.focus() - } catch (err) { + } + catch (err) { alert(`No AWS region can be reached: ${err.message}`) wizard.getButton('cancel').disabled = false } } - private preview(lines) { + private preview(lines: string | string[]): string { if (Array.isArray(lines)) { let preview = '' for (const line of lines) { if (line.startsWith('(4)(+')) continue // DB statements - preview += line + '\n' - if (preview.length > this.previewSize) return preview + ' ...' + preview += `${line}\n` + if (preview.length > this.previewSize) return `${preview} ...` } return preview - - } else if (lines.length > this.previewSize) { - return lines.substr(0, this.previewSize) + ' ...' - + } + else if (lines.length > this.previewSize) { + return `${lines.substr(0, this.previewSize)} ...` } return lines @@ -248,7 +253,8 @@ export = new class ErrorReport { }) return - } catch (err) { + } + catch (err) { log.error('ErrorReport: failed to PUT to', url, attempt) error = err @@ -269,7 +275,8 @@ export = new class ErrorReport { xhr.onload = function() { if (this.status >= 200 && this.status < 300) { // tslint:disable-line:no-magic-numbers resolve(xhr.response) - } else { + } + else { reject({ status: this.status, statusText: xhr.statusText }) } } @@ -281,8 +288,8 @@ export = new class ErrorReport { } */ - private async submit(filename, contentType, data, prefix = '') { - if (data.then) data = await data + private async submit(filename, contentType, data: Promise | string | string[], prefix = '') { + data = await Promise.resolve(data) const headers = { 'x-amz-storage-class': 'STANDARD', @@ -303,6 +310,7 @@ export = new class ErrorReport { const debugLogDir = Prefs.get('debugLogDir') if (debugLogDir) { + if (Array.isArray(data)) data = data.join('\n') await Zotero.File.putContentsAsync(`${debugLogDir}/${filename}.${ext}`, prefix + data) return } @@ -317,21 +325,23 @@ export = new class ErrorReport { if ((chunk.length + line.length) > this.chunkSize) { if (chunk.length !== 0) chunks.push(chunk) - chunk = line + '\n' + chunk = `${line}\n` - } else { - chunk += line + '\n' + } + else { + chunk += `${line}\n` } } if (chunk.length) chunks.push(chunk) - } else { - chunks = fastChunkString(prefix + data, { size: this.chunkSize }) + } + else { + chunks = fastChunkString(`${prefix}${data}`, { size: this.chunkSize }) } - chunks = chunks.map((chunk, n) => ({ n: '.' + (n + 1).toString().padStart(4, '0'), body: chunk })) // eslint-disable-line no-magic-numbers + chunks = chunks.map((chunk, n) => ({ n: `.${(n + 1).toString().padStart(4, '0')}`, body: chunk })) // eslint-disable-line no-magic-numbers if (chunks.length === 1) chunks[0].n = '' await Promise.all(chunks.map(chunk => this.put(`${url}${chunk.n}.${ext}`, { diff --git a/content/ExportOptions.ts b/content/ExportOptions.ts index 828c2cc8cd..6c74d7122f 100644 --- a/content/ExportOptions.ts +++ b/content/ExportOptions.ts @@ -9,14 +9,16 @@ import { patch as $patch$ } from './monkey-patch' let DOM_OBSERVER = null let reset = true -$patch$(Zotero_File_Interface_Export, 'init', original => function(options) { +$patch$(Zotero_File_Interface_Export, 'init', original => function(_options) { for (const translator of window.arguments[0].translators) { if (translator.label === 'BetterBibTeX JSON') translator.label = 'BetterBibTeX debug JSON' } + // eslint-disable-next-line prefer-rest-params original.apply(this, arguments) }) -$patch$(Zotero_File_Interface_Export, 'updateOptions', original => function(options) { +$patch$(Zotero_File_Interface_Export, 'updateOptions', original => function(_options) { + // eslint-disable-next-line prefer-rest-params original.apply(this, arguments) const index = document.getElementById('format-menu').selectedIndex @@ -55,7 +57,8 @@ function mutex(e) { if ((e.target.id === exportFileData.id) && exportFileData.checked) { keepUpdated.checked = false - } else if ((e.target.id === keepUpdated.id) && keepUpdated.checked) { + } + else if ((e.target.id === keepUpdated.id) && keepUpdated.checked) { exportFileData.checked = false } } diff --git a/content/FirstRun.ts b/content/FirstRun.ts index cc1e6c007b..3d1d2b68ea 100644 --- a/content/FirstRun.ts +++ b/content/FirstRun.ts @@ -3,7 +3,7 @@ declare const document: any export = new class FirstRun { private prefix = 'better-bibtex-first-run-' - private params: { citekeyFormat: String, dragndrop: boolean } + private params: { citekeyFormat: string, dragndrop: boolean } public load() { const wizard = document.getElementById('better-bibtex-first-run') diff --git a/content/ItemPane.ts b/content/ItemPane.ts index d661b1fcef..923b750f44 100644 --- a/content/ItemPane.ts +++ b/content/ItemPane.ts @@ -17,7 +17,7 @@ function display(itemID) { menuitem = zotero_field_transform_menu.appendChild(document.createElementNS('http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul', 'menuitem')) menuitem.setAttribute('id', 'zotero-field-transform-menu-better-sentencecase') menuitem.setAttribute('label', 'BBT sentence case') - menuitem.addEventListener('command', function(e) { title_sentenceCase.call(document.getBindingParent(this), document.popupNode) }, false) + menuitem.addEventListener('command', function(_e) { title_sentenceCase.call(document.getBindingParent(this), document.popupNode) }, false) } } @@ -29,7 +29,7 @@ function display(itemID) { const pin = ' \uD83D\uDCCC' const label = document.getElementById('better-bibtex-citekey-label') - label.value = label.value.replace(pin, '') + (citekey.pinned ? pin : '') + label.value = `${label.value.replace(pin, '')}${(citekey.pinned ? pin : '')}` } let observer = null @@ -82,7 +82,8 @@ function unload() { } } -$patch$(ZoteroItemPane, 'viewItem', original => async function(item, mode, index) { +$patch$(ZoteroItemPane, 'viewItem', original => async function(item, _mode, _index) { + // eslint-disable-next-line prefer-rest-params await original.apply(this, arguments) init() diff --git a/content/Preferences.ts b/content/Preferences.ts index 29383a5f17..91e5be7c7a 100644 --- a/content/Preferences.ts +++ b/content/Preferences.ts @@ -82,11 +82,14 @@ class AutoExportPane { // hide/show per-translator options const enabled = `autoexport-${Translators.byId[ae.translatorID].label.replace(/ /g, '')}` + // eslint is wrong here. tsc complains that hidden is not present on element, and I think tsc is correct here + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion for (const node of (Array.from(tabpanel.getElementsByClassName('autoexport-options')) as XUL.Element[])) { node.hidden = !node.classList.contains(enabled) } - } else { + } + else { tab = tabs.children[index] tabpanel = tabpanels.children[index] } @@ -100,7 +103,7 @@ class AutoExportPane { switch (field) { case 'type': - (node as XUL.Textbox).value = Zotero.BetterBibTeX.getString(`Preferences.auto-export.type.${ae.type}`) + ':' + (node as XUL.Textbox).value = `${Zotero.BetterBibTeX.getString(`Preferences.auto-export.type.${ae.type}`)}:` break case 'name': @@ -129,7 +132,9 @@ class AutoExportPane { break case 'cached': + // eslint-disable-next-line no-case-declarations const items = this.items[`${ae.type}=${ae.id}`] || [] + // eslint-disable-next-line no-case-declarations let ratio = 100 if (items.length) { @@ -212,22 +217,24 @@ class AutoExportPane { this.refresh() } - private collection(id, form) { - if (isNaN(parseInt(id))) return '' + private collection(id: number | string, form: 'long' | 'short'): string { + if (typeof id === 'string') id = parseInt(id) + if (isNaN(id)) return '' const coll = Zotero.Collections.get(id) if (!coll) return '' if (form === 'long' && !isNaN(parseInt(coll.parentID))) { return `${this.collection(coll.parentID, form)} / ${coll.name}` - } else { + } + else { return `${Zotero.Libraries.get(coll.libraryID).name} : ${coll.name}` } } - private name(ae, form) { + private name(ae: { type: string, id: number, path: string }, form: 'long' | 'short'): string { switch (ae.type) { case 'library': - return Zotero.Libraries.get(ae.id).name + return (Zotero.Libraries.get(ae.id).name as string) case 'collection': return this.collection(ae.id, form) @@ -257,9 +264,10 @@ export = new class PrefPane { Formatter.parsePattern(this.keyformat.value) msg = '' if (this.keyformat.value) this.saveCitekeyFormat(target) - } catch (err) { + } + catch (err) { msg = err.message - if (err.location) msg += ` at ${err.location.start.offset + 1}` + if (err.location) msg += ` at ${(err.location.start.offset as number) + 1}` log.error('prefs: key format error:', msg) } @@ -274,7 +282,8 @@ export = new class PrefPane { try { Formatter.parsePattern(this.keyformat.value) Prefs.set('citekeyFormat', this.keyformat.value) - } catch (error) { + } + catch (error) { // restore previous value log.error('prefs: error saving new citekey format', this.keyformat.value, 'restoring previous') this.getCitekeyFormat() @@ -290,7 +299,8 @@ export = new class PrefPane { try { // don't care about the return value, just if it throws an error new Function(postscript.value) // eslint-disable-line @typescript-eslint/no-unused-expressions - } catch (err) { + } + catch (err) { log.error('PrefPane.checkPostscript: error compiling postscript:', err) error = `${err}` } @@ -368,7 +378,9 @@ export = new class PrefPane { if (document.getElementsByTagName('prefwindow')[0].currentPane.helpTopic === 'BetterBibTeX') { const id = document.getElementById('better-bibtex-prefs-tabbox').selectedPanel.id if (id) this.openURL(`https://retorque.re/zotero-better-bibtex/configuration/#${id.replace('better-bibtex-prefs-', '')}`) - } else { + } + else { + // eslint-disable-next-line prefer-rest-params original.apply(this, arguments) } }) @@ -378,14 +390,14 @@ export = new class PrefPane { if (document.location.hash === '#better-bibtex') { // runs into the 'TypeError: aId is undefined' problem for some reason unless I delay the activation of the pane - // eslint-disable-next-line no-magic-numbers + // eslint-disable-next-line no-magic-numbers, @typescript-eslint/no-unsafe-return Zotero.setTimeout(() => document.getElementById('zotero-prefs').showPane(document.getElementById('zotero-prefpane-better-bibtex')), 500) } window.sizeToContent() // no other way that I know of to know that I've just been selected - this.timer = this.timer || window.setInterval(this.refresh.bind(this), 500) as any // eslint-disable-line no-magic-numbers + this.timer = this.timer || window.setInterval(this.refresh.bind(this), 500) // eslint-disable-line no-magic-numbers } private refresh() { @@ -415,7 +427,7 @@ export = new class PrefPane { if (client === 'jurism') { Zotero.Styles.init().then(() => { - const styles = Zotero.Styles.getVisible().filter(style => style.usesAbbreviation) + const styles = Zotero.Styles.getVisible().filter((style: { usesAbbreviation: boolean }) => style.usesAbbreviation) const stylebox = document.getElementById('better-bibtex-abbrev-style-popup') const refill = stylebox.children.length === 0 diff --git a/content/TestSupport.ts b/content/TestSupport.ts index 528433cbec..7d4b48befc 100644 --- a/content/TestSupport.ts +++ b/content/TestSupport.ts @@ -18,7 +18,7 @@ import { upgrade } from './db/upgrade' import * as pref_defaults from '../gen/preferences/defaults.json' export = new class { - public async removeAutoExports() { + public removeAutoExports() { AutoExport.db.findAndRemove({ type: { $ne: '' } }) } @@ -32,17 +32,14 @@ export = new class { Cache.reset() let collections - log.debug('TestSupport.reset: start') const prefix = 'translators.better-bibtex.' for (const [pref, value] of Object.entries(pref_defaults)) { if (pref === 'testing') continue Zotero.Prefs.set(prefix + pref, value) } - Zotero.Prefs.set(prefix + 'testing', true) - log.debug('TestSupport.reset: preferences reset') + Zotero.Prefs.set(`${prefix}testing`, true) - log.debug('TestSupport.reset: removing collections') // remove collections before items to work around https://github.com/zotero/zotero/issues/1317 and https://github.com/zotero/zotero/issues/1314 // ^%&^%@#&^% you can't just loop and erase because subcollections are also deleted while ((collections = Zotero.Collections.getByLibrary(Zotero.Libraries.userLibraryID, true) || []).length) { @@ -55,24 +52,20 @@ export = new class { while (items.length) { // eslint-disable-next-line no-magic-numbers const chunk = items.splice(0, 100) - log.debug('TestSupport.reset: deleting', chunk.length, 'items') await Zotero.Items.erase(chunk) } - log.debug('TestSupport.reset: empty trash') await Zotero.Items.emptyTrash(Zotero.Libraries.userLibraryID) - log.debug('TestSupport.reset: remove auto-exports') AutoExport.db.findAndRemove({ type: { $ne: '' } }) - log.debug('TestSupport.reset: done') items = await Zotero.Items.getAll(Zotero.Libraries.userLibraryID, false, true, true) if (items.length !== 0) throw new Error('library not empty after reset') } - public async librarySize() { - const itemIDs = await Zotero.Items.getAll(Zotero.Libraries.userLibraryID, true, false, true) + public async librarySize(): Promise { + const itemIDs: number[] = await Zotero.Items.getAll(Zotero.Libraries.userLibraryID, true, false, true) return itemIDs.length } @@ -82,14 +75,13 @@ export = new class { preferences = preferences || {} if (Object.keys(preferences).length) { - log.debug(`importing references and preferences from ${path}`) for (let [pref, value] of Object.entries(preferences)) { - log.debug(`${typeof pref_defaults[pref] === 'undefined' ? 'not ' : ''}setting preference ${pref} to ${value}`) if (typeof pref_defaults[pref] === 'undefined') throw new Error(`Unsupported preference ${pref} in test case`) if (Array.isArray(value)) value = value.join(',') Zotero.Prefs.set(`translators.better-bibtex.${pref}`, value) } - } else { + } + else { log.debug(`importing references from ${path}`) } @@ -104,7 +96,8 @@ export = new class { await AUXScanner.scan(path) // for some reason, the imported collection shows up as empty right after the import >: await sleep(1500) // eslint-disable-line no-magic-numbers - } else { + } + else { await Zotero_File_Interface.importFile({ file: Zotero.File.pathToFile(path), createNewCollection: !!createNewCollection }) } log.debug(`import finished at ${new Date()}`) @@ -118,7 +111,7 @@ export = new class { return (after - before) } - public async exportLibrary(translatorID, displayOptions, path, collection) { + public async exportLibrary(translatorID, displayOptions, path, collection): Promise { let scope log.debug('TestSupport.exportLibrary', { translatorID, displayOptions, path, collection }) if (collection) { @@ -129,7 +122,8 @@ export = new class { } log.debug('TestSupport.exportLibrary', { name, scope }) if (!scope) throw new Error(`Collection '${name}' not found`) - } else { + } + else { scope = null } return await Translators.exportItems(translatorID, displayOptions, scope, path) @@ -148,7 +142,8 @@ export = new class { let selected try { selected = zoteroPane.getSelectedItems(true) - } catch (err) { // zoteroPane.getSelectedItems() doesn't test whether there's a selection and errors out if not + } + catch (err) { // zoteroPane.getSelectedItems() doesn't test whether there's a selection and errors out if not log.error('Could not get selected items:', err) selected = [] } @@ -166,7 +161,7 @@ export = new class { let ids: number[] = [] - if (query.is) ids = ids.concat(KeyManager.keys.find({ citekey: query.is }).map(item => item.itemID)) + if (query.is) ids = ids.concat(KeyManager.keys.find({ citekey: query.is }).map((item: { itemID: number }) => item.itemID)) const s = new Zotero.Search() for (const [mode, text] of Object.entries(query)) { @@ -182,9 +177,11 @@ export = new class { public async pick(format, citations) { for (const citation of citations) { - citation.citekey = KeyManager.get(citation.id).citekey - citation.uri = Zotero.URI.getItemURI(await getItemsAsync(citation.id)) + if (citation.id.length !== 1) throw new Error(`Expected 1 item, got ${citation.id.length}`) + citation.citekey = KeyManager.get(citation.id[0]).citekey + citation.uri = Zotero.URI.getItemURI(await getItemsAsync(citation.id[0])) } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return await CAYWFormatter[format](citations, {}) } @@ -192,7 +189,8 @@ export = new class { let ids if (typeof itemID === 'number') { ids = [itemID] - } else { + } + else { ids = [] const items = await ZoteroDB.queryAsync(` SELECT item.itemID diff --git a/content/ZoteroPane.ts b/content/ZoteroPane.ts index 5d3804c392..4cccf96f65 100644 --- a/content/ZoteroPane.ts +++ b/content/ZoteroPane.ts @@ -16,6 +16,7 @@ import * as CAYW from './cayw' const pane = Zotero.getActiveZoteroPane() $patch$(pane, 'buildCollectionContextMenu', original => async function() { + // eslint-disable-next-line prefer-rest-params await original.apply(this, arguments) try { @@ -31,20 +32,20 @@ $patch$(pane, 'buildCollectionContextMenu', original => async function() { if (isLibrary) { tagDuplicates.hidden = false tagDuplicates.setAttribute('libraryID', treeRow.ref.libraryID.toString()) - } else { + } + else { tagDuplicates.hidden = true } let query = null if (Prefs.get('autoExport') === 'immediate') { query = null - - } else if (isCollection) { + } + else if (isCollection) { query = { type: 'collection', id: treeRow.ref.id } - - } else if (isLibrary) { + } + else if (isLibrary) { query = { type: 'library', id: treeRow.ref.libraryID } - } const auto_exports = query ? AutoExport.db.find(query) : [] @@ -61,13 +62,15 @@ $patch$(pane, 'buildCollectionContextMenu', original => async function() { } } - } catch (err) { + } + catch (err) { log.error('ZoteroPane.buildCollectionContextMenu:', err) } }) // Monkey patch because of https://groups.google.com/forum/#!topic/zotero-dev/zy2fSO1b0aQ $patch$(pane, 'serializePersist', original => function() { + // eslint-disable-next-line prefer-rest-params original.apply(this, arguments) if (Zotero.BetterBibTeX.uninstalled) clean_pane_persist() }) @@ -75,12 +78,13 @@ $patch$(pane, 'serializePersist', original => function() { export = new class ZoteroPane { public constructor() { window.addEventListener('load', () => { - BetterBibTeX.load(document).then(() => { - log.debug('Better BibTeX load finished successfully') - }) - .catch(err => { - log.error('Better BibTeX load failed', err) - }) + BetterBibTeX.load(document) + .then(() => { + log.debug('Better BibTeX load finished successfully') + }) + .catch(err => { + log.error('Better BibTeX load failed', err) + }) }, false) } @@ -127,7 +131,8 @@ export = new class ZoteroPane { if (ae) { AutoExport.run(ae.$loki) - } else { + } + else { log.error('cannot find ae for', { path }) } } @@ -140,7 +145,7 @@ export = new class ZoteroPane { } const extra = items[0].getField('extra') || '' - const citations = new Set(extra.split('\n').filter(line => line.startsWith('cites:'))) + const citations = new Set(extra.split('\n').filter((line: string) => line.startsWith('cites:'))) const picked = (await CAYW.pick({ format: 'citationLinks' })).split('\n').filter(citation => !citations.has(citation)) if (picked.length) { @@ -165,7 +170,8 @@ export = new class ZoteroPane { case 'items': try { scope = { type: 'items', items: pane.getSelectedItems() } - } catch (err) { // zoteroPane.getSelectedItems() doesn't test whether there's a selection and errors out if not + } + catch (err) { // zoteroPane.getSelectedItems() doesn't test whether there's a selection and errors out if not log.error('Could not get selected items:', err) scope = {} } @@ -180,7 +186,7 @@ export = new class ZoteroPane { ww.openWindow(null, 'chrome://zotero-better-bibtex/content/ErrorReport.xul', 'better-bibtex-error-report', 'chrome,centerscreen,modal', params) } - public async sentenceCase(label) { + public async sentenceCase() { const items = Zotero.getActiveZoteroPane().getSelectedItems() for (const item of items) { let save = false diff --git a/content/arXiv.ts b/content/arXiv.ts index 6a3553d22c..455ccd1805 100644 --- a/content/arXiv.ts +++ b/content/arXiv.ts @@ -1,6 +1,6 @@ // export singleton: https://k94n.com/es6-modules-single-instance-pattern -export let arXiv = new class { +export const arXiv = new class { // new-style IDs // arXiv:0707.3168 [hep-th] // arXiv:YYMM.NNNNv# [category] diff --git a/content/auto-export.ts b/content/auto-export.ts index e817afb638..f557413088 100644 --- a/content/auto-export.ts +++ b/content/auto-export.ts @@ -49,13 +49,15 @@ class Git { case 'always': try { repo.path = OS.Path.dirname(bib) - } catch (err) { + } + catch (err) { log.error('git.repo:', err) return repo } break case 'config': + // eslint-disable-next-line no-case-declarations let config = null for (let root = OS.Path.dirname(bib); (await OS.File.exists(root)) && (await OS.File.stat(root)).isDir && root !== OS.Path.dirname(root); root = OS.Path.dirname(root)) { config = OS.Path.join(root, '.git') @@ -73,7 +75,8 @@ class Git { try { const enabled = ini.parse(Zotero.File.getContents(config))['zotero "betterbibtex"']?.push if (enabled !== 'true' && enabled !== true) return repo - } catch (err) { + } + catch (err) { log.error('git.repo: error parsing config', config.path, err) return repo } @@ -98,7 +101,8 @@ class Git { try { await this.exec(this.git, ['-C', this.path, 'pull']) - } catch (err) { + } + catch (err) { log.error(`could not pull in ${this.path}:`, err) this.enabled = false } @@ -111,13 +115,14 @@ class Git { await this.exec(this.git, ['-C', this.path, 'add', this.bib]) await this.exec(this.git, ['-C', this.path, 'commit', '-m', msg]) await this.exec(this.git, ['-C', this.path, 'push']) - } catch (err) { + } + catch (err) { log.error(`could not push ${this.bib} in ${this.path}`, err) this.enabled = false } } - private async exec(cmd, args) { + private async exec(cmd, args): Promise { if (typeof cmd === 'string') cmd = new FileUtils.File(cmd) if (!cmd.isExecutable()) throw new Error(`${cmd.path} is not an executable`) @@ -130,14 +135,16 @@ class Git { proc.runwAsync(args, args.length, { observe: function(subject, topic) { // eslint-disable-line object-shorthand, prefer-arrow/prefer-arrow-functions if (topic !== 'process-finished') { deferred.reject(new Error(`${cmd.path} failed`)) - } else if (proc.exitValue !== 0) { + } + else if (proc.exitValue !== 0) { deferred.reject(new Error(`${cmd.path} returned exit status ${proc.exitValue}`)) - } else { + } + else { deferred.resolve(true) } }}) - return deferred.promise + return (deferred.promise as Promise) } } const git = new Git() @@ -238,17 +245,18 @@ const queue = new class TaskQueue { const root = scope.type === 'collection' ? scope.collection : false const dir = OS.Path.dirname(ae.path) - const base = OS.Path.basename(ae.path).replace(new RegExp(ext.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$'), '') + const base = OS.Path.basename(ae.path).replace(new RegExp(`${ext.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')}$`), '') - const autoExportPathReplaceDiacritics = Prefs.get('autoExportPathReplaceDiacritics') - const autoExportPathReplaceDirSep = Prefs.get('autoExportPathReplaceDirSep') - const autoExportPathReplaceSpace = Prefs.get('autoExportPathReplaceSpace') + const autoExportPathReplaceDiacritics: boolean = Prefs.get('autoExportPathReplaceDiacritics') + const autoExportPathReplaceDirSep: string = Prefs.get('autoExportPathReplaceDirSep') + const autoExportPathReplaceSpace: string = Prefs.get('autoExportPathReplaceSpace') for (const collection of collections) { const path = OS.Path.join(dir, [base] .concat(this.getCollectionPath(collection, root)) - .map(p => p.replace(/[<>:'"\/\\\|\?\*\u0000-\u001F]/g, '')) - .map(p => p.replace(/ +/g, Prefs.get(autoExportPathReplaceSpace) || '')) - .map(p => autoExportPathReplaceDiacritics ? foldMaintaining(p) : p) + // eslint-disable-next-line no-control-regex + .map((p: string) => p.replace(/[<>:'"/\\|?*\u0000-\u001F]/g, '')) + .map((p: string) => p.replace(/ +/g, Prefs.get(autoExportPathReplaceSpace) || '')) + .map((p: string) => autoExportPathReplaceDiacritics ? (foldMaintaining(p) as string) : p) .join(autoExportPathReplaceDirSep || '-') + ext ) jobs.push({ scope: { type: 'collection', collection: collection.id }, path } ) @@ -260,7 +268,8 @@ const queue = new class TaskQueue { await repo.push(Zotero.BetterBibTeX.getString('Preferences.auto-export.git.message', { type: Translators.byId[ae.translatorID].label.replace('Better ', '') })) ae.error = '' - } catch (err) { + } + catch (err) { log.error('AutoExport.queue.run: failed', ae, err) ae.error = `${err}` } @@ -269,14 +278,14 @@ const queue = new class TaskQueue { this.autoexports.update(ae) } - private getCollectionPath(coll, root) { - let path = [ coll.name ] + private getCollectionPath(coll: {name: string, parentID: number}, root: number): string[] { + let path: string[] = [ coll.name ] if (coll.parentID && coll.parentID !== root) path = this.getCollectionPath(Zotero.Collections.get(coll.parentID), root).concat(path) return path } // idle observer - protected observe(subject, topic, data) { + protected observe(_subject, topic, _data) { if (!this.started || Prefs.get('autoExport') === 'off') return switch (topic) { @@ -330,7 +339,7 @@ Events.on('preference-changed', pref => { }) // export singleton: https://k94n.com/es6-modules-single-instance-pattern -export let AutoExport = new class CAutoExport { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match +export const AutoExport = new class CAutoExport { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match public db: any constructor() { @@ -366,6 +375,8 @@ export let AutoExport = new class CAutoExport { // eslint-disable-line @typescri git.repo(ae.path).then(repo => { if (repo.enabled || schedule) this.schedule(ae.type, [ae.id]) // causes initial push to overleaf at the cost of a unnecesary extra export + }).catch(err => { + log.error('AutoExport.add:', err) }) } @@ -383,6 +394,6 @@ export let AutoExport = new class CAutoExport { // eslint-disable-line @typescri } public run(id) { - queue.run(id) + queue.run(id).catch(err => log.error('AutoExport.run:', err)) } } diff --git a/content/aux-scanner.ts b/content/aux-scanner.ts index fed7ba21fc..24e58a9f05 100644 --- a/content/aux-scanner.ts +++ b/content/aux-scanner.ts @@ -9,14 +9,16 @@ import { KeyManager } from './key-manager' import { Translators } from './translators' import { Preferences as Prefs } from './prefs' -export let AUXScanner = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match +export const AUXScanner = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match private decoder = new TextDecoder + // eslint-disable-next-line @typescript-eslint/require-await public async pick(): Promise { const fp = Components.classes['@mozilla.org/filepicker;1'].createInstance(Components.interfaces.nsIFilePicker) fp.init(window, Zotero.getString('fileInterface.import'), Components.interfaces.nsIFilePicker.modeOpen) fp.appendFilter('AUX file', '*.aux') + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return new Zotero.Promise(resolve => { fp.open(userChoice => { switch (userChoice) { @@ -46,16 +48,18 @@ export let AUXScanner = new class { // eslint-disable-line @typescript-eslint/na if (typeof options.libraryID === 'number') { collection = null libraryID = options.libraryID - } else if (options.collection) { + } + else if (options.collection) { collection = Zotero.Collections.getByLibraryAndKey(options.collection.libraryID, options.collection.key) libraryID = options.collection.libraryID - } else { + } + else { const azp = Zotero.getActiveZoteroPane() collection = azp.getSelectedCollection() libraryID = collection ? collection.libraryID : azp.getSelectedLibraryID() } - const found = KeyManager.keys.find({ libraryID, citekey: { $in: citekeys } }) + const found = (KeyManager.keys.find({ libraryID, citekey: { $in: citekeys } }) as { itemID: number, citekey: string }[]) const itemIDs = found.map(key => key.itemID) @@ -67,7 +71,7 @@ export let AUXScanner = new class { // eslint-disable-line @typescript-eslint/na if (missing.length) { const bibtex = Object.values(bibfiles).join('\n').trim() newImports = bibtex ? await Translators.importString(`@comment{zotero-better-bibtex:whitelist:${missing.join(',')}}\n${bibtex}`) : [] - itemIDs.push(...(newImports.map(item => item.id))) + itemIDs.push(...(newImports.map((item: { id: number}) => item.id))) missing = [] } } @@ -75,12 +79,15 @@ export let AUXScanner = new class { // eslint-disable-line @typescript-eslint/na const basename = OS.Path.basename(path).replace(/\.[^.]*$/, '') if (options.tag) { await this.saveToTag(itemIDs, options.tag, libraryID) - } else { + } + else { if (collection && (options.collection?.replace || !collection.hasChildItems())) { await this.saveToCollection(itemIDs, missing, { collection }) - } else if (collection) { + } + else if (collection) { await this.saveToCollection(itemIDs, missing, { collection, basename }) - } else { + } + else { await this.saveToCollection(itemIDs, missing, { libraryID, basename }) } } @@ -100,7 +107,7 @@ export let AUXScanner = new class { // eslint-disable-line @typescript-eslint/na // bib files used re = /\\bibdata\{([^}]+)\}/g while (m = re.exec(contents)) { - for (const bib of [m[1], m[1] + '.bib']) { + for (const bib of [m[1], `${m[1]}.bib`]) { if (!bibfiles[bib] && await OS.File.exists(bib)) { bibfiles[bib] = await this.read(bib) break @@ -127,14 +134,20 @@ export let AUXScanner = new class { // eslint-disable-line @typescript-eslint/na if (typeof target.libraryID === 'number') { if (target.collection) throw new Error('cannot have both collection and library target') if (!target.basename) throw new Error('Saving to library needs a name') - } else if (!target.collection) { + } + else if (!target.collection) { throw new Error('need either library + name or collection') } const libraryID = typeof target.libraryID === 'number' ? target.libraryID : target.collection.libraryID if (target.basename) { - const siblings = new Set((target.collection ? Zotero.Collections.getByParent(target.collection.id) : Zotero.Collections.getByLibrary(target.libraryID)).map(coll => coll.name)) + const siblings = new Set( + (target.collection + ? Zotero.Collections.getByParent(target.collection.id) + : Zotero.Collections.getByLibrary(target.libraryID) + ).map((coll: { name: string }) => coll.name) + ) let timestamp = '' @@ -150,7 +163,8 @@ export let AUXScanner = new class { // eslint-disable-line @typescript-eslint/na }) await target.collection.saveTx() - } else { + } + else { // saving into existing collection, remove items that are not cited const obsolete = target.collection.getChildItems(true).filter(itemID => !itemIDs.includes(itemID)) if (obsolete.length) await Zotero.DB.executeTransaction(async () => { await target.collection.removeItems(obsolete) }) @@ -158,7 +172,8 @@ export let AUXScanner = new class { // eslint-disable-line @typescript-eslint/na } if (missing_keys.length) { - missing_keys.sort((new Intl.Collator('en')).compare) + const collator = new Intl.Collator('en') + missing_keys.sort(collator.compare.bind(collator)) let report = '

BibTeX AUX scan

Missing references:

    ' for (const citekey of missing_keys) { report += `
  • ${citekey.replace(/&/g, '&').replace(//g, '>').replace(/'/g, '"').replace(/'/g, ''')}
  • ` @@ -175,7 +190,7 @@ export let AUXScanner = new class { // eslint-disable-line @typescript-eslint/na if (itemIDs.length) await Zotero.DB.executeTransaction(async () => { await target.collection.addItems(itemIDs) }) } - private async saveToTag(cited: number[], tag: string, libraryID: number) { + private async saveToTag(cited: number[], tag: string, _libraryID: number) { const tagged: number[] = await Zotero.DB.columnQueryAsync('SELECT itemID FROM itemTags JOIN tags ON tags.tagID = itemTags.tagID WHERE LOWER(tags.name) = LOWER(?)', [tag]) // cited but not tagged diff --git a/content/better-bibtex.ts b/content/better-bibtex.ts index 99b70eeee4..628f985032 100644 --- a/content/better-bibtex.ts +++ b/content/better-bibtex.ts @@ -1,3 +1,4 @@ +/* eslint-disable prefer-rest-params */ declare const Components: any declare const Zotero: any @@ -33,33 +34,37 @@ import format = require('string-template') // UNINSTALL AddonManager.addAddonListener({ - onUninstalling(addon, needsRestart) { + // eslint-disable-next-line prefer-arrow/prefer-arrow-functions + onUninstalling(addon: { id: string }, _needsRestart: any) { if (addon.id !== 'better-bibtex@iris-advies.com') return null clean_pane_persist() const quickCopy = Zotero.Prefs.get('export.quickCopy.setting') - for (const [label, metadata] of (Object.entries(Translators.byName) as [string, ITranslatorHeader][])) { + for (const [label, metadata] of (Object.entries(Translators.byName) )) { if (quickCopy === `export=${metadata.translatorID}`) Zotero.Prefs.clear('export.quickCopy.setting') try { Translators.uninstall(label) - } catch (error) {} + } + catch (error) {} } Zotero.BetterBibTeX.uninstalled = true }, - onDisabling(addon, needsRestart) { this.onUninstalling(addon, needsRestart) }, + onDisabling(addon: any, needsRestart: any) { this.onUninstalling(addon, needsRestart) }, - onOperationCancelled(addon, needsRestart) { + // eslint-disable-next-line prefer-arrow/prefer-arrow-functions + async onOperationCancelled(addon: { id: string, pendingOperations: number }, _needsRestart: any) { if (addon.id !== 'better-bibtex@iris-advies.com') return null // eslint-disable-next-line no-bitwise if (addon.pendingOperations & (AddonManager.PENDING_UNINSTALL | AddonManager.PENDING_DISABLE)) return null for (const header of Object.values(Translators.byId)) { try { - Translators.install(header) - } catch (err) { + await Translators.install(header) + } + catch (err) { log.error(err) } } @@ -73,24 +78,26 @@ AddonManager.addAddonListener({ */ if (Prefs.get('citeprocNoteCitekey')) { - $patch$(Zotero.Utilities, 'itemToCSLJSON', original => function itemToCSLJSON(zoteroItem) { + $patch$(Zotero.Utilities, 'itemToCSLJSON', original => function itemToCSLJSON(zoteroItem: { itemID: any }) { const cslItem = original.apply(this, arguments) if (typeof Zotero.Item !== 'undefined' && !(zoteroItem instanceof Zotero.Item)) { const citekey = KeyManager.get(zoteroItem.itemID) if (citekey) { cslItem.note = citekey.citekey - } else { + } + else { delete cslItem.note } } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return cslItem }) } // https://github.com/retorquere/zotero-better-bibtex/issues/1221 -$patch$(Zotero.Items, 'merge', original => async function Zotero_Items_merge(item, otherItems) { +$patch$(Zotero.Items, 'merge', original => async function Zotero_Items_merge(item: { getField: (arg0: string) => string, id: any, setField: (arg0: string, arg1: string) => void }, otherItems: any[]) { try { const merge = { citekeys: Prefs.get('extraMergeCitekeys'), @@ -103,8 +110,9 @@ $patch$(Zotero.Items, 'merge', original => async function Zotero_Items_merge(ite // get citekeys of other items if (merge.citekeys) { - const otherIDs = otherItems.map(i => parseInt(i.id)) - extra.extraFields.aliases = extra.extraFields.aliases.concat(KeyManager.keys.find({ itemID: { $in: otherIDs }}).map(i => i.citekey)) + const otherIDs = otherItems.map((i: { id: string }) => parseInt(i.id)) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + extra.extraFields.aliases = extra.extraFields.aliases.concat(KeyManager.keys.find({ itemID: { $in: otherIDs }}).map((i: { citekey: any }) => i.citekey)) } // add any aliases they were already holding @@ -127,7 +135,8 @@ $patch$(Zotero.Items, 'merge', original => async function Zotero_Items_merge(ite const existing = extra.extraFields.kv[name] if (!existing) { extra.extraFields.kv[name] = value - } else if (Array.isArray(existing) && Array.isArray(value)) { + } + else if (Array.isArray(existing) && Array.isArray(value)) { for (const creator in value) { if (!existing.includes(creator)) existing.push(creator) } @@ -150,82 +159,91 @@ $patch$(Zotero.Items, 'merge', original => async function Zotero_Items_merge(ite })) log.debug('bbt merge: extra-field:', item.getField('extra')) - - } catch (err) { + } + catch (err) { log.error('Zotero.Items.merge:', err) } - return original.apply(this, arguments) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return await original.apply(this, arguments) }) // https://github.com/retorquere/zotero-better-bibtex/issues/769 -$patch$(Zotero.DataObjects.prototype, 'parseLibraryKeyHash', original => function Zotero_DataObjects_prototype_parseLibraryKeyHash(id) { +$patch$(Zotero.DataObjects.prototype, 'parseLibraryKeyHash', original => function Zotero_DataObjects_prototype_parseLibraryKeyHash(id: string) { try { - const _id = decodeURIComponent(id) - if (_id[0] === '@') { - const item = KeyManager.keys.findOne({ citekey: _id.substring(1) }) + const decoded_id = decodeURIComponent(id) + if (decoded_id[0] === '@') { + const item = KeyManager.keys.findOne({ citekey: decoded_id.substring(1) }) if (item) return { libraryID: item.libraryID, key: item.itemKey } } - const m = _id.match(/^bbt:(?:{([0-9]+)})?(.*)/) + const m = decoded_id.match(/^bbt:(?:{([0-9]+)})?(.*)/) if (m) { const [_libraryID, citekey] = m.slice(1) const libraryID: number = (!_libraryID || _libraryID === '1') ? Zotero.Libraries.userLibraryID : parseInt(_libraryID) const item = KeyManager.keys.findOne({ libraryID, citekey }) if (item) return { libraryID: item.libraryID, key: item.itemKey } } - } catch (err) { + } + catch (err) { log.error('parseLibraryKeyHash:', id, err) } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return original.apply(this, arguments) }) // otherwise the display of the citekey in the item pane flames out -$patch$(Zotero.ItemFields, 'isFieldOfBase', original => function Zotero_ItemFields_isFieldOfBase(field, baseField) { +$patch$(Zotero.ItemFields, 'isFieldOfBase', original => function Zotero_ItemFields_isFieldOfBase(field: string, _baseField: any) { if (['citekey', 'itemID'].includes(field)) return false + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return original.apply(this, arguments) }) // because the zotero item editor does not check whether a textbox is read-only. *sigh* -$patch$(Zotero.Item.prototype, 'setField', original => function Zotero_Item_prototype_setField(field, value, loadIn) { +$patch$(Zotero.Item.prototype, 'setField', original => function Zotero_Item_prototype_setField(field: string, _value: any, _loadIn: any) { if (['citekey', 'itemID'].includes(field)) return false + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return original.apply(this, arguments) }) // To show the citekey in the reference list -$patch$(Zotero.Item.prototype, 'getField', original => function Zotero_Item_prototype_getField(field, unformatted, includeBaseMapped) { +$patch$(Zotero.Item.prototype, 'getField', original => function Zotero_Item_prototype_getField(field: any, unformatted: any, includeBaseMapped: any) { try { switch (field) { case 'citekey': if (BetterBibTeX.ready.isPending()) return '' // eslint-disable-line @typescript-eslint/no-use-before-define - const citekey = KeyManager.get(this.id) - return citekey.citekey + return KeyManager.get(this.id).citekey case 'itemID': return `${this.id}` } - } catch (err) { + } + catch (err) { log.error('patched getField:', {field, unformatted, includeBaseMapped, err}) } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return original.apply(this, arguments) }) // #1579 -$patch$(Zotero.Item.prototype, 'clone', original => function Zotero_Item_prototype_clone(libraryID, options = {}) { +$patch$(Zotero.Item.prototype, 'clone', original => function Zotero_Item_prototype_clone(libraryID: any, options = {}) { const item = original.apply(this, arguments) try { - if (item.isRegularItem()) item.setField('extra', (item.getField('extra') || '').split('\n').filter(line => !(line.toLowerCase().startsWith('citation key:'))).join('\n')) - } catch (err) { + if (item.isRegularItem()) item.setField('extra', (item.getField('extra') || '').split('\n').filter((line: string) => !(line.toLowerCase().startsWith('citation key:'))).join('\n')) + } + catch (err) { log.error('patched clone:', {libraryID, options, err}) } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return item }) const itemTreeViewWaiting: Record = {} -$patch$(Zotero.ItemTreeView.prototype, 'getCellText', original => function Zotero_ItemTreeView_prototype_getCellText(row, col) { +$patch$(Zotero.ItemTreeView.prototype, 'getCellText', original => function Zotero_ItemTreeView_prototype_getCellText(row: any, col: { id: string }) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return if (col.id !== 'zotero-items-column-citekey') return original.apply(this, arguments) const item = this.getRow(row).ref @@ -234,6 +252,7 @@ $patch$(Zotero.ItemTreeView.prototype, 'getCellText', original => function Zoter if (BetterBibTeX.ready.isPending()) { // eslint-disable-line @typescript-eslint/no-use-before-define if (!itemTreeViewWaiting[item.id]) { // eslint-disable-next-line @typescript-eslint/no-use-before-define + // eslint-disable-next-line @typescript-eslint/no-unsafe-return BetterBibTeX.ready.then(() => this._treebox.invalidateCell(row, col)) itemTreeViewWaiting[item.id] = true } @@ -246,8 +265,9 @@ $patch$(Zotero.ItemTreeView.prototype, 'getCellText', original => function Zoter }) import * as CAYW from './cayw' -$patch$(Zotero.Integration, 'getApplication', original => function Zotero_Integration_getApplication(agent, command, docId) { +$patch$(Zotero.Integration, 'getApplication', original => function Zotero_Integration_getApplication(agent: string, _command: any, _docId: any) { if (agent === 'BetterBibTeX') return CAYW.Application + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return original.apply(this, arguments) }) @@ -257,17 +277,20 @@ import * as DateParser from './dateparser' import { qualityReport } from './qr-check' import { titleCase } from './case' import { HTMLParser } from './markupparser' +import { ParsedDate } from './typings/bbt' Zotero.Translate.Export.prototype.Sandbox.BetterBibTeX = { - qrCheck(sandbox, value, test, params = null) { return qualityReport(value, test, params) }, + qrCheck(_sandbox: any, value: string, test: string, params = null) { return qualityReport(value, test, params) }, - parseDate(sandbox, date) { return DateParser.parse(date, Zotero.BetterBibTeX.localeDateOrder) }, - getLocaleDateOrder(sandbox) { return Zotero.BetterBibTeX.localeDateOrder }, + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + parseDate(_sandbox: any, date: string): ParsedDate { return DateParser.parse(date, Zotero.BetterBibTeX.localeDateOrder) }, + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + getLocaleDateOrder(_sandbox: any): string { return Zotero.BetterBibTeX.localeDateOrder }, - isEDTF(sandbox, date, minuteLevelPrecision = false) { return DateParser.isEDTF(date, minuteLevelPrecision) }, + isEDTF(_sandbox: any, date: string, minuteLevelPrecision = false) { return DateParser.isEDTF(date, minuteLevelPrecision) }, - titleCase(sandbox, text) { return titleCase(text) }, - parseHTML(sandbox, text, options) { + titleCase(_sandbox: any, text: string): string { return titleCase(text) }, + parseHTML(_sandbox: any, text: { toString: () => any }, options: { html?: boolean, caseConversion?: boolean, exportBraceProtection: boolean, csquotes: string, exportTitleCase: boolean }) { options = { ...options, exportBraceProtection: Prefs.get('exportBraceProtection'), @@ -276,11 +299,11 @@ Zotero.Translate.Export.prototype.Sandbox.BetterBibTeX = { } return HTMLParser.parse(text.toString(), options) }, - // extractFields(sandbox, item) { return Extra.get(item.extra) }, - debugEnabled(sandbox) { return Zotero.Debug.enabled }, + // extractFields(_sandbox, item) { return Extra.get(item.extra) }, + debugEnabled(_sandbox: any): boolean { return (Zotero.Debug.enabled as boolean) }, - cacheFetch(sandbox, itemID, options, prefs) { - const collection = Cache.getCollection(sandbox.translator[0].label) + cacheFetch(_sandbox: { translator: { label: string }[] }, itemID: number, options: { exportNotes: boolean, useJournalAbbreviation: boolean }, prefs: any) { + const collection = Cache.getCollection(_sandbox.translator[0].label) if (!collection) return false const query = cacheSelector(itemID, options, prefs) @@ -300,10 +323,11 @@ Zotero.Translate.Export.prototype.Sandbox.BetterBibTeX = { collection.dirty = true // freeze object, because it was not fetched using clone + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return Object.freeze(cached) }, - cacheStore(sandbox, itemID, options, prefs, reference, metadata) { + cacheStore(sandbox: { translator: { label: string }[] }, itemID: number, options: { exportNotes: boolean, useJournalAbbreviation: boolean }, prefs: any, reference: any, metadata: any) { if (!metadata) metadata = {} const collection = Cache.getCollection(sandbox.translator[0].label) @@ -320,7 +344,8 @@ Zotero.Translate.Export.prototype.Sandbox.BetterBibTeX = { cached.metadata = metadata cached = collection.update(cached) - } else { + } + else { cached = collection.insert({...selector, reference, metadata}) } @@ -328,12 +353,12 @@ Zotero.Translate.Export.prototype.Sandbox.BetterBibTeX = { return true }, - strToISO(sandbox, str) { return DateParser.strToISO(str, Zotero.BetterBibTeX.localeDateOrder) }, + strToISO(_sandbox: any, str: string) { return DateParser.strToISO(str, Zotero.BetterBibTeX.localeDateOrder) }, } Zotero.Translate.Import.prototype.Sandbox.BetterBibTeX = { - debugEnabled(sandbox) { return Zotero.Debug.enabled }, - parseHTML(sandbox, text, options) { + debugEnabled(_sandbox: any): boolean { return (Zotero.Debug.enabled as boolean) }, + parseHTML(_sandbox: any, text: { toString: () => any }, options: { html?: boolean, caseConversion?: boolean, exportBraceProtection: boolean, csquotes: string, exportTitleCase: boolean }) { options = { ...options, exportBraceProtection: Prefs.get('exportBraceProtection'), @@ -342,21 +367,24 @@ Zotero.Translate.Import.prototype.Sandbox.BetterBibTeX = { } return HTMLParser.parse(text.toString(), options) }, - parseDate(sandbox, date) { return DateParser.parse(date, Zotero.BetterBibTeX.localeDateOrder) }, + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + parseDate(_sandbox: any, date: string): ParsedDate { return DateParser.parse(date, Zotero.BetterBibTeX.localeDateOrder) }, } -$patch$(Zotero.Utilities.Internal, 'itemToExportFormat', original => function Zotero_Utilities_Internal_itemToExportFormat(zoteroItem, legacy, skipChildItems) { +$patch$(Zotero.Utilities.Internal, 'itemToExportFormat', original => function Zotero_Utilities_Internal_itemToExportFormat(zoteroItem: any, _legacy: any, _skipChildItems: any) { const serialized = original.apply(this, arguments) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return Serializer.enrich(serialized, zoteroItem) }) // so BBT-JSON can be imported without extra-field meddling -$patch$(Zotero.Utilities.Internal, 'extractExtraFields', original => function Zotero_Utilities_Internal_extractExtraFields(extra, item, additionalFields) { +$patch$(Zotero.Utilities.Internal, 'extractExtraFields', original => function Zotero_Utilities_Internal_extractExtraFields(extra: string, _item: any, additionalFields: any) { if (extra && extra.startsWith('\x1BBBT\x1B')) { log.debug('bbt merge:extractExtraFields disabled:', JSON.stringify({ extra: extra.replace('\x1BBBT\x1B', ''), additionalFields })) return { itemType: null, fields: new Map(), creators: [], extra: extra.replace('\x1BBBT\x1B', '') } } log.debug('bbt merge:extractExtraFields:', JSON.stringify({ extra, additionalFields })) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return original.apply(this, arguments) }) @@ -372,7 +400,8 @@ $patch$(Zotero.Translate.Export.prototype, 'translate', original => function Zot if (this._displayOptions.exportFileData) { // when exporting file data, the user was asked to pick a directory rather than a file this._displayOptions.exportDir = this.location.path this._displayOptions.exportPath = OS.Path.join(this.location.path, `${this.location.leafName}.${translator.target}`) - } else { + } + else { this._displayOptions.exportDir = this.location.parent.path this._displayOptions.exportPath = this.location.path } @@ -386,7 +415,8 @@ $patch$(Zotero.Translate.Export.prototype, 'translate', original => function Zot // adding the literal 'Translator.exportDir' makes sure caching is disabled this._displayOptions.preference_postscript = `// postscript override in Translator.exportDir ${this._displayOptions.exportDir}\n\n${Zotero.File.getContents(postscript)}` } - } catch (err) { + } + catch (err) { log.error('failed to load postscript override', postscript, err) } } @@ -436,6 +466,7 @@ $patch$(Zotero.Translate.Export.prototype, 'translate', original => function Zot Translators.exportItemsByQueuedWorker(translatorID, this._displayOptions, { scope: { ...this._export, getter: this._itemGetter }, path }) .then(result => { + // eslint-disable-next-line id-blacklist this.string = result this.complete(result) }) @@ -446,10 +477,12 @@ $patch$(Zotero.Translate.Export.prototype, 'translate', original => function Zot return } } - } catch (err) { + } + catch (err) { log.error('Zotero.Translate.Export::translate error:', err) } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return original.apply(this, arguments) }) @@ -457,27 +490,29 @@ $patch$(Zotero.Translate.Export.prototype, 'translate', original => function Zot EVENTS */ -function notify(event, handler) { +function notify(event: string, handler: any) { Zotero.Notifier.registerObserver({ - notify(...args) { + // eslint-disable-next-line prefer-arrow/prefer-arrow-functions + notify(...args: any[]) { BetterBibTeX.ready.then(() => { // eslint-disable-line @typescript-eslint/no-use-before-define + // eslint-disable-next-line prefer-spread handler.apply(null, args) }) }, }, [event], 'BetterBibTeX', 1) } -notify('item-tag', (action, type, ids, extraData) => { - ids = ids.map(item_tag => parseInt(item_tag.split('-')[0])) +notify('item-tag', (_action: any, _type: any, ids: any[], _extraData: any) => { + ids = ids.map((item_tag: string) => parseInt(item_tag.split('-')[0])) Cache.remove(ids, `item ${ids} changed`) Events.emit('items-changed', ids) }) -notify('item', (action, type, ids, extraData) => { +notify('item', (action: string, type: any, ids: any[], extraData: { [x: string]: { bbtCitekeyUpdate: any } }) => { // prevents update loop -- see KeyManager.init() if (action === 'modify') { - ids = ids.filter(id => !extraData[id] || !extraData[id].bbtCitekeyUpdate) + ids = ids.filter((id: string | number) => !extraData[id] || !extraData[id].bbtCitekeyUpdate) if (!ids.length) return } @@ -486,7 +521,7 @@ notify('item', (action, type, ids, extraData) => { // safe to use Zotero.Items.get(...) rather than Zotero.Items.getAsync here // https://groups.google.com/forum/#!topic/zotero-dev/99wkhAk-jm0 const parents = [] - const items = action === 'delete' ? [] : Zotero.Items.get(ids).filter(item => { + const items = action === 'delete' ? [] : Zotero.Items.get(ids).filter((item: { isNote: () => boolean, isAttachment: () => boolean, parentID: number }) => { if (item.isNote() || item.isAttachment()) { if (typeof item.parentID !== 'boolean') parents.push(item.parentID) return false @@ -505,6 +540,7 @@ notify('item', (action, type, ids, extraData) => { case 'add': case 'modify': + // eslint-disable-next-line no-case-declarations let warn_titlecase = Prefs.get('warnTitleCased') ? 0 : null for (const item of items) { KeyManager.update(item) @@ -531,15 +567,15 @@ notify('item', (action, type, ids, extraData) => { notifyItemsChanged(items) }) -notify('collection', (event, type, ids, extraData) => { +notify('collection', (event: string, _type: any, ids: string | any[], _extraData: any) => { if ((event === 'delete') && ids.length) Events.emit('collections-removed', ids) }) -notify('group', (event, type, ids, extraData) => { +notify('group', (event: string, _type: any, ids: string | any[], _extraData: any) => { if ((event === 'delete') && ids.length) Events.emit('libraries-removed', ids) }) -notify('collection-item', (event, type, collection_items) => { +notify('collection-item', (_event: any, _type: any, collection_items: any) => { const changed = new Set() for (const collection_item of collection_items) { @@ -565,11 +601,12 @@ class Progress { private msg: string private progressWin: any private progress: any - private name: string = 'Startup progress' + private name = 'Startup progress' private timer: TimerHandle private waiting() { - function show(v) { + // eslint-disable-next-line prefer-arrow/prefer-arrow-functions + function show(v: any) { if (typeof v === 'undefined') return 'unset' return v ? 'pending' : 'resolved' } @@ -588,7 +625,7 @@ class Progress { } } - public async start(msg) { + public start(msg: string) { this.started = this.timestamp = Date.now() this.timer = setInterval(this.waiting.bind(this), 500) // eslint-disable-line no-magic-numbers @@ -601,7 +638,7 @@ class Progress { log.debug(`${this.name}: progress window up`) } - public update(msg) { + public update(msg: any) { this.bench(msg) log.debug(`${this.name}: ${msg}...`) @@ -616,7 +653,7 @@ class Progress { clearTimeout(this.timer) } - private bench(msg) { + private bench(msg: string) { const ts = Date.now() // eslint-disable-next-line no-magic-numbers if (this.msg) log.debug(`${this.name}:`, this.msg, 'took', (ts - this.timestamp) / 1000.0, 's') @@ -624,7 +661,7 @@ class Progress { this.timestamp = ts } - private toggle(busy) { + private toggle(busy: boolean) { if (busy) { this.progressWin = new Zotero.ProgressWindow({ closeOnClick: false }) this.progressWin.changeHeadline('Better BibTeX: Initializing') @@ -632,21 +669,22 @@ class Progress { const icon = `chrome://zotero/skin/treesource-unfiled${Zotero.hiDPI ? '@2x' : ''}.png` this.progress = new this.progressWin.ItemProgress(icon, `${this.msg}...`) this.progressWin.show() - } else { + } + else { this.progress.setText('Ready') this.progressWin.startCloseTimer(500) // eslint-disable-line no-magic-numbers } } } -export let BetterBibTeX = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match +export const BetterBibTeX = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match public localeDateOrder: string = Zotero.Date.getLocaleDateOrder() public ready: any public loaded: any public dir: string private strings: any - private firstRun: { citekeyFormat: String, dragndrop: boolean, unabbreviate: boolean, strings: boolean } + private firstRun: { citekeyFormat: string, dragndrop: boolean, unabbreviate: boolean, strings: boolean } private document: any public async load(document: any) { @@ -657,26 +695,27 @@ export let BetterBibTeX = new class { // eslint-disable-line @typescript-eslint/ if (!this.loaded) await this.init() } - public debugEnabled() { - return Zotero.Debug.enabled + public debugEnabled(): boolean { + return (Zotero.Debug.enabled as boolean) } - public getString(id, params = null) { + public getString(id: string, params: any = null): string { if (!this.strings || typeof this.strings.getString !== 'function') { log.error('getString called before strings were loaded', id) return id } try { - const str = this.strings.getString(id) - return params ? format(str, params) : str - } catch (err) { + const str: string = this.strings.getString(id) + return params ? (format(str, params) as string) : str + } + catch (err) { log.error('getString', id, err) return id } } - public async scanAUX(target) { + public async scanAUX(target: any) { if (!this.loaded) return await this.loaded @@ -689,10 +728,13 @@ export let BetterBibTeX = new class { // eslint-disable-line @typescript-eslint/ break case 'tag': + // eslint-disable-next-line no-case-declarations const ps = Components.classes['@mozilla.org/embedcomp/prompt-service;1'].getService(Components.interfaces.nsIPromptService) + // eslint-disable-next-line no-case-declarations let name = OS.Path.basename(aux) name = name.lastIndexOf('.') > 0 ? name.substr(0, name.lastIndexOf('.')) : name + // eslint-disable-next-line no-case-declarations const tag = { value: name } if (!ps.prompt(null, this.getString('BetterBibTeX.auxScan.title'), this.getString('BetterBibTeX.auxScan.prompt'), tag, null, {})) return if (!tag.value) return @@ -746,7 +788,8 @@ export let BetterBibTeX = new class { // eslint-disable-line @typescript-eslint/ Prefs.set('citekeyFormat', (this.firstRun.citekeyFormat === 'zotero') ? '[zotero:clean]' : citekeyFormat.substr(1)) Prefs.set('importJabRefAbbreviations', this.firstRun.unabbreviate) Prefs.set('importJabRefStrings', this.firstRun.strings) - } else { + } + else { this.firstRun = null } @@ -758,7 +801,7 @@ export let BetterBibTeX = new class { // eslint-disable-line @typescript-eslint/ ) } const progress = new Progress - await progress.start(this.getString('BetterBibTeX.startup.waitingForZotero')) + progress.start(this.getString('BetterBibTeX.startup.waitingForZotero')) // Zotero startup is a hot mess; https://groups.google.com/d/msg/zotero-dev/QYNGxqTSpaQ/uvGObVNlCgAJ // await (Zotero.isStandalone ? Zotero.uiReadyPromise : Zotero.initializationPromise) @@ -800,7 +843,7 @@ export let BetterBibTeX = new class { // eslint-disable-line @typescript-eslint/ await KeyManager.start() // inits the key cache by scanning the DB and generating missing keys progress.update(this.getString('BetterBibTeX.startup.autoExport')) - await AutoExport.start() + AutoExport.start() deferred.ready.resolve(true) diff --git a/content/case.ts b/content/case.ts index f0d4784f43..7014414923 100644 --- a/content/case.ts +++ b/content/case.ts @@ -1,12 +1,11 @@ +/* eslint-disable */ import * as CSL from '../gen/citeproc' -/* eslint-disable no-var, prefer-const, @typescript-eslint/semi,@typescript-eslint/member-delimiter-style, prefer-template, @typescript-eslint/quotes */ function makeRegExp(lst) { var lst = lst.slice(); var ret = new RegExp( "(?:(?:[?!:]*\\s+|-|^)(?:" + lst.join("|") + ")(?=[!?:]*\\s+|-|$))", "g"); return ret; } -/* eslint-enable */ class State { public opt = { lang: 'en' } @@ -22,19 +21,20 @@ class State { } } -export function titleCase(text) { +export function titleCase(text: string): string { return CSL.Output.Formatters.title(new State, text) } -export function sentenceCase(text) { +export function sentenceCase(text: string): string { let sentencecased = text.replace(/((?:^|[?!]|[-.:;\[\]<>'*\\(),{}_“”‘’])?\s*)([^-\s;?:.!\[\]<>'*\\(),{}_“”‘’]+)/g, (match, leader, word) => { if (leader && !leader.match(/^[?!]/) && word.match(/^[A-Z][^A-Z]*$/)) word = word.toLowerCase() return (leader || '') + word }) // restore protected parts from original - text.replace(/.*?<\/span>|.*?<\/nc>/gi, (match, offset) => { + text.replace(/.*?<\/span>|.*?<\/nc>/gi, (match: string, offset: number) => { sentencecased = sentencecased.substr(0, offset) + match + sentencecased.substr(offset + match.length) + return match }) return sentencecased diff --git a/content/cayw.ts b/content/cayw.ts index 7c424f65b3..b081211599 100644 --- a/content/cayw.ts +++ b/content/cayw.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-magic-numbers */ declare const Components: any declare const XPCOMUtils: any declare const Zotero: any @@ -66,6 +67,7 @@ class Field { /** * Sets the text inside this field to a specified plain text string or pseudo-RTF formatted text * string. + * * @param {String} text * @param {Boolean} isRich */ @@ -76,24 +78,28 @@ class Field { /** * Gets the text inside this field, preferably with formatting, but potentially without + * * @returns {String} */ public getText() { return this.text } /** * Sets field's code + * * @param {String} code */ public setCode(code) { this.code = code } /** * Gets field's code. + * * @returns {String} */ public getCode() { return this.code } /** * Returns true if this field and the passed field are actually references to the same field. + * * @param {Field} field * @returns {Boolean} */ @@ -101,17 +107,33 @@ class Field { /** * This field's note index, if it is in a footnote or endnote; otherwise zero. + * * @returns {Number} */ public getNoteIndex() { return 0 } } +type DocumentData = { +} +type Citation = { + id: number + locator: string + suppressAuthor: boolean + prefix: string + suffix: string + label: string + citekey: string + + uri: string + itemType: string + title: string +} /** * The Document class corresponds to a single word processing document. */ class Document { public fields: Field[] = [] - public data: any + public data: DocumentData public id: number constructor(docId, options) { @@ -129,21 +151,22 @@ class Document { } data.style = {styleID: options.style, locale: 'en-US', hasBibliography: true, bibliographyStyleHasBeenSet: true} data.sessionID = Zotero.Utilities.randomString(10) // eslint-disable-line no-magic-numbers - this.data = data.serialize() + this.data = (data.serialize() as DocumentData) } /** * Displays a dialog in the word processing application + * * @param {String} dialogText * @param {Number} icon - one of the constants defined in integration.js for dialog icons * @param {Number} buttons - one of the constants defined in integration.js for dialog buttons * @returns {Number} - * - Yes: 2, No: 1, Cancel: 0 - * - Yes: 1, No: 0 - * - Ok: 1, Cancel: 0 - * - Ok: 0 + * - Yes: 2, No: 1, Cancel: 0 + * - Yes: 1, No: 0 + * - Ok: 1, Cancel: 0 + * - Ok: 0 */ - public displayAlert(dialogText, icon, buttons) { return 0 } + public displayAlert(_dialogText, _icon, _buttons) { return 0 } /** * Brings this document to the foreground (if necessary to return after displaying a dialog) @@ -152,32 +175,37 @@ class Document { /** * Determines whether a field can be inserted at the current position. + * * @param {String} fieldType * @returns {Boolean} */ - public canInsertField(fieldType) { return true } + public canInsertField(_fieldType) { return true } /** * Returns the field in which the cursor resides, or NULL if none. + * * @param {String} fieldType * @returns {Boolean} */ - public cursorInField(fieldType) { return false } + public cursorInField(_fieldType) { return false } /** * Get document data property from the current document + * * @returns {String} */ public getDocumentData() { return this.data } /** * Set document data property + * * @param {String} data */ public setDocumentData(data) { this.data = data } /** * Inserts a field at the given position and initializes the field object. + * * @param {String} fieldType * @param {Integer} noteType * @returns {Field} @@ -193,16 +221,18 @@ class Document { /** * Gets all fields present in the document. + * * @param {String} fieldType * @returns {FieldEnumerator} */ - public getFields(fieldType) { return new FieldEnumerator(this) } + public getFields(_fieldType) { return new FieldEnumerator(this) } /** * Gets all fields present in the document. The observer will receive notifications for two * topics: "fields-progress", with the document as the subject and percent progress as data, and * "fields-available", with an nsISimpleEnumerator of fields as the subject and the length as * data + * * @param {String} fieldType * @param {nsIObserver} observer */ @@ -213,13 +243,14 @@ class Document { /** * Sets the bibliography style, overwriting the current values for this document */ - public setBibliographyStyle(firstLineIndent, bodyIndent, lineSpacing, entrySpacing, tabStops, tabStopsCount) { return 0 } + public setBibliographyStyle(_firstLineIndent, _bodyIndent, _lineSpacing, _entrySpacing, _tabStops, _tabStopsCount) { return 0 } /** * Converts all fields in a document to a different fieldType or noteType + * * @params {FieldEnumerator} fields */ - public convert(fields, toFieldType, toNoteType, count) { return 0 } + public convert(_fields, _toFieldType, _toNoteType, _count) { return 0 } /** * Cleans up the document state and resumes processor for editing @@ -234,44 +265,46 @@ class Document { /** * Gets the citation */ - public citation() { + public citation(): Citation[] { if (!this.fields[0] || !this.fields[0].code || !this.fields[0].code.startsWith('ITEM CSL_CITATION ')) return [] - return JSON.parse(this.fields[0].code.replace(/ITEM CSL_CITATION /, '')).citationItems.map(item => { - return { - id: item.id, - locator: item.locator || '', - suppressAuthor: !!item['suppress-author'], - prefix: item.prefix || '', - suffix: item.suffix || '', - label: item.locator ? (item.label || 'page') : '', - citekey: KeyManager.get(item.id).citekey, - - uri: Array.isArray(item.uri) ? item.uri[0] : undefined, - itemType: item.itemData ? item.itemData.type : undefined, - title: item.itemData ? item.itemData.title : undefined, - } - }) + const citationItems = JSON.parse(this.fields[0].code.replace(/ITEM CSL_CITATION /, '')).citationItems + const items = (citationItems.map(item => ({ + id: item.id, + locator: item.locator || '', + suppressAuthor: !!item['suppress-author'], + prefix: item.prefix || '', + suffix: item.suffix || '', + label: item.locator ? (item.label || 'page') : '', + citekey: KeyManager.get(item.id).citekey, + + uri: Array.isArray(item.uri) ? item.uri[0] : undefined, + itemType: item.itemData ? item.itemData.type : undefined, + title: item.itemData ? item.itemData.title : undefined, + } as Citation)) as Citation[]) + return items } } // export singleton: https://k94n.com/es6-modules-single-instance-pattern -export let Application = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match +export const Application = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match public primaryFieldType = 'Field' public secondaryFieldType = 'Bookmark' public fields: any[] = [] private docs: { [key: number]: Document } = {} - private docId: number = 0 + private docId = 0 /** * Gets the active document. + * * @returns {Document} */ public getActiveDocument() { return this.docs[this.docId] } /** * Gets the document by some app-specific identifier. + * * @param {String|Number} id */ public getDocument(id) { @@ -291,14 +324,15 @@ export let Application = new class { // eslint-disable-line @typescript-eslint/n } } -export async function pick(options) { +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export async function pick(options: any): Promise { await Zotero.BetterBibTeX.ready const doc = Application.createDocument(options) await Zotero.Integration.execCommand('BetterBibTeX', 'addEditCitation', doc.id) const picked = doc.citation() - const citation = picked.length ? await Formatter[options.format || 'playground'](picked, options) : '' + const citation: string = picked.length ? await Formatter[options.format || 'playground'](picked, options) : '' Application.closeDocument(doc) if (options.select && picked.length) { @@ -310,10 +344,10 @@ export async function pick(options) { return citation } -async function selected(options) { +async function selected(options): Promise { const pane = Zotero.getActiveZoteroPane() const items = pane.getSelectedItems() - const picked = items.map(item => ({ + const picked: Citation[] = items.map(item => ({ id: item.id, locator: '', suppressAuthor: false, @@ -326,6 +360,7 @@ async function selected(options) { itemType: undefined, title: item.getField('title'), })) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return picked.length ? await Formatter[options.format || 'playground'](picked, options) : '' } @@ -373,13 +408,14 @@ Zotero.Server.Endpoints['/better-bibtex/cayw'] = class { if (options.texstudio) { if (!TeXstudio.enabled) return [this.SERVER_ERROR, 'application/text', 'TeXstudio not found'] - TeXstudio.push(citation) + await TeXstudio.push(citation) } if (options.clipboard) toClipboard(citation) return [this.OK, 'text/html; charset=utf-8', citation] - } catch (err) { + } + catch (err) { flash('CAYW Failed', `${err}\n${err.stack}`) return [this.SERVER_ERROR, 'application/text', `CAYW failed: ${err}\n${err.stack}`] } diff --git a/content/cayw/formatter.ts b/content/cayw/formatter.ts index 715b602e7d..eb3d105d8c 100644 --- a/content/cayw/formatter.ts +++ b/content/cayw/formatter.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/require-await, @typescript-eslint/no-unsafe-return */ declare const Zotero: any declare const Components: any declare const AddonManager: any @@ -9,12 +10,13 @@ import { log } from '../logger' import * as unicode_table from 'unicode2latex/tables/unicode.json' const unicode2latex = Object.entries(unicode_table).reduce((acc, pair) => { - const unicode = pair[0] as string + const unicode = pair[0] const latex = pair[1] as { text: string, math: string } acc[unicode] = { text: latex.text || latex.math, math: !(latex.text) } return acc }, {}) -function tolatex(s) { + +function tolatex(s: string): string { if (!s) return '' return s.split('') @@ -23,7 +25,8 @@ function tolatex(s) { const last = acc[acc.length - 1] if (last && last.math === c.math) { last.text += c.text - } else { + } + else { acc.push(c) } return acc @@ -32,7 +35,7 @@ function tolatex(s) { .join('') } -function shortLabel(label, options) { +function shortLabel(label: string, options): string { if (typeof options[label] === 'string') return options[label] return { @@ -64,20 +67,23 @@ function shortLabel(label, options) { function citation2latex(citation, options) { let formatted = '' // despite Mozilla's claim that trimStart === trimLeft, and that trimStart should be preferred, trimStart does not seem to exist in FF chrome code. - const label = (shortLabel(citation.label, { page: '', ...options }) + ' ').trimLeft() + const label = (`${shortLabel(citation.label, { page: '', ...options })} `).trimLeft() if (citation.prefix) formatted += `[${tolatex(citation.prefix)}]` if (citation.locator && citation.suffix) { formatted += `[${tolatex(label)}${tolatex(citation.locator)}, ${tolatex(citation.suffix)}]` - } else if (citation.locator) { + } + else if (citation.locator) { formatted += `[${tolatex(label)}${tolatex(citation.locator)}]` - } else if (citation.suffix) { + } + else if (citation.suffix) { formatted += `[${tolatex(citation.suffix)}]` - } else if (citation.prefix) { + } + else if (citation.prefix) { formatted += '[]' } @@ -87,14 +93,14 @@ function citation2latex(citation, options) { } // export singleton: https://k94n.com/es6-modules-single-instance-pattern -export let Formatter = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match +export const Formatter = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match public async playground(citations, options) { - const formatted = citations.map(cit => `${options.keyprefix || ''}${cit.citekey}${options.keypostfix || ''}`) + const formatted = await citations.map(cit => `${options.keyprefix || ''}${cit.citekey}${options.keypostfix || ''}`) return formatted.length ? `${options.citeprefix || ''}${formatted.join(options.separator || ',')}${options.citekeypostfix || ''}` : '' } - public async citationLinks(citations, options) { - return citations.map(citation => `cites: ${citation.citekey}`).join('\n') + public async citationLinks(citations, _options): Promise { + return await citations.map(citation => `cites: ${citation.citekey}`).join('\n') } public async cite(citations, options) { return this.natbib(citations, options) } @@ -111,6 +117,7 @@ export let Formatter = new class { // eslint-disable-line @typescript-eslint/nam if (citations.length > 1) { const state = citations.reduce((acc, cit) => { for (const field of ['prefix', 'suffix', 'suppressAuthor', 'locator', 'label']) { + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands acc[field] = (acc[field] || 0) + (cit[field] ? 1 : 0) } return acc @@ -149,7 +156,8 @@ export let Formatter = new class { // eslint-disable-line @typescript-eslint/nam if (citations.includes('[')) { // there are some pre/post notes → generate a full \XYcites command command = command.endsWith('s') ? command : `${command}s` - } else { + } + else { // there are no pre/post-notes, the citations can be a simple // comma-separated list of keys citations = citations.replace(/\}\{/g, ',') @@ -157,13 +165,14 @@ export let Formatter = new class { // eslint-disable-line @typescript-eslint/nam return `\\${command}${citations}` } - public async mmd(citations, options) { + public async mmd(citations, _options) { const formatted = [] for (const citation of citations) { if (citation.prefix) { formatted.push(`[${citation.prefix}][#${citation.citekey}]`) - } else { + } + else { formatted.push(`[#${citation.citekey}][]`) } } @@ -217,7 +226,7 @@ export let Formatter = new class { // eslint-disable-line @typescript-eslint/nam let citation = '' for (const item of citations) { - const [ , kind, lib, key ] = item.uri.match(/^http:\/\/zotero\.org\/(users|groups)\/((?:local\/)?[^\/]+)\/items\/(.+)/) + const [ , kind, lib, key ] = item.uri.match(/^http:\/\/zotero\.org\/(users|groups)\/((?:local\/)?[^/]+)\/items\/(.+)/) const id = `${kind === 'users' ? 'zu' : 'zg'}:${lib.startsWith('local/') ? '0' : lib}:${key}` if (!labels[id]) throw new Error(`No formatted citation found for ${id}`) @@ -290,7 +299,7 @@ export let Formatter = new class { // eslint-disable-line @typescript-eslint/nam return await Translators.exportItems(translator, exportOptions, { type: 'items', items }) } - public async json(citations, options) { + public async json(citations, _options) { return JSON.stringify(citations) } } diff --git a/content/collection.ts b/content/collection.ts index b2b7066ec2..2a7173e730 100644 --- a/content/collection.ts +++ b/content/collection.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +declare const Zotero: any import { get as getLibrary } from './library' class CollectionError extends Error { @@ -46,7 +48,7 @@ async function getCollection(parent, name, path, create) { return collection } -export async function get(path: string, create: boolean = false) { +export async function get(path: string, create = false): Promise { const names = (path || '').split('/') if (names.shift() !== '') throw new CollectionError('path must be absolute', 'notfound') const root = names.shift() @@ -54,11 +56,11 @@ export async function get(path: string, create: boolean = false) { let collection = root.match(/^[0-9]+$/) ? Zotero.Libraries.get(root) : getLibrary(root) if (!collection) throw new CollectionError(`Library ${root} not found`, 'notfound') - let _path = `/${root}` + let tmp_path = `/${root}` for (const name of names) { - _path += `/${name}` - collection = await getCollection(collection, name, _path, create) + tmp_path += `/${name}` + collection = await getCollection(collection, name, tmp_path, create) } return collection diff --git a/content/dateparser.ts b/content/dateparser.ts index 6782302d85..a61d9f701d 100644 --- a/content/dateparser.ts +++ b/content/dateparser.ts @@ -1,9 +1,12 @@ +/* eslint-disable no-case-declarations */ +declare const Zotero import EDTF = require('edtf') import edtfy = require('edtfy') // import escapeStringRegexp = require('escape-string-regexp') import * as months from '../gen/dateparser-months.json' +import { ParsedDate } from './typings/bbt' const months_re = new RegExp(Object.keys(months).sort((a, b) => b.length - a.length).join('|'), 'i') /* @@ -17,7 +20,7 @@ regex = { const SPRING = 21 const WINTER = 24 -function seasonize(date) { +function seasonize(date: ParsedDate): ParsedDate { if (date.type === 'date' && typeof date.month === 'number' && date.month >= SPRING && date.month <= WINTER && !date.day) { date.type = 'season' date.season = (date.month - SPRING) + 1 @@ -26,13 +29,13 @@ function seasonize(date) { return date } -function doubt(date, state) { +function doubt(date: ParsedDate, state: { uncertain: boolean, approximate: boolean }): ParsedDate { if (state.uncertain) date.uncertain = true if (state.approximate) date.approximate = true return date } -function normalize_edtf(date) { +function normalize_edtf(date: any): ParsedDate { let year, month, day switch (date.type) { @@ -44,8 +47,8 @@ function normalize_edtf(date) { case 'Interval': // eslint-disable-next-line no-magic-numbers if (date.values.length !== 2) throw new Error(JSON.stringify(date)) - const from = date.values[0] ? normalize_edtf(date.values[0]) : { type: 'open' } - const to = date.values[1] ? normalize_edtf(date.values[1]) : { type: 'open' } + const from: ParsedDate = date.values[0] ? normalize_edtf(date.values[0]) : { type: 'open' } + const to: ParsedDate = date.values[1] ? normalize_edtf(date.values[1]) : { type: 'open' } return { type: 'interval', from, to } case 'Season': @@ -61,7 +64,7 @@ function normalize_edtf(date) { } } -function upgrade_edtf(date) { +function upgrade_edtf(date: string): string { return date .replace(/unknown/g, '') .replace(/u/g, 'X') @@ -71,7 +74,7 @@ function upgrade_edtf(date) { .replace(/y/g, 'Y') } -function is_valid_month(month, allowseason) { +function is_valid_month(month: number, allowseason: boolean) { if (month >= 1 && month <= 12) return true // eslint-disable-line no-magic-numbers if (allowseason && month >= 21 && month <= 24) return true // eslint-disable-line no-magic-numbers @@ -79,24 +82,26 @@ function is_valid_month(month, allowseason) { } // swap day/month for our American friends -function swap_day_month(day, month, localeDateOrder) { +function swap_day_month(day: number, month: number, localeDateOrder: string): number[] { if (!day) day = undefined if (day && localeDateOrder === 'mdy' && is_valid_month(day, false)) { return [month, day] - } else if (day && is_valid_month(day, false) && !is_valid_month(month, false)) { + } + else if (day && is_valid_month(day, false) && !is_valid_month(month, false)) { return [month, day] } return [day, month] } -function stripTime(date) { +function stripTime(date: string): string { return date.replace(/(\s+|T)[0-9]{2}:[0-9]{2}(:[0-9]{2}(Z|\+[0-9]{2}:?[0-9]{2})?)?$/, '') } -export function parse(value: string, localeDateOrder: string, as_range_part: boolean = false) { +export function parse(value: string, localeDateOrder: string, as_range_part = false): ParsedDate { value = (value || '').trim() - let parsed, m + let parsed: ParsedDate + let m: RegExpMatchArray if (value === 'today') { const now = new Date @@ -113,10 +118,14 @@ export function parse(value: string, localeDateOrder: string, as_range_part: boo if ((m = (/^([0-9]+) (de )?([a-z]+) (de )?([0-9]+)$/i).exec(value)) && (m[2] || m[4]) && (months[m[3].toLowerCase()])) return parse(`${m[1]} ${m[3]} ${m[5]}`, localeDateOrder, as_range_part) // '30-Mar-2020' + Zotero.debug(`${value}: 30-Mar-2020`) if (!as_range_part && (m = (/^([0-9]+)-([a-z]+)-([0-9]+)$/i).exec(value))) { + Zotero.debug('30-Mar-2020: yes') + // eslint-disable-next-line @typescript-eslint/tslint/config let [ , day, month, year ] = m - if (day > 31 && year < 31) [ day, year ] = [ year, day ] // eslint-disable-line no-magic-numbers + if (parseInt(day) > 31 && parseInt(year) < 31) [ day, year ] = [ year, day ] // eslint-disable-line no-magic-numbers const date = parse(`${month} ${day} ${year}`, localeDateOrder, false) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return if (date.type === 'date') return date } @@ -125,6 +134,7 @@ export function parse(value: string, localeDateOrder: string, as_range_part: boo const [ , _orig, _date ] = m const date = parse(_date, localeDateOrder, false) const orig = parse(_orig, localeDateOrder, false) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return if (date.type === 'date' && orig.type === 'date') return {...date, ...{ orig } } } @@ -133,6 +143,7 @@ export function parse(value: string, localeDateOrder: string, as_range_part: boo const [ , _date, _orig ] = m const date = parse(_date, localeDateOrder, false) const orig = parse(_orig, localeDateOrder, false) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return if (date.type === 'date' && orig.type === 'date') return {...date, ...{ orig } } } @@ -192,7 +203,7 @@ export function parse(value: string, localeDateOrder: string, as_range_part: boo // these assume a sensible d/m/y format by default. There's no sane way to guess between m/d/y and d/m/y, and m/d/y is // just wrong. https://en.wikipedia.org/wiki/Date_format_by_country - if (m = /^(-?[0-9]{3,})([-\s\/\.])([0-9]{1,2})(\2([0-9]{1,2}))?$/.exec(exactish)) { + if (m = /^(-?[0-9]{3,})([-\s/.])([0-9]{1,2})(\2([0-9]{1,2}))?$/.exec(exactish)) { const [ , _year, , _month, , _day ] = m const year = parseInt(_year) const [day, month] = swap_day_month(parseInt(_day), parseInt(_month), 'ymd') @@ -213,7 +224,7 @@ export function parse(value: string, localeDateOrder: string, as_range_part: boo if (is_valid_month(month, false)) return seasonize(doubt({ type: 'date', year, month, day }, state)) } - if (m = /^([0-9]{1,2})([-\s\/\.])([0-9]{1,2})(\2([0-9]{3,}))$/.exec(exactish)) { + if (m = /^([0-9]{1,2})([-\s/.])([0-9]{1,2})(\2([0-9]{3,}))$/.exec(exactish)) { const [ , _day, , _month, , _year ] = m const year = parseInt(_year) const [day, month] = swap_day_month(parseInt(_day), parseInt(_month), localeDateOrder) @@ -223,7 +234,7 @@ export function parse(value: string, localeDateOrder: string, as_range_part: boo if (is_valid_month(month, false)) return seasonize(doubt({ type: 'date', year, month, day }, state)) } - if (m = /^([0-9]{1,2})[-\s\/\.]([0-9]{3,})$/.exec(exactish)) { + if (m = /^([0-9]{1,2})[-\s/.]([0-9]{3,})$/.exec(exactish)) { const [ , _month, _year ] = m const month = parseInt(_month) const year = parseInt(_year) @@ -232,7 +243,7 @@ export function parse(value: string, localeDateOrder: string, as_range_part: boo if (is_valid_month(month, false)) return seasonize(doubt({ type: 'date', year, month }, state)) } - if (m = /^([0-9]{3,})[-\s\/\.]([0-9]{1,2})$/.exec(exactish)) { + if (m = /^([0-9]{3,})[-\s/.]([0-9]{1,2})$/.exec(exactish)) { const [ , _year, _month ] = m const year = parseInt(_year) const month = parseInt(_month) @@ -248,7 +259,8 @@ export function parse(value: string, localeDateOrder: string, as_range_part: boo try { // https://github.com/inukshuk/edtf.js/issues/5 parsed = normalize_edtf(EDTF.parse(upgrade_edtf(stripTime(value.replace(/_|--/, '/'))))) - } catch (err) { + } + catch (err) { parsed = null } @@ -257,21 +269,25 @@ export function parse(value: string, localeDateOrder: string, as_range_part: boo parsed = normalize_edtf(EDTF.parse(edtfy(value .normalize('NFC') .replace(/\. /, ' ') // 8. july 2011 + // eslint-disable-next-line @typescript-eslint/no-unsafe-return .replace(months_re, _ => months[_.toLowerCase()] || _) ))) - } catch (err) { + } + catch (err) { parsed = null } } // https://github.com/retorquere/zotero-better-bibtex/issues/868 if (!parsed) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return if (m = /^([0-9]+)\s([^0-9]+)(?:\s+([0-9]+))?$/.exec(value.normalize('NFC').replace(months_re, _ => months[_.toLowerCase()] || _))) { const [ , year, month, day ] = m if (months[month]) { try { parsed = normalize_edtf(EDTF.parse(edtfy(`${day || ''} ${month} ${year}`.trim()))) - } catch (err) { + } + catch (err) { parsed = null } } @@ -294,21 +310,22 @@ export function parse(value: string, localeDateOrder: string, as_range_part: boo return parsed || { type: 'verbatim', verbatim: value } } -function testEDTF(value) { +function testEDTF(value: string): boolean { try { - return EDTF.parse(value, { level: 1 }) - } catch (err) { + return (EDTF.parse(value, { level: 1 }) as boolean) + } + catch (err) { return false } } -export function isEDTF(value, minuteLevelPrecision = false) { +export function isEDTF(value: string, minuteLevelPrecision = false): boolean { value = upgrade_edtf(value) return testEDTF(value) || (minuteLevelPrecision && testEDTF(`${value}:00`)) } -export function strToISO(str, localeDateOrder: string) { +export function strToISO(str: string, localeDateOrder: string): string { let date = parse(str, localeDateOrder) if (date.type === 'interval') date = date.from diff --git a/content/db/cache.ts b/content/db/cache.ts index c98d5308b9..86ba5b85be 100644 --- a/content/db/cache.ts +++ b/content/db/cache.ts @@ -10,11 +10,12 @@ import * as translators from '../../gen/translators.json' import * as prefOverrides from '../../gen/preferences/auto-export-overrides.json' import * as prefOverridesSchema from '../../gen/preferences/auto-export-overrides-schema.json' +import { IPreferences } from '../../gen/typings/preferences' class Cache extends Loki { private initialized = false - public remove(ids, reason) { + public remove(ids, _reason) { if (!this.initialized) return const query = Array.isArray(ids) ? { itemID : { $in : ids } } : { itemID: ids } @@ -116,7 +117,7 @@ class Cache extends Loki { } } // export singleton: https://k94n.com/es6-modules-single-instance-pattern -export let DB = new Cache('cache', { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match +export const DB = new Cache('cache', { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match autosave: true, adapter: new Store({ storage: 'file', deleteAfterLoad: true, allowPartial: true }), }) @@ -134,7 +135,8 @@ function clearOnUpgrade(coll, property, current) { const msg = drop ? { dropping: 'dropping', because: 'because' } : { dropping: 'keeping', because: 'even though' } if (dbVersion) { Zotero.debug(`:Cache:${msg.dropping} cache ${coll.name} ${msg.because} ${property} went from ${dbVersion} to ${current}`) - } else { + } + else { Zotero.debug(`:Cache:${msg.dropping} cache ${coll.name} ${msg.because} ${property} was not set (current: ${current})`) } @@ -147,24 +149,24 @@ function clearOnUpgrade(coll, property, current) { } // the preferences influence the output way too much, no keeping track of that -Events.on('preference-changed', async () => { - await Zotero.BetterBibTeX.loaded - DB.reset() +Events.on('preference-changed', () => { + Zotero.BetterBibTeX.loaded.then(() => { DB.reset() }) }) // cleanup if (DB.getCollection('cache')) { DB.removeCollection('cache') } if (DB.getCollection('serialized')) { DB.removeCollection('serialized') } -export function selector(itemID, options, prefs) { - const _selector = { +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function selector(itemID: number | number[], options: any, prefs: IPreferences): any { + const query = { itemID: Array.isArray(itemID) ? { $in: itemID } : itemID, exportNotes: !!options.exportNotes, useJournalAbbreviation: !!options.useJournalAbbreviation, } for (const pref of prefOverrides) { - _selector[pref] = prefs[pref] + query[pref] = prefs[pref] } - return _selector + return query } diff --git a/content/db/loki.ts b/content/db/loki.ts index e9dcbaee22..7b6f1534d5 100644 --- a/content/db/loki.ts +++ b/content/db/loki.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types, prefer-arrow/prefer-arrow-functions, prefer-rest-params, @typescript-eslint/no-unsafe-return */ declare const Components: any declare const Zotero: any @@ -23,6 +24,7 @@ require('ajv-keywords')(validator) $patch$(Loki.Collection.prototype, 'findOne', original => function() { if (!this.data.length) return null + log.debug('findOne', Array.from(arguments)) return original.apply(this, arguments) }) @@ -49,7 +51,8 @@ $patch$(Loki.prototype, 'close', original => function(callback) { return original.call(this, errClose => { if (this.persistenceAdapter && (typeof this.persistenceAdapter.close === 'function')) { return this.persistenceAdapter.close(this.filename, errCloseAdapter => callback(errClose || errCloseAdapter)) - } else { + } + else { return callback(errClose) } }) @@ -66,13 +69,14 @@ const autoSaveOnIdle = [] const idleService = Components.classes['@mozilla.org/widget/idleservice;1'].getService(Components.interfaces.nsIIdleService) idleService.addIdleObserver({ - async observe(subject, topic, data) { + async observe(_subject: string, _topic: string, _data: any) { for (const db of autoSaveOnIdle) { if (!db.autosaveDirty()) continue try { await db.saveDatabaseAsync() - } catch (err) { + } + catch (err) { log.error('idle, saving failed', db.filename, err) } } @@ -81,7 +85,7 @@ idleService.addIdleObserver({ // https://github.com/Microsoft/TypeScript/issues/17032 export class XULoki extends Loki { - constructor(name, options: any = {}) { + constructor(name: string, options: any = {}) { const nullStore = !options.adapter options.adapter = options.adapter || new NullStore() options.env = 'XUL-Chrome' @@ -93,7 +97,8 @@ export class XULoki extends Loki { if (periodicSave) { autoSaveOnIdle.push(this) - } else { + } + else { // workaround for https://github.com/techfort/LokiJS/issues/597 this.autosaveDisable() } @@ -110,18 +115,20 @@ export class XULoki extends Loki { await this.saveDatabaseAsync() await this.closeAsync() Zotero.debug(`Loki.${this.persistenceAdapter.constructor.name || 'Unknown'}.shutdown: close of ${name} completed`) - } catch (err) { + } + catch (err) { Zotero.debug(`Loki.${this.persistenceAdapter.constructor.name || 'Unknown'}.shutdown: close of ${name} failed`) log.error(`Loki.${this.persistenceAdapter.constructor.name || 'Unknown'}.shutdown: close of ${name} failed`, err) } }) - } catch (err) { + } + catch (err) { log.error(`Loki.${this.persistenceAdapter.constructor.name || 'Unknown'} failed to install shutdown blocker!`, err) } } } - public loadDatabaseAsync(options = {}) { + public loadDatabaseAsync(options = {}): Promise { const deferred = Zotero.Promise.defer() this.loadDatabase(options, err => { if (err) return deferred.reject(err) @@ -130,7 +137,7 @@ export class XULoki extends Loki { return deferred.promise } - public saveDatabaseAsync() { + public saveDatabaseAsync(): Promise { const deferred = Zotero.Promise.defer() this.saveDatabase(err => { if (err) return deferred.reject(err) @@ -139,7 +146,7 @@ export class XULoki extends Loki { return deferred.promise } - public closeAsync() { + public closeAsync(): Promise { const deferred = Zotero.Promise.defer() this.close(err => { if (err) return deferred.reject(err) @@ -148,7 +155,7 @@ export class XULoki extends Loki { return deferred.promise } - public schemaCollection(name, options) { + public schemaCollection(name: string, options: any) { options.cloneObjects = true options.clone = true const coll = this.getCollection(name) || this.addCollection(name, options); diff --git a/content/db/main.ts b/content/db/main.ts index 387bb669de..83419a4e0b 100644 --- a/content/db/main.ts +++ b/content/db/main.ts @@ -135,7 +135,8 @@ class Main extends Loki { let corrupt try { corrupt = coll.checkAllIndexes({ repair: true }) - } catch (err) { + } + catch (err) { corrupt = [ '*' ] coll.ensureAllIndexes(true) } @@ -143,7 +144,8 @@ class Main extends Loki { for (const index of corrupt) { if (index === '*') { Zotero.logError(new Error(`LokiJS: rebuilt index ${name}.${index}`)) - } else { + } + else { Zotero.logError(new Error(`LokiJS: corrupt index ${name}.${index} repaired`)) } } @@ -170,7 +172,7 @@ class Main extends Loki { } // export singleton: https://k94n.com/es6-modules-single-instance-pattern -export let DB = new Main('better-bibtex', { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match +export const DB = new Main('better-bibtex', { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match autosave: true, autosaveInterval: 5000, autosaveOnIdle: true, diff --git a/content/db/store.ts b/content/db/store.ts index db051c7189..04de0c06b2 100644 --- a/content/db/store.ts +++ b/content/db/store.ts @@ -26,7 +26,7 @@ export class Store { if (this.storage !== 'sqlite' && this.storage !== 'file') throw new Error(`Unsupported DBStore storage ${this.storage}`) } - public close(name, callback) { + public close(name: string, callback: ((v: null) => void)): void { if (this.storage !== 'sqlite') return callback(null) if (!this.conn[name]) return callback(null) @@ -43,13 +43,14 @@ export class Store { }) } - public exportDatabase(name, dbref, callback) { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + public exportDatabase(name: string, dbref: any, callback: ((v: null) => void)): void { this.exportDatabaseAsync(name, dbref) .then(() => callback(null)) .catch(callback) } - private async closeDatabase(conn, name, reason) { + private async closeDatabase(conn, name, _reason) { if (!conn) { log.debug('DB.Store.closeDatabase: ', name, typeof conn) return @@ -62,7 +63,8 @@ export class Store { try { await conn.closeDatabase(true) - } catch (err) { + } + catch (err) { log.debug('DB.Store.closeDatabase FAILED', name, err) } } @@ -83,7 +85,7 @@ export class Store { await this.roll(name) const parts = [ - this.save(name, {...dbref, ...{collections: dbref.collections.map(coll => coll.name)}}, true), + this.save(name, {...dbref, ...{collections: dbref.collections.map((coll: { name: string }) => coll.name)}}, true), ] for (const coll of dbref.collections) { parts.push(this.save(`${name}.${coll.name}`, coll, coll.dirty)) @@ -106,7 +108,7 @@ export class Store { } await conn.executeTransaction(async () => { - const names = (await conn.queryAsync(`SELECT name FROM "${name}"`)).map(coll => coll.name) + const names = (await conn.queryAsync(`SELECT name FROM "${name}"`)).map((coll: { name: string }) => coll.name) const parts = [] for (const coll of dbref.collections) { @@ -140,14 +142,16 @@ export class Store { await (new OS.File.DirectoryIterator(root)).forEach(entry => { // really weird half-promise thing try { [ , collection, version ] = entry.name.match(re) - } catch (err) { + } + catch (err) { return } const v: number = version ? parseInt(version) : 0 if (v >= this.versions) { roll.push({ version, remove: OS.Path.join(root, entry.path) }) - } else { + } + else { roll.push({ version, move: OS.Path.join(root, entry.path), to: OS.Path.join(root, `${name}${collection || ''}.${v + 1}.${ext}`) } ) } }) @@ -157,25 +161,27 @@ export class Store { try { if (file.remove) { await OS.File.remove(file.remove, { ignoreAbsent: true }) - } else { + } + else { await OS.File.move(file.move, file.to) } - } catch (err) { + } + catch (err) { log.debug('DB.Store.roll:', file, err) } } } - private async save(name, data, dirty) { + private async save(name: string, data, dirty: boolean) { const path = OS.Path.join(Zotero.BetterBibTeX.dir, `${name}.json`) const save = dirty || !(await OS.File.exists(path)) if (!save) return null - await OS.File.writeAtomic(path, JSON.stringify(data), { encoding: 'utf-8', tmpPath: path + '.tmp'}) + await OS.File.writeAtomic(path, JSON.stringify(data), { encoding: 'utf-8', tmpPath: `${path}.tmp`}) } - public loadDatabase(name, callback) { + public loadDatabase(name: string, callback: ((v: null) => void)): void { this.loadDatabaseAsync(name) .then(callback) .catch(err => { @@ -184,11 +190,13 @@ export class Store { }) } - public async loadDatabaseAsync(name) { + public async loadDatabaseAsync(name: string): Promise { try { const db = await this.loadDatabaseSQLiteAsync(name) // always try sqlite first, may be a migration to file + // eslint-disable-next-line @typescript-eslint/no-unsafe-return if (db) return db - } catch (err) { + } + catch (err) { log.debug('DB.Store.loadDatabaseAsync:', err) } @@ -196,6 +204,7 @@ export class Store { const versions = this.versions || 1 for (let version = 0; version < versions; version++) { const db = await this.loadDatabaseVersionAsync(name, version) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return if (db) return db } } @@ -203,7 +212,7 @@ export class Store { return null } - private async loadDatabaseSQLiteAsync(name) { + private async loadDatabaseSQLiteAsync(name: string): Promise { const path = OS.Path.join(Zotero.DataDirectory.dir, `${name}.sqlite`) const exists = await OS.File.exists(path) @@ -213,7 +222,7 @@ export class Store { await conn.queryAsync(`CREATE TABLE IF NOT EXISTS "${name}" (name TEXT PRIMARY KEY NOT NULL, data TEXT NOT NULL)`) let db = null - const collections = {} + const collections: Record = {} let failed = false @@ -223,14 +232,16 @@ export class Store { try { if (row.name === name) { db = JSON.parse(row.data) - } else { + } + else { collections[row.name] = JSON.parse(row.data) collections[row.name].cloneObjects = true // https://github.com/techfort/LokiJS/issues/47#issuecomment-362425639 collections[row.name].adaptiveBinaryIndices = false // https://github.com/techfort/LokiJS/issues/654 collections[row.name].dirty = true } - } catch (err) { + } + catch (err) { log.debug(`DB.Store.loadDatabaseSQLiteAsync: failed to load ${name}:`, row.name) failed = true } @@ -238,13 +249,15 @@ export class Store { if (db) { const missing = db.collections.filter(coll => !collections[coll]) - db.collections = db.collections.map(coll => collections[coll]).filter(coll => coll) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + db.collections = db.collections.map((coll: string) => collections[coll]).filter(coll => coll) if (missing.length) { failed = !this.allowPartial log.debug(`DB.Store.loadDatabaseSQLiteAsync: could not find ${name}.${missing.join('.')}`) } - } else if (exists && rows) { + } + else if (exists && rows) { log.debug('DB.Store.loadDatabaseSQLiteAsync: could not find metadata for', name, rows) failed = true @@ -254,7 +267,8 @@ export class Store { await this.closeDatabase(conn, name, 'migrated') await OS.File.move(path, `${path}.migrated`) - } else { + } + else { this.conn[name] = conn if (failed || this.deleteAfterLoad) await conn.queryAsync(`DELETE FROM "${name}"`) @@ -264,6 +278,7 @@ export class Store { log.debug('DB.Store.loadDatabaseSQLiteAsync failed, returning empty database') return null } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return db } @@ -272,10 +287,12 @@ export class Store { const conn = new Zotero.DBConnection(name) try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return if (await conn.integrityCheck()) return conn throw new Error(`DB.Store.openDatabaseSQLiteAsync(${JSON.stringify(name)}) failed: integrity check not OK`) - } catch (err) { + } + catch (err) { log.debug('DB.Store.openDatabaseSQLiteAsync:', { name, fatal }, err) if (fatal) throw err @@ -285,6 +302,7 @@ export class Store { null, // parent Zotero.BetterBibTeX.getString('DB.corrupt'), // dialogTitle Zotero.BetterBibTeX.getString('DB.corrupt.explanation', { error: err.message }), // text + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING + ps.BUTTON_POS_0_DEFAULT // buttons + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_IS_STRING + 0, // disabled: (fatal ? 0 : ps.BUTTON_POS_2 * ps.BUTTON_TITLE_IS_STRING), @@ -304,6 +322,7 @@ export class Store { case 1: // reset if (await OS.File.exists(path)) await OS.File.move(path, `${path}.ignore.corrupt`) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return await this.openDatabaseSQLiteAsync(name, true) default: // restore @@ -325,6 +344,7 @@ export class Store { if (coll) { coll.cloneObjects = true // https://github.com/techfort/LokiJS/issues/47#issuecomment-362425639 coll.adaptiveBinaryIndices = false // https://github.com/techfort/LokiJS/issues/654 + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return coll } @@ -333,10 +353,13 @@ export class Store { if (this.allowPartial) { log.debug('DB.Store.loadDatabaseVersionAsync:', msg) return null - } else { + } + else { throw new Error(msg) } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return })).filter(coll => coll) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return db } @@ -350,8 +373,9 @@ export class Store { // this is intentional. If all is well, the database will be retained in memory until it's saved at // shutdown. If all is not well, this will make sure the caches are rebuilt from scratch on next start - if (this.deleteAfterLoad) await OS.File.move(path, path + '.bak') + if (this.deleteAfterLoad) await OS.File.move(path, `${path}.bak`) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return data } } diff --git a/content/db/upgrade-extra.ts b/content/db/upgrade-extra.ts index 6dd3acc826..a9238faf03 100644 --- a/content/db/upgrade-extra.ts +++ b/content/db/upgrade-extra.ts @@ -1,24 +1,24 @@ import JSON5 = require('json5') const bibtex = /(?:^|\s)bibtex:[^\S\n]*([^\s]*)(?:\s|$)/ -const biblatexcitekey = /(?:^|\s)biblatexcitekey\[([^\[\]\s]*)\](?:\s|$)/ +const biblatexcitekey = /(?:^|\s)biblatexcitekey\[([^[\]\s]*)\](?:\s|$)/ const citekey = new RegExp(`${bibtex.source}|${biblatexcitekey.source}`, 'ig') const bibtexJSON = /(biblatexdata|bibtex|biblatex)(\*)?{/ -function indexOfRE(str, re, start) { +function indexOfRE(str: string, re: RegExp, start: number): number { const index = str.substring(start).search(re) return (index >= 0) ? (index + start) : index } -function makeName(name) { +function makeName(name: string): string { return `tex.${name.replace(/[:=]/g, '-').toLowerCase()}` } -function makeValue(value) { +function makeValue(value: string | number): string { return `${value}`.replace(/\n+/g, ' ') } -export function upgradeExtra(extra) { +export function upgradeExtra(extra: string): string { let extraFields = [] // replace citekey markers with 'Citation Key' @@ -28,13 +28,13 @@ export function upgradeExtra(extra) { }).trim() // replace old-style key-value fields - extra = extra.replace(/(?:biblatexdata|bibtex|biblatex)(\*)?\[([^\[\]]*)\]/g, (match, cook, fields) => { + extra = extra.replace(/(?:biblatexdata|bibtex|biblatex)(\*)?\[([^[\]]*)\]/g, (match, cook, fields) => { const legacy = [] for (const field of fields.split(';')) { const kv = field.match(/^([^=]+)(?:=)([\S\s]*)/) if (!kv) return match - const [ , name, value ] = kv.map(v => v.trim()) + const [ , name, value ] = kv.map((v: string) => v.trim()) legacy.push(`${makeName(name)}${cook ? ':' : '='} ${makeValue(value)}`) } @@ -56,10 +56,12 @@ export function upgradeExtra(extra) { const candidate = extra.substring(start, end + 1) try { json = JSON.parse(candidate) - } catch (err) { + } + catch { try { json = JSON5.parse(candidate) - } catch (err) { + } + catch { json = null } } @@ -69,7 +71,7 @@ export function upgradeExtra(extra) { extra = extra.substring(0, marker) + extra.substring(end + 1) - for (const [name, value] of Object.entries(json)) { + for (const [name, value] of (Object.entries(json))) { if (typeof value !== 'number' && typeof value !== 'string') throw new Error(`unexpected field of type ${typeof value}`) extraFields.push(`${makeName(name)}${cook ? ':' : '='} ${makeValue(value)}`) } @@ -80,9 +82,7 @@ export function upgradeExtra(extra) { end = extra.lastIndexOf('}', end - 1) } - if (!json) { - marker = start - } + if (!json) marker = start } extra = extraFields.sort().concat(extra).join('\n').trim() diff --git a/content/db/upgrade.ts b/content/db/upgrade.ts index efe013a58b..c620c4d177 100644 --- a/content/db/upgrade.ts +++ b/content/db/upgrade.ts @@ -7,7 +7,7 @@ import { DB as Cache } from './cache' import { log } from '../logger' import { flash } from '../flash' -export async function upgrade(progress) { +export async function upgrade(progress: { (msg: any): void, (arg0: any): void }): Promise { progress(Zotero.BetterBibTeX.getString('BetterBibTeX.startup.dbUpgrade', { n: '?', total: '?' })) const patterns = [] @@ -26,7 +26,7 @@ export async function upgrade(progress) { WHERE items.itemID NOT IN (select itemID from deletedItems) AND fields.fieldName = 'extra' - AND (${patterns.map(pattern => 'itemDataValues.value like ?').join(' OR ')}) + AND (${patterns.map(() => 'itemDataValues.value like ?').join(' OR ')}) ` let notEditable = 0 @@ -51,10 +51,12 @@ export async function upgrade(progress) { log.error('dbUpgrade:', item.extra, 'required upgrade, but', item.itemID, 'is not editable') notEditable++ - } else { + } + else { try { upgraded.setField('extra', extra) - } catch (err) { + } + catch (err) { await upgraded.loadAllData() upgraded.setField('extra', extra) } diff --git a/content/db/zotero.ts b/content/db/zotero.ts index 3ab96a2207..4e79a532de 100644 --- a/content/db/zotero.ts +++ b/content/db/zotero.ts @@ -1,3 +1,4 @@ declare const Zotero: any -export function queryAsync(query, args?) { return Zotero.DB.queryAsync(query.replace(/[\s\n]+/g, ' ').trim(), args) } +// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await +export async function queryAsync(query: string, args?: any[]): Promise { return Zotero.DB.queryAsync(query.replace(/[\s\n]+/g, ' ').trim(), args) } diff --git a/content/escape.ts b/content/escape.ts index 399d2b9a6c..1fe11ba787 100644 --- a/content/escape.ts +++ b/content/escape.ts @@ -1,4 +1,4 @@ -export function html(str) { +export function html(str: string): string { const entity = { '&': '&', '<': '<', @@ -6,10 +6,11 @@ export function html(str) { '"': '"', } // return str.replace(/[\u00A0-\u9999<>\&]/gim, c => entity[c] || `&#${c.charCodeAt(0)};`) - return str.replace(/[<>\&"']/g, c => entity[c] || `&#${c.charCodeAt(0)};`) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return str.replace(/[<>&"']/g, c => entity[c] || `&#${c.charCodeAt(0)};`) } -export function rtf(str) { +export function rtf(str: string): string { return str .replace(/([{}\\])/g, '\\$1') .replace(/\n/g, '\\par ') diff --git a/content/events.ts b/content/events.ts index 86c8630a23..9f4b3fd96e 100644 --- a/content/events.ts +++ b/content/events.ts @@ -4,7 +4,7 @@ import { EventEmitter } from 'eventemitter3' import { patch as $patch$ } from './monkey-patch' // export singleton: https://k94n.com/es6-modules-single-instance-pattern -export let Events = new EventEmitter() // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match +export const Events = new EventEmitter() // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match if (Zotero.Debug.enabled) { const events = [ @@ -20,17 +20,21 @@ if (Zotero.Debug.enabled) { ] $patch$(Events, 'on', original => function() { + // eslint-disable-next-line prefer-rest-params if (!events.includes(arguments[0])) throw new Error(`Unsupported event ${arguments[0]}`) + // eslint-disable-next-line prefer-rest-params original.apply(this, arguments) }) $patch$(Events, 'emit', original => function() { + // eslint-disable-next-line prefer-rest-params if (!events.includes(arguments[0])) throw new Error(`Unsupported event ${arguments[0]}`) + // eslint-disable-next-line prefer-rest-params original.apply(this, arguments) }) } -export function itemsChanged(items) { +export function itemsChanged(items: any[]): void { const changed = { collections: new Set, libraries: new Set, diff --git a/content/extra.ts b/content/extra.ts index 82d653375f..8de7f8f9a2 100644 --- a/content/extra.ts +++ b/content/extra.ts @@ -18,10 +18,11 @@ type ZoteroCreator = { name?: string, lastName?: string, firstName?: string } export function cslCreator(value: string): CSLCreator { const creator = value.split(/\s*\|\|\s*/) if (creator.length === 2) { // eslint-disable-line no-magic-numbers - const _creator = { family: creator[0] || '', given: creator[1] || ''} - CSL.parseParticles(_creator) - return _creator - } else { + const csl_creator = { family: creator[0] || '', given: creator[1] || ''} + CSL.parseParticles(csl_creator) + return csl_creator + } + else { // return { literal: value, isInstitution: 1 } return { literal: value } } @@ -31,7 +32,8 @@ export function zoteroCreator(value: string): ZoteroCreator { const creator = value.split(/\s*\|\|\s*/) if (creator.length === 2) { // eslint-disable-line no-magic-numbers return { lastName: creator[0] || '', firstName: creator[1] || '' } - } else { + } + else { return { name: value } } } @@ -89,7 +91,8 @@ export function get(extra: string, mode: 'zotero' | 'csl', options?: GetOptions) if (tex) { key = key.trim().toLowerCase() - } else { + } + else { key = key.trim().replace(/[-_]/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase() } value = value.trim() @@ -133,7 +136,7 @@ export function get(extra: string, mode: 'zotero' | 'csl', options?: GetOptions) } if (options.tex && !tex && otherFields.includes(key.replace(/[- ]/g, ''))) { - extraFields.tex['tex.' + key.replace(/[- ]/g, '')] = { value } + extraFields.tex[`tex.${key.replace(/[- ]/g, '')}`] = { value } return false } @@ -147,7 +150,7 @@ export function get(extra: string, mode: 'zotero' | 'csl', options?: GetOptions) return { extra, extraFields } } -export function set(extra, options: SetOptions = {}) { +export function set(extra: string, options: SetOptions = {}): string { log.debug('bbt merge: extra.set.options', options) if (options.citationKey && !options.aliases) options.aliases = [] if (!options.citationKey && options.aliases?.length) options.citationKey = options.aliases.shift() @@ -168,7 +171,8 @@ export function set(extra, options: SetOptions = {}) { const m = name.match(/^((?:bib(?:la)?)?tex\.)(.*)/) if (m) { [ , prefix, field ] = m - } else { + } + else { prefix = 'tex.' field = name } @@ -182,8 +186,9 @@ export function set(extra, options: SetOptions = {}) { for (const name of Object.keys(options.kv).sort()) { const value = options.kv[name] if (Array.isArray(value)) { // creators - parsed.extra += value.map(creator => `\n${name}: ${value}`).join('') // do not sort!! - } else { + parsed.extra += value.map(creator => `\n${name}: ${creator}`).join('') // do not sort!! + } + else { parsed.extra += `\n${name}: ${value}` } } diff --git a/content/flash.ts b/content/flash.ts index 3f214c4458..d6fda3f5d7 100644 --- a/content/flash.ts +++ b/content/flash.ts @@ -4,7 +4,8 @@ import { log } from './logger' const seconds = 1000 -export function flash(title, body = null, timeout = 8) { +// eslint-disable-next-line no-magic-numbers +export function flash(title: string, body?: string, timeout = 8): void { try { log.debug('flash:', {title, body}) const pw = new Zotero.ProgressWindow() @@ -14,7 +15,8 @@ export function flash(title, body = null, timeout = 8) { pw.addDescription(body) pw.show() pw.startCloseTimer(timeout * seconds) - } catch (err) { + } + catch (err) { log.error('@flash failed:', {title, body}, err) } } diff --git a/content/get-items-async.ts b/content/get-items-async.ts index 359270db7b..4f0e2bd8ac 100644 --- a/content/get-items-async.ts +++ b/content/get-items-async.ts @@ -1,10 +1,11 @@ declare const Zotero: any -export async function getItemsAsync(ids) { - let returnSingle +export async function getItemsAsync(ids: number | number[]): Promise { + let returnSingle: boolean if (Array.isArray(ids)) { returnSingle = false - } else { + } + else { returnSingle = true ids = [ids] } @@ -25,5 +26,6 @@ export async function getItemsAsync(ids) { } if (returnSingle) items = items[0] + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return items } diff --git a/content/issn.ts b/content/issn.ts index c2b4ff23bc..e6e517216e 100644 --- a/content/issn.ts +++ b/content/issn.ts @@ -3,7 +3,7 @@ const isIssnStrict = /^(\d{4})-?(\d{3})([\dX])$/ const isIssnLax = new RegExp(isIssnStrict.source, 'i') -export function validate(issn) { +export function validate(issn: string): boolean { const matches = text(issn).match(isIssnStrict) if (!matches) return false @@ -12,23 +12,23 @@ export function validate(issn) { return expectedCheckDigit === actualCheckDigit } -export function format(issn) { +export function format(issn: string): string { const matches = text(issn).match(isIssnLax) return matches ? `${matches[1]}-${matches[2]}${matches[3]}`.toUpperCase() : undefined } const isDigitsForChecksum = /^(\d{7})$/ -export function calculateCheckDigit(digits) { +export function calculateCheckDigit(digits: string): string { if (typeof digits !== 'string') throw new Error('Digits must be a string of 7 numeric characters.') if (!digits.match(isDigitsForChecksum)) throw new Error('Digits are malformed; expecting 7 numeric characters.') return calculateCheckDigitFor(digits) } -function calculateCheckDigitFor(digits) { +function calculateCheckDigitFor(digits: string): string { const result = digits.split('') .reverse() - .reduce((sum, value, index) => sum + (value * (index + 2)), 0) % 11 + .reduce((sum, digit, index) => sum + (parseInt(digit) * (index + 2)), 0) % 11 const checkDigit = (result === 0) ? 0 : 11 - result return checkDigit === 10 ? 'X' : checkDigit.toString() diff --git a/content/journal-abbrev.ts b/content/journal-abbrev.ts index edac184650..eac76f7b89 100644 --- a/content/journal-abbrev.ts +++ b/content/journal-abbrev.ts @@ -7,7 +7,7 @@ import { client } from './client' import { log } from './logger' // export singleton: https://k94n.com/es6-modules-single-instance-pattern -export let JournalAbbrev = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match +export const JournalAbbrev = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match private initialized: boolean private style: any private abbrevs: any @@ -33,6 +33,7 @@ export let JournalAbbrev = new class { // eslint-disable-line @typescript-eslint public reset() { this.style = Prefs.get('autoAbbrevStyle') if (client === 'jurism' && !this.style) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return this.style = Zotero.Styles.getVisible().filter(style => style.usesAbbreviation)[0].styleID } @@ -43,6 +44,7 @@ export let JournalAbbrev = new class { // eslint-disable-line @typescript-eslint 'institution-entire': { }, 'institution-part': { }, nickname: { }, + // eslint-disable-next-line id-blacklist number: { }, title: { }, place: { }, @@ -54,14 +56,17 @@ export let JournalAbbrev = new class { // eslint-disable-line @typescript-eslint } } - public get(item, force = false) { - let abbrev, journal + public get(item, force = false): string { + let abbrev:string + let journal: string if (item.getField) { try { abbrev = item.getField('journalAbbreviation', false, true) - } catch (error) {} - } else { + } + catch (error) {} + } + else { abbrev = item.journalAbbreviation } @@ -77,7 +82,8 @@ export let JournalAbbrev = new class { // eslint-disable-line @typescript-eslint if (!journal) continue break - } catch (err) { + } + catch (err) { log.error('JournalAbbrev.get: err', err) } } @@ -88,7 +94,7 @@ export let JournalAbbrev = new class { // eslint-disable-line @typescript-eslint if (!this.abbrevs.default['container-title'][journal] && typeof Zotero.Cite.getAbbreviation === 'function') { Zotero.Cite.getAbbreviation(this.style, this.abbrevs, 'default', 'container-title', journal) } - const abbr = this.abbrevs.default['container-title'][journal] + const abbr: string = this.abbrevs.default['container-title'][journal] if (abbr === journal) return null return abbr || journal diff --git a/content/json-rpc.ts b/content/json-rpc.ts index 3593ba1704..292a067e27 100644 --- a/content/json-rpc.ts +++ b/content/json-rpc.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable no-throw-literal, max-len */ declare const Zotero: any import { log } from './logger' @@ -35,6 +37,7 @@ class NSCollection { class NSAutoExport { /** * Add an auto-export for the given collection. The target collection will be created if it does not exist + * * @param collection The forward-slash separated path to the collection. The first part of the path must be the library name, or empty (`//`); empty is your personal library. Intermediate collections that do not exist will be created as needed. * @param translator The name or GUID of a BBT translator * @param path The absolute path to which the collection will be auto-exported @@ -54,10 +57,12 @@ class NSAutoExport { if (ae && ae.translatorID === translatorID && ae.type === 'collection' && ae.id === coll.id) { AutoExport.schedule(ae.type, [ae.id]) - } else if (ae && !replace) { + } + else if (ae && !replace) { throw { code: INVALID_PARAMETERS, message: "Auto-export exists with incompatible parameters, but no 'replace' was requested" } - } else { + } + else { AutoExport.add({ type: 'collection', id: coll.id, @@ -78,7 +83,8 @@ class NSUser { * List the libraries (also known as groups) the user has in Zotero */ public async groups() { - return Zotero.Libraries.getAll().map(lib => ({ id: lib.libraryID, name: lib.name })) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return await Zotero.Libraries.getAll().map(lib => ({ id: lib.libraryID, name: lib.name })) } } @@ -104,7 +110,7 @@ class NSItem { search.addCondition('quicksearch-titleCreatorYear', 'contains', terms) search.addCondition('itemType', 'isNot', 'attachment') - const ids = new Set(await search.search()) + const ids: Set = new Set(await search.search()) // add partial-citekey search results. for (const partialCitekey of terms.split(/\s+/)) { @@ -116,9 +122,11 @@ class NSItem { const items = await getItemsAsync(Array.from(ids)) const libraries = {} + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return items.map(item => { libraries[item.libraryID] = libraries[item.libraryID] || Zotero.Libraries.get(item.libraryID).name + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { ...Zotero.Utilities.itemToCSLJSON(item), library: libraries[item.libraryID], @@ -137,6 +145,7 @@ class NSItem { if (!key) throw { code: INVALID_PARAMETERS, message: `${citekey} not found` } const item = await getItemsAsync(key.itemID) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return (await getItemsAsync(item.getAttachments())).map(att => ({ open: `zotero://open-pdf/${Zotero.API.getLibraryPrefix(item.libraryID || Zotero.Libraries.userLibraryID)}/items/${att.key}`, path: att.getFilePath(), @@ -155,6 +164,7 @@ class NSItem { const notes = {} for (const key of keys) { const item = await getItemsAsync(key.itemID) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return notes[key.citekey] = (await getItemsAsync(item.getNotes())).map(note => note.getNote()) } return notes @@ -189,9 +199,11 @@ class NSItem { if (((format as any).mode || 'bibliography') !== 'bibliography') throw new Error(`mode must be bibliograpy, not ${(format as any).mode}`) - const items = await getItemsAsync(KeyManager.keys.find({ citekey: { $in: citekeys.map(citekey => citekey.replace('@', '')) } }).map(key => key.itemID)) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + const items = await getItemsAsync(KeyManager.keys.find({ citekey: { $in: citekeys.map((citekey: string) => citekey.replace('@', '')) } }).map(key => key.itemID)) const bibliography = Zotero.QuickCopy.getContentFromItems(items, { ...format, mode: 'bibliography' }, null, false) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return bibliography[format.contentType || 'html'] } @@ -203,16 +215,17 @@ class NSItem { public async citationkey(item_keys) { const keys = {} - let _libraryID: string + let libraryIDstr: string let libraryID: number let itemKey: string for (const key of item_keys) { if (key.includes(':')) { - [ _libraryID, itemKey ] = key.split(':') - libraryID = parseInt(_libraryID) + [ libraryIDstr, itemKey ] = key.split(':') + libraryID = parseInt(libraryIDstr) if (isNaN(libraryID)) throw new Error(`Could not parse library ID from ${key}`) - } else { + } + else { libraryID = Zotero.Libraries.userLibraryID itemKey = key } @@ -232,6 +245,7 @@ class NSItem { * @param libraryID ID of library to select the items from. When omitted, assume 'My Library' */ public async export(citekeys: string[], translator: string, libraryID: number) { + // eslint-disable-next-line no-underscore-dangle, prefer-rest-params const args = { citekeys, translator, libraryID, ...(arguments[0].__arguments__ || {}) } if (typeof args.libraryID === 'undefined') args.libraryID = Zotero.Libraries.userLibraryID @@ -240,7 +254,8 @@ class NSItem { if (Prefs.get('keyScope') === 'global') { if (typeof args.libraryID === 'number') throw { code: INVALID_PARAMETERS, message: 'keyscope is global, do not provide a library ID' } delete query.libraryID - } else { + } + else { if (typeof args.libraryID !== 'number') throw { code: INVALID_PARAMETERS, message: 'keyscope is per-library, you should provide a library ID' } } @@ -275,6 +290,7 @@ class NSItem { throw { code: INVALID_PARAMETERS, message } } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return [OK, 'text/plain', await Translators.exportItems(Translators.getTranslatorId(args.translator), null, { type: 'items', items: await getItemsAsync(found.map(key => key.itemID)) }) ] } } @@ -301,14 +317,17 @@ const api = new class API { try { if (!request.params) return {jsonrpc: '2.0', result: await method(), id: request.id || null} + // eslint-disable-next-line prefer-spread if (Array.isArray(request.params)) return {jsonrpc: '2.0', result: await method.apply(null, request.params), id: request.id || null} if (typeof request.params === 'object') return {jsonrpc: '2.0', result: await method.call(null, { __arguments__: request.params }), id: request.id || null} throw new Error('params must be either an array or an object') - } catch (err) { + } + catch (err) { log.error('JSON-RPC:', err) if (err.code) { return {jsonrpc: '2.0', error: { code: err.code, message: err.message }, id: null} - } else { + } + else { return {jsonrpc: '2.0', error: { code: INTERNAL_ERROR, message: `${err}` }, id: null} } } @@ -327,7 +346,7 @@ Zotero.Server.Endpoints['/better-bibtex/json-rpc'] = class { public supportedDataTypes = 'application/json' public permitBookmarklet = false - public async init({ data, headers }) { + public async init({ data }) { await Zotero.BetterBibTeX.ready try { @@ -335,7 +354,8 @@ Zotero.Server.Endpoints['/better-bibtex/json-rpc'] = class { const response = await (Array.isArray(data) ? Promise.all(data.map(req => api.handle(req))) : api.handle(data)) return [OK, 'application/json', JSON.stringify(response)] - } catch (err) { + } + catch (err) { return [OK, 'application/json', JSON.stringify({jsonrpc: '2.0', error: {code: PARSE_ERROR, message: `Parse error: ${err} in ${data}`}, id: null})] } } diff --git a/content/key-manager.ts b/content/key-manager.ts index 50440b3a55..05beef3a41 100644 --- a/content/key-manager.ts +++ b/content/key-manager.ts @@ -27,12 +27,12 @@ import { sprintf } from 'sprintf-js' import { intToExcelCol } from 'excel-column-name' // export singleton: https://k94n.com/es6-modules-single-instance-pattern -export let KeyManager = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match +export const KeyManager = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match public keys: any public query: { field: { extra?: number } type: { - note?: number, + note?: number attachment?: number } } @@ -52,16 +52,19 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na if (results.status && (results.status < 200 || results.status > 299)) { // eslint-disable-line no-magic-numbers flash(`Could not fetch inspireHEP key from ${type}`, `Could not fetch inspireHEP key for ${type} ${JSON.stringify(id)},\n\nInspireHEP says: ${results.message}`) - } else if (results.metadata.texkeys.length === 0) { + } + else if (results.metadata.texkeys.length === 0) { flash(`No inspireHEP key found for ${type}`) - } else { + } + else { if (results.metadata.texkeys.length > 1) { flash(`Multiple inspireHEP keys found for ${type}`, `Multiple inspireHEP keys found for ${type} (${results.metadata.texkeys.join(' / ')}), selected ${results.metadata.texkeys[0]}`) } - return results.metadata.texkeys[0] + return (results.metadata.texkeys[0] as string) } - } catch (err) { + } + catch (err) { flash(`Error fetching inspireHEP key from ${type}`, `Could not fetch inspireHEP key for ${type} ${JSON.stringify(id)}\n\n${err.message}`) log.error('inspireHEP', url, err) } @@ -69,10 +72,11 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na return null } - private getField(item, field): string { + private getField(item: { getField: ((str: string) => string)}, field: string): string { try { return item.getField(field) || '' - } catch (err) { + } + catch (err) { return '' } } @@ -91,7 +95,7 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na await item.saveTx() // this should cause an update and key registration } - public async pin(ids, inspireHEP = false) { + public async pin(ids: any[], inspireHEP = false) { ids = this.expandSelection(ids) for (const item of await getItemsAsync(ids)) { @@ -111,7 +115,8 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na if (parsed.extraFields.citationKey === citationKey) continue - } else { + } + else { if (parsed.extraFields.citationKey) continue citationKey = this.get(item.id).citekey || this.update(item) @@ -122,7 +127,7 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na } } - public async unpin(ids) { + public async unpin(ids: any) { ids = this.expandSelection(ids) for (const item of await getItemsAsync(ids)) { @@ -137,7 +142,7 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na } - public async refresh(ids, manual = false) { + public async refresh(ids: 'selected' | number[], manual = false) { ids = this.expandSelection(ids) Cache.remove(ids, `refreshing keys for ${ids}`) @@ -179,7 +184,8 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na if (aliases.extraFields.aliases.length) { item.setField('extra', Extra.set(aliases.extra, { aliases: aliases.extraFields.aliases })) - } else { + } + else { item.setField('extra', aliases.extra) } } @@ -234,7 +240,7 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na field: 'citekey', localized: 'Citation Key', } - $patch$(Zotero.Search.prototype, 'addCondition', original => function addCondition(condition, operator, value, required) { + $patch$(Zotero.Search.prototype, 'addCondition', original => function addCondition(condition: string, operator: any, value: any, _required: any) { // detect a quick search being set up if (condition.match(/^quicksearch/)) this.__add_bbt_citekey = true // creator is always added in a quick search so use it as a trigger @@ -242,25 +248,31 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na original.call(this, citekeySearchCondition.name, operator, value, false) delete this.__add_bbt_citekey } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, prefer-rest-params return original.apply(this, arguments) }) - $patch$(Zotero.SearchConditions, 'hasOperator', original => function hasOperator(condition, operator) { + $patch$(Zotero.SearchConditions, 'hasOperator', original => function hasOperator(condition: string, operator: string | number) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return if (condition === citekeySearchCondition.name) return citekeySearchCondition.operators[operator] + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, prefer-rest-params return original.apply(this, arguments) }) - $patch$(Zotero.SearchConditions, 'get', original => function get(condition) { + $patch$(Zotero.SearchConditions, 'get', original => function get(condition: string) { if (condition === citekeySearchCondition.name) return citekeySearchCondition + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, prefer-rest-params return original.apply(this, arguments) }) $patch$(Zotero.SearchConditions, 'getStandardConditions', original => function getStandardConditions() { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, prefer-rest-params return original.apply(this, arguments).concat({ name: citekeySearchCondition.name, localized: citekeySearchCondition.localized, operators: citekeySearchCondition.operators, - }).sort((a, b) => a.localized.localeCompare(b.localized)) + }).sort((a: { localized: string }, b: { localized: any }) => a.localized.localeCompare(b.localized)) }) - $patch$(Zotero.SearchConditions, 'getLocalizedName', original => function getLocalizedName(str) { + $patch$(Zotero.SearchConditions, 'getLocalizedName', original => function getLocalizedName(str: string) { if (str === citekeySearchCondition.name) return citekeySearchCondition.localized + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, prefer-rest-params return original.apply(this, arguments) }) @@ -270,7 +282,7 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na } }) - this.keys.on(['insert', 'update'], async citekey => { + this.keys.on(['insert', 'update'], async (citekey: { itemID: number, itemKey: any, citekey: any, pinned: any }) => { await ZoteroDB.queryAsync('INSERT OR REPLACE INTO betterbibtexcitekeys.citekeys (itemID, itemKey, citekey) VALUES (?, ?, ?)', [ citekey.itemID, citekey.itemKey, citekey.citekey ]) // async is just a heap of fun. Who doesn't enjoy a good race condition? @@ -280,7 +292,8 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na try { await Zotero.Items.getAsync(citekey.itemID) - } catch (err) { + } + catch (err) { // assume item has been deleted before we could get to it -- did I mention I hate async? I hate async log.error('could not load', citekey.itemID, err) return @@ -293,7 +306,7 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na this.autopin.schedule(citekey.itemID, () => { this.pin([citekey.itemID]).catch(err => log.error('failed to pin', citekey.itemID, ':', err)) }) } }) - this.keys.on('delete', async citekey => { + this.keys.on('delete', async (citekey: { itemID: any }) => { await ZoteroDB.queryAsync('DELETE FROM betterbibtexcitekeys.citekeys WHERE itemID = ?', [ citekey.itemID ]) }) @@ -302,6 +315,7 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na public async rescan(clean?: boolean) { if (Prefs.get('scrubDatabase')) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, no-prototype-builtins for (const item of this.keys.where(i => i.hasOwnProperty('extra'))) { // 799 delete item.extra this.keys.update(item) @@ -309,10 +323,11 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na } if (Array.isArray(this.scanning)) { - let left + let left: string if (this.scanning.length) { left = `, ${this.scanning.length} items left` - } else { + } + else { left = '' } flash('Scanning still in progress', `Scan is still running${left}`) @@ -325,7 +340,6 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na const marker = '\uFFFD' - let bench = this.bench('cleanup') const ids = [] const items = await ZoteroDB.queryAsync(` SELECT item.itemID, item.libraryID, item.key, extra.value as extra, item.itemTypeID @@ -346,20 +360,20 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na // if the extra doesn't have a citekey, insert marker, next phase will find & fix it this.keys.insert({ citekey: extra.extraFields.citationKey || marker, pinned: !!extra.extraFields.citationKey, itemID: item.itemID, libraryID: item.libraryID, itemKey: item.key }) - } else if (extra.extraFields.citationKey && ((extra.extraFields.citationKey !== existing.citekey) || !existing.pinned)) { + } + else if (extra.extraFields.citationKey && ((extra.extraFields.citationKey !== existing.citekey) || !existing.pinned)) { // we have an existing key in the DB, extra says it should be pinned to the extra value, but it's not. // update the DB to have the itemkey if necessaru this.keys.update({ ...existing, citekey: extra.extraFields.citationKey, pinned: true, itemKey: item.key }) - } else if (!existing.itemKey) { + } + else if (!existing.itemKey) { this.keys.update({ ...existing, itemKey: item.key }) } } this.keys.findAndRemove({ itemID: { $nin: ids } }) - this.bench(bench) - bench = this.bench('regenerate') // find all references without citekey this.scanning = this.keys.find({ citekey: marker }) @@ -387,7 +401,8 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na try { this.update(item, key) - } catch (err) { + } + catch (err) { log.error('KeyManager.rescan: update', done, 'failed:', err) } @@ -407,12 +422,11 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na // eslint-disable-next-line no-magic-numbers progressWin.startCloseTimer(500) } - this.bench(bench) this.scanning = null } - public update(item, current?) { + public update(item: any, current?: { pinned: boolean, citekey: string }) { if (item.isNote() || item.isAttachment()) return null current = current || this.keys.findOne({ itemID: item.id }) @@ -425,30 +439,30 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na current.pinned = proposed.pinned current.citekey = proposed.citekey this.keys.update(current) - } else { + } + else { this.keys.insert({ itemID: item.id, libraryID: item.libraryID, itemKey: item.key, pinned: proposed.pinned, citekey: proposed.citekey }) } return proposed.citekey } - public remove(ids) { - if (!Array.isArray(ids)) ids = [ids] - - this.keys.findAndRemove({ itemID : { $in : ids } }) - } + public remove(ids: any[]) { + if (!Array.isArray(ids)) ids = [ids] + this.keys.findAndRemove({ itemID : { $in : ids } }) + } - public get(itemID) { + public get(itemID: number): { citekey: string, pinned: boolean, retry?: boolean } { // I cannot prevent being called before the init is done because Zotero unlocks the UI *way* before I'm getting the // go-ahead to *start* my init. if (!this.keys || !this.started) return { citekey: '', pinned: false, retry: true } - const key = this.keys.findOne({ itemID }) + const key = (this.keys.findOne({ itemID }) as { citekey: string, pinned: boolean }) if (key) return key return { citekey: '', pinned: false, retry: true } } - public propose(item) { + public propose(item: { getField: (field: string) => string, libraryID: number, id: number }) { const citekey: string = Extra.get(item.getField('extra'), 'zotero', { citationKey: true }).extraFields.citationKey if (citekey) return { citekey, pinned: true } @@ -458,13 +472,15 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na const conflictQuery = { libraryID: item.libraryID, itemID: { $ne: item.id } } if (Prefs.get('keyScope') === 'global') delete conflictQuery.libraryID - let postfix + let postfix: string const seen = {} + // eslint-disable-next-line no-constant-condition for (let n = proposed.postfix.start; true; n += 1) { if (n) { const alpha = intToExcelCol(n) postfix = sprintf(proposed.postfix.format, { a: alpha.toLowerCase(), A: alpha, n }) - } else { + } + else { postfix = '' } @@ -481,7 +497,7 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na } } - public async tagDuplicates(libraryID) { + public async tagDuplicates(libraryID: any) { const tag = '#duplicate-citation-key' const scope = Prefs.get('keyScope') @@ -491,7 +507,7 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na JOIN itemTags ON itemTags.itemID = items.itemID JOIN tags ON tags.tagID = itemTags.tagID WHERE (items.libraryID = ? OR 'global' = ?) AND tags.name = ? AND items.itemID NOT IN (select itemID from deletedItems) - `, [ libraryID, scope, tag ])).map(item => item.itemID) + `, [ libraryID, scope, tag ])).map((item: { itemID: number }) => item.itemID) const citekeys: {[key: string]: any[]} = {} for (const item of this.keys.find(scope === 'global' ? undefined : { libraryID })) { @@ -500,11 +516,13 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na if (citekeys[item.citekey].length > 1) citekeys[item.citekey].forEach(i => i.duplicate = true) } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return const mistagged = Object.values(citekeys).reduce((acc, val) => acc.concat(val), []).filter(i => i.tagged !== i.duplicate).map(i => i.itemID) for (const item of await getItemsAsync(mistagged)) { if (tagged.includes(item.id)) { item.removeTag(tag) - } else { + } + else { item.addTag(tag) } @@ -512,13 +530,15 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na } } - private expandSelection(ids) { + private expandSelection(ids: 'selected' | number[]): number[] { if (Array.isArray(ids)) return ids if (ids === 'selected') { try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return Zotero.getActiveZoteroPane().getSelectedItems(true) - } catch (err) { // zoteroPane.getSelectedItems() doesn't test whether there's a selection and errors out if not + } + catch (err) { // zoteroPane.getSelectedItems() doesn't test whether there's a selection and errors out if not log.error('Could not get selected items:', err) return [] } @@ -526,8 +546,4 @@ export let KeyManager = new class { // eslint-disable-line @typescript-eslint/na return [ids] } - - private bench(id) { - if (typeof id === 'string') return { id, start: Date.now() } - } } diff --git a/content/key-manager/formatter.ts b/content/key-manager/formatter.ts index 5ef43d3b1e..c9d146d93f 100644 --- a/content/key-manager/formatter.ts +++ b/content/key-manager/formatter.ts @@ -1,6 +1,4 @@ declare const Zotero: any -declare const Node: any -declare const Components: any import { log } from '../logger' import { foldMaintaining } from 'fold-to-ascii' @@ -27,14 +25,17 @@ const htmlParser = new parse5() import { sprintf } from 'sprintf-js' -function innerText(node) { +function innerText(node): string { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return if (node.nodeName === '#text') return node.value + // eslint-disable-next-line @typescript-eslint/no-unsafe-return if (node.childNodes) return node.childNodes.map(innerText).join('') return '' } const script = { - han: new RegExp('([' + scripts.find(s => s.name === 'Han').bmp + '])', 'g'), // eslint-disable-line prefer-template + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + han: new RegExp('([' + scripts.find((s: { name: string }) => s.name === 'Han').bmp + '])', 'g'), // eslint-disable-line prefer-template } type PartialDate = { @@ -54,7 +55,7 @@ type PartialDate = { const safechars = '-:\\p{L}0-9_!$*+./;\\[\\]' class PatternFormatter { - public generate: Function + public generate: () => { citekey: string, postfix: { start: number, format: string } } private re = { unsafechars_allow_spaces: Zotero.Utilities.XRegExp(`[^${safechars}\\s]`), @@ -101,10 +102,12 @@ class PatternFormatter { // private fold: boolean private citekeyFormat: string - public update(reason) { - this.skipWords = new Set(Prefs.get('skipWords').split(',').map(word => word.trim()).filter(word => word)) + public update(_reason: string) { + this.skipWords = new Set(Prefs.get('skipWords').split(',').map((word: string) => word.trim()).filter((word: string) => word)) for (const attempt of ['get', 'strip', 'reset']) { + let citekeyFormat = '' + const errors = [] switch (attempt) { case 'get': // the zero-width-space is a marker to re-save the current default so it doesn't get replaced when the default changes later, which would change new keys suddenly @@ -112,13 +115,12 @@ class PatternFormatter { break case 'strip': - let citekeyFormat = '' - const errors = [] - for (const chunk of Prefs.get('citekeyFormat').replace(/^\u200B/, '').match(/[^\]]*\]*/g)) { + for (const chunk of (Prefs.get('citekeyFormat').replace(/^\u200B/, '').match(/[^\]]*\]*/g) as string[])) { try { this.parsePattern(citekeyFormat + chunk) citekeyFormat += chunk - } catch (err) { + } + catch (err) { errors.push(chunk) } } @@ -127,7 +129,8 @@ class PatternFormatter { // eslint-disable-next-line no-magic-numbers if (errors.length) flash('Malformed citation pattern', `removed malformed patterns:\n${errors.join('\n')}`, 20) Prefs.set('citekeyFormat', this.citekeyFormat = citekeyFormat) - } else { + } + else { continue } break @@ -140,29 +143,31 @@ class PatternFormatter { } try { + // @ts-ignore this.generate = new Function(this.parsePattern(this.citekeyFormat)) break - } catch (err) { + } + catch (err) { log.error('PatternFormatter.update: Error parsing citekeyFormat ', {pattern: this.citekeyFormat}, err, err.location) } } } public parsePattern(pattern) { - const { formatter, postfixes } = parser.parse(pattern, { items, methods }) + const { formatter, postfixes } = (parser.parse(pattern, { items, methods }) as { formatter: string, postfixes: string[]}) log.debug('key formatter=', formatter) log.debug('key postfixes=', postfixes) for (const postfix of postfixes) { - const expected = '' + Date.now() + const expected = `${Date.now()}` const found = sprintf(postfix, { a: expected, A: expected, n: expected }) - if (!found.includes(expected)) throw new Error(`postfix ${formatter.postfix} does not contain %(a)s, %(A)s or %(n)s`) - if (found.split(expected).length > 2) throw new Error(`postfix ${formatter.postfix} contains multiple instances of %(a)s/%(A)s/%(n)s`) + if (!found.includes(expected)) throw new Error(`postfix ${postfix} does not contain %(a)s, %(A)s or %(n)s`) + if (found.split(expected).length > 2) throw new Error(`postfix ${postfix} contains multiple instances of %(a)s/%(A)s/%(n)s`) } return formatter } - public format(item) { + public format(item): { citekey: string, postfix: { start: number, format: string } } { this.item = { item, type: Zotero.ItemTypes.getName(item.itemTypeID), @@ -170,11 +175,12 @@ class PatternFormatter { extra: Extra.get(item.getField('extra'), 'zotero', { kv: true }).extraFields.kv, } - if (['attachment', 'note'].includes(this.item.type)) return {} + if (['attachment', 'note'].includes(this.item.type)) return { citekey: '', postfix: { start: 0, format: ''} } try { this.item.date = this.parseDate(item.getField('date', false, true)) - } catch (err) { + } + catch (err) { this.item.date = {} } if (this.item.extra.originalDate) { @@ -188,7 +194,8 @@ class PatternFormatter { try { this.item.title = item.getField('title', false, true) || '' if (this.item.title.includes('<')) this.item.title = innerText(htmlParser.parseFragment(this.item.title)) - } catch (err) { + } + catch (err) { this.item.title = '' } @@ -222,14 +229,16 @@ class PatternFormatter { break case 'verbatim': + // eslint-disable-next-line no-case-declarations const reparsed = Zotero.Date.strToDate(date.verbatim) if (typeof reparsed.year === 'number' || reparsed.year) { parsed.y = reparsed.year parsed.m = parseInt(reparsed.month) || undefined parsed.d = parseInt(reparsed.day) || undefined - } else { - parsed.y = parsed.oy = date.verbatim + } + else { + parsed.y = parsed.oy = (date.verbatim as unknown as number) // a bit cheaty } @@ -241,7 +250,8 @@ class PatternFormatter { if (date.orig) { Object.assign(parsed, { oy: date.orig.year, om: date.orig.month, od: date.orig.day }) if (typeof date.year !== 'number') Object.assign(parsed, { y: date.orig.year, m: date.orig.month, d: date.orig.day }) - } else { + } + else { Object.assign(parsed, { oy: date.year, om: date.month, od: date.day }) } break @@ -256,27 +266,30 @@ class PatternFormatter { const res: PartialDate = {} - res.m = (typeof parsed.m !== 'undefined') ? ('' + parsed.m) : '' - res.d = (typeof parsed.d !== 'undefined') ? ('' + parsed.d) : '' - res.y = (typeof parsed.y !== 'undefined') ? ('' + (parsed.y % 100)) : '' // eslint-disable-line no-magic-numbers - res.Y = (typeof parsed.y !== 'undefined') ? ('' + parsed.y) : '' - res.om = (typeof parsed.om !== 'undefined') ? ('' + parsed.om) : '' - res.od = (typeof parsed.od !== 'undefined') ? ('' + parsed.od) : '' - res.oy = (typeof parsed.oy !== 'undefined') ? ('' + (parsed.oy % 100)) : '' // eslint-disable-line no-magic-numbers - res.oY = (typeof parsed.oy !== 'undefined') ? ('' + parsed.oy) : '' + res.m = (typeof parsed.m !== 'undefined') ? (`${parsed.m}`) : '' + res.d = (typeof parsed.d !== 'undefined') ? (`${parsed.d}`) : '' + res.y = (typeof parsed.y !== 'undefined') ? (`${parsed.y % 100}`) : '' // eslint-disable-line no-magic-numbers + res.Y = (typeof parsed.y !== 'undefined') ? (`${parsed.y}`) : '' + res.om = (typeof parsed.om !== 'undefined') ? (`${parsed.om}`) : '' + res.od = (typeof parsed.od !== 'undefined') ? (`${parsed.od}`) : '' + res.oy = (typeof parsed.oy !== 'undefined') ? (`${parsed.oy % 100}`) : '' // eslint-disable-line no-magic-numbers + res.oY = (typeof parsed.oy !== 'undefined') ? (`${parsed.oy}`) : '' if (date.type !== 'verbatim') { const [ , H, M, S ] = v.match(/(?: |T)([0-9]{2}):([0-9]{2})(?::([0-9]{2}))?(?:[A-Z]+|[-+][0-9]+)?$/) || [null, '', '', ''] Object.assign(res, { H, M, S }) res.S = res.S || '' - } else { + } + else { Object.assign(res, { H: '', M: '', S: '' }) } return res } + // eslint-disable-next-line max-len /** Generates citation keys as the stock Zotero Bib(La)TeX export does. Note that this pattern inherits all the problems of the original Zotero citekey generation -- you should really only use this if you have existing papers that rely on this behavior. */ public $zotero() { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return zotero_buildCiteKey({ creators: this.item.item.getCreators(), title: this.item.item.getField('title'), @@ -288,23 +301,26 @@ class PatternFormatter { public $property(name: string) { try { return this.innerText(this.item.item.getField(name, false, true) || '') - } catch (err) {} + } + catch (err) {} try { return this.innerText(this.item.item.getField(name[0].toLowerCase() + name.slice(1), false, true) || '') - } catch (err) {} + } + catch (err) {} return '' } /** returns the name of the shared group library, or nothing if the reference is in your personal library */ - public $library() { + public $library(): string { if (this.item.item.libraryID === Zotero.Libraries.userLibraryID) return '' + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return Zotero.Libraries.get(this.item.item.libraryID).name } /** The first `N` (default: all) characters of the `M`th (default: first) author's last name. */ - public $auth(onlyEditors: boolean, withInitials:boolean, joiner: string, n?: number, m?:number) { + public $auth(onlyEditors: boolean, withInitials:boolean, joiner: string, n?: number, m?:number): string { const authors = this.creators(onlyEditors, {withInitials}) if (!authors || !authors.length) return '' let author = authors[m ? m - 1 : 0] @@ -327,7 +343,7 @@ class PatternFormatter { } /** The last name of the last author */ - public $authorLast(onlyEditors: boolean, withInitials: boolean, joiner: string) { + public $authorLast(onlyEditors: boolean, withInitials: boolean, _joiner: string) { const authors = this.creators(onlyEditors, {withInitials}) if (!authors || !authors.length) return '' return authors[authors.length - 1] @@ -336,7 +352,7 @@ class PatternFormatter { /** returns the journal abbreviation, or, if not found, the journal title, If 'automatic journal abbreviation' is enabled in the BBT settings, * it will use the same abbreviation filter Zotero uses in the wordprocessor integration. You might want to use the `abbr` filter on this. */ - public $journal() { return JournalAbbrev.get(this.item.item, true) || this.item.item.getField('publicationTitle', false, true) } + public $journal() { return JournalAbbrev.get(this.item.item, true) || this.item.item.getField('publicationTitle', false, true) } // eslint-disable-line @typescript-eslint/no-unsafe-return /** The last name of up to N authors. If there are more authors, "EtAl" is appended. */ public $authors(onlyEditors: boolean, withInitials: boolean, joiner: string, n?:number) { @@ -370,7 +386,7 @@ class PatternFormatter { default: // eslint-disable-next-line no-magic-numbers - return authors.slice(0, 3).map(author => author.substring(0, 1)).join(joiner || ' ') + '+' + return `${authors.slice(0, 3).map(author => author.substring(0, 1)).join(joiner || ' ') }+` } } @@ -388,7 +404,7 @@ class PatternFormatter { const firstAuthor = authors.shift() // eslint-disable-next-line no-magic-numbers - return [firstAuthor.substring(0, 5)].concat(authors.map(auth => auth.map(name => name.substring(0, 1)).join('.'))).join(joiner || '.') + return [firstAuthor.substring(0, 5)].concat(authors.map(name => name.substring(0, 1)).join('.')).join(joiner || '.') } /** The last name of the first two authors, and ".ea" if there are more than two. */ @@ -400,6 +416,7 @@ class PatternFormatter { return authors.slice(0, 2).concat(authors.length > 2 ? ['ea'] : []).join(joiner || '.') } + // eslint-disable-next-line max-len /** The last name of the first author, and the last name of the second author if there are two authors or "EtAl" if there are more than two. This is similar to `auth.etal`. The difference is that the authors are not separated by "." and in case of more than 2 authors "EtAl" instead of ".etal" is appended. */ public $authEtAl(onlyEditors: boolean, withInitials: boolean, joiner: string) { const authors = this.creators(onlyEditors, {withInitials}) @@ -452,12 +469,13 @@ class PatternFormatter { /** Tag number `N` */ public $keyword(n: number) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return this.item.tags = this.item.tags || this.item.item.getTags().map(tag => tag.tag).sort((a, b) => a.localeCompare(b)) return this.item.tags[n] || '' } /** The first `N` (default: 3) words of the title, apply capitalization to first `M` (default: 0) of those */ - public $shorttitle(n: number = 3, m: number = 0) { // eslint-disable-line no-magic-numbers + public $shorttitle(n: number = 3, m: number = 0) { // eslint-disable-line no-magic-numbers, @typescript-eslint/no-inferrable-types const words = this.titleWords(this.item.title, { skipWords: true, asciiOnly: true}) if (!words) return '' @@ -465,7 +483,7 @@ class PatternFormatter { } /** The first `N` (default: 1) words of the title, apply capitalization to first `M` (default: 0) of those */ - public $veryshorttitle(n: number = 1, m: number = 0) { // eslint-disable-line no-magic-numbers + public $veryshorttitle(n: number = 1, m: number = 0) { // eslint-disable-line no-magic-numbers, @typescript-eslint/no-inferrable-types return this.$shorttitle(n, m) } @@ -480,6 +498,7 @@ class PatternFormatter { } /** The date of the publication */ + // eslint-disable-next-line @typescript-eslint/no-inferrable-types public $date(format: string = '%Y-%m-%d') { return this._format_date(this.item.date, format) } @@ -496,30 +515,31 @@ class PatternFormatter { /** the month of the publication */ public $month() { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return this.months[this.item.date.m] || '' } /** Capitalize all the significant words of the title, and concatenate them. For example, `An awesome paper on JabRef` will become `AnAwesomePaperJabref` */ public $title() { return (this.titleWords(this.item.title) || []).join(' ') } - private padYear(year, length) { - return year ? year.replace(/[0-9]+/, y => y.length >= length ? y : ('0000' + y).slice(-length)): '' + private padYear(year: string, length): string { + return year ? year.replace(/[0-9]+/, y => y.length >= length ? y : (`0000${y}`).slice(-length)): '' } /** formats date as by replacing y, m and d in the format */ - public _format_date(value: string | PartialDate, format: string='%Y-%m-%d') { + public _format_date(value: string | PartialDate, format: string='%Y-%m-%d') { // eslint-disable-line @typescript-eslint/no-inferrable-types if (!value) return '' const date = (typeof value === 'string') ? this.parseDate(value) : value let keep = true - const formatted = format.split(/(%-?o?[a-z]|%%)/i).map((spec, i, arr) => { + const formatted = format.split(/(%-?o?[a-z]|%%)/i).map((spec, i) => { if ((i % 2) === 0) return spec if (spec === '%%') return '%' const pad = spec[1] !== '-' const field = spec.substring(pad ? 1 : 2) - let repl = date[field] + let repl: string = date[field] if (typeof repl !== 'string') throw new Error(`:format-date: unsupported formatter ${JSON.stringify(spec)}`) if (!repl) return null @@ -529,9 +549,10 @@ class PatternFormatter { }).filter((field, i, arr) => { if ((i % 2) === 0) { // separator, peek ahead - keep = keep && arr[i + 1] - } else { - keep = keep && field + keep = keep && !!arr[i + 1] + } + else { + keep = keep && !!field } return keep @@ -546,10 +567,10 @@ class PatternFormatter { } /** replaces text, case insensitive; `:replace=.etal,&etal` will replace `.EtAl` with `&etal` */ - public _replace(value, find: string, replace: string, mode?: 'string' | 'regex') { + public _replace(value: string, find: string, replace: string, mode?: 'string' | 'regex') { if (!find) return (value || '') log.debug(':replace', { find, value: value || '', mode}) - const re = mode === 'regex' ? find : find.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&') + const re = mode === 'regex' ? find : find.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&') log.debug(':replace', { find, value: value || '', mode}, (value || '').replace(new RegExp(re, 'ig'), replace || '')) return (value || '').replace(new RegExp(re, 'ig'), replace || '') } @@ -559,7 +580,7 @@ class PatternFormatter { * parameter, e.g `condense=_` will replace spaces with underscores. **Parameters should not contain spaces** unless * you want the spaces in the value passed in to be replaced with those spaces in the parameter */ - public _condense(value, sep: string = '') { + public _condense(value: string, sep: string = '') { // eslint-disable-line @typescript-eslint/no-inferrable-types return (value || '').replace(/\s/g, sep) } @@ -568,7 +589,7 @@ class PatternFormatter { * it is supposed to prefix isn't empty. If you want to use a reserved character (such as `:` or `\`), you'll need to * add a backslash (`\`) in front of it. */ - public _prefix(value, prefix: string) { + public _prefix(value: string, prefix: string) { value = value || '' if (value && prefix) return `${prefix}${value}` return value @@ -578,7 +599,7 @@ class PatternFormatter { * postfixes with its parameter, so `postfix=_` will add an underscore to the end if, and only if, the value * it is supposed to postfix isn't empty */ - public _postfix(value, postfix: string) { + public _postfix(value: string, postfix: string) { value = value || '' if (value && postfix) return `${value}${postfix}` return value @@ -587,17 +608,17 @@ class PatternFormatter { /** * Abbreviates the text. Only the first character and subsequent characters following white space will be included. */ - public _abbr(value) { + public _abbr(value: string): string { return (value || '').split(/\s+/).map(word => word.substring(0, 1)).join(' ') } /** Forces the text inserted by the field marker to be in lowercase. For example, `[auth:lower]` expands the last name of the first author in lowercase. */ - public _lower(value) { + public _lower(value: string): string { return (value || '').toLowerCase() } /** Forces the text inserted by the field marker to be in uppercase. For example, `[auth:upper]` expands the last name of the first author in uppercase. */ - public _upper(value) { + public _upper(value: string): string { return (value || '').toUpperCase() } @@ -610,7 +631,7 @@ class PatternFormatter { * after adding `jr` to the skipWords list. * Note that this filter is always applied if you use `title` (which is different from `Title`) or `shorttitle`. */ - public _skipwords(value) { + public _skipwords(value: string): string { return (value || '').split(/\s+/).filter(word => !this.skipWords.has(word.toLowerCase())).join(' ').trim() } @@ -619,44 +640,47 @@ class PatternFormatter { * would select the first four words. If `number` is not given, all words from `start` to the end of the list are * selected. */ - public _select(value, start: number = 1, n?: number) { - value = (value || '').split(/\s+/) - let end = value.length + // eslint-disable-next-line @typescript-eslint/no-inferrable-types + public _select(value: string, start: number = 1, n?: number): string { + const values = (value || '').split(/\s+/) + let end = values.length start -= 1 if (typeof n !== 'undefined') end = start + n - return value.slice(start, end).join(' ') + return values.slice(start, end).join(' ') } /** (`substring=start,n`) selects `n` (default: all) characters starting at `start` (default: 1) */ - public _substring(value, start: number = 1, n?: number) { + // eslint-disable-next-line @typescript-eslint/no-inferrable-types + public _substring(value: string, start: number = 1, n?: number): string { if (typeof n === 'undefined') n = value.length return (value || '').slice(start - 1, (start - 1) + n) } /** removes all non-ascii characters */ - public _ascii(value) { + public _ascii(value: string): string { return (value || '').replace(/[^ -~]/g, '').split(/\s+/).join(' ').trim() } /** clears out everything but unicode alphanumeric characters (unicode character classes `L` and `N`) */ - public _alphanum(value) { + public _alphanum(value: string): string { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return Zotero.Utilities.XRegExp.replace(value || '', this.re.alphanum, '', 'all').split(/\s+/).join(' ').trim() } /** tries to replace diacritics with ascii look-alikes. Removes non-ascii characters it cannot match */ - public _fold(value, mode?: 'german' | 'japanese') { + public _fold(value: string, mode?: 'german' | 'japanese'): string { return this.removeDiacritics(value, mode).split(/\s+/).join(' ').trim() } /** uppercases the first letter of each word */ - public _capitalize(value) { + public _capitalize(value: string): string { return (value || '').replace(/((^|\s)[a-z])/g, m => m.toUpperCase()) } /** Removes punctuation */ - public _nopunct(value) { + public _nopunct(value: string): string { value = value || '' value = Zotero.Utilities.XRegExp.replace(value, this.re.dash, '-', 'all') value = Zotero.Utilities.XRegExp.replace(value, this.re.punct, '', 'all') @@ -664,7 +688,7 @@ class PatternFormatter { } /** Removes punctuation and word-connecting dashes */ - public _nopunctordash(value) { + public _nopunctordash(value: string): string { value = value || '' value = Zotero.Utilities.XRegExp.replace(value, this.re.dash, '', 'all') value = Zotero.Utilities.XRegExp.replace(value, this.re.punct, '', 'all') @@ -672,23 +696,23 @@ class PatternFormatter { } /** Treat ideaographs as individual words */ - public _split_ideographs(value) { + public _split_ideographs(value: string): string { return (value || '').replace(script.han, ' $1 ').trim() } /** transliterates the citation key and removes unsafe characters */ - public _clean(value) { + public _clean(value: string): string { if (!value) return '' return this.clean(value) } /** transliterates the citation key */ - public _transliterate(value) { + public _transliterate(value: string): string { if (!value) return '' return this.removeDiacritics(value) } - private removeDiacritics(str, mode?: string) { + private removeDiacritics(str: string, mode?: string): string { mode = mode || this.item.language if (mode === 'japanese') mode = null @@ -715,11 +739,12 @@ class PatternFormatter { return str } - private clean(str, allow_spaces = false) { + private clean(str: string, allow_spaces = false): string { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return Zotero.Utilities.XRegExp.replace(this.removeDiacritics(str), allow_spaces ? this.re.unsafechars_allow_spaces : this.re.unsafechars, '', 'all').trim() } - private titleWords(title, options: { asciiOnly?: boolean, skipWords?: boolean} = {}) { + private titleWords(title, options: { asciiOnly?: boolean, skipWords?: boolean} = {}): string[] { if (!title) return null title = this.innerText(title) @@ -727,21 +752,21 @@ class PatternFormatter { if (options.asciiOnly && kuroshiro.enabled) title = kuroshiro.convert(title, {to: 'romaji', mode: 'spaced'}) // 551 - let words = (Zotero.Utilities.XRegExp.matchChain(title, [this.re.word]).map(word => this.clean(word).replace(/-/g, ''))) + let words: string[] = (Zotero.Utilities.XRegExp.matchChain(title, [this.re.word]).map(word => this.clean(word).replace(/-/g, ''))) - if (options.asciiOnly) words = words.map(word => word.replace(/[^ -~]/g, '')) + if (options.asciiOnly) words = words.map((word: string) => word.replace(/[^ -~]/g, '')) words = words.filter(word => word) - if (options.skipWords) words = words.filter(word => !this.skipWords.has(word.toLowerCase()) && (ucs2decode(word).length > 1) || word.match(script.han)) + if (options.skipWords) words = words.filter((word: string) => !this.skipWords.has(word.toLowerCase()) && (ucs2decode(word).length > 1) || word.match(script.han)) if (words.length === 0) return null return words } - private innerText(str) { + private innerText(str: string): string { if (!str) return '' return this.DOMParser.parseFromString(`${str}`, 'text/html').documentElement.textContent } - private stripQuotes(name) { + private stripQuotes(name: string): string { if (!name) return '' if (name.length >= 2 && name[0] === '"' && name[name.length - 1] === '"') return name.slice(1, -1) return name @@ -755,61 +780,60 @@ class PatternFormatter { let initial, m if (m = firstName.match(/(.+)\u0097/)) { initial = m[1] - } else { + } + else { initial = firstName[0] } return this.removeDiacritics(initial) } - private creators(onlyEditors, options: { initialOnly?: boolean, withInitials?: boolean} = {}) { - const format = `creators${options.initialOnly ? '_io' : ''}${options.initialOnly ? '_wi' : ''}` - let creators = this.item[format] - if (!creators) { - let types = Zotero.CreatorTypes.getTypesForItemType(this.item.item.itemTypeID) - types = types.reduce((map, type) => { map[type.name] = type.id; return map }, {}) - const primary = Zotero.CreatorTypes.getPrimaryIDForType(this.item.item.itemTypeID) + private creators(onlyEditors, options: { initialOnly?: boolean, withInitials?: boolean} = {}): string[] { + let types = Zotero.CreatorTypes.getTypesForItemType(this.item.item.itemTypeID) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + types = types.reduce((map, type) => { map[type.name] = type.id; return map }, {}) + const primary = Zotero.CreatorTypes.getPrimaryIDForType(this.item.item.itemTypeID) - creators = this.item[format] = {} + const creators: Record = {} - for (const creator of this.item.item.getCreators()) { - if (onlyEditors && ![types.editor, types.seriesEditor].includes(creator.creatorTypeID)) continue + for (const creator of this.item.item.getCreators()) { + if (onlyEditors && ![types.editor, types.seriesEditor].includes(creator.creatorTypeID)) continue - let name = options.initialOnly ? this.initial(creator) : this.stripQuotes(this.innerText(creator.lastName)) - if (name) { - if (options.withInitials && creator.firstName) { - let initials = Zotero.Utilities.XRegExp.replace(this.stripQuotes(creator.firstName), this.re.caseNotUpperTitle, '', 'all') - initials = this.removeDiacritics(initials) - initials = Zotero.Utilities.XRegExp.replace(initials, this.re.caseNotUpper, '', 'all') - name += initials - } - } else { - name = this.stripQuotes(this.innerText(creator.firstName)) + let name = options.initialOnly ? this.initial(creator) : this.stripQuotes(this.innerText(creator.lastName)) + if (name) { + if (options.withInitials && creator.firstName) { + let initials = Zotero.Utilities.XRegExp.replace(this.stripQuotes(creator.firstName), this.re.caseNotUpperTitle, '', 'all') + initials = this.removeDiacritics(initials) + initials = Zotero.Utilities.XRegExp.replace(initials, this.re.caseNotUpper, '', 'all') + name += initials } + } + else { + name = this.stripQuotes(this.innerText(creator.firstName)) + } - if (!name) continue + if (!name) continue - switch (creator.creatorTypeID) { - case types.editor: - case types.seriesEditor: - creators.editors = creators.editors || [] - creators.editors.push(name) - break + switch (creator.creatorTypeID) { + case types.editor: + case types.seriesEditor: + creators.editors = creators.editors || [] + creators.editors.push(name) + break - case types.translator: - creators.translators = creators.translators || [] - creators.translators.push(name) - break + case types.translator: + creators.translators = creators.translators || [] + creators.translators.push(name) + break - case primary: - creators.authors = creators.authors || [] - creators.authors.push(name) - break + case primary: + creators.authors = creators.authors || [] + creators.authors.push(name) + break - default: - creators.collaborators = creators.collaborators || [] - creators.collaborators.push(name) - } + default: + creators.collaborators = creators.collaborators || [] + creators.collaborators.push(name) } } @@ -819,4 +843,4 @@ class PatternFormatter { } // export singleton: https://k94n.com/es6-modules-single-instance-pattern -export let Formatter = new PatternFormatter // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match +export const Formatter = new PatternFormatter // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match diff --git a/content/key-manager/kuroshiro.ts b/content/key-manager/kuroshiro.ts index 8c39581e50..9d11501370 100644 --- a/content/key-manager/kuroshiro.ts +++ b/content/key-manager/kuroshiro.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import Kuroshiro from 'kuroshiro/src/core-sync' import _kuromojiLoader = require('kuromoji/src/loader/NodeDictionaryLoader') import { log } from '../logger' @@ -25,7 +26,7 @@ _kuromojiLoader.prototype.loadArrayBuffer = function(url, callback) { // eslint- xhr.send() } -export let kuroshiro = new class { +export const kuroshiro = new class { public enabled = false private kuroshiro: any @@ -35,7 +36,8 @@ export let kuroshiro = new class { try { this.kuroshiro = new Kuroshiro() await this.kuroshiro.init(new KuromojiAnalyzer('resource://zotero-better-bibtex/kuromoji')) - } catch (err) { + } + catch (err) { log.error('kuroshiro: initializing failed') throw err } @@ -43,7 +45,7 @@ export let kuroshiro = new class { this.enabled = true } - public convert(str, options) { + public convert(str: string, options): string { if (!this.enabled) throw new Error('kuroshiro not initialized') if (str && Kuroshiro.Util.hasJapanese(str)) return this.kuroshiro.convert(str, options) return str diff --git a/content/library.ts b/content/library.ts index b767087ef9..7237e18ee9 100644 --- a/content/library.ts +++ b/content/library.ts @@ -1,4 +1,8 @@ -export function get(name) { +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +declare const Zotero: any + +export function get(name: string) { if (!name) return Zotero.Libraries.get(Zotero.Libraries.userLibraryID) const libraries = Zotero.Libraries.getAll().filter(lib => lib.name === name) diff --git a/content/logger.ts b/content/logger.ts index a22bea743a..4cf7bbc95c 100644 --- a/content/logger.ts +++ b/content/logger.ts @@ -14,23 +14,26 @@ class Logger { this.timestamp = now if (typeof msg !== 'string') { - let _msg = '' + let output = '' for (const m of msg) { const type = typeof m if (type === 'string' || m instanceof String || type === 'number' || type === 'undefined' || type === 'boolean' || m === null) { - _msg += m - } else if (m instanceof Error) { - _msg += `` - } else if (m && type === 'object' && m.message) { // mozilla exception, no idea on the actual instance type + output += m + } + else if (m instanceof Error) { + output += `` + } + else if (m && type === 'object' && m.message) { // mozilla exception, no idea on the actual instance type // message,fileName,lineNumber,column,stack,errorCode - _msg += `` - } else { - _msg += stringify(m) + output += `` + } + else { + output += stringify(m) } - _msg += ' ' + output += ' ' } - msg = _msg + msg = output } const translator = typeof Translator !== 'undefined' && Translator.header.label diff --git a/content/markupparser.ts b/content/markupparser.ts index cc108493f4..d71c3afe7c 100644 --- a/content/markupparser.ts +++ b/content/markupparser.ts @@ -33,7 +33,9 @@ const re = { whitespace: null, } +// eslint-disable-next-line @typescript-eslint/restrict-plus-operands re.lcChar = re.Ll + re.Lt + re.Lm + re.Lo + re.Mn + re.Mc + re.Nd + re.Nl +// eslint-disable-next-line @typescript-eslint/restrict-plus-operands re.char = re.Lu + re.lcChar re.protectedWord = `[${re.lcChar}]*[${re.Lu}][${re.char}]*` @@ -75,14 +77,14 @@ const ligatures = { // export singleton: https://k94n.com/es6-modules-single-instance-pattern type HTMLParserOptions = { - html?: boolean, + html?: boolean caseConversion?: boolean exportBraceProtection: boolean - csquotes: string, + csquotes: string exportTitleCase: boolean } -export let HTMLParser = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match +export const HTMLParser = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match private caseConversion: boolean private braceProtection: boolean private sentenceStart: boolean @@ -94,7 +96,7 @@ export let HTMLParser = new class { // eslint-disable-line @typescript-eslint/na public parse(html, options: HTMLParserOptions): IZoteroMarkupNode { this.html = html - let doc + let doc: IZoteroMarkupNode this.caseConversion = options.caseConversion this.braceProtection = options.caseConversion && options.exportBraceProtection @@ -105,7 +107,7 @@ export let HTMLParser = new class { // eslint-disable-line @typescript-eslint/na if (csquotes) { const space = '\\s*' for (const close of [0, 1]) { - const chars = csquotes.replace(/./g, (c, i) => [c, ''][(i + close) & 1]).replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]\s*/g, '\\$&') // eslint-disable-line no-bitwise + const chars = csquotes.replace(/./g, (c: string, i: number) => [c, ''][(i + close) & 1]).replace(/[-[\]/{}()*+?.\\^$|]\s*/g, '\\$&') // eslint-disable-line no-bitwise this.html = this.html.replace(new RegExp(`${close ? space : ''}[${chars}]${close ? '' : space}`, 'g'), close ? '' : '') } } @@ -136,11 +138,12 @@ export let HTMLParser = new class { // eslint-disable-line @typescript-eslint/na this.titleCase(doc) } - doc = this.unwrapNocase(doc) - if (doc.length === 1) { - doc = doc[0] - } else { - doc = { nodeName: 'span', attr: {}, class: {}, childNodes: doc } + const unwrapped = this.unwrapNocase(doc) + if (unwrapped.length === 1) { + doc = unwrapped[0] + } + else { + doc = { nodeName: 'span', attr: {}, class: {}, childNodes: unwrapped } } this.cleanupNocase(doc) } @@ -236,11 +239,12 @@ export let HTMLParser = new class { // eslint-disable-line @typescript-eslint/na private plaintext(childNodes: IZoteroMarkupNode[], text, offset) { // replace ligatures so titlecasing works for things like "figures" - text = text.replace(this.ligatures, ligature => ligatures[ligature]) + text = text.replace(this.ligatures, (ligature: string) => (ligatures[ligature] as string)) const l = childNodes.length if (l === 0 || (childNodes[l - 1].nodeName !== '#text')) { childNodes.push({ nodeName: '#text', offset, value: text, attr: {}, class: {} }) - } else { + } + else { childNodes[l - 1].value += text } } @@ -263,69 +267,72 @@ export let HTMLParser = new class { // eslint-disable-line @typescript-eslint/na private walk(node, isNocased = false) { // debug('walk:', node.nodeName) - const _node: IZoteroMarkupNode = { nodeName: node.nodeName, childNodes: [], attr: {}, class: {} } + const normalized_node: IZoteroMarkupNode = { nodeName: node.nodeName, childNodes: [], attr: {}, class: {} } for (const {name, value} of (node.attrs || [])) { - _node.attr[name] = value + normalized_node.attr[name] = value } - for (const cls of (_node.attr.class || '').trim().split(/\s+/)) { - if (cls) _node.class[cls] = true + for (const cls of (normalized_node.attr.class || '').trim().split(/\s+/)) { + if (cls) normalized_node.class[cls] = true } switch (node.type?.toLowerCase()) { case 'smallcaps': - _node.attr.smallcaps = 'smallcaps' + normalized_node.attr.smallcaps = 'smallcaps' break } - if (node.type) _node.class[node.type] = true + if (node.type) normalized_node.class[node.type] = true switch (node.nodeName) { case '#document': case '#document-fragment': case 'pre': - _node.nodeName = 'span' + normalized_node.nodeName = 'span' break case 'nc': - _node.nodeName = 'span' - _node.attr.nocase = 'nocase' + normalized_node.nodeName = 'span' + normalized_node.attr.nocase = 'nocase' break case 'emphasis': - _node.nodeName = 'i' + normalized_node.nodeName = 'i' break case 'sc': - _node.nodeName = 'span' - _node.attr.smallcaps = 'smallcaps' + normalized_node.nodeName = 'span' + normalized_node.attr.smallcaps = 'smallcaps' break } - if (_node.attr.nocase || _node.class.nocase) _node.nocase = !isNocased - if (_node.attr.relax || _node.class.relax) _node.relax = true - if (_node.class.enquote || _node.attr.enquote) _node.enquote = true - if (!_node.attr.smallcaps && (_node.attr.style || '').match(/small-caps/i)) _node.attr.smallcaps = 'smallcaps' - if (_node.class.smallcaps || _node.attr.smallcaps) _node.smallcaps = true + if (normalized_node.attr.nocase || normalized_node.class.nocase) normalized_node.nocase = !isNocased + if (normalized_node.attr.relax || normalized_node.class.relax) normalized_node.relax = true + if (normalized_node.class.enquote || normalized_node.attr.enquote) normalized_node.enquote = true + if (!normalized_node.attr.smallcaps && (normalized_node.attr.style || '').match(/small-caps/i)) normalized_node.attr.smallcaps = 'smallcaps' + if (normalized_node.class.smallcaps || normalized_node.attr.smallcaps) normalized_node.smallcaps = true - if (_node.nodeName === 'script') { + if (normalized_node.nodeName === 'script') { if (!node.childNodes || node.childNodes.length === 0) { - _node.value = '' - _node.childNodes = [] - } else if (node.childNodes.length === 1 && node.childNodes[0].nodeName === '#text') { - _node.value = node.childNodes[0].value - _node.childNodes = [] - } else { + normalized_node.value = '' + normalized_node.childNodes = [] + } + else if (node.childNodes.length === 1 && node.childNodes[0].nodeName === '#text') { + normalized_node.value = node.childNodes[0].value + normalized_node.childNodes = [] + } + else { throw new Error(`Unexpected script body ${JSON.stringify(node)}`) } - } else if (node.childNodes) { + } + else if (node.childNodes) { let m for (const child of node.childNodes) { if (child.nodeName !== '#text') { - _node.childNodes.push(this.walk(child, isNocased || _node.nocase)) + normalized_node.childNodes.push(this.walk(child, isNocased || normalized_node.nocase)) continue } if (!this.caseConversion || isNocased) { - this.plaintext(_node.childNodes, child.value, child.sourceCodeLocation.startOffset) + this.plaintext(normalized_node.childNodes, child.value, child.sourceCodeLocation.startOffset) continue } @@ -333,14 +340,16 @@ export let HTMLParser = new class { // eslint-disable-line @typescript-eslint/na const length = text.length while (text) { if (m = re.whitespace.exec(text)) { - this.plaintext(_node.childNodes, m[0], child.sourceCodeLocation.startOffset + (length - text.length)) + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + this.plaintext(normalized_node.childNodes, m[0], child.sourceCodeLocation.startOffset + (length - text.length)) text = text.substring(m[0].length) continue } - if (this.sentenceStart && (m = re.leadingUnprotectedWord.exec(text + ' '))) { + if (this.sentenceStart && (m = re.leadingUnprotectedWord.exec(`${text} `))) { this.sentenceStart = false - this.plaintext(_node.childNodes, m[1], child.sourceCodeLocation.startOffset + (length - text.length)) + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + this.plaintext(normalized_node.childNodes, m[1], child.sourceCodeLocation.startOffset + (length - text.length)) text = text.substring(m[1].length) continue } @@ -348,25 +357,32 @@ export let HTMLParser = new class { // eslint-disable-line @typescript-eslint/na this.sentenceStart = false if (!isNocased && this.braceProtection && (m = re.protectedWords.exec(text))) { - this.nocase(_node.childNodes, m[0], child.sourceCodeLocation.startOffset + (length - text.length)) + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + this.nocase(normalized_node.childNodes, m[0], child.sourceCodeLocation.startOffset + (length - text.length)) text = text.substring(m[0].length) - } else if (m = re.url.exec(text)) { - this.nocase(_node.childNodes, m[0], child.sourceCodeLocation.startOffset + (length - text.length)) + } + else if (m = re.url.exec(text)) { + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + this.nocase(normalized_node.childNodes, m[0], child.sourceCodeLocation.startOffset + (length - text.length)) text = text.substring(m[0].length) - } else if (m = re.unprotectedWord.exec(text)) { - this.plaintext(_node.childNodes, m[0], child.sourceCodeLocation.startOffset + (length - text.length)) + } + else if (m = re.unprotectedWord.exec(text)) { + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + this.plaintext(normalized_node.childNodes, m[0], child.sourceCodeLocation.startOffset + (length - text.length)) text = text.substring(m[0].length) - } else { - this.plaintext(_node.childNodes, text[0], child.sourceCodeLocation.startOffset + (length - text.length)) + } + else { + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + this.plaintext(normalized_node.childNodes, text[0], child.sourceCodeLocation.startOffset + (length - text.length)) text = text.substring(1) } } } } - return _node + return normalized_node } } diff --git a/content/monkey-patch.ts b/content/monkey-patch.ts index c24d7f77f6..9e5423c2ae 100644 --- a/content/monkey-patch.ts +++ b/content/monkey-patch.ts @@ -6,21 +6,22 @@ import { flash } from './flash' import { client } from './client' import * as min_version from '../gen/min-version.json' -export function clean_pane_persist() { +export function clean_pane_persist(): void { let persisted = Zotero.Prefs.get('pane.persist') if (persisted) { try { persisted = JSON.parse(persisted) delete persisted['zotero-items-column-citekey'] Zotero.Prefs.set('pane.persist', JSON.stringify(persisted)) - } catch (err) { + } + catch (err) { Zotero.logError(err) } } } const versionCompare = Components.classes['@mozilla.org/xpcom/version-comparator;1'].getService(Components.interfaces.nsIVersionComparator) -export let enabled = versionCompare.compare(Zotero.version.replace('m', '.').replace(/-beta.*/, ''), min_version[client].replace('m', '.')) >= 0 +export const enabled = versionCompare.compare(Zotero.version.replace('m', '.').replace(/-beta.*/, ''), min_version[client].replace('m', '.')) >= 0 Zotero.debug(`monkey-patch: ${Zotero.version}: BBT ${enabled ? 'en' : 'dis'}abled`) if (!enabled) { @@ -29,7 +30,7 @@ if (!enabled) { Components.utils.import('resource://gre/modules/AddonManager.jsm') AddonManager.getAddonByID('better-bibtex@iris-advies.com', addon => { addon.userDisabled = true }) - /* + /* // Add-on cannot be uninstalled if (!(addon.permissions & AddonManager.PERM_CAN_UNINSTALL)) return // tslint:disable-line:no-bitwise @@ -45,13 +46,15 @@ if (!enabled) { const marker = 'BetterBibTeXMonkeyPatched' -export function repatch(object, method, patcher) { +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/ban-types +export function repatch(object: any, method: string, patcher: ((Function) => Function)): void { if (!enabled) return object[method] = patcher(object[method]) object[method][marker] = true } -export function patch(object, method, patcher) { +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/ban-types +export function patch(object: any, method: string, patcher: ((Function) => Function)): void { if (!enabled) return if (object[method][marker]) throw new Error(`${method} re-patched`) repatch(object, method, patcher) diff --git a/content/path-search.ts b/content/path-search.ts index e901ebd256..a9489f0d6a 100644 --- a/content/path-search.ts +++ b/content/path-search.ts @@ -13,13 +13,14 @@ function permutations(word) { for (const [i, c] of word.split('').entries()) { config.charactersAt[i] = [ c.toUpperCase(), c.toLowerCase() ] } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return permutater(config) } const alias: { [key: string]: string } = {} -function getEnv(variable) { +function getEnv(variable): string { const ENV = Components.classes['@mozilla.org/process/environment;1'].getService(Components.interfaces.nsIEnvironment) - const value = ENV.get(variable) + const value: string = ENV.get(variable) if (value || !Zotero.isWin) return value if (typeof alias[variable] === 'undefined') { @@ -33,10 +34,11 @@ function getEnv(variable) { } if (!alias[variable]) return '' + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return ENV.get(alias[variable]) } -function expandWinVars(value) { +function expandWinVars(value: string): string { let more = true while (more) { more = false @@ -49,8 +51,8 @@ function expandWinVars(value) { } // https://searchfox.org/mozilla-central/source/toolkit/modules/subprocess/subprocess_win.jsm#135 doesn't seem to work on Windows. -export async function pathSearch(bin, installationDirectory: { mac?: string, win?: string } = {}) { - const env = { +export async function pathSearch(bin: string, installationDirectory: { mac?: string, win?: string } = {}): Promise { + const env: {path: string[], pathext: string[], sep: string} = { path: [], pathext: [], sep: '', @@ -69,12 +71,14 @@ export async function pathSearch(bin, installationDirectory: { mac?: string, win return null } - } else { + } + else { const ENV = Components.classes['@mozilla.org/process/environment;1'].getService(Components.interfaces.nsIEnvironment) env.sep = '/' env.path = [] if (Zotero.isMac && installationDirectory.mac) env.path.push(installationDirectory.mac) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return env.path = env.path.concat((ENV.get('PATH') || '').split(':').filter(p => p)) env.pathext = [''] @@ -93,18 +97,21 @@ export async function pathSearch(bin, installationDirectory: { mac?: string, win const cmd = OS.Path.join(path, bin + pathext) if (!(await OS.File.exists(cmd))) continue + // eslint-disable-next-line @typescript-eslint/await-thenable const stat = await OS.File.stat(cmd) if (stat.isDir) continue // eslint-disable-next-line no-bitwise, no-magic-numbers if (!Zotero.isWin && (stat.unixMode & 111) === 0) { // bit iffy -- we don't know if *we* can execute this. + // eslint-disable-next-line no-magic-numbers log.debug(`pathSearch: ${cmd} exists but has mode ${(stat.unixMode).toString(8)}`) continue } log.debug(`pathSearch: ${bin} found at ${cmd}`) return cmd - } catch (err) { + } + catch (err) { log.error('pathSearch:', err) } } diff --git a/content/prefs.ts b/content/prefs.ts index 4a5d8e6056..658939c5f7 100644 --- a/content/prefs.ts +++ b/content/prefs.ts @@ -7,7 +7,7 @@ import * as defaults from '../gen/preferences/defaults.json' const supported = Object.keys(defaults) // export singleton: https://k94n.com/es6-modules-single-instance-pattern -export let Preferences = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match +export const Preferences = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match public branch: any public testing: boolean public platform: 'win' | 'lin' | 'mac' | 'unix' @@ -33,7 +33,8 @@ export let Preferences = new class { // eslint-disable-line @typescript-eslint/n if (typeof (old = Zotero.Prefs.get(key = this.key('suppressSentenceCase'))) !== 'undefined') { if (old) { Zotero.Prefs.set(this.key('importSentenceCase'), 'off') - } else { + } + else { Zotero.Prefs.set(this.key('importSentenceCase'), 'on+guess') } Zotero.Prefs.clear(key) @@ -41,7 +42,8 @@ export let Preferences = new class { // eslint-disable-line @typescript-eslint/n if (typeof (old = Zotero.Prefs.get(key = this.key('suppressNoCase'))) !== 'undefined') { if (old) { Zotero.Prefs.set(this.key('importCaseProtection'), 'off') - } else { + } + else { Zotero.Prefs.set(this.key('importCaseProtection'), 'as-needed') } Zotero.Prefs.clear(key) @@ -56,7 +58,7 @@ export let Preferences = new class { // eslint-disable-line @typescript-eslint/n if (typeof this.get(name) === 'undefined') this.set(name, value); (pref => { - Zotero.Prefs.registerObserver(`${this.prefix}.${pref}`, newValue => { + Zotero.Prefs.registerObserver(`${this.prefix}.${pref}`, _newValue => { Events.emit('preference-changed', pref) }) })(name) @@ -73,15 +75,18 @@ export let Preferences = new class { // eslint-disable-line @typescript-eslint/n public get(pref) { if (this.testing && !supported.includes(pref)) throw new Error(`Getting unsupported preference "${pref}"`) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return Zotero.Prefs.get(this.key(pref)) } public clear(pref) { try { Zotero.Prefs.clear(this.key(pref)) - } catch (err) { + } + catch (err) { log.error('Prefs.clear', pref, err) } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return this.get(pref) } diff --git a/content/pull-export.ts b/content/pull-export.ts index 6a745d885c..ee75393aa8 100644 --- a/content/pull-export.ts +++ b/content/pull-export.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ declare const Zotero: any const OK = 200 @@ -39,8 +40,9 @@ Zotero.Server.Endpoints['/better-bibtex/export/collection'] = Zotero.Server.Endp return [ OK, 'text/plain', await Translators.exportItems(Translators.getTranslatorId(translator), displayOptions(request), { type: 'collection', collection }) ] - } catch (err) { - return [{ notfound: NOT_FOUND, duplicate: CONFLICT, error: SERVER_ERROR}[err.kind || 'error'], 'text/plain', '' + err] + } + catch (err) { + return [{ notfound: NOT_FOUND, duplicate: CONFLICT, error: SERVER_ERROR}[err.kind || 'error'], 'text/plain', `${err}`] } } } @@ -61,8 +63,9 @@ Zotero.Server.Endpoints['/better-bibtex/export/library'] = Zotero.Server.Endpoin return [OK, 'text/plain', await Translators.exportItems(Translators.getTranslatorId(translator), displayOptions(request), { type: 'library', id: libID }) ] - } catch (err) { - return [SERVER_ERROR, 'text/plain', '' + err] + } + catch (err) { + return [SERVER_ERROR, 'text/plain', `${err}`] } } } @@ -86,8 +89,9 @@ Zotero.Server.Endpoints['/better-bibtex/export/selected'] = Zotero.Server.Endpoi } return [OK, 'text/plain', await Translators.exportItems(Translators.getTranslatorId(translator), displayOptions(request), { type: 'items', items }) ] - } catch (err) { - return [SERVER_ERROR, 'text/plain', '' + err] + } + catch (err) { + return [SERVER_ERROR, 'text/plain', `${err}`] } } } @@ -106,23 +110,28 @@ Zotero.Server.Endpoints['/better-bibtex/export/item'] = class { if ((isSet(libraryID) + isSet(library) + isSet(groupID) + isSet(group)) > 1) { return [BAD_REQUEST, 'text/plain', 'specify at most one of library(/ID) or group(/ID)' ] - } else if (libraryID) { + } + else if (libraryID) { if (!libraryID.match(/^[0-9]+$/)) return [BAD_REQUEST, 'text/plain', `${libraryID} is not a number` ] libraryID = parseInt(libraryID) - } else if (groupID) { + } + else if (groupID) { if (!groupID.match(/^[0-9]+$/)) return [BAD_REQUEST, 'text/plain', `${libraryID} is not a number` ] try { groupID = parseInt(groupID) libraryID = Zotero.Groups.getAll().find(g => g.groupID === groupID).libraryID - } catch (err) { + } + catch (err) { libraryID = null } - } else if (library || group) { + } + else if (library || group) { libraryID = getLibrary(library || group).libraryID - } else { + } + else { libraryID = Zotero.Libraries.userLibraryID } @@ -157,19 +166,19 @@ Zotero.Server.Endpoints['/better-bibtex/export/item'] = class { let contents = await Translators.exportItems(translatorID, displayOptions(request), { type: 'items', items: Object.values(items) }) if (pandocFilterData) { - let _items + let filtered_items switch (Translators.byId[translatorID]?.label) { case 'Better CSL JSON': - _items = JSON.parse(contents) + filtered_items = JSON.parse(contents) break case 'BetterBibTeX JSON': - _items = JSON.parse(contents).items + filtered_items = JSON.parse(contents).items break default: throw new Error(`Unexpected translator ${translatorID}`) } - for (const item of _items) { + for (const item of filtered_items) { // jzon gives citationKey, CSL gives id const citekey = item.citationKey || item.id response.items[citekey] = item @@ -183,8 +192,9 @@ Zotero.Server.Endpoints['/better-bibtex/export/item'] = class { } return [OK, 'text/plain', contents ] - } catch (err) { - return [SERVER_ERROR, 'text/plain', '' + err] + } + catch (err) { + return [SERVER_ERROR, 'text/plain', `${err}`] } } } diff --git a/content/qr-check.ts b/content/qr-check.ts index 568f9bcf2d..e47a97ee31 100644 --- a/content/qr-check.ts +++ b/content/qr-check.ts @@ -5,7 +5,7 @@ import * as DateParser from './dateparser' const ismn_prefix = '9790' -export function qualityReport(value, test, params = null) { +export function qualityReport(value: string, test: string, params = null): string { switch (test) { case 'isbn': return IsISBN(value.replace(/-/g, '')) ? '' : 'not a valid ISBN' diff --git a/content/scheduler.ts b/content/scheduler.ts index 298393920c..591bd87430 100644 --- a/content/scheduler.ts +++ b/content/scheduler.ts @@ -9,7 +9,7 @@ export class Scheduler { private handlers: Map = new Map private held: Map = null - constructor(delay: string | number, factor: number = 1) { + constructor(delay: string | number, factor = 1) { this._delay = delay this.factor = factor } @@ -18,11 +18,11 @@ export class Scheduler { return (typeof this._delay === 'string' ? Prefs.get(this._delay) : this._delay) * this.factor } - public get enabled() { + public get enabled(): boolean { return this.delay !== 0 } - public get paused() { + public get paused(): boolean { return this.held !== null } @@ -31,7 +31,8 @@ export class Scheduler { if (paused) { this.held = new Map - } else { + } + else { const held = this.held this.held = null @@ -41,19 +42,21 @@ export class Scheduler { } } - public schedule(id: number, handler: Handler) { + public schedule(id: number, handler: Handler): void { if (this.held) { this.held.set(id, handler) - } else { + } + else { if (this.handlers.has(id)) clearTimeout(this.handlers.get(id)) this.handlers.set(id, setTimeout(handler, this.delay)) } } - public cancel(id: number) { + public cancel(id: number): void { if (this.held) { this.held.delete(id) - } else if (this.handlers.has(id)) { + } + else if (this.handlers.has(id)) { clearTimeout(this.handlers.get(id)) this.handlers.delete(id) } diff --git a/content/serializer.ts b/content/serializer.ts index f7198165c9..aa114277f0 100644 --- a/content/serializer.ts +++ b/content/serializer.ts @@ -7,12 +7,15 @@ import { DB as Cache } from './db/cache' import { KeyManager } from './key-manager' // export singleton: https://k94n.com/es6-modules-single-instance-pattern -export let Serializer = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match +export const Serializer = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match private cache public init() { - JournalAbbrev.init() - this.cache = Cache.getCollection('itemToExportFormat') + JournalAbbrev.init().then(() => { + this.cache = Cache.getCollection('itemToExportFormat') + }).catch(err => { + Zotero.debug(`Serializer.init failed: ${err.message}`) + }) } private fetch(item) { @@ -21,19 +24,23 @@ export let Serializer = new class { // eslint-disable-line @typescript-eslint/na const cached = this.cache.findOne({ itemID: item.id }) if (!cached) return null + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return this.enrich(cached.item, item) } private store(item, serialized) { if (this.cache) { this.cache.insert({ itemID: item.id, item: serialized }) - } else { + } + else { Zotero.debug('Serializer.store ignored, DB not yet loaded') } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return this.enrich(serialized, item) } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return public serialize(item) { return Zotero.Utilities.Internal.itemToExportFormat(item, false, true) } public fast(item, count?: { cached: number }) { @@ -41,7 +48,8 @@ export let Serializer = new class { // eslint-disable-line @typescript-eslint/na if (serialized) { if (count) count.cached += 1 - } else { + } + else { serialized = item.toJSON() serialized.uri = Zotero.URI.getItemURI(item) serialized.itemID = item.id @@ -57,11 +65,13 @@ export let Serializer = new class { // eslint-disable-line @typescript-eslint/na default: serialized.attachments = item.getAttachments().map(id => { const att = Zotero.Items.get(id) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return this.fastAttachment({ ...att.toJSON(), uri: Zotero.URI.getItemURI(att) }, att) }) serialized.notes = item.getNotes().map(id => { const note = Zotero.Items.get(id) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { ...note.toJSON(), uri: Zotero.URI.getItemURI(note) } }) } @@ -69,6 +79,7 @@ export let Serializer = new class { // eslint-disable-line @typescript-eslint/na } // since the cache doesn't clone, these will be written into the cache, but since we override them always anyways, that's OK + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return this.enrich(serialized, item) } @@ -77,6 +88,7 @@ export let Serializer = new class { // eslint-disable-line @typescript-eslint/na serialized.localPath = att.getFilePath() if (serialized.localPath) serialized.defaultPath = `files/${att.id}/${OS.Path.basename(serialized.localPath)}` } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return serialized } @@ -97,6 +109,8 @@ export let Serializer = new class { // eslint-disable-line @typescript-eslint/na serialized.itemID = item.id serialized.key = item.key serialized.libraryID = item.libraryID + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return serialized } } diff --git a/content/sleep.ts b/content/sleep.ts index 4072bd99e8..15c43db729 100644 --- a/content/sleep.ts +++ b/content/sleep.ts @@ -1,6 +1,6 @@ // declare const Zotero: any -export function sleep(ms) { +export function sleep(ms: number): Promise { // return new Promise(resolve => Zotero.setTimeout(resolve, ms)) return new Promise(resolve => setTimeout(resolve, ms)) } diff --git a/content/stringify.ts b/content/stringify.ts index cde3aa86e7..312f9a4e4c 100644 --- a/content/stringify.ts +++ b/content/stringify.ts @@ -1,11 +1,12 @@ // import _stringify from 'fast-safe-stringify' import _stringify from 'safe-stable-stringify' -export function asciify(str) { - return str.replace(/[\u007F-\uFFFF]/g, chr => '\\u' + ('0000' + chr.charCodeAt(0).toString(16)).substr(-4)) // eslint-disable-line no-magic-numbers +export function asciify(str: string): string { + return str.replace(/[\u007F-\uFFFF]/g, chr => `\\u${(`0000${chr.charCodeAt(0).toString(16)}`).substr(-4)}`) // eslint-disable-line no-magic-numbers } -export function stringify(obj, replacer?, indent?, ucode?) { +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function stringify(obj: any, replacer?: any, indent?: string | number, ucode?: boolean): string { const stringified = _stringify(obj, replacer, indent) return ucode ? asciify(stringified) : stringified diff --git a/content/tex-studio.ts b/content/tex-studio.ts index 7442e1b587..e9437a9c12 100644 --- a/content/tex-studio.ts +++ b/content/tex-studio.ts @@ -5,7 +5,7 @@ import { KeyManager } from './key-manager' import { log } from './logger' // export singleton: https://k94n.com/es6-modules-single-instance-pattern -export let TeXstudio = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match +export const TeXstudio = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match public enabled: boolean public texstudio: string @@ -14,7 +14,8 @@ export let TeXstudio = new class { // eslint-disable-line @typescript-eslint/nam this.enabled = !!this.texstudio if (this.enabled) { log.debug('TeXstudio: found at', this.texstudio) - } else { + } + else { log.debug('TeXstudio: not found') } } @@ -26,8 +27,10 @@ export let TeXstudio = new class { // eslint-disable-line @typescript-eslint/nam try { const pane = Zotero.getActiveZoteroPane() // can Zotero 5 have more than one pane at all? const items = pane.getSelectedItems() + // eslint-disable-next-line @typescript-eslint/no-unsafe-return citation = items.map(item => KeyManager.get(item.id).citekey).filter(citekey => citekey).join(',') - } catch (err) { // zoteroPane.getSelectedItems() doesn't test whether there's a selection and errors out if not + } + catch (err) { // zoteroPane.getSelectedItems() doesn't test whether there's a selection and errors out if not log.error('TeXstudio: Could not get selected items:', err) return } @@ -37,7 +40,8 @@ export let TeXstudio = new class { // eslint-disable-line @typescript-eslint/nam try { await Zotero.Utilities.Internal.exec(this.texstudio, ['--insert-cite', citation]) - } catch (err) { + } + catch (err) { log.error('TeXstudio: Could not get execute texstudio:', err) } } diff --git a/content/translators.ts b/content/translators.ts index 649be32386..8889324221 100644 --- a/content/translators.ts +++ b/content/translators.ts @@ -1,3 +1,6 @@ +/* eslint-disable no-case-declarations */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ + declare const Zotero: any declare const Components: any @@ -13,7 +16,6 @@ import { Serializer } from './serializer' import { log } from './logger' import { DB as Cache, selector as cacheSelector } from './db/cache' import { DB } from './db/main' -// import * as Extra from './extra' import { sleep } from './sleep' import { flash } from './flash' @@ -54,7 +56,7 @@ type ExportJob = { } // export singleton: https://k94n.com/es6-modules-single-instance-pattern -export let Translators = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match +export const Translators = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match public byId: Record public byName: Record public byLabel: Record @@ -102,6 +104,7 @@ export let Translators = new class { // eslint-disable-line @typescript-eslint/n Zotero.BetterBibTeX.getString('BetterBibTeX.startup.installingTranslators.new.DnD'), // text // button flags + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING + ps.BUTTON_POS_0_DEFAULT + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_IS_STRING, @@ -123,25 +126,26 @@ export let Translators = new class { // eslint-disable-line @typescript-eslint/n try { await Zotero.Translators.reinit() - } catch (err) { + } + catch (err) { log.error('Translator.inits: reinit failed @', (new Date()).valueOf() - start, err) } } } public getTranslatorId(name) { - const _name = name.toLowerCase() + const name_lc = name.toLowerCase() // shortcuts - if (_name === 'jzon') return Translators.byLabel.BetterBibTeXJSON.translatorID - if (_name === 'bib') return Translators.byLabel.BetterBibLaTeX.translatorID + if (name_lc === 'jzon') return Translators.byLabel.BetterBibTeXJSON.translatorID + if (name_lc === 'bib') return Translators.byLabel.BetterBibLaTeX.translatorID - for (const [id, translator] of (Object.entries(this.byId) as [string, ITranslatorHeader][])) { + for (const [id, translator] of (Object.entries(this.byId))) { if (! ['yaml', 'json', 'bib'].includes(translator.target) ) continue if (! translator.label.startsWith('Better ') ) continue - if (translator.label.replace('Better ', '').replace(' ', '').toLowerCase() === _name) return id - if (translator.label.split(' ').pop().toLowerCase() === _name) return id + if (translator.label.replace('Better ', '').replace(' ', '').toLowerCase() === name_lc) return id + if (translator.label.split(' ').pop().toLowerCase() === name_lc) return id } // allowed to pass GUID @@ -177,7 +181,8 @@ export let Translators = new class { // eslint-disable-line @typescript-eslint/n if (this.workers.running.size > workers) { return this.queue.schedule(this.exportItemsByWorker.bind(this, translatorID, displayOptions, options), [], { priority: 1, timestamp: (new Date()).getTime() }) - } else { + } + else { return this.exportItemsByWorker(translatorID, displayOptions, options) } } @@ -256,7 +261,8 @@ export let Translators = new class { // eslint-disable-line @typescript-eslint/n try { if (attempt > 0) await sleep(2 * 1000 * attempt) // eslint-disable-line no-magic-numbers worker = new ChromeWorker(`resource://zotero-better-bibtex/worker/Zotero.js?${params}`) - } catch (err) { + } + catch (err) { log.error('new ChromeWorker:', err) } } @@ -328,7 +334,8 @@ export let Translators = new class { // eslint-disable-line @typescript-eslint/n cached.metadata = metadata cached = cache.update(cached) - } else { + } + else { cache.insert({...selector, reference, metadata}) } break @@ -443,7 +450,8 @@ export let Translators = new class { // eslint-disable-line @typescript-eslint/n try { worker.postMessage(JSON.parse(JSON.stringify(config))) - } catch (err) { + } + catch (err) { worker.terminate() this.workers.running.delete(id) log.error(err) @@ -454,7 +462,7 @@ export let Translators = new class { // eslint-disable-line @typescript-eslint/n return deferred.promise } - public async exportItems(translatorID: string, displayOptions: any, scope: ExportScope, path = null) { + public async exportItems(translatorID: string, displayOptions: any, scope: ExportScope, path = null): Promise { await Zotero.BetterBibTeX.ready const start = Date.now() @@ -491,7 +499,8 @@ export let Translators = new class { // eslint-disable-line @typescript-eslint/n file = Zotero.File.pathToFile(path) // path could exist but not be a regular file if (file.exists() && !file.isFile()) file = null - } catch (err) { + } + catch (err) { // or Zotero.File.pathToFile could have thrown an error log.error('Translators.exportItems:', err) file = null @@ -513,7 +522,8 @@ export let Translators = new class { // eslint-disable-line @typescript-eslint/n translation.setHandler('done', (obj, success) => { if (success) { deferred.resolve(obj ? obj.string : undefined) - } else { + } + else { log.error('Translators.exportItems failed in', { time: Date.now() - start, translatorID, displayOptions, path }) deferred.reject('translation failed') } @@ -527,12 +537,13 @@ export let Translators = new class { // eslint-disable-line @typescript-eslint/n public uninstall(label) { try { const destFile = Zotero.getTranslatorsDirectory() - destFile.append(label + '.js') + destFile.append(`${label}.js`) if (destFile.exists()) { destFile.remove(false) return true } - } catch (err) { + } + catch (err) { log.error(`Translators.uninstall: failed to remove ${label}:`, err) return true } @@ -546,7 +557,8 @@ export let Translators = new class { // eslint-disable-line @typescript-eslint/n let installed = null try { installed = Zotero.Translators.get(header.translatorID) - } catch (err) { + } + catch (err) { log.error('Translators.install', header, err) installed = null } @@ -570,7 +582,8 @@ export let Translators = new class { // eslint-disable-line @typescript-eslint/n try { await Zotero.Translators.save(header, code) - } catch (err) { + } + catch (err) { log.error('Translator.install', header, 'failed:', err) this.uninstall(header.label) } @@ -588,7 +601,8 @@ export let Translators = new class { // eslint-disable-line @typescript-eslint/n for (const pref of prefOverrides) { if (typeof displayOptions[`preference_${pref}`] === 'undefined') { query[pref] = Prefs.get(pref) - } else { + } + else { query[pref] = displayOptions[`preference_${pref}`] } } @@ -603,10 +617,12 @@ export let Translators = new class { // eslint-disable-line @typescript-eslint/n if (scope.library) { sql = `SELECT i.itemID FROM items i WHERE i.libraryID = ${scope.library} AND ${cond}` - } else if (scope.collection) { + } + else if (scope.collection) { sql = `SELECT i.itemID FROM collectionItems ci JOIN items i ON i.itemID = ci.itemID WHERE ci.collectionID = ${scope.collection.id} AND ${cond}` - } else { + } + else { log.error('Translators.uncached: no active scope') return [] @@ -645,12 +661,13 @@ const SERVER_ERROR = 500 Zotero.Server.Endpoints['/better-bibtex/translations/stats'] = class { public supportedMethods = ['GET'] - public init(request) { + public init(_request) { try { return [ OK, 'application/json', JSON.stringify(trace) ] - } catch (err) { - return [SERVER_ERROR, 'text/plain', '' + err] + } + catch (err) { + return [SERVER_ERROR, 'text/plain', `${err}`] } } } diff --git a/content/typings/bbt.d.ts b/content/typings/bbt.d.ts new file mode 100644 index 0000000000..324f84aed3 --- /dev/null +++ b/content/typings/bbt.d.ts @@ -0,0 +1,14 @@ +export type ParsedDate = { + type?: 'date' | 'open' | 'verbatim' | 'season' | 'interval' | 'list' + year?: number + month?: number + day?: number + orig?: ParsedDate + verbatim?: string + from?: ParsedDate + to?: ParsedDate + dates?: ParsedDate[] + season?: number + uncertain?: boolean + approximate?: boolean +} diff --git a/minitests/bibertool.ts b/minitests/bibertool.ts new file mode 100644 index 0000000000..fd0d328377 --- /dev/null +++ b/minitests/bibertool.ts @@ -0,0 +1 @@ +const bibertool = require('../translators/bibtex/35440492ce37b2caf4b5fe1edfe86f832f1df8321688b1f546292255ae57720c.bibertool') diff --git a/minitests/text2latex.ts b/minitests/text2latex.ts index 093b24a437..dd1d991be9 100644 --- a/minitests/text2latex.ts +++ b/minitests/text2latex.ts @@ -1,4 +1,4 @@ -// tslint:disable:no-console +/* eslint-disable no-console */ const Zotero = { // tslint:disable-line:variable-name Debug: { diff --git a/schema/hashes.json b/schema/hashes.json index 40c03dab5c..f18d0b0a72 100644 --- a/schema/hashes.json +++ b/schema/hashes.json @@ -107,7 +107,9 @@ "5.0.93": "5680d7d212e2849188d29522a414a633d6504d94cfdfa60f39caececc348b77fc9c9c479bc735af9f3ed15fab44ddbbcca2b57810eb9db09290281c2657305a1", "5.0.94": "5680d7d212e2849188d29522a414a633d6504d94cfdfa60f39caececc348b77fc9c9c479bc735af9f3ed15fab44ddbbcca2b57810eb9db09290281c2657305a1", "5.0.95": "5680d7d212e2849188d29522a414a633d6504d94cfdfa60f39caececc348b77fc9c9c479bc735af9f3ed15fab44ddbbcca2b57810eb9db09290281c2657305a1", - "5.0.95.1": null + "5.0.95.1": null, + "5.0.95.2": null, + "5.0.95.3": null }, "jurism": { "5.0m1": null, diff --git a/setup/citeproc.ts b/setup/citeproc.ts index 3b9242958b..974267b2cb 100644 --- a/setup/citeproc.ts +++ b/setup/citeproc.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import j = require('jscodeshift') import * as fs from 'fs' diff --git a/setup/key-formatter-methods.ts b/setup/key-formatter-methods.ts index 9998679fe7..b9bccdab6f 100755 --- a/setup/key-formatter-methods.ts +++ b/setup/key-formatter-methods.ts @@ -16,24 +16,28 @@ const doc = { functions: {}, } -function function_name(name, parameters) { +function function_name(name: string, parameters: { name: string }[]) { name = name.replace(/_/g, '.') let names = [ name ] if (parameters.find(p => p.name === 'onlyEditors')) { if (name.startsWith('authors')) { - names = [ 'authors', 'editors' ].map(prefix => name.replace(/^authors/, prefix)) - } else if (name === 'auth.auth.ea') { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + names = [ 'authors', 'editors' ].map((prefix: string) => name.replace(/^authors/, prefix)) + } + else if (name === 'auth.auth.ea') { names = [ 'auth.auth.ea', 'edtr.edtr.ea' ] - } else { - names = [ 'auth', 'edtr' ].map(prefix => name.replace(/^auth/, prefix)) + } + else { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + names = [ 'auth', 'edtr' ].map((prefix: string) => name.replace(/^auth/, prefix)) } } if (parameters.find(p => p.name === 'n')) { - names = names.map(n => n + 'N') + names = names.map(n => `${n}N`) } if (parameters.find(p => p.name === 'm')) { - names = names.map(n => n + '_M') + names = names.map(n => `${n}_M`) } let quoted = names.map(n => '`' + n + '`').join(' / ') if (parameters.find(p => p.name === 'withInitials')) { @@ -45,7 +49,7 @@ function function_name(name, parameters) { return quoted } -function filter_name(name, parameters) { +function filter_name({ name, parameters }: { name: string, parameters: { name: string, optional: boolean, type: string} [] }) { name = '`' + name.replace(/_/g, '-') + '`' if (parameters && parameters.length) { name += '=' + parameters.map(p => `${p.name}${p.optional ? '?' : ''} (${p.type})`).join(', ') @@ -92,7 +96,7 @@ ast.forEachChild((node: ts.Node) => { doc.functions[function_name(name, parameters)] = comment break case '_': - doc.filters[filter_name(name, parameters)] = comment + doc.filters[filter_name({ name, parameters })] = comment break } } diff --git a/setup/loaders/bibertool.ts b/setup/loaders/bibertool.ts index 16e5ec3524..dd5b999241 100644 --- a/setup/loaders/bibertool.ts +++ b/setup/loaders/bibertool.ts @@ -16,7 +16,7 @@ function select(selector, node) { return xpath.select(selector, node) as Element[] } -export = source => { +export = (source: string): string => { const doc = (new DOMParser).parseFromString(source) const BiberTool = { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match @@ -47,9 +47,9 @@ export = source => { // eslint-disable-next-line prefer-template const dateprefix = '^(' + select('//fields/field[@fieldtype="field" and @datatype="date"]', doc) - .map(field => field.textContent.replace(/date$/, '')) - .filter(field => field) - .join('|') + .map(field => field.textContent.replace(/date$/, '')) + .filter(field => field) + .join('|') + ')?' const dateprefixRE = new RegExp(dateprefix) @@ -64,7 +64,7 @@ export = source => { for (const node of select('//entryfields', doc)) { const types = select('./entrytype', node).map(type => type.textContent).sort() - const setname = types.length === 0 ? 'optional' : 'optional_' + types.join('_') + const setname = types.length === 0 ? 'optional' : `optional_${types.join('_')}` if (fieldSet[setname]) throw new Error(`field set ${setname} exists`) // find all the field names allowed by this set @@ -74,7 +74,8 @@ export = source => { if (m) { fieldSet[setname].push(`${m[1] || ''}date`) if (field === 'month' || field === 'year') fieldSet[setname].push(field) - } else { + } + else { fieldSet[setname].push(field) } } @@ -84,7 +85,8 @@ export = source => { for (const type of types) { if (!BiberTool.allowed[type]) { throw new Error(`Unknown reference type ${type}`) - } else { + } + else { BiberTool.allowed[type] = _.uniq(BiberTool.allowed[type].concat(setname)) } } @@ -92,7 +94,7 @@ export = source => { for (const node of select('.//constraints', doc)) { const types = select('./entrytype', node).map(type => type.textContent).sort() - const setname = types.length === 0 ? 'required' : 'required_' + types.join('_') + const setname = types.length === 0 ? 'required' : `required_${types.join('_')}` if (fieldSet[setname] || BiberTool.required[setname]) throw new Error(`constraint set ${setname} exists`) @@ -148,16 +150,17 @@ export = source => { if (!constraint.localName) continue const test = { - test: (constraint as Element).getAttribute('datatype'), + test: constraint.getAttribute('datatype'), fields: Array.from(select('./field', constraint)).map(field => field.textContent), params: null, } - if (test.test === 'pattern') test.params = (constraint as Element).getAttribute('pattern') + if (test.test === 'pattern') test.params = constraint.getAttribute('pattern') BiberTool.data.push(test) } } BiberTool.fieldSet = jsesc(fieldSet, { compact: false, indent: ' ' }) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return ejs.render(fs.readFileSync(path.join(__dirname, 'bibertool.ejs'), 'utf8'), BiberTool) } diff --git a/setup/loaders/inline-ts.ts b/setup/loaders/inline-ts.ts index 91fe22e8bd..3b0037d417 100644 --- a/setup/loaders/inline-ts.ts +++ b/setup/loaders/inline-ts.ts @@ -1,6 +1,6 @@ import * as ts from 'typescript' -export = function loader(source) { +export = function loader(source: string): string { if (this.cacheable) this.cacheable() const js = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.None } }) diff --git a/setup/loaders/trace.ts b/setup/loaders/trace.ts index 6c8fd31fac..fb95ae1d70 100644 --- a/setup/loaders/trace.ts +++ b/setup/loaders/trace.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-console */ +/* eslint-disable prefer-arrow/prefer-arrow-functions */ import * as fs from 'fs' import * as path from 'path' import * as shell from 'shelljs' @@ -5,11 +7,11 @@ import { filePathFilter } from 'file-path-filter' import Injector = require('njstrace/lib/injector') const injector = new Injector({ - emit(kind, err) { throw err }, + emit(_kind, err) { throw err }, log() { }, // eslint-disable-line no-empty,@typescript-eslint/no-empty-function }) -let selected = function(filename) { return false } // eslint-disable-line prefer-arrow/prefer-arrow-functions +let selected = function(_filename) { return false } // eslint-disable-line prefer-arrow/prefer-arrow-functions if (fs.existsSync(path.join(__dirname, '../../.trace.json'))) { const branch = process.env.TRAVIS_BRANCH || shell.exec('git rev-parse --abbrev-ref HEAD', { silent: true }).stdout.trim() if (branch !== 'master') { @@ -56,7 +58,7 @@ function __njsOnCatchClause__(call) { const logArguments = false const logExceptions = true -export = function loader(source) { +export = function loader(source: string): string { const filename = this.resourcePath.substring(process.cwd().length + 1) if (filename.split('.').pop() !== 'ts') throw new Error(`Unexpected extension on ${filename}`) @@ -65,5 +67,6 @@ export = function loader(source) { console.log(`!!!!!!!!!!!!!! Instrumenting ${filename} for trace logging !!!!!!!!!!!!!`) + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands return tracer + injector.injectTracing(filename, source, logExceptions, logArguments) } diff --git a/setup/plugins/log-used.ts b/setup/plugins/log-used.ts index e5232d8673..d37484ec64 100644 --- a/setup/plugins/log-used.ts +++ b/setup/plugins/log-used.ts @@ -1,16 +1,17 @@ import * as path from 'path' -import * as fs from 'fs-extra' +import * as fs from 'fs' export class LogUsedFilesPlugin { private name: string private type: string - constructor(name, type = '') { + constructor(name: string, type = '') { this.name = name this.type = type } - public apply(compiler) { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + public apply(compiler: { hooks: { afterEmit: { tap: (arg0: string, arg1: (compilation: any) => void) => void } } }) { compiler.hooks.afterEmit.tap('LogUsedFilesPlugin', compilation => { const used = new Set @@ -31,7 +32,7 @@ export class LogUsedFilesPlugin { */ const output = `gen/log-used/${this.type}${this.type ? '.' : ''}${this.name.replace(/ /g, '')}.json` - fs.ensureDirSync(path.dirname(output)) + fs.mkdirSync(path.dirname(output), { recursive: true }) fs.writeFileSync(output, JSON.stringify(Array.from(used).sort(), null, 2), 'utf-8') }) } diff --git a/setup/preferences.py b/setup/preferences.py index 78590e1043..e5b52f8830 100755 --- a/setup/preferences.py +++ b/setup/preferences.py @@ -231,7 +231,7 @@ def save(self): os.makedirs(os.path.join(root, 'gen/typings'), exist_ok=True) with open(os.path.join(root, 'gen', 'typings', 'preferences.d.ts'), 'w') as f: - print('interface IPreferences {', file=f) + print('export interface IPreferences {', file=f) for name, pref in preferences.items(): print(f' {name}: {pref.type}', file=f) print('}', file=f) diff --git a/setup/shims/assert.js b/setup/shims/assert.js new file mode 100644 index 0000000000..61c3f947af --- /dev/null +++ b/setup/shims/assert.js @@ -0,0 +1,3 @@ +module.exports = function assert(cond, msg) { + if (cond) throw new Error(msg) +} diff --git a/setup/templates/items/items.ts.mako b/setup/templates/items/items.ts.mako index 12386790dc..25903dce85 100644 --- a/setup/templates/items/items.ts.mako +++ b/setup/templates/items/items.ts.mako @@ -1,6 +1,7 @@ -declare const Zotero: any +/* eslint-disable id-blacklist, @typescript-eslint/no-unsafe-return, @typescript-eslint/explicit-module-boundary-types */ import { client } from '../../content/client' +import { ZoteroTranslator } from '../typings/serialized-item' const jurism = client === 'jurism' const zotero = !jurism @@ -57,7 +58,7 @@ export const label: Record = { %endfor } -function unalias(item) { +function unalias(item: any) { delete item.inPublications let v %for client, indent in [('both', ''), ('zotero', ' '), ('jurism', ' ')]: @@ -84,7 +85,7 @@ function unalias(item) { } // import & export translators expect different creator formats... nice -export function simplifyForExport(item, dropAttachments = false) { +export function simplifyForExport(item: any, dropAttachments = false): ZoteroTranslator.Item { unalias(item) if (item.filingDate) item.filingDate = item.filingDate.replace(/^0000-00-00 /, '') @@ -103,15 +104,15 @@ export function simplifyForExport(item, dropAttachments = false) { if (item.itemType === 'attachment' || item.itemType === 'note') { delete item.attachments delete item.notes - } else { + } + else { item.attachments = (!dropAttachments && item.attachments) || [] - item.notes = item.notes ? item.notes.map(note => note.note || note ) : [] } - return item + return (item as ZoteroTranslator.Item) } -export function simplifyForImport(item) { +export function simplifyForImport(item: any): ZoteroTranslator.Item { unalias(item) if (item.creators) { @@ -128,5 +129,5 @@ export function simplifyForImport(item) { if (!jurism) delete item.multi - return item + return (item as ZoteroTranslator.Item) } diff --git a/setup/templates/items/serialized-item.d.ts.mako b/setup/templates/items/serialized-item.d.ts.mako index bf54febda6..571aabe340 100644 --- a/setup/templates/items/serialized-item.d.ts.mako +++ b/setup/templates/items/serialized-item.d.ts.mako @@ -1,15 +1,31 @@ import { Fields } from '../../content/extra' -declare global { - interface ISerializedItem { + +export namespace ZoteroTranslator { + interface Collection { + // id?: string + key: string + name: string + // collections: string[] | ZoteroCollection[] + collections: string[] + items: number[] + parent?: string + } + + interface Note { note: string } + interface Tag { tag: string, type?: number } + interface Attachment { path: string, title?: string, mimeType?: string } + interface Creator { creatorType?: string, name?: string, firstName?: string, lastName?:string, fieldMode?: number, source?: string } + + interface Item { // fields common to all items - itemID: string | number + itemID: number itemType: ${' | '.join(["'" + itemType + "'" for itemType in itemTypes])} dateAdded: string dateModified: string - creators: { creatorType?: string, name?: string, firstName?: string, lastName?:string, fieldMode?: number, source?: string }[] - tags: Array<{ tag: string, type?: number }> - notes: string[] - attachments: { path: string, title?: string, mimeType?: string } + creators: Array + tags: Array + notes: Array + attachments: Array raw: boolean cachable?: boolean autoJournalAbbreviation?: string @@ -26,7 +42,14 @@ declare global { collections: string[] extraFields: Fields arXiv: { source?: string, id: string, category?: string } - // Juris-M extras - multi: any + + multi?: { + _keys: { + title: Record + } + main: { + title: string + } + } } } diff --git a/setup/templates/preferences/preferences.ts.mako b/setup/templates/preferences/preferences.ts.mako index 563a34a2b4..dc5f4bfa4b 100644 --- a/setup/templates/preferences/preferences.ts.mako +++ b/setup/templates/preferences/preferences.ts.mako @@ -1,32 +1,40 @@ -/* eslint-disable no-magic-numbers, @typescript-eslint/quotes */ +/* eslint-disable no-magic-numbers, @typescript-eslint/quotes, max-len */ <% import json %> declare const Zotero: any -class PreferenceManager { -% for name, pref in preferences.items(): <% + for name, pref in preferences.items(): if 'options' in pref: - valid = ' | '.join([ json.dumps(option) for option in pref.options ]) - options = json.dumps(list(pref.options.keys())) + pref.valid = ' | '.join([ json.dumps(option) for option in pref.options ]) + pref.quoted_options = json.dumps(list(pref.options.keys())) else: - valid = pref.type + pref.valid = pref.type %> - set ${name}(v: ${valid | n} | undefined) { + +type Preferences = { +% for name, pref in preferences.items(): + ${name}: ${pref.valid | n } +% endfor +} + +export class PreferenceManager { +% for name, pref in preferences.items(): + set ${name}(v: ${pref.valid | n} | undefined) { if (typeof v === 'undefined') v = ${json.dumps(pref.default) | n} if (typeof v !== '${pref.type}') throw new Error(`${name} must be of type ${pref.type}, got '<%text>${typeof v}'`) -% if 'options' in pref: - if (!${options | n}.includes(v)) throw new Error(`${name} must be one of ${options}, got '<%text>${v}'`) +% if 'quoted_options' in pref: + if (!${pref.quoted_options | n}.includes(v)) throw new Error(`${name} must be one of ${pref.quoted_options}, got '<%text>${v}'`) % endif Zotero.Prefs.set('translators.better-bibtex.${name}', v) } - get ${name}() { - return Zotero.Prefs.get('translators.better-bibtex.${name}') + get ${name}(): ${pref.valid} { + return (Zotero.Prefs.get('translators.better-bibtex.${name}') as ${pref.valid}) } % endfor - public defaults() { + public defaults(): Preferences { return { % for name, pref in preferences.items(): ${name}: ${json.dumps(pref.default) | n }, @@ -34,7 +42,7 @@ class PreferenceManager { } } - public all() { + public all(): Preferences { return { % for name in preferences.keys(): ${name}: this.${name}, diff --git a/setup/translators.py b/setup/translators.py index ff757636c6..ff42b4ac13 100755 --- a/setup/translators.py +++ b/setup/translators.py @@ -53,16 +53,6 @@ def jstype(v): variables.labels = translators.byLabel.keys() template = """ -type ZoteroCollection = { - // id?: string - key: string - name: string - // collections: string[] | ZoteroCollection[] - collections: string[] - items: number[] - parent?: string -} - interface ITranslator { preferences: IPreferences skipFields: string[] diff --git a/translators/Better BibLaTeX.ts b/translators/Better BibLaTeX.ts index 17173cc886..b431a74d52 100644 --- a/translators/Better BibLaTeX.ts +++ b/translators/Better BibLaTeX.ts @@ -2,6 +2,7 @@ declare const Zotero: any import { Translator } from './lib/translator' export { Translator } +import { ZoteroTranslator } from '../gen/typings/serialized-item' import { Reference } from './bibtex/reference' import { Exporter } from './bibtex/exporter' @@ -66,7 +67,8 @@ Reference.prototype.addCreators = function() { if (['video', 'movie'].includes(this.referencetype)) { creators.editor.push(creator) creators.editor.type = 'director' - } else { + } + else { creators.author.push(creator) } break @@ -120,7 +122,7 @@ Reference.prototype.addCreators = function() { for (const [field, value] of Object.entries(creators)) { this.remove(field) - this.remove(field + 'type') + this.remove(`${field}type`) if (!value.length) continue @@ -207,22 +209,22 @@ Reference.prototype.typeMap = { }, } -function looks_like_number(n) { +function looks_like_number(n): string | boolean { if (n.match(/^(?=[MDCLXVI])M*(C[MD]|D?C*)(X[CL]|L?X*)(I[XV]|V?I*)$/)) return 'roman' if (n.match(/^[A-Z]?[0-9]+(\.[0-9]+)?$/i)) return 'arabic' if (n.match(/^[A-Z]$/i)) return 'arabic' return false } -function looks_like_number_field(n) { +function looks_like_number_field(n: string): boolean { if (!n) return false - n = n.split(/-+|–|,|\//).map(_n => _n.trim()) - switch (n.length) { + const ns: string[] = n.trim().split(/\s*-+|–|,|\/\s*/) + switch (ns.length) { case 1: - return looks_like_number(n[0]) + return (looks_like_number(ns[0]) as boolean) case 2: - return looks_like_number(n[0]) && (looks_like_number(n[0]) === looks_like_number(n[1])) + return (looks_like_number(ns[0]) as boolean) && (looks_like_number(ns[0]) === looks_like_number(ns[1])) default: return false @@ -233,17 +235,17 @@ const patent = new class { private countries = ['de', 'eu', 'fr', 'uk', 'us'] private prefix = {us: 'us', ep: 'eu', gb: 'uk', de: 'de', fr: 'fr' } - public region(item) { + public region(item): string { if (item.itemType !== 'patent') return '' if (item.country) { - const country = item.country.toLowerCase() + const country: string = item.country.toLowerCase() if (this.countries.includes(country)) return country } for (const patentNumber of [item.number, item.applicationNumber]) { if (patentNumber) { - const prefix = this.prefix[patentNumber.substr(0, 2).toLowerCase()] + const prefix: string = this.prefix[patentNumber.substr(0, 2).toLowerCase()] if (prefix) return prefix } } @@ -251,15 +253,17 @@ const patent = new class { return '' } - public number(item) { + // eslint-disable-next-line id-blacklist + public number(item): string { if (item.itemType !== 'patent' || (!item.number && !item.applicationNumber)) return '' - for (const patentNumber of [item.number, item.applicationNumber]) { + for (const patentNumber of ([item.number, item.applicationNumber] as string[])) { if (patentNumber) { const country = patentNumber.substr(0, 2).toLowerCase() if (this.prefix[country]) return patentNumber.substr(country.length) } } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return item.number || item.applicationNumber } @@ -275,7 +279,7 @@ const patent = new class { } } -export function doExport() { +export function doExport(): void { Translator.init('export') Reference.installPostscript() Exporter.prepare_strings() @@ -283,7 +287,7 @@ export function doExport() { // Zotero.write(`\n% ${Translator.header.label}\n`) Zotero.write('\n') - let item: ISerializedItem + let item: ZoteroTranslator.Item while (item = Exporter.nextItem()) { Zotero.debug(`exporting ${item.citationKey}`) const ref = new Reference(item) @@ -303,7 +307,8 @@ export function doExport() { delete item.url ref.remove('url') - } else if (item.url && (m = item.url.match(/^https?:\/\/books.google.com\/books?id=([\S]+)$/i))) { + } + else if (item.url && (m = item.url.match(/^https?:\/\/books.google.com\/books?id=([\S]+)$/i))) { ref.override({ name: 'eprinttype', value: 'googlebooks'}) ref.override({ name: 'eprint', value: m[1] }) ref.remove('archivePrefix') @@ -311,7 +316,8 @@ export function doExport() { delete item.url ref.remove('url') - } else if (item.url && (m = item.url.match(/^https?:\/\/www.ncbi.nlm.nih.gov\/pubmed\/([\S]+)$/i))) { + } + else if (item.url && (m = item.url.match(/^https?:\/\/www.ncbi.nlm.nih.gov\/pubmed\/([\S]+)$/i))) { ref.override({ name: 'eprinttype', value: 'pubmed'}) ref.override({ name: 'eprint', value: m[1] }) ref.remove('archivePrefix') @@ -325,9 +331,11 @@ export function doExport() { if (ref.referencetype === 'patent') { if (item.country && !patent.region(item)) ref.add({ name: 'location', value: item.country || item.extraFields.kv['publisher-place'] }) - } else if (ref.referencetype === 'unpublished' && item.itemType === 'presentation') { + } + else if (ref.referencetype === 'unpublished' && item.itemType === 'presentation') { ref.add({ name: 'venue', value: item.place, enc: 'literal' }) - } else { + } + else { ref.add({ name: 'location', value: item.place || item.extraFields.kv['publisher-place'] , enc: 'literal' }) } @@ -386,15 +394,18 @@ export function doExport() { if (ref.getBibString(item.publicationTitle)) { ref.add({ name: 'journaltitle', value: item.publicationTitle, bibtexStrings: true }) - } else if (Translator.options.useJournalAbbreviation && item.publicationTitle && item.journalAbbreviation) { - ref.add({ name: 'journaltitle', value: item.journalAbbreviation, bibtexStrings: true }) + } + else if (Translator.options.useJournalAbbreviation && item.publicationTitle && item.journalAbbreviation) { + ref.add({ name: 'journaltitle', value: item.journalAbbreviation, bibtexStrings: true }) - } else { + } + else { ref.add({ name: 'journaltitle', value: item.publicationTitle, bibtexStrings: true }) if (ref.has.entrysubtype?.value === 'newspaper') { ref.add({ name: 'journalsubtitle', value: item.section }) - } else { + } + else { ref.add({ name: 'shortjournal', value: item.journalAbbreviation, bibtexStrings: true }) } } @@ -405,7 +416,9 @@ export function doExport() { } let main + // eslint-disable-next-line no-underscore-dangle if (item.multi?._keys?.title && (main = item.multi.main?.title || item.language)) { + // eslint-disable-next-line no-underscore-dangle const languages = Object.keys(item.multi._keys.title).filter(lang => lang !== main) main += '-' languages.sort((a, b) => { @@ -418,6 +431,7 @@ export function doExport() { for (let i = 0; i < languages.length; i++) { ref.add({ name: i === 0 ? 'titleaddon' : `user${String.fromCharCode('d'.charCodeAt(0) + i)}`, + // eslint-disable-next-line no-underscore-dangle value: item.multi._keys.title[languages[i]], }) } @@ -443,13 +457,14 @@ export function doExport() { ref.add({ name: 'publisher', value: item.publisher, bibtexStrings: true }) } + let thesistype: string switch (ref.referencetype) { case 'letter': ref.add({ name: 'type', value: item.type || (item.itemType === 'email' ? 'E-mail' : 'Letter') }) break case 'thesis': - const thesistype = { + thesistype = { phdthesis: 'phdthesis', phd: 'phdthesis', mastersthesis: 'mathesis', @@ -464,7 +479,8 @@ export function doExport() { case 'report': if (item.type?.toLowerCase().trim() === 'techreport') { ref.add({ name: 'type', value: 'techreport' }) - } else { + } + else { ref.add({ name: 'type', value: item.type }) } break diff --git a/translators/Better BibTeX Citation Key Quick Copy.ts b/translators/Better BibTeX Citation Key Quick Copy.ts index 9b4661a2b8..56089698fb 100644 --- a/translators/Better BibTeX Citation Key Quick Copy.ts +++ b/translators/Better BibTeX Citation Key Quick Copy.ts @@ -1,47 +1,55 @@ +/* eslint-disable prefer-arrow/prefer-arrow-functions */ declare const Zotero: any import format = require('string-template') import { Translator } from './lib/translator' export { Translator } +import { ZoteroTranslator } from '../gen/typings/serialized-item' import { Exporter } from './bibtex/exporter' function select_by_key(item) { - const [ , kind, lib, key ] = item.uri.match(/^https?:\/\/zotero\.org\/(users|groups)\/((?:local\/)?[^\/]+)\/items\/(.+)/) + const [ , kind, lib, key ] = item.uri.match(/^https?:\/\/zotero\.org\/(users|groups)\/((?:local\/)?[^/]+)\/items\/(.+)/) return (kind === 'users') ? `zotero://select/library/items/${key}` : `zotero://select/groups/${lib}/items/${key}` } function select_by_citekey(item) { return `zotero://select/items/@${encodeURIComponent(item.citationKey)}` } -const Mode = { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match +const Mode = { + // eslint-disable-next-line prefer-arrow/prefer-arrow-functions gitbook(items) { - const citations = items.map(item => `{{ \"${item.citationKey}\" | cite }}`) + const citations = items.map(item => `{{ "${item.citationKey}" | cite }}`) Zotero.write(citations.join('')) }, atom(items) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return const keys = items.map(item => item.citationKey) if (keys.length === 1) { Zotero.write(`[](#@${keys[0]})`) - } else { + } + else { Zotero.write(`[](?@${keys.join(',')})`) } }, latex(items) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return const keys = items.map(item => item.citationKey) const cmd = `${Translator.preferences.citeCommand}`.trim() if (cmd === '') { Zotero.write(keys.join(',')) - } else { + } + else { Zotero.write(`\\${cmd}{${keys.join(',')}}`) } }, citekeys(items) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return const keys = items.map(item => item.citationKey) Zotero.write(keys.join(',')) }, @@ -55,6 +63,7 @@ const Mode = { // eslint-disable-line @typescript-eslint/naming-convention,no-un orgRef(items) { if (!items.length) return '' + // eslint-disable-next-line @typescript-eslint/no-unsafe-return Zotero.write(`cite:${items.map(item => item.citationKey).join(',')}`) }, @@ -97,10 +106,12 @@ const Mode = { // eslint-disable-line @typescript-eslint/naming-convention,no-un if (date.type === 'verbatim' || !date.year) { ref.push(item.date) - } else { + } + else { ref.push(date.year) } - } else { + } + else { ref.push('no date') } @@ -112,17 +123,19 @@ const Mode = { // eslint-disable-line @typescript-eslint/naming-convention,no-un 'string-template'(items) { try { const { citation, item, sep } = JSON.parse(Translator.preferences.citeCommand) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return Zotero.write(format(citation || '{citation}', { citation: items.map(i => format(item || '{item}', { item: i })).join(sep || '') })) - } catch (err) { + } + catch (err) { Zotero.write(`${err}`) } }, } -export function doExport() { +export function doExport(): void { Translator.init('export') - let item: ISerializedItem + let item: ZoteroTranslator.Item const items = [] while ((item = Exporter.nextItem())) { if (item.citationKey) items.push(item) @@ -131,7 +144,8 @@ export function doExport() { const mode = Mode[`${Translator.options.quickCopyMode}`] || Mode[`${Translator.preferences.quickCopyMode}`] if (mode) { mode.call(null, items) - } else { + } + else { throw new Error(`Unsupported Quick Copy format '${Translator.options.quickCopyMode || Translator.preferences.quickCopyMode}', I only know about: ${Object.keys(Mode).join(', ')}`) } } diff --git a/translators/Better BibTeX.ts b/translators/Better BibTeX.ts index 22d863c9bb..4c6965f377 100644 --- a/translators/Better BibTeX.ts +++ b/translators/Better BibTeX.ts @@ -2,9 +2,12 @@ declare const Zotero: any import { log } from '../content/logger' +import { ZoteroTranslator } from '../gen/typings/serialized-item' + const toWordsOrdinal = require('number-to-words/src/toWordsOrdinal') -function edition(n) { - if (typeof n === 'number' || (typeof n === 'string' && n.match(/^[0-9]+$/))) return toWordsOrdinal(n).replace(/^\w/, c => c.toUpperCase()) +function edition(n: string | number): string { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + if (typeof n === 'number' || (typeof n === 'string' && n.match(/^[0-9]+$/))) return toWordsOrdinal(n).replace(/^\w/, (c: string) => c.toUpperCase()) return n } import wordsToNumbers from 'words-to-numbers' @@ -100,7 +103,7 @@ const lint: Record = { } lint.conference = lint.inproceedings -Reference.prototype.lint = function(explanation) { +Reference.prototype.lint = function(_explanation) { const type = lint[this.referencetype.toLowerCase()] if (!type) return @@ -113,7 +116,8 @@ Reference.prototype.lint = function(explanation) { const match = required.split('/').find(field => this.has[field]) if (match) { // fields = fields.filter(field => field !== match) - } else { + } + else { warnings.push(`Missing required field '${required}'`) } } @@ -246,7 +250,7 @@ Reference.prototype.typeMap = { const months = [ 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec' ] -export function doExport() { +export function doExport(): void { Translator.init('export') Reference.installPostscript() Exporter.prepare_strings() @@ -254,7 +258,7 @@ export function doExport() { // Zotero.write(`\n% ${Translator.header.label}\n`) Zotero.write('\n') - let item: ISerializedItem + let item: ZoteroTranslator.Item while (item = Exporter.nextItem()) { const ref = new Reference(item) if (item.itemType === 'report' && item.type?.toLowerCase().includes('manual')) ref.referencetype = 'manual' @@ -283,10 +287,12 @@ export function doExport() { if (['zotero.bookSection', 'zotero.conferencePaper', 'tex.chapter', 'csl.chapter'].includes(ref.referencetype_source)) { ref.add({ name: 'booktitle', value: item.publicationTitle || item.conferenceName, bibtexStrings: true }) - } else if (ref.getBibString(item.publicationTitle)) { + } + else if (ref.getBibString(item.publicationTitle)) { ref.add({ name: 'journal', value: item.publicationTitle, bibtexStrings: true }) - } else { + } + else { ref.add({ name: 'journal', value: (Translator.options.useJournalAbbreviation && item.journalAbbreviation) || item.publicationTitle, bibtexStrings: true }) } @@ -374,7 +380,8 @@ export function doExport() { if (date.month) ref.add({ name: 'month', value: months[date.month - 1], bare: true }) if (date.orig?.type === 'date') { ref.add({ name: 'year', value: `[${date.orig.year}] ${date.year}` }) - } else { + } + else { ref.add({ name: 'year', value: `${date.year}` }) } break @@ -394,12 +401,12 @@ export function doExport() { Zotero.write('\n') } -export function detectImport() { - let detected = Zotero.getHiddenPref('better-bibtex.import') +export function detectImport(): boolean { + let detected = (Zotero.getHiddenPref('better-bibtex.import') as boolean) if (detected) { const input = Zotero.read(102400) // eslint-disable-line no-magic-numbers const bib = bibtexParser.chunker(input, { max_entries: 1 }) - detected = bib.find(chunk => chunk.entry) + detected = !!bib.find(chunk => chunk.entry) } return detected } @@ -408,6 +415,7 @@ function importGroup(group, itemIDs, root = null) { const collection = new Zotero.Collection() collection.type = 'collection' collection.name = group.name + // eslint-disable-next-line @typescript-eslint/no-unsafe-return collection.children = group.entries.filter(citekey => itemIDs[citekey]).map(citekey => ({type: 'item', id: itemIDs[citekey]})) for (const subgroup of group.groups || []) { @@ -415,6 +423,7 @@ function importGroup(group, itemIDs, root = null) { } if (root) collection.complete() + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return collection } @@ -460,7 +469,7 @@ class ZoteroItem { this.bibtex.type = this.bibtex.type.toLowerCase() this.type = this.typeMap[this.bibtex.type] if (!this.type) { - this.errors.push({ message: `Don't know what Zotero type to make of '${this.bibtex.type}' for ${this.bibtex.key ? '@' + this.bibtex.key : 'unnamed item'}, importing as ${this.type = 'journalArticle'}` }) + this.errors.push({ message: `Don't know what Zotero type to make of '${this.bibtex.type}' for ${this.bibtex.key ? `@${this.bibtex.key}` : 'unnamed item'}, importing as ${this.type = 'journalArticle'}` }) this.hackyFields.push(`tex.referencetype: ${this.bibtex.type}`) } if (this.type === 'book' && (this.bibtex.fields.title || []).length && (this.bibtex.fields.booktitle || []).length) this.type = 'bookSection' @@ -470,10 +479,11 @@ class ZoteroItem { this.validFields = valid.field[this.type] if (!Object.keys(this.bibtex.fields).length) { - this.errors.push({ message: `No fields in ${this.bibtex.key ? '@' + this.bibtex.key : 'unnamed item'}` }) + this.errors.push({ message: `No fields in ${this.bibtex.key ? `@${this.bibtex.key}` : 'unnamed item'}` }) this.item = null - } else { + } + else { this.item = new Zotero.Item(this.type) this.item.itemID = this.id if (this.type === 'report' && this.bibtex.type === 'manual') this.$type('manual') @@ -500,7 +510,7 @@ class ZoteroItem { return false } - protected $title(value) { + protected $title(_value) { let title = [] if (this.bibtex.fields.title) title = title.concat(this.bibtex.fields.title) if (this.bibtex.fields.titleaddon) title = title.concat(this.bibtex.fields.titleaddon) @@ -508,7 +518,8 @@ class ZoteroItem { if (this.type === 'encyclopediaArticle') { this.item.publicationTitle = title.join(' - ') - } else { + } + else { this.item.title = title.join(' - ') } return true @@ -516,9 +527,9 @@ class ZoteroItem { protected $titleaddon(value) { return this.$title(value) } protected $subtitle(value) { return this.$title(value) } - protected $holder(value, field) { + protected $holder(_value, _field) { if (this.item.itemType === 'patent') { - this.item.assignee = this.bibtex.fields.holder.map(name => name.replace(/"/g, '')).join('; ') + this.item.assignee = this.bibtex.fields.holder.map((name: string) => name.replace(/"/g, '')).join('; ') } return true } @@ -582,16 +593,19 @@ class ZoteroItem { if (this.bibtex.fields.journal) { abbr = this.bibtex.fields.journal[0] - } else if (this.bibtex.fields.journaltitle) { + } + else if (this.bibtex.fields.journaltitle) { abbr = this.bibtex.fields.journaltitle[0] } if (abbr === journal) abbr = null - } else if (this.bibtex.fields.journal) { + } + else if (this.bibtex.fields.journal) { journal = this.bibtex.fields.journal[0] - } else if (this.bibtex.fields.journaltitle) { + } + else if (this.bibtex.fields.journaltitle) { journal = this.bibtex.fields.journaltitle[0] } @@ -601,7 +615,8 @@ class ZoteroItem { if (abbr) { if (this.validFields.journalAbbreviation) { this.item.journalAbbreviation = abbr - } else if (!this.hackyFields.find(line => line.startsWith('Journal abbreviation:'))) { + } + else if (!this.hackyFields.find(line => line.startsWith('Journal abbreviation:'))) { this.hackyFields.push(`Journal abbreviation: ${abbr}`) } } @@ -634,7 +649,7 @@ class ZoteroItem { this.set('numPages', value) return true } - protected $numpages(value, field) { return this.$pagetotal(value) } + protected $numpages(value, _field) { return this.$pagetotal(value) } protected $volume(value) { return this.set('volume', value) } @@ -642,11 +657,11 @@ class ZoteroItem { protected $abstract(value) { return this.set('abstractNote', value) } - protected $keywords(value) { + protected $keywords(_value) { let tags = this.bibtex.fields.keywords || [] tags = tags.concat(this.bibtex.fields.keyword || []) for (const mesh of this.bibtex.fields.mesh || []) { - tags = tags.concat((mesh || '').trim().split(/\s*;\s*/).filter(tag => tag)) + tags = tags.concat((mesh || '').trim().split(/\s*;\s*/).filter(tag => tag)) // eslint-disable-line @typescript-eslint/no-unsafe-return } tags = tags.sort() tags = tags.filter((item, pos, ary) => !pos || (item !== ary[pos - 1])) @@ -657,7 +672,7 @@ class ZoteroItem { protected $keyword(value) { return this.$keywords(value) } protected $mesh(value) { return this.$keywords(value) } // bibdesk - protected $date(value) { + protected $date(_value) { if (this.item.date) return true const dates = (this.bibtex.fields.date || []).slice() @@ -666,19 +681,23 @@ class ZoteroItem { let month = (this.bibtex.fields.month && this.bibtex.fields.month[0]) || '' const monthno = months.indexOf(month.toLowerCase()) - if (monthno >= 0) month = `0${monthno + 1}`.slice(-2) // eslint-disable-line no-magic-numbers + if (monthno >= 0) month = `0${monthno + 1}`.slice(-2) // eslint-disable-line no-magic-numbers const day = (this.bibtex.fields.day && this.bibtex.fields.day[0]) || '' if (year && month.match(/^[0-9]+$/) && day.match(/^[0-9]+$/)) { dates.push(`${year}-${month}-${day}`) - } else if (year && month.match(/^[0-9]+$/)) { + } + else if (year && month.match(/^[0-9]+$/)) { dates.push(`${year}-${month}`) - } else if (year && month && day) { + } + else if (year && month && day) { dates.push(`${day} ${month} ${year}`) - } else if (year && month) { + } + else if (year && month) { dates.push(`${month} ${year}`) - } else if (year) { + } + else if (year) { dates.push(year) } @@ -702,6 +721,7 @@ class ZoteroItem { '\u0013': '\\', } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return for (const record of value.replace(/\\[\\;:]/g, escaped => replace[escaped]).split(';')) { const att = { mimeType: '', @@ -709,6 +729,7 @@ class ZoteroItem { title: '', } + // eslint-disable-next-line no-control-regex, @typescript-eslint/no-unsafe-return const parts = record.split(':').map(str => str.replace(/[\u0011\u0012\u0013]/g, escaped => replace[escaped])) switch (parts.length) { case 1: @@ -753,7 +774,8 @@ class ZoteroItem { if (this.validFields.rights) { this.set('rights', value) return true - } else { + } + else { return this.fallback(['rights'], value) } } @@ -762,7 +784,8 @@ class ZoteroItem { if (this.validFields.versionNumber) { this.set('versionNumber', value) return true - } else { + } + else { return this.fallback(['versionNumber'], value) } } @@ -806,9 +829,11 @@ class ZoteroItem { if (m = value.match(/^(\\url{)(https?:\/\/|mailto:)}$/i)) { url = m[2] - } else if (field === 'url' || /^(https?:\/\/|mailto:)/i.test(value)) { + } + else if (field === 'url' || /^(https?:\/\/|mailto:)/i.test(value)) { url = value - } else { + } + else { url = null } @@ -861,9 +886,9 @@ class ZoteroItem { return true } - protected $language(value, field) { + protected $language(_value, _field) { const language = (this.bibtex.fields.language || []).concat(this.bibtex.fields.langid || []) - .map(lang => ['en', 'eng', 'usenglish', 'english'].includes(lang.toLowerCase()) ? this.english : lang) + .map(lang => ['en', 'eng', 'usenglish', 'english'].includes(lang.toLowerCase()) ? this.english : lang) // eslint-disable-line @typescript-eslint/no-unsafe-return .join(' and ') return this.set('language', language) @@ -892,7 +917,7 @@ class ZoteroItem { return true } protected $eprintclass(value, field) { return this.$eprint(value, field) } - protected $primaryclass(value, field) { return this.$eprint(value, 'eprintclass') } + protected $primaryclass(value, _field) { return this.$eprint(value, 'eprintclass') } protected $slaccitation(value, field) { return this.$eprint(value, field) } protected $nationality(value) { return this.set('country', value) } @@ -957,7 +982,8 @@ class ZoteroItem { if (creator.literal) { name.lastName = creator.literal.replace(/\u00A0/g, ' ') name.fieldMode = 1 - } else { + } + else { name.firstName = creator.firstName || '' name.lastName = creator.lastName || '' if (creator.prefix) name.lastName = `${creator.prefix} ${name.lastName}`.trim() @@ -973,7 +999,7 @@ class ZoteroItem { // do this before because some handlers directly access this.bibtex.fields for (const [field, values] of Object.entries(this.bibtex.fields)) { - this.bibtex.fields[field] = (values as string[]).map(value => typeof value === 'string' ? value.replace(/\u00A0/g, ' ').trim() : ('' + value)) + this.bibtex.fields[field] = (values as string[]).map(value => typeof value === 'string' ? value.replace(/\u00A0/g, ' ').trim() : `${value}`) } const zoteroField = { @@ -984,7 +1010,8 @@ class ZoteroItem { if (field.match(/^(local-zo-url-[0-9]+)|(file-[0-9]+)$/)) { if (this.$file(value)) continue - } else if (field.match(/^bdsk-url-[0-9]+$/)) { + } + else if (field.match(/^bdsk-url-[0-9]+$/)) { if (this.$url(value, field)) continue } @@ -1015,14 +1042,17 @@ class ZoteroItem { default: if (value.indexOf('\n') >= 0) { this.item.notes.push(`

    ${Zotero.Utilities.text2html(field, false)}

    ${Zotero.Utilities.text2html(value, false)}`) - } else { + } + else { const candidates = [field, zoteroField[field]] let name if ((name = candidates.find(f => this.validFields[f])) && !this.item[field]) { this.item[name] = value - } else if (name = candidates.find(f => label[f])) { + } + else if (name = candidates.find(f => label[f])) { this.hackyFields.push(`${label[name]}: ${value}`) - } else { + } + else { this.hackyFields.push(`tex.${field}: ${value}`) } } @@ -1036,6 +1066,7 @@ class ZoteroItem { this.item.tags.push({ tag: Translator.preferences.rawLaTag, type: 1 }) } + // eslint-disable-next-line id-blacklist if (this.numberPrefix && this.item.number && !this.item.number.toLowerCase().startsWith(this.numberPrefix.toLowerCase())) this.item.number = `${this.numberPrefix}${this.item.number}` if (this.bibtex.key) this.hackyFields.push(`Citation Key: ${this.bibtex.key}`) // Endnote has no citation keys in their bibtex @@ -1057,7 +1088,8 @@ class ZoteroItem { const eprintclass = this.eprint.eprintType === 'arXiv' && this.eprint.eprintclass ? ` [${this.eprint.eprintclass}]` : '' this.hackyFields.push(`${this.eprint.eprintType}: ${this.eprint.eprint}${eprintclass}`) - } else { + } + else { delete this.eprint.eprintType for (const [k, v] of Object.entries(this.eprint)) { @@ -1098,7 +1130,8 @@ class ZoteroItem { private addToExtra(str) { if (this.item.extra && this.item.extra !== '') { this.item.extra += `\n${str}` - } else { + } + else { this.item.extra = str } } @@ -1151,15 +1184,16 @@ class ZoteroItem { // @item.publicationTitle = value // return true -async function _fetch(url): Promise<{ json: () => Promise }> { +async function fetch_polyfill(url): Promise<{ json: () => Promise }> { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest() xhr.open('GET', url) xhr.onload = function() { if (this.status >= 200 && this.status < 300) { // eslint-disable-line no-magic-numbers - resolve({ json: () => JSON.parse(xhr.response) }) - } else { + resolve({ json: () => JSON.parse(xhr.response) }) // eslint-disable-line @typescript-eslint/no-unsafe-return + } + else { reject({ status: this.status, statusText: xhr.statusText, @@ -1178,11 +1212,11 @@ async function _fetch(url): Promise<{ json: () => Promise }> { }) } -export async function doImport() { +export async function doImport(): Promise { Translator.init('import') - const unabbreviate = Translator.preferences.importJabRefAbbreviations ? await (await _fetch('resource://zotero-better-bibtex/unabbrev/unabbrev.json')).json() : null - const strings = Translator.preferences.importJabRefStrings ? await (await _fetch('resource://zotero-better-bibtex/unabbrev/strings.json')).json() : null + const unabbreviate = Translator.preferences.importJabRefAbbreviations ? await (await fetch_polyfill('resource://zotero-better-bibtex/unabbrev/unabbrev.json')).json() : null + const strings = Translator.preferences.importJabRefStrings ? await (await fetch_polyfill('resource://zotero-better-bibtex/unabbrev/strings.json')).json() : null let read let input = '' @@ -1223,9 +1257,10 @@ export async function doImport() { try { await (new ZoteroItem(id, bibtex, jabref, errors)).complete() - } catch (err) { + } + catch (err) { log.error('bbt import error:', err) - errors.push({ message: '' + err.message }) + errors.push({ message: err.message }) } imported += 1 diff --git a/translators/Better CSL JSON.ts b/translators/Better CSL JSON.ts index 228337a410..55b6dc9ef0 100644 --- a/translators/Better CSL JSON.ts +++ b/translators/Better CSL JSON.ts @@ -4,23 +4,24 @@ export { Translator } import { CSLExporter as Exporter } from './csl/csl' function date2csl(date) { + let csl switch (date.type) { case 'open': return [0] case 'date': - const csl = [date.year > 0 ? date.year : date.year - 1] + csl = [date.year > 0 ? date.year : date.year - 1] if (date.month) { csl.push(date.month) if (date.day) { csl.push(date.day) } } - return csl + return csl // eslint-disable-line @typescript-eslint/no-unsafe-return case 'season': // https://github.com/retorquere/zotero-better-bibtex/issues/860 - return [ date.year > 0 ? date.year : date.year - 1, date.season + 12 ] // eslint-disable-line no-magic-numbers + return [ date.year > 0 ? date.year : date.year - 1, date.season + 12 ] // eslint-disable-line no-magic-numbers, @typescript-eslint/restrict-plus-operands, @typescript-eslint/no-unsafe-return default: throw new Error(`Expected date or open, got ${date.type}`) @@ -60,7 +61,7 @@ Exporter.serialize = csl => JSON.stringify(csl) Exporter.flush = items => `[\n${(items.map(item => ` ${item}`)).join(',\n')}\n]\n` -export function doExport() { +export function doExport(): void { Translator.init('export') Exporter.initialize() Exporter.doExport() diff --git a/translators/Better CSL YAML.ts b/translators/Better CSL YAML.ts index cef9dcd52e..6f9ddebf13 100644 --- a/translators/Better CSL YAML.ts +++ b/translators/Better CSL YAML.ts @@ -21,7 +21,7 @@ const htmlConverter = new class HTML { if (!tag) return if (['#text', 'pre', 'script'].includes(tag.nodeName)) { - this.markdown += tag.value.replace(/([\[*~^])/g, '\\$1') + this.markdown += tag.value.replace(/([[*~^])/g, '\\$1') return } @@ -132,7 +132,8 @@ function date2csl(date) { } } -Exporter.date2CSL = date => { +// eslint-disable-next-line prefer-arrow/prefer-arrow-functions, @typescript-eslint/no-unsafe-return +Exporter.date2CSL = function(date) { switch (date.type) { case 'date': case 'season': @@ -149,16 +150,18 @@ Exporter.date2CSL = date => { } } -Exporter.serialize = csl => { +// eslint-disable-next-line prefer-arrow/prefer-arrow-functions +Exporter.serialize = function(csl): string { for (const [k, v] of Object.entries(csl)) { if (typeof v === 'string' && v.indexOf('<') >= 0) csl[k] = htmlConverter.convert(v) } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return YAML.dump([csl], {skipInvalid: true}) } Exporter.flush = items => `---\nreferences:\n${items.join('\n')}...\n` -export function doExport() { +export function doExport(): void { Translator.init('export') Exporter.initialize() Exporter.doExport() diff --git a/translators/BetterBibTeX JSON.ts b/translators/BetterBibTeX JSON.ts index 825c250f4b..abcf635378 100644 --- a/translators/BetterBibTeX JSON.ts +++ b/translators/BetterBibTeX JSON.ts @@ -11,7 +11,7 @@ import { log } from '../content/logger' const chunkSize = 0x100000 -export function detectImport() { +export function detectImport(): boolean { let str let json = '' while ((str = Zotero.read(chunkSize)) !== false) { @@ -22,7 +22,8 @@ export function detectImport() { let data try { data = JSON.parse(json) - } catch (err) { + } + catch (err) { return false } @@ -30,7 +31,7 @@ export function detectImport() { return true } -export async function doImport() { +export async function doImport(): Promise { Translator.init('import') let str @@ -77,7 +78,8 @@ export async function doImport() { const msg = `${valid}: unexpected ${source.itemType}.${field} for ${Translator.isZotero ? 'zotero' : 'juris-m'} in ${JSON.stringify(source)} / ${JSON.stringify(validFields)}` if (valid === false) { log.error(msg) - } else { + } + else { throw new Error(msg) } } @@ -88,7 +90,7 @@ export async function doImport() { Object.assign(item, source) // marker so BBT-JSON can be imported without extra-field meddling - item.extra = '\x1BBBT\x1B' + (item.extra || '') + item.extra = `\x1BBBT\x1B${item.extra || ''}` for (const att of item.attachments || []) { if (att.url) delete att.path @@ -103,7 +105,7 @@ export async function doImport() { const collections: any[] = Object.values(data.collections || {}) for (const collection of collections) { - collection.zoteroCollection = (new Zotero.Collection()) as any + collection.zoteroCollection = new Zotero.Collection() collection.zoteroCollection.type = 'collection' collection.zoteroCollection.name = collection.name collection.zoteroCollection.children = collection.items.filter(id => { @@ -115,7 +117,8 @@ export async function doImport() { for (const collection of collections) { if (collection.parent && data.collections[collection.parent]) { data.collections[collection.parent].zoteroCollection.children.push(collection.zoteroCollection) - } else { + } + else { if (collection.parent) log.debug(`Collection ${collection.key} has non-existent parent ${collection.parent}`) collection.parent = false } @@ -126,7 +129,7 @@ export async function doImport() { } } -export function doExport() { +export function doExport(): void { Translator.init('export') let item @@ -152,7 +155,7 @@ export function doExport() { if (Translator.options.dropAttachments && item.itemType === 'attachment') continue if (!Translator.options.Normalize) { - const [ , kind, lib, key ] = item.uri.match(/^https?:\/\/zotero\.org\/(users|groups)\/((?:local\/)?[^\/]+)\/items\/(.+)/) + const [ , kind, lib, key ] = item.uri.match(/^https?:\/\/zotero\.org\/(users|groups)\/((?:local\/)?[^/]+)\/items\/(.+)/) item.select = (kind === 'users') ? `zotero://select/library/items/${key}` : `zotero://select/groups/${lib}/items/${key}` } @@ -165,12 +168,13 @@ export function doExport() { if (Translator.options.exportFileData && att.saveFile && att.defaultPath) { att.saveFile(att.defaultPath, true) att.path = att.defaultPath - } else if (att.localPath) { + } + else if (att.localPath) { att.path = att.localPath } if (!Translator.options.Normalize) { - const [ , kind, lib, key ] = att.uri.match(/^https?:\/\/zotero\.org\/(users|groups)\/((?:local\/)?[^\/]+)\/items\/(.+)/) + const [ , kind, lib, key ] = att.uri.match(/^https?:\/\/zotero\.org\/(users|groups)\/((?:local\/)?[^/]+)\/items\/(.+)/) att.select = (kind === 'users') ? `zotero://select/library/items/${key}` : `zotero://select/groups/${lib}/items/${key}` } diff --git a/translators/Citation graph.ts b/translators/Citation graph.ts index 602248b78e..3a03398af7 100644 --- a/translators/Citation graph.ts +++ b/translators/Citation graph.ts @@ -4,17 +4,17 @@ import { Translator } from './lib/translator' export { Translator } function node(id, attributes = {}) { - let _node = JSON.stringify(id) + let n = JSON.stringify(id) const attrs = Object.entries(attributes).map(([key, value]) => `${key}=${JSON.stringify(value)}`).join(', ') - if (attrs) _node += ` [${attrs}]` - Zotero.write(` ${_node};\n`) + if (attrs) n += ` [${attrs}]` + Zotero.write(` ${n};\n`) } function edge(source, target, attributes = {}) { - let _edge = `${JSON.stringify(source)} -> ${JSON.stringify(target)}` + let e = `${JSON.stringify(source)} -> ${JSON.stringify(target)}` const attrs = Object.entries(attributes).map(([key, value]) => `${key}=${JSON.stringify(value)}`).join(', ') - if (attrs) _edge += ` [${attrs}]` - Zotero.write(` ${_edge};\n`) + if (attrs) e += ` [${attrs}]` + Zotero.write(` ${e};\n`) } type Item = { @@ -26,7 +26,7 @@ type Item = { uri: string } -export function doExport() { +export function doExport(): void { Translator.init('export') Zotero.write('digraph CitationGraph {\n') @@ -50,6 +50,7 @@ export function doExport() { const author = [] if (add.authors && item.creators && item.creators.length) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return const name = item.creators?.map(creator => (creator.name || creator.lastName || '').replace(/"/g, "'")).filter(creator => creator).join(', ') if (name) author.push(name) } @@ -61,9 +62,10 @@ export function doExport() { if (author.length) label.push(author.join(' ')) items.push({ - id: 'node-' + item.uri.replace(/.*\//, ''), + id: `node-${item.uri.replace(/.*\//, '')}`, label: label.join('\n'), relations: (item.relations?.['dc:relation'] || []), + // eslint-disable-next-line prefer-spread cites: [].concat.apply([], (item.extra || '') .split('\n') @@ -71,7 +73,7 @@ export function doExport() { .map(line => line.replace(/^cites:/, '').trim()) .filter(keys => keys) .map(keys => keys.split(/\s*,\s*/)) - ), + ), citationKey: item.citationKey, uri: item.uri, }) @@ -84,7 +86,8 @@ export function doExport() { const other = items.find(o => o.uri === uri) if (other) { edge(item.id, other.id) - } else { + } + else { edge(item.id, uri.replace(/.*\//, ''), { style: 'dashed', dir: 'both' }) } } @@ -94,7 +97,8 @@ export function doExport() { if (other) { edge(item.id, other.id) - } else { + } + else { edge(item.id, citationKey, { style: 'dashed' }) } } diff --git a/translators/Collected notes.ts b/translators/Collected notes.ts index 84759dda2f..5c72e222ce 100644 --- a/translators/Collected notes.ts +++ b/translators/Collected notes.ts @@ -1,8 +1,12 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ + declare const Zotero: any import { Translator } from './lib/translator' export { Translator } +import { ZoteroTranslator } from '../gen/typings/serialized-item' + import * as escape from '../content/escape' import * as Extra from '../content/extra' @@ -14,7 +18,7 @@ function cleanExtra(extra) { class Exporter { private levels = 0 private body = '' - private items: Record = {} + private items: Record = {} public html = '' constructor() { @@ -30,8 +34,6 @@ class Exporter { if (!Translator.collections[collection.parent]) delete collection.parent if (!collection.parent && !this.prune(collection)) root.push(collection) // prune empty roots } - Zotero.debug('root collections: ' + JSON.stringify(root)) - Zotero.debug('items: ' + JSON.stringify(Object.keys(this.items))) for (const item of (Object.values(this.items) as { itemID: number }[])) { if (!filed[item.itemID] && this.keep(item)) this.item(item) @@ -57,7 +59,6 @@ class Exporter { } collection(collection, level = 1) { - this.show('collection', arguments) if (level > this.levels) this.levels = level this.body += `${ escape.html(collection.name) }\n` @@ -71,7 +72,6 @@ class Exporter { } item(item) { - this.show('item', arguments) switch (item.itemType) { case 'note': this.note(item.note, 'note') @@ -86,7 +86,6 @@ class Exporter { } prune(collection) { - this.show('prune', arguments) if (!collection) return true collection.items = collection.items.filter(itemID => this.keep(this.items[itemID])) @@ -96,7 +95,6 @@ class Exporter { } note(note, type) { - this.show('note', arguments) switch (type) { case 'extra': if (!note) return @@ -114,12 +112,10 @@ class Exporter { } creator(cr) { - this.show('creator', arguments) return [cr.lastName, cr.firstName, cr.name].filter(v => v).join(', ') } reference(item) { - this.show('reference', arguments) let notes = [] let title = '' @@ -127,10 +123,10 @@ class Exporter { if (item.note) notes = [ { note: item.note } ] if (item.title) title = `${ escape.html(item.title) }` - } else { + } + else { notes = (item.notes || []).filter(note => note.note) - Zotero.debug('this.reference: ' + JSON.stringify(item)) const creators = item.creators.map(creator => this.creator(creator)).filter(v => v).join(' and ') let date = null @@ -161,19 +157,17 @@ class Exporter { } reset(starting) { - this.show('reset', arguments) if (starting > this.levels) return '' let reset = 'counter-reset:' for (let level = starting; level <= this.levels; level++) { reset += ` h${ level }counter 0` } - return reset + ';' + return `${reset};` // return `counter-reset: h${ starting }counter;` } keep(item) { - this.show('keep', arguments) if (!item) return false if (item.extra) return true if (item.note) return true @@ -183,7 +177,7 @@ class Exporter { } } -export function doExport() { +export function doExport(): void { Translator.init('export') Zotero.write((new Exporter).html) } diff --git a/translators/bibtex/datefield.ts b/translators/bibtex/datefield.ts index ea217140db..e942be18d5 100644 --- a/translators/bibtex/datefield.ts +++ b/translators/bibtex/datefield.ts @@ -1,6 +1,9 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ + +import { ParsedDate } from '../../content/typings/bbt' import { Translator } from '../lib/translator' -function pad(v, padding) { +function pad(v:string, padding: string): string { if (v.length >= padding.length) return v return (padding + v).slice(-padding.length) } @@ -9,7 +12,8 @@ function year(y) { // eslint-disable-next-line no-magic-numbers if (Math.abs(y) > 999) { return `${y}` - } else { + } + else { // eslint-disable-next-line no-magic-numbers return (y < 0 ? '-' : '') + (`000${Math.abs(y)}`).slice(-4) } @@ -21,14 +25,17 @@ function format(date) { if (typeof date.year === 'number' && date.month && date.day) { formatted = `${year(date.year)}-${pad(date.month, '00')}-${pad(date.day, '00')}` - } else if (typeof date.year === 'number' && (date.month || date.season)) { + } + else if (typeof date.year === 'number' && (date.month || date.season)) { // eslint-disable-next-line no-magic-numbers - formatted = `${year(date.year)}-${pad((date.month || (date.season + 20)), '00')}` + formatted = `${year(date.year)}-${pad((date.month || ((date.season as number)+ 20)), '00')}` - } else if (typeof date.year === 'number') { + } + else if (typeof date.year === 'number') { formatted = year(date.year) - } else { + } + else { formatted = '' } @@ -41,7 +48,7 @@ function format(date) { return formatted } -export function datefield(date, field) { +export function datefield(date: ParsedDate, field: IField): IField { field = JSON.parse(JSON.stringify({ ...field, value: '', enc: 'latex' })) if (!date) return field @@ -53,17 +60,21 @@ export function datefield(date, field) { if (date.verbatim === 'n.d.') { field.value = '
    \\bibstring{nodate}
    ' - } else { + } + else { field.value = date.verbatim } - } else if (date.type === 'date' || date.type === 'season') { + } + else if (date.type === 'date' || date.type === 'season') { field.value = format(date) - } else if (date.type === 'interval') { + } + else if (date.type === 'interval') { field.value = `${format(date.from)}/${format(date.to)}` - } else if (date.year) { + } + else if (date.year) { field.value = format(date) } @@ -73,7 +84,7 @@ export function datefield(date, field) { // well this is fairly dense... the date field is not an verbatim field, so the 'circa' symbol ('~') ought to mean a // NBSP... but some magic happens in that field (always with the magic, BibLaTeX...). But hey, if I insert an NBSP, // guess what that gets translated to! - if (date.type !== 'verbatim') field.value = field.value.replace(/~/g, '\u00A0') + if (date.type !== 'verbatim' && typeof field.value == 'string') field.value = field.value.replace(/~/g, '\u00A0') return field } diff --git a/translators/bibtex/exporter.ts b/translators/bibtex/exporter.ts index 7f7c484b38..a28b7c7c88 100644 --- a/translators/bibtex/exporter.ts +++ b/translators/bibtex/exporter.ts @@ -1,6 +1,7 @@ declare const Zotero: any import { Translator } from '../lib/translator' +import { ZoteroTranslator } from '../../gen/typings/serialized-item' import { JabRef } from '../bibtex/jabref' // not so nice... BibTeX-specific code import * as itemfields from '../../gen/items/items' @@ -9,7 +10,7 @@ import { Postfix } from '../bibtex/postfix.ts' import * as Extra from '../../content/extra' // export singleton: https://k94n.com/es6-modules-single-instance-pattern -export let Exporter = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match +export const Exporter = new class { public postfix: Postfix public jabref: JabRef public strings: {[key: string]: string} = {} @@ -39,19 +40,19 @@ export let Exporter = new class { // eslint-disable-line @typescript-eslint/nami return uniq } - public nextItem(): ISerializedItem { + public nextItem(): ZoteroTranslator.Item { this.postfix = this.postfix || (new Postfix(Translator.preferences.qualityReport)) - let item + let item: ZoteroTranslator.Item while (item = Translator.nextItem()) { if (['note', 'attachment'].includes(item.itemType)) continue - if (!item.citekey) { + if (!item.citationKey) { throw new Error(`No citation key in ${JSON.stringify(item)}`) } - this.citekeys[item.citekey] = (this.citekeys[item.citekey] || 0) + 1 + this.citekeys[item.citationKey] = (this.citekeys[item.citationKey] || 0) + 1 - this.jabref.citekeys.set(item.itemID, item.citekey) + this.jabref.citekeys.set(item.itemID, item.citationKey) // this is not automatically lazy-evaluated?!?! const cached: Types.DB.Cache.ExportedItem = item.cachable ? Zotero.BetterBibTeX.cacheFetch(item.itemID, Translator.options, Translator.preferences) : null @@ -59,12 +60,13 @@ export let Exporter = new class { // eslint-disable-line @typescript-eslint/nami if (cached) { Zotero.write(cached.reference) - this.postfix.add(cached) + this.postfix.add(cached.metadata) continue } itemfields.simplifyForExport(item) Object.assign(item, Extra.get(item.extra, 'zotero')) + // strip extra.tex fields that are not for me const prefix = Translator.BetterBibLaTeX ? 'biblatex.' : 'bibtex.' for (const [name, field] of Object.entries(item.extraFields.tex).sort((a, b) => b[0].localeCompare(a[0]))) { // sorts the fields from tex. to biblatex. to bibtex. diff --git a/translators/bibtex/jabref.ts b/translators/bibtex/jabref.ts index b795afea11..4ee18a9ad5 100644 --- a/translators/bibtex/jabref.ts +++ b/translators/bibtex/jabref.ts @@ -10,15 +10,17 @@ export class JabRef { this.citekeys = new Map } - public exportGroups() { + public exportGroups(): void { if ((Object.keys(Translator.collections).length === 0) || !Translator.preferences.jabrefFormat) return let meta if (Translator.preferences.jabrefFormat === 3) { // eslint-disable-line no-magic-numbers meta = 'groupsversion:3' - } else if (Translator.BetterBibLaTeX) { + } + else if (Translator.BetterBibLaTeX) { meta = 'databaseType:biblatex' - } else { + } + else { meta = 'databaseType:bibtex' } @@ -36,7 +38,7 @@ export class JabRef { Zotero.write('}\n') } - private exportGroup(collection, level) { + private exportGroup(collection, level: number): void { let group = [`${level} ${Translator.preferences.jabrefFormat === 5 ? 'Static' : 'Explicit'}Group:${this.quote(collection.name)}`, '0'] // eslint-disable-line no-magic-numbers if (Translator.preferences.jabrefFormat === 3) { // eslint-disable-line no-magic-numbers @@ -47,12 +49,14 @@ export class JabRef { if (Translator.preferences.jabrefFormat === 5) { // eslint-disable-line no-magic-numbers group = group.concat(['1', '0x8a8a8aff', '', '']) // isexpanded?, color, icon, description - } else { + } + else { group.push('') // what is the meaning of the empty cell at the end, JabRef? } this.groups.push(group.join(';')) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return const children = (collection.collections || []).map(key => Translator.collections[key]).filter(coll => coll) if (Translator.preferences.testing) children.sort((a, b) => Translator.stringCompare(a.name, b.name)) for (const child of children) { @@ -60,7 +64,7 @@ export class JabRef { } } - private quote(s, wrap = false) { + private quote(s:string, wrap = false): string { s = s.replace(/([\\;])/g, '\\$1') if (wrap) s = s.match(/.{1,70}/g).join('\n') return s diff --git a/translators/bibtex/postfix.ts b/translators/bibtex/postfix.ts index 3377f23331..da72ff47af 100644 --- a/translators/bibtex/postfix.ts +++ b/translators/bibtex/postfix.ts @@ -12,19 +12,19 @@ export class Postfix { this.declarePrefChars = '' } - public add(item) { - if (!item.metadata) return + public add(metadata: { DeclarePrefChars: string, noopsort: any, packages: any }): void { + if (!metadata) return - if (item.metadata.DeclarePrefChars) this.declarePrefChars += item.metadata.DeclarePrefChars - if (item.metadata.noopsort) this.noopsort = true - if (item.metadata.packages) { - for (const pkg of item.metadata.packages) { + if (metadata.DeclarePrefChars) this.declarePrefChars += metadata.DeclarePrefChars + if (metadata.noopsort) this.noopsort = true + if (metadata.packages) { + for (const pkg of metadata.packages) { this.packages[pkg] = true } } } - public toString() { + public toString(): string { let postfix = '' let preamble = [] diff --git a/translators/bibtex/reference.ts b/translators/bibtex/reference.ts index 213ec5f3a1..e371cf0f65 100644 --- a/translators/bibtex/reference.ts +++ b/translators/bibtex/reference.ts @@ -1,5 +1,10 @@ +/* eslint-disable @typescript-eslint/ban-types */ +/* eslint-disable @typescript-eslint/no-unsafe-return, @typescript-eslint/explicit-module-boundary-types */ + declare const Zotero: any +import { ZoteroTranslator } from '../../gen/typings/serialized-item' + import { Translator } from '../lib/translator' import { Exporter } from './exporter' @@ -12,17 +17,17 @@ import { log } from '../../content/logger' import { arXiv } from '../../content/arXiv' -const Path = { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match - normalize(path) { +const Path = { // eslint-disable-line @typescript-eslint/naming-convention + normalize(path) { // eslint-disable-line prefer-arrow/prefer-arrow-functions return Translator.paths.caseSensitive ? path : path.toLowerCase() }, - drive(path) { + drive(path) { // eslint-disable-line prefer-arrow/prefer-arrow-functions if (Translator.preferences.platform !== 'win') return '' return path.match(/^[a-z]:\//) ? path.substring(0, 2) : '' }, - relative(path) { + relative(path) { // eslint-disable-line prefer-arrow/prefer-arrow-functions if (this.drive(Translator.export.dir) !== this.drive(path)) return path const from = Translator.export.dir.split(Translator.paths.sep) @@ -32,29 +37,11 @@ const Path = { // eslint-disable-line @typescript-eslint/naming-convention,no-u from.shift() to.shift() } + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands return `..${Translator.paths.sep}`.repeat(from.length) + to.join(Translator.paths.sep) }, } -interface IField { - name: string - verbatim?: string - value: string | string[] | number | null | { path: string; title?: string; mimeType?: string; } | { tag: string, type?: number }[] - enc?: 'raw' | 'url' | 'verbatim' | 'creators' | 'literal' | 'latex' | 'tags' | 'attachments' | 'date' - orig?: { name?: string, verbatim?: string, inherit?: boolean } - bibtexStrings?: boolean - bare?: boolean - raw?: boolean - - // kept as seperate booleans for backwards compat - replace?: boolean - fallback?: boolean - - html?: boolean - - bibtex?: string -} - const Language = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match public babelMap = { af: 'afrikaans', @@ -201,7 +188,8 @@ const Language = new class { // eslint-disable-line @typescript-eslint/naming-co } if (matches.length === 1) { this.prefix[langcode] = matches[0] - } else { + } + else { this.prefix[langcode] = false } } @@ -232,7 +220,8 @@ const Language = new class { // eslint-disable-line @typescript-eslint/naming-co if (pairs1[0] < pairs2[0]) { pairs1.shift() - } else { + } + else { pairs2.shift() } } @@ -291,7 +280,8 @@ const fieldOrder = [ ].reduce((acc, field, idx) => { if (field[0] === '-') { acc[field.substring(1)] = -(idx + 1) - } else { + } + else { acc[field] = idx + 1 } return acc @@ -306,7 +296,7 @@ const fieldOrder = [ */ export class Reference { public has: { [key: string]: any } = {} - public item: ISerializedItem + public item: ZoteroTranslator.Item public referencetype: string public referencetype_source: string public useprefix: boolean @@ -328,7 +318,7 @@ export class Reference { private inPostscript = false - public static installPostscript() { + public static installPostscript(): void { let postscript = Translator.preferences.postscript if (typeof postscript !== 'string' || postscript.trim() === '') return @@ -338,7 +328,8 @@ export class Reference { // workaround for https://github.com/Juris-M/zotero/issues/65 Reference.prototype.postscript = new Function('reference', 'item', 'Translator', 'Zotero', postscript) as (reference: any, item: any) => boolean log.debug(`Installed postscript: ${JSON.stringify(postscript)}`) - } catch (err) { + } + catch (err) { if (Translator.preferences.testing) throw err log.error(`Failed to compile postscript: ${err}\n\n${JSON.stringify(postscript)}`) } @@ -358,7 +349,8 @@ export class Reference { if (!this.item.language) { this.english = true - } else { + } + else { const langlc = this.item.language.toLowerCase() let language = Language.babelMap[langlc.replace(/[^a-z0-9]/, '_')] @@ -366,11 +358,13 @@ export class Reference { if (!language) language = Language.fromPrefix(langlc) if (language) { this.language = language[0] - } else { + } + else { const match = Language.lookup(langlc) if (match[0].sim >= 0.9) { // eslint-disable-line no-magic-numbers this.language = match[0].lang - } else { + } + else { this.language = this.item.language } } @@ -385,7 +379,8 @@ export class Reference { let csl_type = this.item.extraFields.kv.type if (this.typeMap.csl[csl_type]) { delete this.item.extraFields.kv.type - } else { + } + else { csl_type = null } @@ -395,16 +390,19 @@ export class Reference { if (this.item.extraFields.tex.referencetype) { referencetype = this.item.extraFields.tex.referencetype.value this.referencetype_source = `tex.${referencetype}` - } else if (csl_type) { + } + else if (csl_type) { referencetype = this.typeMap.csl[csl_type] this.referencetype_source = `csl.${csl_type}` - } else { + } + else { referencetype = this.typeMap.zotero[this.item.itemType] || 'misc' this.referencetype_source = `zotero.${this.item.itemType}` } if (typeof referencetype === 'string') { this.referencetype = referencetype - } else { + } + else { this.add({ name: 'entrysubtype', value: referencetype.subtype }) this.referencetype = referencetype.type } @@ -415,7 +413,8 @@ export class Reference { if (ef.zotero) { if (!item[name] || ef.type === 'date') { item[name] = value - } else { + } + else { log.debug('extra fields: skipping', {name, value}) } delete item.extraFields.kv[name] @@ -434,7 +433,8 @@ export class Reference { if (Translator.preferences.jabrefFormat) { if (Translator.preferences.testing) { this.add({name: 'timestamp', value: '2015-02-24 12:14:36 +0100'}) - } else { + } + else { this.add({name: 'timestamp', value: this.item.dateModified || this.item.dateAdded}) } } @@ -443,10 +443,12 @@ export class Reference { this.item.arXiv.source = 'publicationTitle' if (Translator.BetterBibLaTeX) delete this.item.publicationTitle - } else if ((this.item.arXiv = arXiv.parse(this.item.extraFields.tex.arxiv?.value)) && this.item.arXiv.id) { + } + else if ((this.item.arXiv = arXiv.parse(this.item.extraFields.tex.arxiv?.value)) && this.item.arXiv.id) { this.item.arXiv.source = 'extra' - } else { + } + else { this.item.arXiv = null } @@ -461,7 +463,7 @@ export class Reference { } /** normalize dashes, mainly for use in `pages` */ - public normalizeDashes(str) { + public normalizeDashes(str): string { str = (str || '').trim() if (this.item.raw) return str @@ -489,10 +491,10 @@ export class Reference { * 'enc' means 'enc_latex'. If you pass both 'bibtex' and 'latex', 'bibtex' takes precedence (and 'value' will be * ignored) */ - public add(field: IField) { + public add(field: IField): string { if (!field.value && !field.bibtex && this.inPostscript) { delete this.has[field.name] - return + return null } if (Translator.skipField[field.name]) return null @@ -561,11 +563,12 @@ export class Reference { } if (!field.bibtex) { - let bibstring: string = '' + let bibstring = '' if ((typeof field.value === 'number') || (field.bibtexStrings && (bibstring = this.getBibString(field.value)))) { field.bibtex = `${bibstring || field.value}` - } else { + } + else { const enc = field.enc || this.fieldEncoding[field.name] || 'latex' let value @@ -639,7 +642,7 @@ export class Reference { return removed } - public getBibString(value) { + public getBibString(value): string { if (!value || typeof value !== 'string') return null switch (Translator.preferences.exportBibTeXStrings) { @@ -663,15 +666,16 @@ export class Reference { } } - public hasCreator(type) { return (this.item.creators || []).some(creator => creator.creatorType === type) } + public hasCreator(type): boolean { return (this.item.creators || []).some(creator => creator.creatorType === type) } - public override(field: IField) { + public override(field: IField): void { const itemtype_name = field.name.split('.') let name if (itemtype_name.length === 2) { if (this.referencetype !== itemtype_name[0]) return name = itemtype_name[1] - } else { + } + else { name = field.name } @@ -683,7 +687,7 @@ export class Reference { this.add({ ...field, name, replace: (typeof field.replace !== 'boolean' && typeof field.fallback !== 'boolean') || field.replace }) } - public complete() { + public complete(): void { if (Translator.preferences.jabrefFormat >= 4 && this.item.collections?.length) { // eslint-disable-line no-magic-numbers const groups = Array.from(new Set(this.item.collections.map(key => Translator.collections[key]?.name).filter(name => name))).sort() this.add({ name: 'groups', value: groups.join(',') }) @@ -806,7 +810,8 @@ export class Reference { if (name) { this.override({ name, verbatim: name, orig: { inherit: true }, value, enc, replace, fallback: !replace }) - } else { + } + else { log.debug('Unmapped extra field', key, '=', value) } } @@ -814,7 +819,7 @@ export class Reference { this.add({ name: 'annotation', value: this.item.extra?.replace(/\n+/g, ' ') }) if (Translator.options.exportNotes) { // if bibtexURL === 'note' is active, the note field will have been filled with an URL. In all other cases, if this is attempting to overwrite the 'note' field, I want the test suite to throw an error - if (!(Translator.BetterBibTeX && Translator.preferences.bibtexURL === 'note')) this.add({ name: 'note', value: this.item.notes?.join('

    '), html: true }) + if (!(Translator.BetterBibTeX && Translator.preferences.bibtexURL === 'note')) this.add({ name: 'note', value: this.item.notes?.map((note: { note: string }) => note.note).join('

    '), html: true }) } const bibtexStrings = Translator.preferences.exportBibTeXStrings.startsWith('match') @@ -842,7 +847,8 @@ export class Reference { if (Translator.BetterBibLaTeX) { this.override({ name: 'eprinttype', value: name }) this.override({ name: 'eprint', value: field.value, raw: field.raw }) - } else { + } + else { this.override({ name, value: field.value, raw: field.raw }) } break @@ -850,7 +856,8 @@ export class Reference { if (Translator.BetterBibLaTeX) { this.override({ name: 'eprinttype', value: 'googlebooks' }) this.override({ name: 'eprint', value: field.value, raw: field.raw }) - } else { + } + else { this.override({ name: 'googlebooks', value: field.value, raw: field.raw }) } break @@ -882,7 +889,8 @@ export class Reference { try { if (this.postscript(this, this.item, Translator, Zotero) === false) this.item.cachable = false - } catch (err) { + } + catch (err) { if (Translator.preferences.testing && !Translator.preferences.ignorePostscriptErrors) throw err log.error('Reference.postscript failed:', err) this.item.cachable = false @@ -922,7 +930,7 @@ export class Reference { this.metadata.packages = Object.keys(this.packages) if (this.item.cachable) Zotero.BetterBibTeX.cacheStore(this.item.itemID, Translator.options, Translator.preferences, ref, this.metadata) - Exporter.postfix.add(this) + Exporter.postfix.add(this.metadata) } /* @@ -931,7 +939,7 @@ export class Reference { * @param {field} field to encode * @return {String} unmodified `field.value` */ - protected enc_raw(f) { + protected enc_raw(f): string { return f.value } @@ -941,12 +949,14 @@ export class Reference { * @param {field} field to encode * @return {String} field.value encoded as verbatim LaTeX string (minimal escaping). If in Better BibTeX, wraps return value in `\url{string}` */ - protected enc_url(f) { + protected enc_url(f): string { if (Translator.BetterBibTeX && Translator.preferences.bibtexURL.endsWith('-ish')) { return (f.value || '').replace(/([#\\%&{}])/g, '\\$1') // or maybe enc_latex? - } else if (Translator.BetterBibTeX && Translator.preferences.bibtexURL === 'note') { + } + else if (Translator.BetterBibTeX && Translator.preferences.bibtexURL === 'note') { return `\\url{${this.enc_verbatim(f)}}` - } else { + } + else { return this.enc_verbatim(f) } } @@ -957,12 +967,12 @@ export class Reference { * @param {field} field to encode * @return {String} field.value encoded as verbatim LaTeX string (minimal escaping). */ - protected enc_verbatim(f) { + protected enc_verbatim(f): string { // if (!Translator.unicode) value = value.replace(/[^\x20-\x7E]/g, (chr => `\\%${`00${chr.charCodeAt(0).toString(16).slice(-2)}`}`)) // tslint:disable-line:no-magic-numbers return (f.value || '').replace(/([\\{}])/g, '\\$1') } - protected _enc_creators_scrub_name(name) { + protected _enc_creators_scrub_name(name: string): string { return Zotero.Utilities.XRegExp.replace(name, this.whitespace, ' ', 'all') } /* @@ -971,7 +981,7 @@ export class Reference { * @param {field} field to encode. The 'value' must be an array of Zotero-serialized `creator` objects. * @return {String} field.value encoded as author-style value */ - protected enc_creators(f, raw) { + protected enc_creators(f, raw: boolean) { if (f.value.length === 0) return null const encoded = [] @@ -981,10 +991,12 @@ export class Reference { name = creator.name || creator.lastName if (name !== 'others') name = raw ? `{${name}}` : this.enc_latex({value: new String(this._enc_creators_scrub_name(name))}) // eslint-disable-line no-new-wrappers - } else if (raw) { + } + else if (raw) { name = [creator.lastName || '', creator.firstName || ''].join(', ') - } else if (creator.lastName || creator.firstName) { + } + else if (creator.lastName || creator.firstName) { name = { family: this._enc_creators_scrub_name(creator.lastName || ''), given: this._enc_creators_scrub_name(creator.firstName || ''), @@ -1000,13 +1012,15 @@ export class Reference { if (Translator.BetterBibTeX) { name = this._enc_creators_bibtex(name) - } else { + } + else { name = this._enc_creators_biblatex(name) } name = name.replace(/ and /g, ' {and} ') - } else { + } + else { continue } @@ -1069,7 +1083,7 @@ export class Reference { return value } - protected enc_tags(f) { + protected enc_tags(f): string { const tags = f.value .map(tag => (typeof tag === 'string' ? { tag } : tag)) .filter(tag => (Translator.preferences.automaticTags || (tag.type !== 1)) && tag.tag !== Translator.preferences.rawLaTag) @@ -1080,7 +1094,8 @@ export class Reference { for (const tag of tags) { if (Translator.BetterBibTeX) { tag.tag = tag.tag.replace(/([#\\%&])/g, '\\$1') - } else { + } + else { tag.tag = tag.tag.replace(/([#%\\])/g, '\\$1') } @@ -1102,10 +1117,9 @@ export class Reference { return tags.map(tag => tag.tag).join(',') } - protected enc_attachments(f) { + protected enc_attachments(f): string { if (!f.value || (f.value.length === 0)) return null - const attachments = [] - const errors = [] + const attachments: {title: string, mimetype: string, path: string}[] = [] for (const attachment of f.value) { const att = { @@ -1116,7 +1130,8 @@ export class Reference { if (Translator.options.exportFileData) { att.path = attachment.saveFile ? attachment.defaultPath : '' - } else if (attachment.localPath) { + } + else if (attachment.localPath) { att.path = attachment.localPath } @@ -1128,13 +1143,14 @@ export class Reference { attachment.saveFile(att.path, true) } - if (!att.title) att.title = att.path.replace(/.*[\\\/]/, '') || 'attachment' + if (!att.title) att.title = att.path.replace(/.*[\\/]/, '') || 'attachment' if (!att.mimetype && (att.path.slice(-4).toLowerCase() === '.pdf')) att.mimetype = 'application/pdf' // eslint-disable-line no-magic-numbers if (Translator.preferences.testing) { - att.path = `files/${this.item.citationKey}/${att.path.replace(/.*[\/\\]/, '')}` - } else if (Translator.preferences.relativeFilePaths && Translator.export.dir) { + att.path = `files/${this.item.citationKey}/${att.path.replace(/.*[/\\]/, '')}` + } + else if (Translator.preferences.relativeFilePaths && Translator.export.dir) { const relative = Path.relative(att.path) if (relative !== att.path) { this.item.cachable = false @@ -1145,7 +1161,6 @@ export class Reference { attachments.push(att) } - if (errors.length !== 0) f.errors = errors if (attachments.length === 0) return null // sort attachments for stable tests, and to make non-snapshots the default for JabRef to open (#355) @@ -1159,20 +1174,20 @@ export class Reference { return attachments.map(att => att.path.replace(/([\\{}:;])/g, '\\$1')).join(';') } - private _enc_creators_pad_particle(particle, relax = false) { + private _enc_creators_pad_particle(particle: string, relax = false): string { // space at end is always OK if (particle[particle.length - 1] === ' ') return particle if (Translator.BetterBibLaTeX) { if (Zotero.Utilities.XRegExp.test(particle, this.punctuationAtEnd)) this.metadata.DeclarePrefChars += particle[particle.length - 1] // if BBLT, always add a space if it isn't there - return particle + ' ' + return `${particle} ` } // otherwise, we're in BBT. // If the particle ends in a period, add a space - if (particle[particle.length - 1] === '.') return particle + ' ' + if (particle[particle.length - 1] === '.') return `${particle} ` // if it ends in any other punctuation, it's probably something like d'Medici -- no space if (Zotero.Utilities.XRegExp.test(particle, this.punctuationAtEnd)) { @@ -1181,55 +1196,59 @@ export class Reference { } // otherwise, add a space - return particle + ' ' + return `${particle} ` } - private _enc_creator_part(part) { - const { latex, packages } = text2latex(part, { creator: true, commandspacers: true }) + // eslint-disable-next-line @typescript-eslint/ban-types + private _enc_creator_part(part: string | String): string | String { + const { latex, packages } = text2latex((part as string), { creator: true, commandspacers: true }) for (const pkg of packages) { this.packages[pkg] = true } return (part instanceof String) ? new String(`{${latex}}`) : latex // eslint-disable-line no-new-wrappers } - private _enc_creators_biblatex(name) { - let family, latex + private _enc_creators_biblatex(name: {family?: string, given?: string, suffix?: string}): string { + let family: string | String if ((name.family.length > 1) && (name.family[0] === '"') && (name.family[name.family.length - 1] === '"')) { family = new String(name.family.slice(1, -1)) // eslint-disable-line no-new-wrappers - } else { + } + else { ({ family } = name) } - let initials = (name.given || '').indexOf(this._enc_creators_initials_marker) // end of guarded area + const initials_marker_pos: number = (name.given || '').indexOf(this._enc_creators_initials_marker) // end of guarded area + let initials: string | String if (Translator.preferences.biblatexExtendedNameFormat && (name['dropping-particle'] || name['non-dropping-particle'] || name['comma-suffix'])) { - if (initials >= 0) { - initials = name.given.substring(0, initials) + if (initials_marker_pos >= 0) { + initials = name.given.substring(0, initials_marker_pos) if (initials.length > 1) initials = new String(initials) // eslint-disable-line no-new-wrappers name.given = name.given.replace(this._enc_creators_initials_marker, '') - } else { + } + else { initials = '' } - latex = [] - if (family) latex.push(`family=${this._enc_creator_part(family)}`) - if (name.given) latex.push(`given=${this._enc_creator_part(name.given)}`) - if (initials) latex.push(`given-i=${this._enc_creator_part(initials)}`) - if (name.suffix) latex.push(`suffix=${this._enc_creator_part(name.suffix)}`) + const namebuilder: string[] = [] + if (family) namebuilder.push(`family=${this._enc_creator_part(family)}`) + if (name.given) namebuilder.push(`given=${this._enc_creator_part(name.given)}`) + if (initials) namebuilder.push(`given-i=${this._enc_creator_part(initials)}`) + if (name.suffix) namebuilder.push(`suffix=${this._enc_creator_part(name.suffix)}`) if (name['dropping-particle'] || name['non-dropping-particle']) { - latex.push(`prefix=${this._enc_creator_part(name['dropping-particle'] || name['non-dropping-particle'])}`) - latex.push(`useprefix=${!!name['non-dropping-particle']}`) + namebuilder.push(`prefix=${this._enc_creator_part(name['dropping-particle'] || name['non-dropping-particle'])}`) + namebuilder.push(`useprefix=${!!name['non-dropping-particle']}`) } - if (name['comma-suffix']) latex.push('juniorcomma=true') - return latex.join(', ') + if (name['comma-suffix']) namebuilder.push('juniorcomma=true') + return namebuilder.join(', ') } if (family && Zotero.Utilities.XRegExp.test(family, this.startsWithLowercase)) family = new String(family) // eslint-disable-line no-new-wrappers if (family) family = this._enc_creator_part(family) - if (initials >= 0) name.given = `${name.given.replace(this._enc_creators_initials_marker, '')}` + if (initials_marker_pos >= 0) name.given = `${name.given.replace(this._enc_creators_initials_marker, '')}` - latex = '' + let latex = '' if (name['dropping-particle']) latex += this._enc_creator_part(this._enc_creators_pad_particle(name['dropping-particle'])) if (name['non-dropping-particle']) latex += this._enc_creator_part(this._enc_creators_pad_particle(name['non-dropping-particle'])) if (family) latex += family @@ -1239,11 +1258,12 @@ export class Reference { return latex } - private _enc_creators_bibtex(name) { - let family + private _enc_creators_bibtex(name): string { + let family: string | String if ((name.family.length > 1) && (name.family[0] === '"') && (name.family[name.family.length - 1] === '"')) { // quoted family = new String(name.family.slice(1, -1)) // eslint-disable-line no-new-wrappers - } else { + } + else { family = name.family } @@ -1264,6 +1284,7 @@ export class Reference { in the label, use {\relax van} Gogh or something like this. */ + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands if (name['non-dropping-particle']) family = new String(this._enc_creators_pad_particle(name['non-dropping-particle']) + family) // eslint-disable-line no-new-wrappers if (Zotero.Utilities.XRegExp.test(family, this.startsWithLowercase) || Zotero.Utilities.XRegExp.test(family, this.hasLowercaseWord)) family = new String(family) // eslint-disable-line no-new-wrappers @@ -1273,7 +1294,7 @@ export class Reference { // https://github.com/retorquere/zotero-better-bibtex/issues/976#issuecomment-393442419 if (family[0] !== '{' && name.family.match(/[-\u2014\u2015\u2012\u2013]/)) family = `{${family}}` - if (name['dropping-particle']) family = this._enc_creator_part(this._enc_creators_pad_particle(name['dropping-particle'], true)) + family + if (name['dropping-particle']) family = `${this._enc_creator_part(this._enc_creators_pad_particle(name['dropping-particle'], true))}${family}` if (Translator.BetterBibTeX && Translator.preferences.bibtexParticleNoOp && (name['non-dropping-particle'] || name['dropping-particle'])) { family = `{\\noopsort{${this._enc_creator_part(name.family.toLowerCase())}}}${family}` @@ -1283,7 +1304,7 @@ export class Reference { if (name.given) name.given = this._enc_creator_part(name.given) if (name.suffix) name.suffix = this._enc_creator_part(name.suffix) - let latex = family + let latex: string = (family as string) if (name.suffix) latex += `, ${name.suffix}` if (name.given) latex += `, ${name.given}` @@ -1292,10 +1313,10 @@ export class Reference { private postscript(_reference, _item, _translator, _zotero): boolean { return true } // eslint-disable-line no-empty,@typescript-eslint/no-empty-function - private qualityReport() { + private qualityReport(): string { if (!Translator.preferences.qualityReport) return '' - let report = this.lint({ + let report: string[] = this.lint({ timestamp: `added because JabRef format is set to ${Translator.preferences.jabrefFormat || '?'}`, }) @@ -1318,11 +1339,13 @@ export class Reference { const titleCased = Zotero.BetterBibTeX.titleCase(this.has.title.value) === this.has.title.value if (this.has.title.value.match(/\s/)) { if (titleCased) report.push('? Title looks like it was stored in title-case in Zotero') - } else { + } + else { if (!titleCased) report.push('? Title looks like it was stored in lower-case in Zotero') } } - } else { + } + else { report = [`I don't know how to quality-check ${this.referencetype} references`] } diff --git a/translators/bibtex/unicode_translator.ts b/translators/bibtex/unicode_translator.ts index 7821865ae0..c298cd9324 100644 --- a/translators/bibtex/unicode_translator.ts +++ b/translators/bibtex/unicode_translator.ts @@ -19,19 +19,23 @@ type ConverterOptions = { commandspacers?: boolean } -export function replace_command_spacers(latex) { - return latex.replace(/\0(\s)/g, '{}$1').replace(/\0([^;\.,!?\${}_\^\\\/])/g, ' $1').replace(/\0/g, '') +export function replace_command_spacers(latex: string): string { + return latex.replace(/\0(\s)/g, '{}$1').replace(/\0([^;.,!?${}_^\\/])/g, ' $1').replace(/\0/g, '') } +type ParseResult = { latex: string, raw: boolean, packages: string[] } + +type LatexRepresentation = { text?: string, math?: string, textpackages?: string[], mathpackages?: string[], commandspacer?: boolean } + const htmlConverter = new class HTMLConverter { - private latex: string - private mapping: any - private stack: any[] - private options: ConverterOptions + private latex = '' + private mapping: any = {} + private stack: any[] = [] + private options: ConverterOptions = {} private embraced: boolean - private packages: { [key: string]: boolean } + private packages: { [key: string]: boolean } = {} - public convert(html: string, options: ConverterOptions) { + public convert(html: string, options: ConverterOptions): ParseResult { this.embraced = false this.options = options this.latex = '' @@ -39,7 +43,8 @@ const htmlConverter = new class HTMLConverter { if (Translator.unicode) { this.mapping = unicode2latex.unicode - } else if (options.creator && Translator.BetterBibTeX) { + } + else if (options.creator && Translator.BetterBibTeX) { /* https://github.com/retorquere/zotero-better-bibtex/issues/1189 Needed so that composite characters are counted as single characters for in-text citation generation. This messes with the {} cleanup @@ -50,7 +55,8 @@ const htmlConverter = new class HTMLConverter { that these have turned up. */ this.mapping = unicode2latex.ascii_bibtex_creator - } else { + } + else { this.mapping = unicode2latex.ascii } @@ -73,23 +79,28 @@ const htmlConverter = new class HTMLConverter { } } - } else if (Translator.preferences.mapUnicode === 'minimal-packages') { - for (const tex of (Object.values(this.mapping) as any[])) { + } + else if (Translator.preferences.mapUnicode === 'minimal-packages') { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + for (const tex of (Object.values(this.mapping) as LatexRepresentation[])) { if (tex.text && tex.math) { if (tex.textpackages && !tex.mathpackages) { delete tex.text delete tex.textpackages - } else if (!tex.textpackages && tex.mathpackages) { + } + else if (!tex.textpackages && tex.mathpackages) { delete tex.math delete tex.mathpackages } } } - } else { + } + else { const remove = switchMode[Translator.preferences.mapUnicode] if (remove) { - for (const tex of (Object.values(this.mapping) as any[])) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + for (const tex of (Object.values(this.mapping) as LatexRepresentation[])) { if (tex.text && tex.math) delete tex[remove] } } @@ -221,14 +232,15 @@ const htmlConverter = new class HTMLConverter { break } - if (latex !== '...') latex = this.embrace(latex, latex.match(/^\\[a-z]+{\.\.\.}$/)) + if (latex !== '...') latex = this.embrace(latex, /^\\[a-z]+{\.\.\.}$/.test(latex)) if (tag.smallcaps) latex = this.embrace(`\\textsc{${latex}}`, true) if (tag.nocase) latex = `{{${latex}}}` if (tag.relax) latex = `{\\relax ${latex}}` if (tag.enquote) { if (Translator.BetterBibTeX) { latex = `\\enquote{${latex}}` - } else { + } + else { latex = `\\mkbibquote{${latex}}` } } @@ -245,7 +257,7 @@ const htmlConverter = new class HTMLConverter { } - private embrace(latex, condition) { + private embrace(latex: string, condition: boolean): string { /* holy mother of %^$#^%$@ the bib(la)tex case conversion rules are insane */ /* https://github.com/retorquere/zotero-better-bibtex/issues/541 */ /* https://github.com/plk/biblatex/issues/459 ... oy! */ @@ -267,14 +279,18 @@ const htmlConverter = new class HTMLConverter { } text = text.normalize('NFD') // required for multi-diacritics - let mapped, switched, m, i, diacritic - const l = text.length + let mapped: LatexRepresentation + let switched: boolean + let m: RegExpExecArray | RegExpMatchArray + let i: number + let diacritic: { command: string, mode: string } + const l: number = text.length for (i = 0; i < l; i++) { mapped = null // tie "i","︠","a","︡" if (text[i + 1] === '\ufe20' && text[i + 3] === '\ufe21') { // eslint-disable-line no-magic-numbers - mapped = this.mapping[text.substr(i, 4)] || { text: text[i] + text[i + 2] } // eslint-disable-line no-magic-numbers + mapped = this.mapping[text.substr(i, 4)] || { text: `${text[i]}${text[i + 2]}` } // eslint-disable-line no-magic-numbers i += 3 // eslint-disable-line no-magic-numbers } @@ -293,16 +309,19 @@ const htmlConverter = new class HTMLConverter { if (Translator.BetterBibTeX && diacritic.mode === 'text') { // needs to be braced to count as a single char for name abbreviation - mapped = { [diacritic.mode]: `{\\${diacritic.command}${cmd ? ' ': ''}${char}}` } + mapped = ({ [diacritic.mode]: `{\\${diacritic.command}${cmd ? ' ': ''}${char}}` } as LatexRepresentation) - } else if (cmd && char.length === 1) { - mapped = { [diacritic.mode]: `\\${diacritic.command} ${char}` } + } + else if (cmd && char.length === 1) { + mapped = ({ [diacritic.mode]: `\\${diacritic.command} ${char}` } as LatexRepresentation) - } else if (cmd) { - mapped = { [diacritic.mode]: `\\${diacritic.command}{${char}}` } + } + else if (cmd) { + mapped = ({ [diacritic.mode]: `\\${diacritic.command}{${char}}` } as LatexRepresentation) - } else { - mapped = { [diacritic.mode]: `\\${diacritic.command}${char}` } + } + else { + mapped = ({ [diacritic.mode]: `\\${diacritic.command}${char}` } as LatexRepresentation) } // support for multiple-diacritics is taken from tipa, which doesn't support more than 2 @@ -327,7 +346,8 @@ const htmlConverter = new class HTMLConverter { mode = switchMode[mode] latex += switchTo[mode] switched = true - } else { + } + else { switched = false } @@ -354,8 +374,12 @@ const htmlConverter = new class HTMLConverter { latex = latex.slice(0, latex.length - m[0].length) + m[1] + m[3] // eslint-disable-line no-magic-numbers } - const pkg = mapped[mode + 'package'] || mapped.package - if (pkg) this.packages[pkg] = true + const pkgs = (mapped[`${mode}packages`] as string[]) + if (pkgs) { + for (const pkg of pkgs) { + this.packages[pkg] = true + } + } } // add any missing closing phantom braces @@ -377,12 +401,12 @@ const htmlConverter = new class HTMLConverter { } } -export function html2latex(html:string, options: ConverterOptions) { +export function html2latex(html:string, options: ConverterOptions): ParseResult { if (typeof options.html === 'undefined') options.html = true return htmlConverter.convert(html, options) } -export function text2latex(text:string, options: ConverterOptions = {}) { +export function text2latex(text:string, options: ConverterOptions = {}): ParseResult { if (typeof options.html === 'undefined') options.html = false return html2latex(text, options) } diff --git a/translators/csl/csl.ts b/translators/csl/csl.ts index 3c77a027b1..e5dbd53f10 100644 --- a/translators/csl/csl.ts +++ b/translators/csl/csl.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/ban-types */ +/* eslint-disable @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-assignment */ + declare const Zotero: any import { Translator } from '../lib/translator' @@ -17,10 +20,10 @@ const keyOrder = [ 'month', 'day', 'circa', -].reduce((acc, field, idx, fields) => { acc[field] = idx + 1; return acc }, {}) +].reduce((acc, field, idx) => { acc[field] = idx + 1; return acc }, {}) // export singleton: https://k94n.com/es6-modules-single-instance-pattern -export let CSLExporter = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match +export const CSLExporter = new class { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match public flush: Function // will be added by JSON/YAML exporter public serialize: Function // will be added by JSON/YAML exporter public date2CSL: Function // will be added by JSON/YAML exporter @@ -32,13 +35,15 @@ export let CSLExporter = new class { // eslint-disable-line @typescript-eslint/n try { this.postscript = new Function('reference', 'item', 'Translator', 'Zotero', postscript) as (reference: any, item: any) => boolean log.debug(`Installed postscript: ${JSON.stringify(postscript)}`) - } catch (err) { + } + catch (err) { if (Translator.preferences.testing) throw err + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions log.error(`Failed to compile postscript: ${err}\n\n${JSON.stringify(postscript)}`) } } } - public postscript(reference, item, _translator, _zotero) {} // eslint-disable-line no-empty,@typescript-eslint/no-empty-function + public postscript(_reference, _item, _translator, _zotero) {} // eslint-disable-line @typescript-eslint/no-empty-function public doExport() { const items = [] @@ -113,10 +118,12 @@ export let CSLExporter = new class { // eslint-disable-line @typescript-eslint/n if (ef.type === 'date') { csl[name] = this.date2CSL(Zotero.BetterBibTeX.parseDate(value)) - } else if (name === 'csl-type') { + } + else if (name === 'csl-type') { if (!validCSLTypes.includes(value)) continue // and keep the kv variable, maybe for postscripting csl.type = value - } else if (!csl[name]) { + } + else if (!csl[name]) { csl[name] = value } @@ -142,7 +149,8 @@ export let CSLExporter = new class { // eslint-disable-line @typescript-eslint/n let cache try { cache = this.postscript(csl, item, Translator, Zotero) - } catch (err) { + } + catch (err) { if (Translator.preferences.testing && !Translator.preferences.ignorePostscriptErrors) throw err cache = false } @@ -174,6 +182,7 @@ export let CSLExporter = new class { // eslint-disable-line @typescript-eslint/n private sortObject(obj) { if (obj && !Array.isArray(obj) && typeof obj === 'object') { + // eslint-disable-next-line @typescript-eslint/unbound-method for (const field of Object.keys(obj).sort(this.keySort)) { const value = obj[field] delete obj[field] diff --git a/translators/lib/normalize.ts b/translators/lib/normalize.ts index 44348c5055..e0f74fe9b3 100644 --- a/translators/lib/normalize.ts +++ b/translators/lib/normalize.ts @@ -1,7 +1,10 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-return */ + import { stringify } from '../../content/stringify' +import { ZoteroTranslator } from '../../gen/typings/serialized-item' -function rjust(str, width, padding) { - if (typeof str === 'number') str = '' + str +function rjust(str: string | number, width: number, padding: string): string { + if (typeof str === 'number') str = `${str}` padding = (padding || ' ')[0] return str.length < width ? padding.repeat(width - str.length) + str : str } @@ -49,7 +52,8 @@ function strip(obj) { v = strip(v) if (typeof v === 'undefined') { delete obj[k] - } else { + } + else { obj[k] = v keep = true } @@ -63,8 +67,7 @@ function strip(obj) { return obj } -export function normalize(library: Library) { - +export function normalize(library: Library): void { library.items.sort((a, b) => key(a).localeCompare(key(b))) for (const item of (library.items as any[])) { @@ -78,13 +81,15 @@ export function normalize(library: Library) { if (item.notes?.length) { item.notes = item.notes.map(note => typeof note === 'string' ? note : note.note).sort() - } else { + } + else { delete item.notes } if (item.tags?.length) { item.tags = item.tags.map(tag => typeof tag === 'string' ? { tag } : tag).sort((a, b) => a.tag.localeCompare(b.tag)) - } else { + } + else { delete item.tags } @@ -96,7 +101,8 @@ export function normalize(library: Library) { delete att[prop] } } - } else { + } + else { delete item.attachments } @@ -104,7 +110,8 @@ export function normalize(library: Library) { for (const creator of item.creators) { if (!creator.fieldMode) delete creator.fieldMode } - } else { + } + else { delete item.creators } @@ -114,8 +121,9 @@ export function normalize(library: Library) { strip(item) if (item.extra?.length) { - item.extra = item.extra.split('\n') - } else { + item.extra = (item as ZoteroTranslator.Item).extra.split('\n') + } + else { delete item.extra } } @@ -135,8 +143,9 @@ export function normalize(library: Library) { }, {}) if (library.collections && Object.keys(library.collections).length) { - const collectionOrder = Object.values(library.collections).sort((a, b) => stringify({...a, key: '', parent: ''}).localeCompare(stringify({...b, key: '', parent: ''}))) - const collectionKeys: Record = collectionOrder.reduce((acc, coll, i) => { + const collectionOrder: ZoteroTranslator.Collection[] = Object.values(library.collections) + .sort((a: ZoteroTranslator.Collection, b: ZoteroTranslator.Collection): number => stringify({...a, key: '', parent: ''}).localeCompare(stringify({...b, key: '', parent: ''}))) + const collectionKeys: Record = collectionOrder.reduce((acc: Record, coll: ZoteroTranslator.Collection, i: number): Record => { coll.key = acc[coll.key] = `coll:${rjust(i, 5, '0')}` // eslint-disable-line no-magic-numbers return acc }, {}) @@ -150,7 +159,8 @@ export function normalize(library: Library) { acc[coll.key] = coll return acc }, {}) - } else { + } + else { delete library.collections } } diff --git a/translators/lib/translator.ts b/translators/lib/translator.ts index fb4c4b6559..5b866e7c50 100644 --- a/translators/lib/translator.ts +++ b/translators/lib/translator.ts @@ -3,6 +3,8 @@ declare const ZOTERO_TRANSLATOR_INFO: any import * as preferences from '../../gen/preferences/defaults.json' import { client } from '../../content/client' +import { ZoteroTranslator } from '../../gen/typings/serialized-item' +import { IPreferences } from '../../gen/typings/preferences' type TranslatorMode = 'export' | 'import' @@ -10,17 +12,53 @@ const cacheDisabler = new class { get(target, property) { // collections: jabref 4 stores collection info inside the reference, and collection info depends on which part of your library you're exporting if (['collections'].includes(property)) target.cachable = false + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return target[property] } } -export let Translator = new class implements ITranslator { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match +type TranslatorHeader = { + translatorID: string + translatorType: number + label: string + description: string + creator: string + target: string + minVersion: string + maxVersion: string + priority: number + inRepository: boolean + lastUpdated: string + browserSupport: string + + displayOptions: { + exportNotes: boolean + exportFileData: boolean + useJournalAbbreviation: boolean + keepUpdated: boolean + quickCopyMode: string + Title: boolean + Authors: boolean + Year: boolean + Normalize: boolean + } + + configOptions: { + getCollections: boolean + async: boolean + } +} + +export const Translator = new class implements ITranslator { // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match public preferences: IPreferences public skipFields: string[] public skipField: Record public verbatimFields?: string[] public csquotes: { open: string, close: string } - public export: { dir: string, path: string } = { dir: undefined, path: undefined } + public export: { dir: string, path: string } = { + dir: undefined, + path: undefined, + } public options: { quickCopyMode?: string @@ -54,41 +92,11 @@ export let Translator = new class implements ITranslator { // eslint-disable-lin misses: number } - public header: { - translatorID: string - translatorType: number - label: string - description: string - creator: string - target: string - minVersion: string - maxVersion: string - priority: number - inRepository: boolean - lastUpdated: string - browserSupport: string - - displayOptions: { - exportNotes: boolean - exportFileData: boolean - useJournalAbbreviation: boolean - keepUpdated: boolean - quickCopyMode: string - Title: boolean - Authors: boolean - Year: boolean - Normalize: boolean - } - - configOptions: { - getCollections: boolean - async: boolean - } - } + public header: TranslatorHeader - public collections: Record - private sortedItems: ISerializedItem[] - private currentItem: ISerializedItem + public collections: Record + private sortedItems: ZoteroTranslator.Item[] + private currentItem: ZoteroTranslator.Item public isJurisM: boolean public isZotero: boolean @@ -104,7 +112,7 @@ export let Translator = new class implements ITranslator { // eslint-disable-lin public initialized = false constructor() { - this.header = ZOTERO_TRANSLATOR_INFO + this.header = (ZOTERO_TRANSLATOR_INFO as TranslatorHeader) this[this.header.label.replace(/[^a-z]/ig, '')] = true this.BetterTeX = this.BetterBibTeX || this.BetterBibLaTeX @@ -112,7 +120,8 @@ export let Translator = new class implements ITranslator { // eslint-disable-lin this.preferences = preferences this.options = this.header.displayOptions || {} - this.stringCompare = (new Intl.Collator('en')).compare + const collator = new Intl.Collator('en') + this.stringCompare = (collator.compare.bind(collator) as (left: string, right: string) => number) } public get exportDir(): string { @@ -125,7 +134,7 @@ export let Translator = new class implements ITranslator { // eslint-disable-lin return this.export.path } - private typefield(field) { + private typefield(field: string): string { field = field.trim() if (field.startsWith('bibtex.')) return this.BetterBibTeX ? field.replace(/^bibtex\./, '') : '' if (field.startsWith('biblatex.')) return this.BetterBibLaTeX ? field.replace(/^biblatex\./, '') : '' @@ -133,7 +142,7 @@ export let Translator = new class implements ITranslator { // eslint-disable-lin } public init(mode: TranslatorMode) { - this.platform = Zotero.getHiddenPref('better-bibtex.platform') + this.platform = (Zotero.getHiddenPref('better-bibtex.platform') as string) this.isJurisM = client === 'jurism' this.isZotero = !this.isJurisM @@ -144,8 +153,11 @@ export let Translator = new class implements ITranslator { // eslint-disable-lin for (const key in this.options) { if (typeof this.options[key] === 'boolean') { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment this.options[key] = !!Zotero.getOption(key) - } else { + } + else { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment this.options[key] = Zotero.getOption(key) } } @@ -157,8 +169,8 @@ export let Translator = new class implements ITranslator { // eslint-disable-lin misses: 0, } this.export = { - dir: Zotero.getOption('exportDir'), - path: Zotero.getOption('exportPath'), + dir: (Zotero.getOption('exportDir') as string), + path: (Zotero.getOption('exportPath') as string), } if (this.export.dir && this.export.dir.endsWith(this.paths.sep)) this.export.dir = this.export.dir.slice(0, -1) } @@ -167,13 +179,17 @@ export let Translator = new class implements ITranslator { // eslint-disable-lin let value try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment value = Zotero.getOption(`preference_${pref}`) - } catch (err) { + } + catch (err) { value = undefined } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment if (typeof value === 'undefined') value = Zotero.getHiddenPref(`better-bibtex.${pref}`) + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment this.preferences[pref] = value } @@ -186,7 +202,7 @@ export let Translator = new class implements ITranslator { // eslint-disable-lin if (!this.verbatimFields.length) this.verbatimFields = null this.csquotes = this.preferences.csquotes ? { open: this.preferences.csquotes[0], close: this.preferences.csquotes[1] } : null - this.preferences.testing = Zotero.getHiddenPref('better-bibtex.testing') + this.preferences.testing = (Zotero.getHiddenPref('better-bibtex.testing') as boolean) if (mode === 'export') { this.unicode = (this.BetterBibTeX && !Translator.preferences.asciiBibTeX) || (this.BetterBibLaTeX && !Translator.preferences.asciiBibLaTeX) @@ -198,18 +214,23 @@ export let Translator = new class implements ITranslator { // eslint-disable-lin this.collections = {} if (mode === 'export' && this.header.configOptions?.getCollections && Zotero.nextCollection) { - let collection + let collection: any while (collection = Zotero.nextCollection()) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const children = collection.children || collection.descendents || [] + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const key = (collection.primary ? collection.primary : collection).key this.collections[key] = { // id: collection.id, + key, parent: collection.fields.parentKey, name: collection.name, items: collection.childItems, + // eslint-disable-next-line @typescript-eslint/no-unsafe-return collections: children.filter(coll => coll.type === 'collection').map(coll => coll.key), + // items: (item.itemID for item in children when item.type != 'collection') // descendents: undefined // children: undefined @@ -223,7 +244,8 @@ export let Translator = new class implements ITranslator { // eslint-disable-lin for (collection of Object.values(this.collections)) { if (collection.parent && !this.collections[collection.parent]) { - collection.parent = false + // collection.parent = false + delete collection.parent Zotero.debug(`BBT translator: collection with key ${collection.key} has non-existent parent ${collection.parent}, assuming root collection`) } } @@ -232,11 +254,11 @@ export let Translator = new class implements ITranslator { // eslint-disable-lin this.initialized = true } - public items(): ISerializedItem[] { + public items(): ZoteroTranslator.Item[] { if (!this.sortedItems) { this.sortedItems = [] - let item - while (item = Zotero.nextItem()) { + let item: ZoteroTranslator.Item + while (item = (Zotero.nextItem() as ZoteroTranslator.Item)) { item.cachable = this.cachable item.journalAbbreviation = item.journalAbbreviation || item.autoJournalAbbreviation this.sortedItems.push(new Proxy(item, cacheDisabler)) diff --git a/translators/typings/exporter.d.ts b/translators/typings/exporter.d.ts new file mode 100644 index 0000000000..eace4b2356 --- /dev/null +++ b/translators/typings/exporter.d.ts @@ -0,0 +1,18 @@ +interface IField { + name: string + verbatim?: string + value: string | string[] | number | null | { path: string, title?: string, mimeType?: string } | { tag: string, type?: number }[] + enc?: 'raw' | 'url' | 'verbatim' | 'creators' | 'literal' | 'latex' | 'tags' | 'attachments' | 'date' + orig?: { name?: string, verbatim?: string, inherit?: boolean } + bibtexStrings?: boolean + bare?: boolean + raw?: boolean + + // kept as seperate booleans for backwards compat + replace?: boolean + fallback?: boolean + + html?: boolean + + bibtex?: string +} \ No newline at end of file diff --git a/translators/typings/worker.d.ts b/translators/typings/worker.d.ts index d6cb55e958..581bde1c69 100644 --- a/translators/typings/worker.d.ts +++ b/translators/typings/worker.d.ts @@ -2,8 +2,8 @@ namespace BBTWorker { type Config = { preferences: any, options: any, - items: any[] - collections: any[] + items: ISerializedItem[] + collections: ZoteroCollection[] cslItems?: Record cache: Record } diff --git a/translators/worker/zotero.ts b/translators/worker/zotero.ts index 7efaac5f1e..725de4448f 100644 --- a/translators/worker/zotero.ts +++ b/translators/worker/zotero.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-assignment */ + declare const doExport: () => void declare const Translator: ITranslator @@ -14,6 +16,7 @@ import { titleCase } from '../../content/case' import * as itemCreators from '../../gen/items/creators.json' import { client } from '../../content/client' import { log } from '../../content/logger' +import { ZoteroTranslator } from '../../gen/typings/serialized-item' const ctx: DedicatedWorkerGlobalScope = self as any @@ -28,7 +31,8 @@ export const params = { for(const [key, value] of (new URLSearchParams(ctx.location.search)).entries()) { if (key === 'debugEnabled') { params[key] = value === 'true' - } else { + } + else { params[key] = value } } @@ -100,10 +104,11 @@ class WorkerZoteroUtilities { if (singleNewlineIsParagraph) { // \n =>

    - str = `

    ${str.replace(/\n/g, '

    ').replace(/ /g, '  ')}

    ` - } else { + str = `

    ${str.replace(/\n/g, '

    ').replace(/ {2}/g, '  ')}

    ` + } + else { // \n\n =>

    , \n =>
    - str = `

    ${str.replace(/\n\n/g, '

    ').replace(/\n/g, '
    ').replace(/ /g, '  ')}

    ` + str = `

    ${str.replace(/\n\n/g, '

    ').replace(/\n/g, '
    ').replace(/ {2}/g, '  ')}

    ` } return str.replace(/

    \s*<\/p>/g, '

     

    ') @@ -148,7 +153,8 @@ function saveFile(path, overwrite) { makeDirs(OS.Path.dirname(this.path)) OS.File.copy(this.localPath, this.path, { noOverwrite: !overwrite }) - } else if (this.linkMode === 'imported_url') { + } + else if (this.linkMode === 'imported_url') { const target = OS.Path.dirname(this.path) if (!overwrite && OS.File.exists(target)) throw new Error(`${path} would overwite ${target}`) @@ -163,7 +169,8 @@ function saveFile(path, overwrite) { if (entry.isDir) throw new Error(`Unexpected directory ${entry.path} in snapshot`) OS.File.copy(OS.Path.join(snapshot, entry.name), OS.path.join(target, entry.name), { noOverwrite: !overwrite }) } - } finally { + } + finally { iterator.close() } } @@ -196,14 +203,16 @@ class WorkerZotero { if (this.config.options.exportFileData) { // output path is a directory this.exportDirectory = OS.Path.normalize(params.output) this.exportFile = OS.Path.join(this.exportDirectory, `${OS.Path.basename(this.exportDirectory)}.${Translator.header.target}`) - } else { + } + else { this.exportFile = OS.Path.normalize(params.output) const ext = `.${Translator.header.target}` if (!this.exportFile.endsWith(ext)) this.exportFile += ext this.exportDirectory = OS.Path.dirname(this.exportFile) } makeDirs(this.exportDirectory) - } else { + } + else { this.exportFile = '' this.exportDirectory = '' } @@ -213,7 +222,7 @@ class WorkerZotero { if (this.exportFile) { const encoder = new TextEncoder() const array = encoder.encode(this.output) - OS.File.writeAtomic(this.exportFile, array, {tmpPath: this.exportFile + '.tmp'}) + OS.File.writeAtomic(this.exportFile, array, {tmpPath: `${this.exportFile}.tmp`}) } this.send({ kind: 'done', output: this.exportFile ? true : this.output }) } @@ -246,11 +255,11 @@ class WorkerZotero { return this.config.items.shift() } - public nextCollection() { + public nextCollection(): ZoteroTranslator.Collection { return this.config.collections.shift() } - private patchAttachments(item) { + private patchAttachments(item): void { if (item.itemType === 'attachment') { item.saveFile = saveFile.bind(item) @@ -258,7 +267,8 @@ class WorkerZotero { item.defaultPath = `files/${item.itemID}/${OS.Path.basename(item.localPath)}` } - } else if (item.attachments) { + } + else if (item.attachments) { for (const att of item.attachments) { this.patchAttachments(att) } @@ -269,7 +279,7 @@ class WorkerZotero { export const Zotero = new WorkerZotero // eslint-disable-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match -export function onmessage(e: { data: BBTWorker.Config }) { +export function onmessage(e: { data: BBTWorker.Config }): void { Zotero.BetterBibTeX.localeDateOrder = params.localeDateOrder if (e.data?.items && !Zotero.config) { @@ -277,10 +287,12 @@ export function onmessage(e: { data: BBTWorker.Config }) { Zotero.init(e.data) doExport() Zotero.done() - } catch (err) { + } + catch (err) { Zotero.logError(err) } - } else { + } + else { log.debug('unexpected message in worker:', e) } close()