aboutsummaryrefslogtreecommitdiff
path: root/dev
diff options
context:
space:
mode:
Diffstat (limited to 'dev')
-rw-r--r--dev/bin/build.js66
-rw-r--r--dev/bin/dictionary-validate.js2
-rw-r--r--dev/bin/generate-css-json.js1
-rw-r--r--dev/bin/schema-validate.js6
-rw-r--r--dev/build-libs.js19
-rw-r--r--dev/data-error.js35
-rw-r--r--dev/dictionary-validate.js44
-rw-r--r--dev/generate-css-json.js76
-rw-r--r--dev/jsconfig.json80
-rw-r--r--dev/manifest-util.js84
-rw-r--r--dev/schema-validate.js25
-rw-r--r--dev/translator-vm.js44
-rw-r--r--dev/util.js21
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);