diff --git a/.eslintrc.json b/.eslintrc.json index b5328f41bb..3791be8e1d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -2,15 +2,22 @@ "root": true, "extends": [ "eslint:recommended", - "plugin:jsonc/recommended-with-json" + "plugin:jsonc/recommended-with-json", + "plugin:@typescript-eslint/recommended" ], + "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": 9, "sourceType": "script", "ecmaFeatures": { "globalReturn": false, "impliedStrict": true - } + }, + "project": [ + "./jsconfig.json", + "./dev/jsconfig.json", + "./test/jsconfig.json" + ] }, "env": { "browser": true, @@ -21,7 +28,10 @@ "no-unsanitized", "header", "jsdoc", - "jsonc" + "jsonc", + "unused-imports", + "@typescript-eslint", + "@stylistic/ts" ], "ignorePatterns": [ "/ext/lib/" @@ -54,7 +64,7 @@ "no-param-reassign": "off", "no-prototype-builtins": "error", "no-shadow": [ - "error", + "off", { "builtinGlobals": false } @@ -233,7 +243,7 @@ "no-unsanitized/property": "error", "jsdoc/check-access": "error", "jsdoc/check-alignment": "error", - "jsdoc/check-line-alignment": "error", + "jsdoc/check-line-alignment": ["error", "never", {"wrapIndent": " "}], "jsdoc/check-param-names": "error", "jsdoc/check-property-names": "error", "jsdoc/check-tag-names": "error", @@ -242,10 +252,6 @@ "jsdoc/empty-tags": "error", "jsdoc/implements-on-classes": "error", "jsdoc/multiline-blocks": "error", - "jsdoc/newline-after-description": [ - "error", - "never" - ], "jsdoc/no-bad-blocks": "error", "jsdoc/no-multi-asterisks": "error", "jsdoc/require-asterisk-prefix": "error", @@ -253,23 +259,41 @@ "error", "never" ], - "jsdoc/require-jsdoc": "off", + "jsdoc/require-jsdoc": [ + "error", + { + "require": { + "ClassDeclaration": false, + "FunctionDeclaration": true, + "MethodDefinition": false + }, + "contexts": [ + "MethodDefinition[kind=constructor]>FunctionExpression>BlockStatement>ExpressionStatement>AssignmentExpression[left.object.type=ThisExpression]", + "ClassDeclaration>Classbody>PropertyDefinition", + "MethodDefinition[kind!=constructor][kind!=set]", + "MethodDefinition[kind=constructor][value.params.length>0]" + ], + "checkGetters": "no-setter", + "checkSetters": "no-getter" + } + ], + "jsdoc/require-description": "off", "jsdoc/require-param": "error", - "jsdoc/require-param-description": "error", + "jsdoc/require-param-description": "off", "jsdoc/require-param-name": "error", "jsdoc/require-param-type": "error", "jsdoc/require-property": "error", - "jsdoc/require-property-description": "error", + "jsdoc/require-property-description": "off", "jsdoc/require-property-name": "error", "jsdoc/require-property-type": "error", "jsdoc/require-returns": "error", "jsdoc/require-returns-check": "error", - "jsdoc/require-returns-description": "error", + "jsdoc/require-returns-description": "off", "jsdoc/require-returns-type": "error", "jsdoc/require-throws": "error", "jsdoc/require-yields": "error", "jsdoc/require-yields-check": "error", - "jsdoc/tag-lines": "error", + "jsdoc/tag-lines": ["error", "never", {"startLines": 0}], "jsdoc/valid-types": "error", "jsonc/indent": [ "error", @@ -315,9 +339,162 @@ { "allowAllPropertiesOnSameLine": true } - ] + ], + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/ban-types": [ + "error", + { + "types": { + "object": true + }, + "extendDefaults": true + } + ], + "@typescript-eslint/consistent-type-exports": "off", + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/no-shadow": [ + "error", + { + "builtinGlobals": false + } + ], + "@typescript-eslint/no-this-alias": "error", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-var-requires": "off" }, "overrides": [ + { + "files": [ + "*.ts" + ], + "rules": { + "no-undef": "off", + "unused-imports/no-unused-imports": "error", + "@typescript-eslint/no-unused-vars": [ + "error", + { + "vars": "local", + "args": "after-used", + "argsIgnorePattern": "^_", + "caughtErrors": "none" + } + ], + "comma-dangle": "off", + "@typescript-eslint/comma-dangle": [ + "error", + { + "arrays": "always-multiline", + "objects": "always-multiline", + "imports": "always-multiline", + "exports": "always-multiline", + "functions": "always-multiline", + "enums": "always-multiline", + "generics": "always-multiline", + "tuples": "always-multiline" + } + ], + "@stylistic/ts/block-spacing": "off", + "@stylistic/ts/brace-style": [ + "error", + "1tbs", + { + "allowSingleLine": true + } + ], + "@stylistic/ts/comma-dangle": [ + "error", + { + "arrays": "always-multiline", + "objects": "always-multiline", + "imports": "always-multiline", + "exports": "always-multiline", + "functions": "always-multiline", + "enums": "always-multiline", + "generics": "always-multiline", + "tuples": "always-multiline" + } + ], + "@stylistic/ts/comma-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "@stylistic/ts/function-call-spacing": [ + "error", + "never" + ], + "@stylistic/ts/indent": [ + "error", + 4 + ], + "@stylistic/ts/key-spacing": [ + "error", + { + "beforeColon": false, + "afterColon": true, + "mode": "strict" + } + ], + "@stylistic/ts/keyword-spacing": [ + "error", + { + "before": true, + "after": true + } + ], + "@stylistic/ts/lines-around-comment": "off", + "@stylistic/ts/lines-between-class-members": [ + "error", + "always" + ], + "@stylistic/ts/member-delimiter-style": [ + "error", + { + "multiline": { + "delimiter": "semi", + "requireLast": true + }, + "singleline": { + "delimiter": "comma", + "requireLast": false + }, + "multilineDetection": "brackets" + } + ], + "@stylistic/ts/no-extra-parens": [ + "error", + "all" + ], + "@stylistic/ts/no-extra-semi": "error", + "@stylistic/ts/object-curly-spacing": [ + "error", + "never" + ], + "@stylistic/ts/padding-line-between-statements": "off", + "@stylistic/ts/quotes": [ + "error", + "single", + "avoid-escape" + ], + "@stylistic/ts/semi": "error", + "@stylistic/ts/space-before-blocks": [ + "error", + "always" + ], + "@stylistic/ts/space-before-function-paren": [ + "error", + { + "anonymous": "never", + "named": "never", + "asyncArrow": "always" + } + ], + "@stylistic/ts/space-infix-ops": "error", + "@stylistic/ts/type-annotation-spacing": "error" + } + }, { "files": [ "*.json" @@ -367,24 +544,35 @@ { "files": [ "ext/js/core.js", + "ext/js/core/extension-error.js", "ext/js/**/sandbox/**/*.js" ], "env": { "webextensions": false } }, + { + "files": [ + "ext/**/*.js" + ], + "excludedFiles": [ + "ext/js/core/extension-error.js" + ], + "globals": { + "ExtensionError": "readonly" + } + }, { "files": [ "ext/**/*.js" ], "excludedFiles": [ "ext/js/core.js", + "ext/js/core/extension-error.js", "ext/js/accessibility/google-docs.js", "ext/js/**/sandbox/**/*.js" ], "globals": { - "serializeError": "readonly", - "deserializeError": "readonly", "isObject": "readonly", "stringReverse": "readonly", "promiseTimeout": "readonly", @@ -408,6 +596,7 @@ ], "excludedFiles": [ "ext/js/core.js", + "ext/js/core/extension-error.js", "ext/js/accessibility/google-docs.js", "ext/js/yomichan.js", "ext/js/**/sandbox/**/*.js" @@ -475,6 +664,7 @@ { "files": [ "ext/js/core.js", + "ext/js/core/extension-error.js", "ext/js/yomichan.js", "ext/js/accessibility/accessibility-controller.js", "ext/js/background/backend.js", @@ -522,6 +712,7 @@ { "files": [ "ext/js/core.js", + "ext/js/core/extension-error.js", "ext/js/data/database.js", "ext/js/data/json-schema.js", "ext/js/general/cache-map.js", diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000..a5e243212b --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + "html-validate.vscode-html-validate", + "stylelint.vscode-stylelint" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..9bd9e1c0ab --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "editor.tabSize": 4, + "editor.insertSpaces": true, + "eslint.validate": [ + "javascript", + "typescript" + ], + "files.eol": "\n" +} \ No newline at end of file diff --git a/dev/build-libs.js b/dev/build-libs.js index 36c07edd0c..ec09c62678 100644 --- a/dev/build-libs.js +++ b/dev/build-libs.js @@ -20,6 +20,9 @@ const fs = require('fs'); const path = require('path'); const browserify = require('browserify'); +/** + * @returns {Promise} + */ async function buildParse5() { const parse5Path = require.resolve('parse5'); const cwd = process.cwd(); @@ -45,6 +48,9 @@ async function buildParse5() { } } +/** + * @returns {{path: string, build: () => Promise}[]} + */ function getBuildTargets() { const extLibPath = path.join(__dirname, '..', 'ext', 'lib'); return [ @@ -52,6 +58,7 @@ function getBuildTargets() { ]; } +/** */ async function main() { for (const {path: path2, build} of getBuildTargets()) { const content = await build(); diff --git a/dev/build.js b/dev/build.js index 5222c4c899..cfc3fed4ae 100644 --- a/dev/build.js +++ b/dev/build.js @@ -26,6 +26,14 @@ const {getAllFiles, getArgs, testMain} = util; const {ManifestUtil} = require('./manifest-util'); +/** + * @param {string} directory + * @param {string[]} excludeFiles + * @param {string} outputFileName + * @param {string[]} sevenZipExes + * @param {?import('jszip').OnUpdateCallback} onUpdate + * @param {boolean} dryRun + */ async function createZip(directory, excludeFiles, outputFileName, sevenZipExes, onUpdate, dryRun) { try { fs.unlinkSync(outputFileName); @@ -55,9 +63,16 @@ async function createZip(directory, excludeFiles, outputFileName, sevenZipExes, } } } - return await createJSZip(directory, excludeFiles, outputFileName, onUpdate, dryRun); + await createJSZip(directory, excludeFiles, outputFileName, onUpdate, dryRun); } +/** + * @param {string} directory + * @param {string[]} excludeFiles + * @param {string} outputFileName + * @param {?import('jszip').OnUpdateCallback} onUpdate + * @param {boolean} dryRun + */ async function createJSZip(directory, excludeFiles, outputFileName, onUpdate, dryRun) { const JSZip = util.JSZip; const files = getAllFiles(directory); @@ -87,6 +102,10 @@ async function createJSZip(directory, excludeFiles, outputFileName, onUpdate, dr } } +/** + * @param {string[]} array + * @param {string[]} removeItems + */ function removeItemsFromArray(array, removeItems) { for (const item of removeItems) { const index = getIndexOfFilePath(array, item); @@ -96,6 +115,11 @@ function removeItemsFromArray(array, removeItems) { } } +/** + * @param {string[]} array + * @param {string} item + * @returns {number} + */ function getIndexOfFilePath(array, item) { const pattern = /\\/g; const separator = '/'; @@ -108,6 +132,15 @@ function getIndexOfFilePath(array, item) { return -1; } +/** + * @param {string} buildDir + * @param {string} extDir + * @param {ManifestUtil} manifestUtil + * @param {string[]} variantNames + * @param {string} manifestPath + * @param {boolean} dryRun + * @param {boolean} dryRunBuildZip + */ async function build(buildDir, extDir, manifestUtil, variantNames, manifestPath, dryRun, dryRunBuildZip) { const sevenZipExes = ['7za', '7z']; @@ -117,6 +150,7 @@ async function build(buildDir, extDir, manifestUtil, variantNames, manifestPath, } const dontLogOnUpdate = !process.stdout.isTTY; + /** @type {import('jszip').OnUpdateCallback} */ const onUpdate = (metadata) => { if (dontLogOnUpdate) { return; } @@ -125,7 +159,7 @@ async function build(buildDir, extDir, manifestUtil, variantNames, manifestPath, message += ` (${metadata.currentFile})`; } - readline.clearLine(process.stdout); + readline.clearLine(process.stdout, 0); readline.cursorTo(process.stdout, 0); process.stdout.write(message); }; @@ -169,6 +203,10 @@ async function build(buildDir, extDir, manifestUtil, variantNames, manifestPath, } } +/** + * @param {string} directory + * @param {string[]} files + */ function ensureFilesExist(directory, files) { for (const file of files) { assert.ok(fs.existsSync(path.join(directory, file))); @@ -176,18 +214,21 @@ function ensureFilesExist(directory, files) { } +/** + * @param {string[]} argv + */ async function main(argv) { - const args = getArgs(argv, new Map([ + const args = getArgs(argv, new Map(/** @type {[key: string, value: (boolean|null|number|string|string[])][]} */ ([ ['all', false], ['default', false], ['manifest', null], ['dry-run', false], ['dry-run-build-zip', false], [null, []] - ])); + ]))); - const dryRun = args.get('dry-run'); - const dryRunBuildZip = args.get('dry-run-build-zip'); + const dryRun = /** @type {boolean} */ (args.get('dry-run')); + const dryRunBuildZip = /** @type {boolean} */ (args.get('dry-run-build-zip')); const manifestUtil = new ManifestUtil(); @@ -197,15 +238,15 @@ async function main(argv) { const manifestPath = path.join(extDir, 'manifest.json'); try { - const variantNames = ( + const variantNames = /** @type {string[]} */ (( argv.length === 0 || args.get('all') ? manifestUtil.getVariants().filter(({buildable}) => buildable !== false).map(({name}) => name) : args.get(null) - ); + )); await build(buildDir, extDir, manifestUtil, variantNames, manifestPath, dryRun, dryRunBuildZip); } finally { // Restore manifest - const manifestName = (!args.get('default') && args.get('manifest') !== null) ? args.get('manifest') : null; + const manifestName = /** @type {?string} */ ((!args.get('default') && args.get('manifest') !== null) ? args.get('manifest') : null); const restoreManifest = manifestUtil.getManifest(manifestName); process.stdout.write('Restoring manifest...\n'); if (!dryRun) { diff --git a/dev/css-to-json-util.js b/dev/css-to-json-util.js index 79aae3c9e1..8fb2f08502 100644 --- a/dev/css-to-json-util.js +++ b/dev/css-to-json-util.js @@ -19,6 +19,11 @@ const fs = require('fs'); const css = require('css'); +/** + * @param {import('css-style-applier').RawStyleData} rules + * @param {string[]} selectors + * @returns {number} + */ function indexOfRule(rules, selectors) { const jj = selectors.length; for (let i = 0, ii = rules.length; i < ii; ++i) { @@ -36,6 +41,12 @@ function indexOfRule(rules, selectors) { return -1; } +/** + * @param {import('css-style-applier').RawStyleDataStyleArray} styles + * @param {string} property + * @param {Map} removedProperties + * @returns {number} + */ function removeProperty(styles, property, removedProperties) { let removeCount = removedProperties.get(property); if (typeof removeCount !== 'undefined') { return removeCount; } @@ -52,6 +63,10 @@ function removeProperty(styles, property, removedProperties) { return removeCount; } +/** + * @param {import('css-style-applier').RawStyleData} rules + * @returns {string} + */ function formatRulesJson(rules) { // Manually format JSON, for improved compactness // return JSON.stringify(rules, null, 4); @@ -85,27 +100,39 @@ function formatRulesJson(rules) { return result; } +/** + * @param {string} cssFile + * @param {string} overridesCssFile + * @returns {import('css-style-applier').RawStyleData} + * @throws {Error} + */ function generateRules(cssFile, overridesCssFile) { const content1 = fs.readFileSync(cssFile, {encoding: 'utf8'}); const content2 = fs.readFileSync(overridesCssFile, {encoding: 'utf8'}); - const stylesheet1 = css.parse(content1, {}).stylesheet; - const stylesheet2 = css.parse(content2, {}).stylesheet; + const stylesheet1 = /** @type {css.StyleRules} */ (css.parse(content1, {}).stylesheet); + const stylesheet2 = /** @type {css.StyleRules} */ (css.parse(content2, {}).stylesheet); const removePropertyPattern = /^remove-property\s+([\w\W]+)$/; const removeRulePattern = /^remove-rule$/; const propertySeparator = /\s+/; + /** @type {import('css-style-applier').RawStyleData} */ const rules = []; // Default stylesheet for (const rule of stylesheet1.rules) { if (rule.type !== 'rule') { continue; } - const {selectors, declarations} = rule; + const {selectors, declarations} = /** @type {css.Rule} */ (rule); + if (typeof selectors === 'undefined') { continue; } + /** @type {import('css-style-applier').RawStyleDataStyleArray} */ const styles = []; - for (const declaration of declarations) { - if (declaration.type !== 'declaration') { console.log(declaration); continue; } - const {property, value} = declaration; - styles.push([property, value]); + if (typeof declarations !== 'undefined') { + for (const declaration of declarations) { + if (declaration.type !== 'declaration') { console.log(declaration); continue; } + const {property, value} = /** @type {css.Declaration} */ (declaration); + if (typeof property !== 'string' || typeof value !== 'string') { continue; } + styles.push([property, value]); + } } if (styles.length > 0) { rules.push({selectors, styles}); @@ -115,7 +142,9 @@ function generateRules(cssFile, overridesCssFile) { // Overrides for (const rule of stylesheet2.rules) { if (rule.type !== 'rule') { continue; } - const {selectors, declarations} = rule; + const {selectors, declarations} = /** @type {css.Rule} */ (rule); + if (typeof selectors === 'undefined' || typeof declarations === 'undefined') { continue; } + /** @type {Map} */ const removedProperties = new Map(); for (const declaration of declarations) { switch (declaration.type) { @@ -129,16 +158,18 @@ function generateRules(cssFile, overridesCssFile) { entry = {selectors, styles: []}; rules.push(entry); } - const {property, value} = declaration; - removeProperty(entry.styles, property, removedProperties); - entry.styles.push([property, value]); + const {property, value} = /** @type {css.Declaration} */ (declaration); + if (typeof property === 'string' && typeof value === 'string') { + removeProperty(entry.styles, property, removedProperties); + entry.styles.push([property, value]); + } } break; case 'comment': { const index = indexOfRule(rules, selectors); if (index < 0) { throw new Error('Could not find rule with matching selectors'); } - const comment = declaration.comment.trim(); + const comment = (/** @type {css.Comment} */ (declaration).comment || '').trim(); let m; if ((m = removePropertyPattern.exec(comment)) !== null) { for (const property of m[1].split(propertySeparator)) { diff --git a/dev/data-error.js b/dev/data-error.js new file mode 100644 index 0000000000..5034e3fd96 --- /dev/null +++ b/dev/data-error.js @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +class DataError extends Error { + /** + * @param {string} message + */ + constructor(message) { + super(message); + /** @type {unknown} */ + this._data = void 0; + } + + /** @type {unknown} */ + get data() { return this._data; } + set data(value) { this._data = value; } +} + +module.exports = { + DataError +}; diff --git a/dev/data/manifest-variants.json b/dev/data/manifest-variants.json index 3179734f0a..b6ed847d94 100644 --- a/dev/data/manifest-variants.json +++ b/dev/data/manifest-variants.json @@ -42,6 +42,7 @@ "all_frames": true, "js": [ "js/core.js", + "js/core/extension-error.js", "js/yomichan.js", "js/app/frontend.js", "js/app/popup.js", diff --git a/dev/database-vm.js b/dev/database-vm.js index d557069114..2d424a0bcc 100644 --- a/dev/database-vm.js +++ b/dev/database-vm.js @@ -17,12 +17,13 @@ */ const fs = require('fs'); -const url = require('url'); +const url = require('node:url'); const path = require('path'); const {JSZip} = require('./util'); const {VM} = require('./vm'); require('fake-indexeddb/auto'); +/** @type {import('dev/vm').PseudoChrome} */ const chrome = { runtime: { getURL: (path2) => { @@ -31,6 +32,10 @@ const chrome = { } }; +/** + * @param {string} url2 + * @returns {Promise} + */ async function fetch(url2) { const extDir = path.join(__dirname, '..', 'ext'); let filePath; @@ -50,11 +55,18 @@ async function fetch(url2) { }; } +/** + * @param {string} data + * @returns {string} + */ function atob(data) { return Buffer.from(data, 'base64').toString('ascii'); } class DatabaseVM extends VM { + /** + * @param {import('core').UnknownObject} [globals] + */ constructor(globals={}) { super(Object.assign({ chrome, @@ -65,11 +77,13 @@ class DatabaseVM extends VM { atob }, globals)); this.context.window = this.context; + /** @type {IDBFactory} */ this.indexedDB = global.indexedDB; } } class DatabaseVMDictionaryImporterMediaLoader { + /** @type {import('dictionary-importer-media-loader').GetImageDetailsFunction} */ async getImageDetails(content) { // Placeholder values return {content, width: 100, height: 100}; diff --git a/dev/dictionary-validate.js b/dev/dictionary-validate.js index 0c926acc93..5ec6fa1d2f 100644 --- a/dev/dictionary-validate.js +++ b/dev/dictionary-validate.js @@ -23,6 +23,10 @@ const {JSZip} = require('./util'); const {createJsonSchema} = require('./schema-validate'); +/** + * @param {string} relativeFileName + * @returns {import('dev/dictionary-validate').Schema} + */ function readSchema(relativeFileName) { const fileName = path.join(__dirname, relativeFileName); const source = fs.readFileSync(fileName, {encoding: 'utf8'}); @@ -30,17 +34,24 @@ function readSchema(relativeFileName) { } +/** + * @param {import('dev/schema-validate').ValidateMode} mode + * @param {import('jszip')} zip + * @param {string} fileNameFormat + * @param {import('dev/dictionary-validate').Schema} schema + */ async function validateDictionaryBanks(mode, zip, fileNameFormat, schema) { let jsonSchema; try { jsonSchema = createJsonSchema(mode, schema); } catch (e) { - e.message += `\n(in file ${fileNameFormat})}`; - throw e; + const e2 = e instanceof Error ? e : new Error(`${e}`); + e2.message += `\n(in file ${fileNameFormat})}`; + throw e2; } let index = 1; while (true) { - const fileName = fileNameFormat.replace(/\?/, index); + const fileName = fileNameFormat.replace(/\?/, `${index}`); const file = zip.files[fileName]; if (!file) { break; } @@ -49,14 +60,20 @@ async function validateDictionaryBanks(mode, zip, fileNameFormat, schema) { try { jsonSchema.validate(data); } catch (e) { - e.message += `\n(in file ${fileName})}`; - throw e; + const e2 = e instanceof Error ? e : new Error(`${e}`); + e2.message += `\n(in file ${fileName})}`; + throw e2; } ++index; } } +/** + * @param {import('dev/schema-validate').ValidateMode} mode + * @param {import('jszip')} archive + * @param {import('dev/dictionary-validate').Schemas} schemas + */ async function validateDictionary(mode, archive, schemas) { const fileName = 'index.json'; const indexFile = archive.files[fileName]; @@ -71,8 +88,9 @@ async function validateDictionary(mode, archive, schemas) { const jsonSchema = createJsonSchema(mode, schemas.index); jsonSchema.validate(index); } catch (e) { - e.message += `\n(in file ${fileName})}`; - throw e; + const e2 = e instanceof Error ? e : new Error(`${e}`); + e2.message += `\n(in file ${fileName})}`; + throw e2; } await validateDictionaryBanks(mode, archive, 'term_bank_?.json', version === 1 ? schemas.termBankV1 : schemas.termBankV3); @@ -82,6 +100,9 @@ async function validateDictionary(mode, archive, schemas) { await validateDictionaryBanks(mode, archive, 'tag_bank_?.json', schemas.tagBankV3); } +/** + * @returns {import('dev/dictionary-validate').Schemas} + */ function getSchemas() { return { index: readSchema('../ext/data/schemas/dictionary-index-schema.json'), @@ -96,6 +117,10 @@ function getSchemas() { } +/** + * @param {import('dev/schema-validate').ValidateMode} mode + * @param {string[]} dictionaryFileNames + */ async function testDictionaryFiles(mode, dictionaryFileNames) { const schemas = getSchemas(); @@ -117,6 +142,7 @@ async function testDictionaryFiles(mode, dictionaryFileNames) { } +/** */ async function main() { const dictionaryFileNames = process.argv.slice(2); if (dictionaryFileNames.length === 0) { @@ -127,6 +153,7 @@ async function main() { return; } + /** @type {import('dev/schema-validate').ValidateMode} */ let mode = null; if (dictionaryFileNames[0] === '--ajv') { mode = 'ajv'; diff --git a/dev/generate-css-json.js b/dev/generate-css-json.js index 787173abba..c14c0462f3 100644 --- a/dev/generate-css-json.js +++ b/dev/generate-css-json.js @@ -21,6 +21,9 @@ const path = require('path'); const {testMain} = require('./util'); const {formatRulesJson, generateRules} = require('./css-to-json-util'); +/** + * @returns {{cssFile: string, overridesCssFile: string, outputPath: string}[]} + */ function getTargets() { return [ { @@ -36,6 +39,7 @@ function getTargets() { ]; } +/** */ function main() { for (const {cssFile, overridesCssFile, outputPath} of getTargets()) { const json = formatRulesJson(generateRules(cssFile, overridesCssFile)); diff --git a/dev/jsconfig.json b/dev/jsconfig.json new file mode 100644 index 0000000000..58eba952ce --- /dev/null +++ b/dev/jsconfig.json @@ -0,0 +1,77 @@ +{ + "compilerOptions": { + "module": "ES2015", + "target": "ES2022", + "checkJs": true, + "moduleResolution": "node", + "strict": true, + "strictNullChecks": true, + "noImplicitAny": true, + "strictPropertyInitialization": true, + "suppressImplicitAnyIndexErrors": false, + "skipLibCheck": false, + "baseUrl": ".", + "paths": { + "anki-templates": ["../types/ext/anki-templates"], + "anki-templates-internal": ["../types/ext/anki-templates-internal"], + "cache-map": ["../types/ext/cache-map"], + "core": ["../types/ext/core"], + "css-style-applier": ["../types/ext/css-style-applier"], + "database": ["../types/ext/database"], + "deinflector": ["../types/ext/deinflector"], + "dictionary": ["../types/ext/dictionary"], + "dictionary-data": ["../types/ext/dictionary-data"], + "dictionary-data-util": ["../types/ext/dictionary-data-util"], + "dictionary-database": ["../types/ext/dictionary-database"], + "dictionary-importer": ["../types/ext/dictionary-importer"], + "dictionary-importer-media-loader": ["../types/ext/dictionary-importer-media-loader"], + "dynamic-property": ["../types/ext/dynamic-property"], + "error": ["../types/ext/error"], + "event-listener-collection": ["../types/ext/event-listener-collection"], + "japanese-util": ["../types/ext/japanese-util"], + "json-schema": ["../types/ext/json-schema"], + "log": ["../types/ext/log"], + "settings": ["../types/ext/settings"], + "structured-content": ["../types/ext/structured-content"], + "translator": ["../types/ext/translator"], + "translation": ["../types/ext/translation"], + "translation-internal": ["../types/ext/translation-internal"], + "dev/*": ["../types/dev/*"] + }, + "types": [ + "node", + "events", + "browserify", + "jsdom", + "assert", + "css", + "chrome", + "ajv" + ] + }, + "include": [ + "**/*.js", + "../playwright.config.js", + "../ext/js/core.js", + "../ext/js/core/extension-error.js", + "../ext/js/data/database.js", + "../ext/js/data/json-schema.js", + "../ext/js/general/cache-map.js", + "../ext/js/data/sandbox/anki-note-data-creator.js", + "../ext/js/general/cache-map.js", + "../ext/js/general/regex-util.js", + "../ext/js/general/text-source-map.js", + "../ext/js/language/deinflector.js", + "../ext/js/language/dictionary-importer.js", + "../ext/js/language/dictionary-database.js", + "../ext/js/language/sandbox/dictionary-data-util.js", + "../ext/js/language/sandbox/japanese-util.js", + "../ext/js/language/translator.js", + "../ext/js/media/media-util.js", + "../types/dev/**/*.ts", + "../types/other/web-set-timeout.d.ts" + ], + "exclude": [ + "../node_modules" + ] +} \ No newline at end of file diff --git a/dev/lint/global-declarations.js b/dev/lint/global-declarations.js index 7f90d227ca..648ad3681e 100644 --- a/dev/lint/global-declarations.js +++ b/dev/lint/global-declarations.js @@ -22,14 +22,27 @@ const assert = require('assert'); const {getAllFiles} = require('../util'); +/** + * @param {string} string + * @returns {string} + */ function escapeRegExp(string) { return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); } +/** + * @param {string} string + * @param {RegExp} pattern + * @returns {number} + */ function countOccurences(string, pattern) { return (string.match(pattern) || []).length; } +/** + * @param {string} string + * @returns {'\r'|'\n'|'\r\n'} + */ function getNewline(string) { const count1 = countOccurences(string, /(?:^|[^\r])\n/g); const count2 = countOccurences(string, /\r\n/g); @@ -41,6 +54,11 @@ function getNewline(string) { } } +/** + * @param {string} string + * @param {string} substring + * @returns {number} + */ function getSubstringCount(string, substring) { let count = 0; const pattern = new RegExp(`\\b${escapeRegExp(substring)}\\b`, 'g'); @@ -53,6 +71,11 @@ function getSubstringCount(string, substring) { } +/** + * @param {string} fileName + * @param {boolean} fix + * @returns {boolean} + */ function validateGlobals(fileName, fix) { const pattern = /\/\*\s*global\s+([\w\W]*?)\*\//g; const trimPattern = /^[\s,*]+|[\s,*]+$/g; @@ -81,7 +104,7 @@ function validateGlobals(fileName, fix) { assert.strictEqual(actual, expected); } catch (e) { console.error(`Global declaration error encountered in ${fileName}:`); - console.error(e.message); + console.error(e instanceof Error ? e.message : `${e}`); if (!fix) { return false; } @@ -114,6 +137,7 @@ function validateGlobals(fileName, fix) { } +/** */ function main() { const fix = (process.argv.length >= 2 && process.argv[2] === '--fix'); const directory = path.resolve(__dirname, '..', '..', 'ext'); diff --git a/dev/lint/html-scripts.js b/dev/lint/html-scripts.js index db6e6ca490..da8c2c712d 100644 --- a/dev/lint/html-scripts.js +++ b/dev/lint/html-scripts.js @@ -23,6 +23,10 @@ const {JSDOM} = require('jsdom'); const {getAllFiles} = require('../util'); +/** + * @param {string} fileName + * @returns {?fs.Stats} + */ function lstatSyncSafe(fileName) { try { return fs.lstatSync(fileName); @@ -31,6 +35,11 @@ function lstatSyncSafe(fileName) { } } +/** + * @param {string} src + * @param {string} fileName + * @param {string} extDir + */ function validatePath(src, fileName, extDir) { assert.ok(typeof src === 'string', ` + diff --git a/ext/background.html b/ext/background.html index 71990295c8..8628591f77 100644 --- a/ext/background.html +++ b/ext/background.html @@ -21,6 +21,7 @@ + diff --git a/ext/data/schemas/dictionary-term-bank-v3-schema.json b/ext/data/schemas/dictionary-term-bank-v3-schema.json index 335144c7b4..68ec8c84d8 100644 --- a/ext/data/schemas/dictionary-term-bank-v3-schema.json +++ b/ext/data/schemas/dictionary-term-bank-v3-schema.json @@ -154,6 +154,10 @@ "type": "string", "description": "Hover text for the image." }, + "description": { + "type": "string", + "description": "Description of the image." + }, "pixelated": { "type": "boolean", "description": "Whether or not the image should appear pixelated at sizes larger than the image's native resolution.", diff --git a/ext/data/schemas/options-schema.json b/ext/data/schemas/options-schema.json index 601f5d06ec..2903c3cedc 100644 --- a/ext/data/schemas/options-schema.json +++ b/ext/data/schemas/options-schema.json @@ -540,9 +540,12 @@ "searchKanji", "scanOnTouchMove", "scanOnTouchPress", + "scanOnTouchRelease", "scanOnPenMove", "scanOnPenHover", "scanOnPenReleaseHover", + "scanOnPenPress", + "scanOnPenRelease", "preventTouchScrolling", "preventPenScrolling" ], diff --git a/ext/info.html b/ext/info.html index 38097502f2..bd10d7ea6a 100644 --- a/ext/info.html +++ b/ext/info.html @@ -61,6 +61,7 @@ + diff --git a/ext/js/accessibility/accessibility-controller.js b/ext/js/accessibility/accessibility-controller.js index aa27cbf478..890647f5f0 100644 --- a/ext/js/accessibility/accessibility-controller.js +++ b/ext/js/accessibility/accessibility-controller.js @@ -25,15 +25,19 @@ class AccessibilityController { * @param {ScriptManager} scriptManager An instance of the `ScriptManager` class. */ constructor(scriptManager) { + /** @type {ScriptManager} */ this._scriptManager = scriptManager; + /** @type {?import('core').TokenObject} */ this._updateGoogleDocsAccessibilityToken = null; + /** @type {?Promise} */ this._updateGoogleDocsAccessibilityPromise = null; + /** @type {boolean} */ this._forceGoogleDocsHtmlRenderingAny = false; } /** * Updates the accessibility handlers. - * @param {object} fullOptions The full options object from the `Backend` instance. + * @param {import('settings').Options} fullOptions The full options object from the `Backend` instance. * The value is treated as read-only and is not modified. */ async update(fullOptions) { @@ -50,8 +54,12 @@ class AccessibilityController { // Private + /** + * @param {boolean} forceGoogleDocsHtmlRenderingAny + */ async _updateGoogleDocsAccessibility(forceGoogleDocsHtmlRenderingAny) { // Reentrant token + /** @type {?import('core').TokenObject} */ const token = {}; this._updateGoogleDocsAccessibilityToken = token; @@ -69,6 +77,9 @@ class AccessibilityController { this._updateGoogleDocsAccessibilityPromise = null; } + /** + * @param {boolean} forceGoogleDocsHtmlRenderingAny + */ async _updateGoogleDocsAccessibilityInner(forceGoogleDocsHtmlRenderingAny) { if (this._forceGoogleDocsHtmlRenderingAny === forceGoogleDocsHtmlRenderingAny) { return; } @@ -78,6 +89,7 @@ class AccessibilityController { try { if (forceGoogleDocsHtmlRenderingAny) { if (await this._scriptManager.isContentScriptRegistered(id)) { return; } + /** @type {import('script-manager').RegistrationDetails} */ const details = { allFrames: true, matchAboutBlank: true, diff --git a/ext/js/accessibility/google-docs-util.js b/ext/js/accessibility/google-docs-util.js index 3d9818ef41..6997dcb35f 100644 --- a/ext/js/accessibility/google-docs-util.js +++ b/ext/js/accessibility/google-docs-util.js @@ -30,8 +30,8 @@ class GoogleDocsUtil { * Coordinates are provided in [client space](https://developer.mozilla.org/en-US/docs/Web/CSS/CSSOM_View/Coordinate_systems). * @param {number} x The x coordinate to search at. * @param {number} y The y coordinate to search at. - * @param {GetRangeFromPointOptions} options Options to configure how element detection is performed. - * @returns {?TextSourceRange|TextSourceElement} A range for the hovered text or element, or `null` if no applicable content was found. + * @param {import('document-util').GetRangeFromPointOptions} options Options to configure how element detection is performed. + * @returns {?TextSourceRange} A range for the hovered text or element, or `null` if no applicable content was found. */ static getRangeFromPoint(x, y, {normalizeCssZoom}) { const styleNode = this._getStyleNode(); @@ -47,10 +47,13 @@ class GoogleDocsUtil { return null; } + /** + * @returns {HTMLStyleElement} + */ static _getStyleNode() { // This