diff options
Diffstat (limited to 'dev')
| -rw-r--r-- | dev/bin/build.js | 66 | ||||
| -rw-r--r-- | dev/bin/dictionary-validate.js | 2 | ||||
| -rw-r--r-- | dev/bin/generate-css-json.js | 1 | ||||
| -rw-r--r-- | dev/bin/schema-validate.js | 6 | ||||
| -rw-r--r-- | dev/build-libs.js | 19 | ||||
| -rw-r--r-- | dev/data-error.js | 35 | ||||
| -rw-r--r-- | dev/dictionary-validate.js | 44 | ||||
| -rw-r--r-- | dev/generate-css-json.js | 76 | ||||
| -rw-r--r-- | dev/jsconfig.json | 80 | ||||
| -rw-r--r-- | dev/manifest-util.js | 84 | ||||
| -rw-r--r-- | dev/schema-validate.js | 25 | ||||
| -rw-r--r-- | dev/translator-vm.js | 44 | ||||
| -rw-r--r-- | dev/util.js | 21 | 
13 files changed, 438 insertions, 65 deletions
| diff --git a/dev/bin/build.js b/dev/bin/build.js index 282f0414..deb82618 100644 --- a/dev/bin/build.js +++ b/dev/bin/build.js @@ -19,15 +19,24 @@  import assert from 'assert';  import childProcess from 'child_process';  import fs from 'fs'; +import JSZip from 'jszip'; +import {fileURLToPath} from 'node:url';  import path from 'path';  import readline from 'readline'; -import {fileURLToPath} from 'url';  import {buildLibs} from '../build-libs.js';  import {ManifestUtil} from '../manifest-util.js';  import {getAllFiles, getArgs, testMain} from '../util.js';  const dirname = path.dirname(fileURLToPath(import.meta.url)); +/** + * @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); @@ -57,11 +66,17 @@ 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 = null;      const files = getAllFiles(directory);      removeItemsFromArray(files, excludeFiles);      const zip = new JSZip(); @@ -89,6 +104,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); @@ -98,6 +117,11 @@ function removeItemsFromArray(array, removeItems) {      }  } +/** + * @param {string[]} array + * @param {string} item + * @returns {number} + */  function getIndexOfFilePath(array, item) {      const pattern = /\\/g;      const separator = '/'; @@ -110,6 +134,16 @@ 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 + * @param {string} yomitanVersion + */  async function build(buildDir, extDir, manifestUtil, variantNames, manifestPath, dryRun, dryRunBuildZip, yomitanVersion) {      const sevenZipExes = ['7za', '7z']; @@ -119,6 +153,7 @@ async function build(buildDir, extDir, manifestUtil, variantNames, manifestPath,      }      const dontLogOnUpdate = !process.stdout.isTTY; +    /** @type {import('jszip').OnUpdateCallback} */      const onUpdate = (metadata) => {          if (dontLogOnUpdate) { return; } @@ -127,7 +162,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);      }; @@ -173,6 +208,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))); @@ -180,8 +219,11 @@ function ensureFilesExist(directory, files) {  } +/** + * @param {string[]} argv + */  export 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], @@ -189,11 +231,11 @@ export async function main(argv) {          ['dry-run-build-zip', false],          ['yomitan-version', '0.0.0.0'],          [null, []] -    ])); +    ]))); -    const dryRun = args.get('dry-run'); -    const dryRunBuildZip = args.get('dry-run-build-zip'); -    const yomitanVersion = args.get('yomitan-version'); +    const dryRun = /** @type {boolean} */ (args.get('dry-run')); +    const dryRunBuildZip = /** @type {boolean} */ (args.get('dry-run-build-zip')); +    const yomitanVersion = /** @type {string} */ (args.get('yomitan-version'));      const manifestUtil = new ManifestUtil(); @@ -204,15 +246,15 @@ export async function main(argv) {      try {          await buildLibs(); -        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, yomitanVersion);      } 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/bin/dictionary-validate.js b/dev/bin/dictionary-validate.js index 78ad5198..dc01815e 100644 --- a/dev/bin/dictionary-validate.js +++ b/dev/bin/dictionary-validate.js @@ -18,6 +18,7 @@  import {testDictionaryFiles} from '../dictionary-validate.js'; +/** */  async function main() {      const dictionaryFileNames = process.argv.slice(2);      if (dictionaryFileNames.length === 0) { @@ -28,6 +29,7 @@ async function main() {          return;      } +    /** @type {import('dev/schema-validate').ValidateMode} */      let mode = null;      if (dictionaryFileNames[0] === '--ajv') {          mode = 'ajv'; diff --git a/dev/bin/generate-css-json.js b/dev/bin/generate-css-json.js index 48b42c65..73c406e0 100644 --- a/dev/bin/generate-css-json.js +++ b/dev/bin/generate-css-json.js @@ -19,6 +19,7 @@  import fs from 'fs';  import {formatRulesJson, generateRules, getTargets} from '../generate-css-json.js'; +/** */  function main() {      for (const {cssFile, overridesCssFile, outputPath} of getTargets()) {          const json = formatRulesJson(generateRules(cssFile, overridesCssFile)); diff --git a/dev/bin/schema-validate.js b/dev/bin/schema-validate.js index 86cfebae..206f26ca 100644 --- a/dev/bin/schema-validate.js +++ b/dev/bin/schema-validate.js @@ -17,9 +17,10 @@   */  import fs from 'fs'; -import performance from 'perf_hooks'; -import {createJsonSchema} from '../util.js'; +import {performance} from 'perf_hooks'; +import {createJsonSchema} from '../schema-validate.js'; +/** */  function main() {      const args = process.argv.slice(2);      if (args.length < 2) { @@ -30,6 +31,7 @@ function main() {          return;      } +    /** @type {import('dev/schema-validate').ValidateMode} */      let mode = null;      if (args[0] === '--ajv') {          mode = 'ajv'; diff --git a/dev/build-libs.js b/dev/build-libs.js index 8320a947..5caabec7 100644 --- a/dev/build-libs.js +++ b/dev/build-libs.js @@ -26,19 +26,26 @@ import {fileURLToPath} from 'url';  const dirname = path.dirname(fileURLToPath(import.meta.url));  const extDir = path.join(dirname, '..', 'ext'); -async function buildLib(p) { +/** + * @param {string} scriptPath + */ +async function buildLib(scriptPath) {      await esbuild.build({ -        entryPoints: [p], +        entryPoints: [scriptPath],          bundle: true,          minify: false,          sourcemap: true,          target: 'es2020',          format: 'esm', -        outfile: path.join(extDir, 'lib', path.basename(p)), -        external: ['fs'] +        outfile: path.join(extDir, 'lib', path.basename(scriptPath)), +        external: ['fs'], +        banner: { +            js: '// @ts-nocheck' +        }      });  } +/** */  export async function buildLibs() {      const devLibPath = path.join(dirname, 'lib');      const files = await fs.promises.readdir(devLibPath, { @@ -52,12 +59,12 @@ export async function buildLibs() {      const schemaDir = path.join(extDir, 'data/schemas/');      const schemaFileNames = fs.readdirSync(schemaDir); -    const schemas = schemaFileNames.map((schemaFileName) => JSON.parse(fs.readFileSync(path.join(schemaDir, schemaFileName)))); +    const schemas = schemaFileNames.map((schemaFileName) => JSON.parse(fs.readFileSync(path.join(schemaDir, schemaFileName), {encoding: 'utf8'})));      const ajv = new Ajv({schemas: schemas, code: {source: true, esm: true}});      const moduleCode = standaloneCode(ajv);      // https://github.com/ajv-validator/ajv/issues/2209 -    const patchedModuleCode = "import {ucs2length} from './ucs2length.js';" + moduleCode.replaceAll('require("ajv/dist/runtime/ucs2length").default', 'ucs2length'); +    const patchedModuleCode = "// @ts-nocheck\nimport {ucs2length} from './ucs2length.js';" + moduleCode.replaceAll('require("ajv/dist/runtime/ucs2length").default', 'ucs2length');      fs.writeFileSync(path.join(extDir, 'lib/validate-schemas.js'), patchedModuleCode);  } diff --git a/dev/data-error.js b/dev/data-error.js new file mode 100644 index 00000000..5034e3fd --- /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 <https://www.gnu.org/licenses/>. + */ + +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/dictionary-validate.js b/dev/dictionary-validate.js index eb40beda..a6948bfe 100644 --- a/dev/dictionary-validate.js +++ b/dev/dictionary-validate.js @@ -20,25 +20,39 @@ import fs from 'fs';  import JSZip from 'jszip';  import path from 'path';  import {performance} from 'perf_hooks'; +import {fileURLToPath} from 'url';  import {createJsonSchema} from './schema-validate.js'; +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +/** + * @param {string} relativeFileName + * @returns {import('dev/dictionary-validate').Schema} + */  function readSchema(relativeFileName) { -    const fileName = path.join(__dirname, relativeFileName); +    const fileName = path.join(dirname, relativeFileName);      const source = fs.readFileSync(fileName, {encoding: 'utf8'});      return JSON.parse(source);  } +/** + * @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; } @@ -47,14 +61,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 + */  export async function validateDictionary(mode, archive, schemas) {      const fileName = 'index.json';      const indexFile = archive.files[fileName]; @@ -69,8 +89,9 @@ export 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); @@ -80,6 +101,9 @@ export async function validateDictionary(mode, archive, schemas) {      await validateDictionaryBanks(mode, archive, 'tag_bank_?.json', schemas.tagBankV3);  } +/** + * @returns {import('dev/dictionary-validate').Schemas} + */  export function getSchemas() {      return {          index: readSchema('../ext/data/schemas/dictionary-index-schema.json'), @@ -93,6 +117,10 @@ export function getSchemas() {      };  } +/** + * @param {import('dev/schema-validate').ValidateMode} mode + * @param {string[]} dictionaryFileNames + */  export async function testDictionaryFiles(mode, dictionaryFileNames) {      const schemas = getSchemas(); diff --git a/dev/generate-css-json.js b/dev/generate-css-json.js index 914c1452..e5d4d7f0 100644 --- a/dev/generate-css-json.js +++ b/dev/generate-css-json.js @@ -16,26 +16,36 @@   * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ +import css from 'css';  import fs from 'fs';  import path from 'path'; +import {fileURLToPath} from 'url'; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); +/** + * @returns {{cssFile: string, overridesCssFile: string, outputPath: string}[]} + */  export function getTargets() {      return [          { -            cssFile: path.join(__dirname, '..', 'ext/css/structured-content.css'), -            overridesCssFile: path.join(__dirname, 'data/structured-content-overrides.css'), -            outputPath: path.join(__dirname, '..', 'ext/data/structured-content-style.json') +            cssFile: path.join(dirname, '..', 'ext/css/structured-content.css'), +            overridesCssFile: path.join(dirname, 'data/structured-content-overrides.css'), +            outputPath: path.join(dirname, '..', 'ext/data/structured-content-style.json')          },          { -            cssFile: path.join(__dirname, '..', 'ext/css/display-pronunciation.css'), -            overridesCssFile: path.join(__dirname, 'data/display-pronunciation-overrides.css'), -            outputPath: path.join(__dirname, '..', 'ext/data/pronunciation-style.json') +            cssFile: path.join(dirname, '..', 'ext/css/display-pronunciation.css'), +            overridesCssFile: path.join(dirname, 'data/display-pronunciation-overrides.css'), +            outputPath: path.join(dirname, '..', 'ext/data/pronunciation-style.json')          }      ];  } -import css from '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) { @@ -53,6 +63,12 @@ function indexOfRule(rules, selectors) {      return -1;  } +/** + * @param {import('css-style-applier').RawStyleDataStyleArray} styles + * @param {string} property + * @param {Map<string, number>} removedProperties + * @returns {number} + */  function removeProperty(styles, property, removedProperties) {      let removeCount = removedProperties.get(property);      if (typeof removeCount !== 'undefined') { return removeCount; } @@ -69,6 +85,10 @@ function removeProperty(styles, property, removedProperties) {      return removeCount;  } +/** + * @param {import('css-style-applier').RawStyleData} rules + * @returns {string} + */  export function formatRulesJson(rules) {      // Manually format JSON, for improved compactness      // return JSON.stringify(rules, null, 4); @@ -102,27 +122,39 @@ export function formatRulesJson(rules) {      return result;  } +/** + * @param {string} cssFile + * @param {string} overridesCssFile + * @returns {import('css-style-applier').RawStyleData} + * @throws {Error} + */  export 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}); @@ -132,7 +164,9 @@ export 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<string, number>} */          const removedProperties = new Map();          for (const declaration of declarations) {              switch (declaration.type) { @@ -146,16 +180,18 @@ export 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/jsconfig.json b/dev/jsconfig.json new file mode 100644 index 00000000..a52153a8 --- /dev/null +++ b/dev/jsconfig.json @@ -0,0 +1,80 @@ +{ +    "compilerOptions": { +        "module": "ES2022", +        "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/*"], +            "rollup/parseAst": ["../types/other/rollup-parse-ast"] +        }, +        "types": [ +            "node", +            "events", +            "browserify", +            "jsdom", +            "assert", +            "css", +            "chrome", +            "ajv" +        ] +    }, +    "include": [ +        "**/*.js", +        "../playwright.config.js", +        "../vitest.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/globals.d.ts" +    ], +    "exclude": [ +        "../node_modules", +        "lib" +    ] +}
\ No newline at end of file diff --git a/dev/manifest-util.js b/dev/manifest-util.js index 15175e7f..638706d8 100644 --- a/dev/manifest-util.js +++ b/dev/manifest-util.js @@ -23,6 +23,11 @@ import path from 'path';  const dirname = path.dirname(fileURLToPath(import.meta.url)); +/** + * @template T + * @param {T} value + * @returns {T} + */  function clone(value) {      return JSON.parse(JSON.stringify(value));  } @@ -31,16 +36,24 @@ function clone(value) {  export class ManifestUtil {      constructor() {          const fileName = path.join(dirname, 'data', 'manifest-variants.json'); -        const {manifest, variants, defaultVariant} = JSON.parse(fs.readFileSync(fileName)); +        const {manifest, variants, defaultVariant} = /** @type {import('dev/manifest').ManifestConfig} */ (JSON.parse(fs.readFileSync(fileName, {encoding: 'utf8'}))); +        /** @type {import('dev/manifest').Manifest} */          this._manifest = manifest; +        /** @type {import('dev/manifest').ManifestVariant[]} */          this._variants = variants; +        /** @type {string} */          this._defaultVariant = defaultVariant; +        /** @type {Map<string, import('dev/manifest').ManifestVariant>} */          this._variantMap = new Map();          for (const variant of variants) {              this._variantMap.set(variant.name, variant);          }      } +    /** +     * @param {?string} [variantName] +     * @returns {import('dev/manifest').Manifest} +     */      getManifest(variantName) {          if (typeof variantName === 'string') {              const variant = this._variantMap.get(variantName); @@ -59,20 +72,36 @@ export class ManifestUtil {          return clone(this._manifest);      } +    /** +     * @returns {import('dev/manifest').ManifestVariant[]} +     */      getVariants() {          return [...this._variants];      } +    /** +     * @param {string} name +     * @returns {import('dev/manifest').ManifestVariant|undefined} +     */      getVariant(name) {          return this._variantMap.get(name);      } +    /** +     * @param {import('dev/manifest').Manifest} manifest +     * @returns {string} +     */      static createManifestString(manifest) {          return JSON.stringify(manifest, null, 4) + '\n';      }      // Private +    /** +     * @param {import('dev/manifest').Command} data +     * @returns {string} +     * @throws {Error} +     */      _evaluateModificationCommand(data) {          const {command, args, trim} = data;          const {stdout, stderr, status} = childProcess.spawnSync(command, args, { @@ -89,6 +118,11 @@ export class ManifestUtil {          return result;      } +    /** +     * @param {import('dev/manifest').Manifest} manifest +     * @param {import('dev/manifest').Modification[]} modifications +     * @returns {import('dev/manifest').Manifest} +     */      _applyModifications(manifest, modifications) {          if (Array.isArray(modifications)) {              for (const modification of modifications) { @@ -97,6 +131,7 @@ export class ManifestUtil {                      case 'set':                          {                              let {value, before, after, command} = modification; +                            /** @type {import('core').UnknownObject} */                              const object = this._getObjectProperties(manifest, path2, path2.length - 1);                              const key = path2[path2.length - 1]; @@ -121,6 +156,7 @@ export class ManifestUtil {                      case 'replace':                          {                              const {pattern, patternFlags, replacement} = modification; +                            /** @type {import('core').UnknownObject} */                              const value = this._getObjectProperties(manifest, path2, path2.length - 1);                              const regex = new RegExp(pattern, patternFlags);                              const last = path2[path2.length - 1]; @@ -131,6 +167,7 @@ export class ManifestUtil {                          break;                      case 'delete':                          { +                            /** @type {import('core').UnknownObject} */                              const value = this._getObjectProperties(manifest, path2, path2.length - 1);                              const last = path2[path2.length - 1];                              delete value[last]; @@ -139,6 +176,7 @@ export class ManifestUtil {                      case 'remove':                          {                              const {item} = modification; +                            /** @type {unknown[]} */                              const value = this._getObjectProperties(manifest, path2, path2.length);                              const index = value.indexOf(item);                              if (index >= 0) { value.splice(index, 1); } @@ -147,6 +185,7 @@ export class ManifestUtil {                      case 'splice':                          {                              const {start, deleteCount, items} = modification; +                            /** @type {unknown[]} */                              const value = this._getObjectProperties(manifest, path2, path2.length);                              const itemsNew = items.map((v) => clone(v));                              value.splice(start, deleteCount, ...itemsNew); @@ -158,7 +197,9 @@ export class ManifestUtil {                              const {newPath, before, after} = modification;                              const oldKey = path2[path2.length - 1];                              const newKey = newPath[newPath.length - 1]; +                            /** @type {import('core').UnknownObject} */                              const oldObject = this._getObjectProperties(manifest, path2, path2.length - 1); +                            /** @type {import('core').UnknownObject} */                              const newObject = this._getObjectProperties(manifest, newPath, newPath.length - 1);                              const oldObjectIsNewObject = this._arraysAreSame(path2, newPath, -1);                              const value = oldObject[oldKey]; @@ -184,6 +225,7 @@ export class ManifestUtil {                      case 'add':                          {                              const {items} = modification; +                            /** @type {unknown[]} */                              const value = this._getObjectProperties(manifest, path2, path2.length);                              const itemsNew = items.map((v) => clone(v));                              value.push(...itemsNew); @@ -196,6 +238,13 @@ export class ManifestUtil {          return manifest;      } +    /** +     * @template T +     * @param {T[]} array1 +     * @param {T[]} array2 +     * @param {number} lengthOffset +     * @returns {boolean} +     */      _arraysAreSame(array1, array2, lengthOffset) {          let ii = array1.length;          if (ii !== array2.length) { return false; } @@ -206,10 +255,21 @@ export class ManifestUtil {          return true;      } +    /** +     * @param {import('core').UnknownObject} object +     * @param {string|number} key +     * @returns {number} +     */      _getObjectKeyIndex(object, key) { -        return Object.keys(object).indexOf(key); +        return Object.keys(object).indexOf(typeof key === 'string' ? key : `${key}`);      } +    /** +     * @param {import('core').UnknownObject} object +     * @param {string|number} key +     * @param {unknown} value +     * @param {number} index +     */      _setObjectKeyAtIndex(object, key, value, index) {          if (index < 0 || typeof key === 'number' || Object.prototype.hasOwnProperty.call(object, key)) {              object[key] = value; @@ -229,13 +289,24 @@ export class ManifestUtil {          }      } +    /** +     * @template [TReturn=unknown] +     * @param {unknown} object +     * @param {import('dev/manifest').PropertyPath} path2 +     * @param {number} count +     * @returns {TReturn} +     */      _getObjectProperties(object, path2, count) {          for (let i = 0; i < count; ++i) { -            object = object[path2[i]]; +            object = /** @type {import('core').UnknownObject} */ (object)[path2[i]];          } -        return object; +        return /** @type {TReturn} */ (object);      } +    /** +     * @param {import('dev/manifest').ManifestVariant} variant +     * @returns {import('dev/manifest').ManifestVariant[]} +     */      _getInheritanceChain(variant) {          const visited = new Set();          const inheritance = []; @@ -256,6 +327,11 @@ export class ManifestUtil {          return inheritance;      } +    /** +     * @param {import('dev/manifest').Manifest} manifest +     * @param {import('dev/manifest').ManifestVariant} variant +     * @returns {import('dev/manifest').Manifest} +     */      _createVariantManifest(manifest, variant) {          let modifiedManifest = clone(manifest);          for (const {modifications} of this._getInheritanceChain(variant)) { diff --git a/dev/schema-validate.js b/dev/schema-validate.js index fbd6b06a..a1fe8455 100644 --- a/dev/schema-validate.js +++ b/dev/schema-validate.js @@ -17,31 +17,48 @@   */  import Ajv from 'ajv'; +import {readFileSync} from 'fs';  import {JsonSchema} from '../ext/js/data/json-schema.js'; +import {DataError} from './data-error.js';  class JsonSchemaAjv { +    /** +     * @param {import('dev/schema-validate').Schema} schema +     */      constructor(schema) {          const ajv = new Ajv({              meta: false,              strictTuples: false,              allowUnionTypes: true          }); -        ajv.addMetaSchema(require('ajv/dist/refs/json-schema-draft-07.json')); -        this._validate = ajv.compile(schema); +        const metaSchemaPath = require.resolve('ajv/dist/refs/json-schema-draft-07.json'); +        const metaSchema = JSON.parse(readFileSync(metaSchemaPath, {encoding: 'utf8'})); +        ajv.addMetaSchema(metaSchema); +        /** @type {import('ajv').ValidateFunction} */ +        this._validate = ajv.compile(/** @type {import('ajv').Schema} */ (schema));      } +    /** +     * @param {unknown} data +     * @throws {Error} +     */      validate(data) {          if (this._validate(data)) { return; }          const {errors} = this._validate; -        const error = new Error('Schema validation failed'); +        const error = new DataError('Schema validation failed');          error.data = JSON.parse(JSON.stringify(errors));          throw error;      }  } +/** + * @param {import('dev/schema-validate').ValidateMode} mode + * @param {import('dev/schema-validate').Schema} schema + * @returns {JsonSchema|JsonSchemaAjv} + */  export function createJsonSchema(mode, schema) {      switch (mode) {          case 'ajv': return new JsonSchemaAjv(schema); -        default: return new JsonSchema(schema); +        default: return new JsonSchema(/** @type {import('json-schema').Schema} */ (schema));      }  } diff --git a/dev/translator-vm.js b/dev/translator-vm.js index 9f14523e..7fdda879 100644 --- a/dev/translator-vm.js +++ b/dev/translator-vm.js @@ -34,34 +34,46 @@ const dirname = path.dirname(fileURLToPath(import.meta.url));  export class TranslatorVM {      constructor() { -        global.chrome = { +        /** @type {import('dev/vm').PseudoChrome} */ +        const chrome = {              runtime: {                  getURL: (path2) => {                      return url.pathToFileURL(path.join(dirname, '..', 'ext', path2.replace(/^\//, ''))).href;                  }              }          }; +        // @ts-expect-error - Overwriting a global +        global.chrome = chrome; +        /** @type {?JapaneseUtil} */          this._japaneseUtil = null; +        /** @type {?Translator} */          this._translator = null; +        /** @type {?AnkiNoteDataCreator} */          this._ankiNoteDataCreator = null; +        /** @type {?string} */          this._dictionaryName = null;      } +    /** @type {Translator} */      get translator() { +        if (this._translator === null) { throw new Error('Not prepared'); }          return this._translator;      } +    /** +     * @param {string} dictionaryDirectory +     * @param {string} dictionaryName +     */      async prepare(dictionaryDirectory, dictionaryName) {          // Dictionary          this._dictionaryName = dictionaryName;          const testDictionary = createDictionaryArchive(dictionaryDirectory, dictionaryName); -        // const testDictionaryContent = await testDictionary.arrayBuffer();          const testDictionaryContent = await testDictionary.generateAsync({type: 'arraybuffer'});          // Setup database          const dictionaryImporterMediaLoader = new DictionaryImporterMediaLoader(); -        const dictionaryImporter = new DictionaryImporter(dictionaryImporterMediaLoader, null); +        const dictionaryImporter = new DictionaryImporter(dictionaryImporterMediaLoader);          const dictionaryDatabase = new DictionaryDatabase();          await dictionaryDatabase.prepare(); @@ -73,27 +85,35 @@ export class TranslatorVM {          expect(errors.length).toEqual(0); -        const myDirname = path.dirname(fileURLToPath(import.meta.url)); -          // Setup translator          this._japaneseUtil = new JapaneseUtil(null);          this._translator = new Translator({              japaneseUtil: this._japaneseUtil,              database: dictionaryDatabase          }); -        const deinflectionReasons = JSON.parse(fs.readFileSync(path.join(myDirname, '..', 'ext', 'data/deinflect.json'))); +        const deinflectionReasons = JSON.parse(fs.readFileSync(path.join(dirname, '..', 'ext', 'data/deinflect.json'), {encoding: 'utf8'}));          this._translator.prepare(deinflectionReasons);          // Assign properties          this._ankiNoteDataCreator = new AnkiNoteDataCreator(this._japaneseUtil);      } +    /** +     * @param {import('dictionary').DictionaryEntry} dictionaryEntry +     * @param {import('settings').ResultOutputMode} mode +     * @returns {import('anki-templates').NoteData} +     * @throws {Error} +     */      createTestAnkiNoteData(dictionaryEntry, mode) { +        if (this._ankiNoteDataCreator === null) { +            throw new Error('Not prepared'); +        }          const marker = '{marker}'; +        /** @type {import('anki-templates-internal').CreateDetails} */          const data = {              dictionaryEntry,              resultOutputMode: mode, -            mode: 'mode', +            mode: 'test',              glossaryLayoutMode: 'default',              compactTags: false,              context: { @@ -108,8 +128,16 @@ export class TranslatorVM {          return this._ankiNoteDataCreator.create(marker, data);      } +    /** +     * @template {import('translation').FindTermsOptions|import('translation').FindKanjiOptions} T +     * @param {import('dev/vm').OptionsPresetObject} optionsPresets +     * @param {string|import('dev/vm').OptionsPresetObject|(string|import('dev/vm').OptionsPresetObject)[]} optionsArray +     * @returns {T} +     * @throws {Error} +     */      buildOptions(optionsPresets, optionsArray) {          const dictionaryName = this._dictionaryName; +        /** @type {import('core').UnknownObject} */          const options = {};          if (!Array.isArray(optionsArray)) { optionsArray = [optionsArray]; }          for (const entry of optionsArray) { @@ -160,6 +188,6 @@ export class TranslatorVM {              null          ); -        return options; +        return /** @type {T} */ (options);      }  } diff --git a/dev/util.js b/dev/util.js index cabc40aa..3299dec4 100644 --- a/dev/util.js +++ b/dev/util.js @@ -20,6 +20,11 @@ import fs from 'fs';  import JSZip from 'jszip';  import path from 'path'; +/** + * @param {string[]} args + * @param {Map<?string, (boolean|null|number|string|string[])>} argMap + * @returns {Map<?string, (boolean|null|number|string|string[])>} + */  export function getArgs(args, argMap) {      let key = null;      let canKey = true; @@ -64,11 +69,16 @@ export function getArgs(args, argMap) {      return argMap;  } +/** + * @param {string} baseDirectory + * @param {?(fileName: string) => boolean} predicate + * @returns {string[]} + */  export function getAllFiles(baseDirectory, predicate=null) {      const results = [];      const directories = [baseDirectory];      while (directories.length > 0) { -        const directory = directories.shift(); +        const directory = /** @type {string} */ (directories.shift());          const fileNames = fs.readdirSync(directory);          for (const fileName of fileNames) {              const fullFileName = path.join(directory, fileName); @@ -86,6 +96,11 @@ export function getAllFiles(baseDirectory, predicate=null) {      return results;  } +/** + * @param {string} dictionaryDirectory + * @param {string} [dictionaryName] + * @returns {import('jszip')} + */  export function createDictionaryArchive(dictionaryDirectory, dictionaryName) {      const fileNames = fs.readdirSync(dictionaryDirectory); @@ -125,6 +140,10 @@ export function createDictionaryArchive(dictionaryDirectory, dictionaryName) {      // return zipFileBlob;  } +/** + * @param {(...args: import('core').SafeAny[]) => (unknown|Promise<unknown>)} func + * @param {...import('core').SafeAny} args + */  export async function testMain(func, ...args) {      try {          await func(...args); |