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); |