diff options
35 files changed, 257 insertions, 77 deletions
diff --git a/dev/bin/schema-validate.js b/dev/bin/schema-validate.js index 206f26ca..bbd5ad5f 100644 --- a/dev/bin/schema-validate.js +++ b/dev/bin/schema-validate.js @@ -18,6 +18,7 @@ import fs from 'fs'; import {performance} from 'perf_hooks'; +import {parseJson} from '../../ext/js/core/json.js'; import {createJsonSchema} from '../schema-validate.js'; /** */ @@ -39,14 +40,14 @@ function main() { } const schemaSource = fs.readFileSync(args[0], {encoding: 'utf8'}); - const schema = JSON.parse(schemaSource); + const schema = parseJson(schemaSource); for (const dataFileName of args.slice(1)) { const start = performance.now(); try { console.log(`Validating ${dataFileName}...`); const dataSource = fs.readFileSync(dataFileName, {encoding: 'utf8'}); - const data = JSON.parse(dataSource); + const data = parseJson(dataSource); createJsonSchema(mode, schema).validate(data); const end = performance.now(); console.log(`No issues detected (${((end - start) / 1000).toFixed(2)}s)`); diff --git a/dev/build-libs.js b/dev/build-libs.js index a992f20a..10720010 100644 --- a/dev/build-libs.js +++ b/dev/build-libs.js @@ -22,6 +22,7 @@ import esbuild from 'esbuild'; import fs from 'fs'; import path from 'path'; import {fileURLToPath} from 'url'; +import {parseJson} from './json.js'; const dirname = path.dirname(fileURLToPath(import.meta.url)); const extDir = path.join(dirname, '..', 'ext'); @@ -61,7 +62,11 @@ 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), {encoding: 'utf8'}))); + const schemas = schemaFileNames.map((schemaFileName) => { + /** @type {import('ajv').AnySchema} */ + const result = parseJson(fs.readFileSync(path.join(schemaDir, schemaFileName), {encoding: 'utf8'})); + return result; + }); const ajv = new Ajv({ schemas, code: {source: true, esm: true}, diff --git a/dev/dictionary-validate.js b/dev/dictionary-validate.js index 7842c65e..efc2eb8c 100644 --- a/dev/dictionary-validate.js +++ b/dev/dictionary-validate.js @@ -21,6 +21,7 @@ import JSZip from 'jszip'; import path from 'path'; import {performance} from 'perf_hooks'; import {fileURLToPath} from 'url'; +import {parseJson} from './json.js'; import {createJsonSchema} from './schema-validate.js'; const dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -32,7 +33,7 @@ const dirname = path.dirname(fileURLToPath(import.meta.url)); function readSchema(relativeFileName) { const fileName = path.join(dirname, relativeFileName); const source = fs.readFileSync(fileName, {encoding: 'utf8'}); - return JSON.parse(source); + return parseJson(source); } /** @@ -57,7 +58,7 @@ async function validateDictionaryBanks(mode, zip, fileNameFormat, schema) { const file = zip.files[fileName]; if (!file) { break; } - const data = JSON.parse(await file.async('string')); + const data = parseJson(await file.async('string')); try { jsonSchema.validate(data); } catch (e) { @@ -83,7 +84,8 @@ export async function validateDictionary(mode, archive, schemas) { throw new Error('No dictionary index found in archive'); } - const index = JSON.parse(await indexFile.async('string')); + /** @type {import('dictionary-data').Index} */ + const index = parseJson(await indexFile.async('string')); const version = index.format || index.version; try { diff --git a/dev/json.js b/dev/json.js new file mode 100644 index 00000000..a76edfcd --- /dev/null +++ b/dev/json.js @@ -0,0 +1,18 @@ +/* + * 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/>. + */ + +export {parseJson} from '../ext/js/core/json.js'; diff --git a/dev/manifest-util.js b/dev/manifest-util.js index 638706d8..ac9b58db 100644 --- a/dev/manifest-util.js +++ b/dev/manifest-util.js @@ -20,6 +20,7 @@ import childProcess from 'child_process'; import fs from 'fs'; import {fileURLToPath} from 'node:url'; import path from 'path'; +import {parseJson} from './json.js'; const dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -29,14 +30,14 @@ const dirname = path.dirname(fileURLToPath(import.meta.url)); * @returns {T} */ function clone(value) { - return JSON.parse(JSON.stringify(value)); + return parseJson(JSON.stringify(value)); } export class ManifestUtil { constructor() { const fileName = path.join(dirname, 'data', 'manifest-variants.json'); - const {manifest, variants, defaultVariant} = /** @type {import('dev/manifest').ManifestConfig} */ (JSON.parse(fs.readFileSync(fileName, {encoding: 'utf8'}))); + const {manifest, variants, defaultVariant} = /** @type {import('dev/manifest').ManifestConfig} */ (parseJson(fs.readFileSync(fileName, {encoding: 'utf8'}))); /** @type {import('dev/manifest').Manifest} */ this._manifest = manifest; /** @type {import('dev/manifest').ManifestVariant[]} */ diff --git a/dev/schema-validate.js b/dev/schema-validate.js index 81953f49..57faf96c 100644 --- a/dev/schema-validate.js +++ b/dev/schema-validate.js @@ -20,6 +20,7 @@ import Ajv from 'ajv'; import {readFileSync} from 'fs'; import {JsonSchema} from '../ext/js/data/json-schema.js'; import {DataError} from './data-error.js'; +import {parseJson} from './json.js'; class JsonSchemaAjv { /** @@ -32,7 +33,8 @@ class JsonSchemaAjv { allowUnionTypes: true }); const metaSchemaPath = require.resolve('ajv/dist/refs/json-schema-draft-07.json'); - const metaSchema = JSON.parse(readFileSync(metaSchemaPath, {encoding: 'utf8'})); + /** @type {import('ajv').AnySchemaObject} */ + const metaSchema = parseJson(readFileSync(metaSchemaPath, {encoding: 'utf8'})); ajv.addMetaSchema(metaSchema); /** @type {import('ajv').ValidateFunction} */ this._validate = ajv.compile(/** @type {import('ajv').Schema} */ (schema)); @@ -46,7 +48,7 @@ class JsonSchemaAjv { if (this._validate(data)) { return; } const {errors} = this._validate; const error = new DataError('Schema validation failed'); - error.data = JSON.parse(JSON.stringify(errors)); + error.data = parseJson(JSON.stringify(errors)); throw error; } } diff --git a/dev/util.js b/dev/util.js index f45966c4..6a7fa8f5 100644 --- a/dev/util.js +++ b/dev/util.js @@ -19,6 +19,7 @@ import fs from 'fs'; import JSZip from 'jszip'; import path from 'path'; +import {parseJson} from './json.js'; /** * @param {string[]} args @@ -112,9 +113,9 @@ export function createDictionaryArchive(dictionaryDirectory, dictionaryName) { for (const fileName of fileNames) { if (/\.json$/.test(fileName)) { const content = fs.readFileSync(path.join(dictionaryDirectory, fileName), {encoding: 'utf8'}); - const json = JSON.parse(content); + const json = parseJson(content); if (fileName === 'index.json' && typeof dictionaryName === 'string') { - json.title = dictionaryName; + /** @type {import('dictionary-data').Index} */ (json).title = dictionaryName; } archive.file(fileName, JSON.stringify(json, null, 0)); diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index 20c7a189..09edbd6e 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -24,6 +24,7 @@ import {ClipboardReader} from '../comm/clipboard-reader.js'; import {Mecab} from '../comm/mecab.js'; import {clone, deferPromise, generateId, invokeMessageHandler, isObject, log, promiseTimeout} from '../core.js'; import {ExtensionError} from '../core/extension-error.js'; +import {parseJson, readResponseJson} from '../core/json.js'; import {AnkiUtil} from '../data/anki-util.js'; import {OptionsUtil} from '../data/options-util.js'; import {PermissionsUtil} from '../data/permissions-util.js'; @@ -291,7 +292,8 @@ export class Backend { log.error(e); } - const deinflectionReasons = /** @type {import('deinflector').ReasonsRaw} */ (await this._fetchJson('/data/deinflect.json')); + /** @type {import('deinflector').ReasonsRaw} */ + const deinflectionReasons = await this._fetchJson('/data/deinflect.json'); this._translator.prepare(deinflectionReasons); await this._optionsUtil.prepare(); @@ -764,6 +766,7 @@ export class Backend { const frameId = sender.frameId; const id = generateId(16); + /** @type {import('cross-frame-api').ActionPortDetails} */ const details = { name: 'action-port', id @@ -908,11 +911,13 @@ export class Backend { throw new Error('Port does not have an associated frame ID'); } + /** @type {import('cross-frame-api').CrossFrameCommunicationPortDetails} */ const sourceDetails = { name: 'cross-frame-communication-port', otherTabId: targetTabId, otherFrameId: targetFrameId }; + /** @type {import('cross-frame-api').CrossFrameCommunicationPortDetails} */ const targetDetails = { name: 'cross-frame-communication-port', otherTabId: sourceTabId, @@ -1530,7 +1535,8 @@ export class Backend { hasStarted = true; port.onMessage.removeListener(onMessage); - const messageData = JSON.parse(messageString); + /** @type {{action: string, params?: import('core').SerializableObject}} */ + const messageData = parseJson(messageString); messageString = null; onMessageComplete(messageData); } @@ -2062,12 +2068,13 @@ export class Backend { } /** + * @template [T=unknown] * @param {string} url - * @returns {Promise<unknown>} + * @returns {Promise<T>} */ async _fetchJson(url) { const response = await this._fetchAsset(url); - return await response.json(); + return await readResponseJson(response); } /** diff --git a/ext/js/comm/anki-connect.js b/ext/js/comm/anki-connect.js index bd9a69a2..fa5543d5 100644 --- a/ext/js/comm/anki-connect.js +++ b/ext/js/comm/anki-connect.js @@ -17,6 +17,7 @@ */ import {ExtensionError} from '../core/extension-error.js'; +import {parseJson} from '../core/json.js'; import {AnkiUtil} from '../data/anki-util.js'; /** @@ -419,7 +420,7 @@ export class AnkiConnect { let result; try { responseText = await response.text(); - result = JSON.parse(responseText); + result = parseJson(responseText); } catch (e) { const error = new ExtensionError('Invalid Anki response'); error.data = {action, params, status: response.status, responseText, originalError: e}; diff --git a/ext/js/comm/api.js b/ext/js/comm/api.js index 35a66d9e..43f707e2 100644 --- a/ext/js/comm/api.js +++ b/ext/js/comm/api.js @@ -18,6 +18,7 @@ import {deferPromise} from '../core.js'; import {ExtensionError} from '../core/extension-error.js'; +import {parseJson} from '../core/json.js'; export class API { /** @@ -433,6 +434,7 @@ export class API { return new Promise((resolve, reject) => { /** @type {?import('core').Timeout} */ let timer = null; + /** @type {import('core').DeferredPromiseDetails<import('api').CreateActionPortResult>} */ const portDetails = deferPromise(); /** @@ -441,8 +443,9 @@ export class API { const onConnect = async (port) => { try { const {name: expectedName, id: expectedId} = await portDetails.promise; - const {name, id} = JSON.parse(port.name); - if (name !== expectedName || id !== expectedId || timer === null) { return; } + /** @type {import('cross-frame-api').PortDetails} */ + const portDetails2 = parseJson(port.name); + if (portDetails2.name !== expectedName || portDetails2.id !== expectedId || timer === null) { return; } } catch (e) { return; } @@ -470,7 +473,9 @@ export class API { timer = setTimeout(() => onError(new Error('Timeout')), timeout); chrome.runtime.onConnect.addListener(onConnect); - this._invoke('createActionPort').then(portDetails.resolve, onError); + /** @type {Promise<import('api').CreateActionPortResult>} */ + const createActionPortResult = this._invoke('createActionPort'); + createActionPortResult.then(portDetails.resolve, onError); }); } diff --git a/ext/js/comm/cross-frame-api.js b/ext/js/comm/cross-frame-api.js index 3ac38cf2..0d3f3275 100644 --- a/ext/js/comm/cross-frame-api.js +++ b/ext/js/comm/cross-frame-api.js @@ -18,6 +18,7 @@ import {EventDispatcher, EventListenerCollection, invokeMessageHandler, log} from '../core.js'; import {ExtensionError} from '../core/extension-error.js'; +import {parseJson} from '../core/json.js'; import {yomitan} from '../yomitan.js'; /** @@ -377,9 +378,10 @@ export class CrossFrameAPI { */ _onConnect(port) { try { + /** @type {import('cross-frame-api').PortDetails} */ let details; try { - details = JSON.parse(port.name); + details = parseJson(port.name); } catch (e) { return; } diff --git a/ext/js/core.js b/ext/js/core.js index c95eae01..d16a2099 100644 --- a/ext/js/core.js +++ b/ext/js/core.js @@ -47,7 +47,7 @@ export function stringReverse(string) { } /** - * Creates a deep clone of an object or value. This is similar to `JSON.parse(JSON.stringify(value))`. + * Creates a deep clone of an object or value. This is similar to `parseJson(JSON.stringify(value))`. * @template T * @param {T} value The value to clone. * @returns {T} A new clone of the value. diff --git a/ext/js/core/json.js b/ext/js/core/json.js new file mode 100644 index 00000000..a031f84e --- /dev/null +++ b/ext/js/core/json.js @@ -0,0 +1,40 @@ +/* + * 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/>. + */ + +/** + * This function is used to ensure more safe usage of `JSON.parse`. + * By default, `JSON.parse` returns a value with type `any`, which is easy to misuse. + * By changing the default to `unknown` and allowing it to be templatized, + * this improves how the return type is used. + * @template [T=unknown] + * @param {string} value + * @returns {T} + */ +export function parseJson(value) { + return JSON.parse(value); +} + +/** + * This function is used to ensure more safe usage of `Response.json`, + * which returns the `any` type. + * @template [T=unknown] + * @param {Response} response + * @returns {Promise<T>} + */ +export async function readResponseJson(response) { + return await response.json(); +} diff --git a/ext/js/data/options-util.js b/ext/js/data/options-util.js index c3c0e685..a17763e9 100644 --- a/ext/js/data/options-util.js +++ b/ext/js/data/options-util.js @@ -17,6 +17,7 @@ */ import {escapeRegExp, isObject} from '../core.js'; +import {parseJson, readResponseJson} from '../core/json.js'; import {TemplatePatcher} from '../templates/template-patcher.js'; import {JsonSchema} from './json-schema.js'; @@ -30,7 +31,8 @@ export class OptionsUtil { /** */ async prepare() { - const schema = /** @type {import('json-schema').Schema} */ (await this._fetchJson('/data/schemas/options-schema.json')); + /** @type {import('json-schema').Schema} */ + const schema = await this._fetchJson('/data/schemas/options-schema.json'); this._optionsSchema = new JsonSchema(schema); } @@ -115,7 +117,7 @@ export class OptionsUtil { } }); }); - options = JSON.parse(optionsStr); + options = parseJson(optionsStr); } catch (e) { // NOP } @@ -477,12 +479,13 @@ export class OptionsUtil { } /** + * @template [T=unknown] * @param {string} url - * @returns {Promise<unknown>} + * @returns {Promise<T>} */ async _fetchJson(url) { const response = await this._fetchGeneric(url); - return await response.json(); + return await readResponseJson(response); } /** diff --git a/ext/js/dom/sandbox/css-style-applier.js b/ext/js/dom/sandbox/css-style-applier.js index 200fd05f..6925f263 100644 --- a/ext/js/dom/sandbox/css-style-applier.js +++ b/ext/js/dom/sandbox/css-style-applier.js @@ -16,6 +16,8 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import {readResponseJson} from '../../core/json.js'; + /** * This class is used to apply CSS styles to elements using a consistent method * that is the same across different browsers. @@ -99,8 +101,9 @@ export class CssStyleApplier { /** * Fetches and parses a JSON file. + * @template [T=unknown] * @param {string} url The URL to the file. - * @returns {Promise<*>} A JSON object. + * @returns {Promise<T>} A JSON object. * @throws {Error} An error is thrown if the fetch fails. */ async _fetchJsonAsset(url) { @@ -115,7 +118,7 @@ export class CssStyleApplier { if (!response.ok) { throw new Error(`Failed to fetch ${url}: ${response.status}`); } - return await response.json(); + return await readResponseJson(response); } /** diff --git a/ext/js/input/hotkey-help-controller.js b/ext/js/input/hotkey-help-controller.js index 4a3c0264..14063d9a 100644 --- a/ext/js/input/hotkey-help-controller.js +++ b/ext/js/input/hotkey-help-controller.js @@ -17,6 +17,7 @@ */ import {isObject} from '../core.js'; +import {parseJson} from '../core/json.js'; import {yomitan} from '../yomitan.js'; import {HotkeyUtil} from './hotkey-util.js'; @@ -149,7 +150,7 @@ export class HotkeyHelpController { _getNodeInfo(node) { const {hotkey} = node.dataset; if (typeof hotkey !== 'string') { return null; } - const data = /** @type {unknown} */ (JSON.parse(hotkey)); + const data = /** @type {unknown} */ (parseJson(hotkey)); if (!Array.isArray(data)) { return null; } const [action, attributes, values] = data; if (typeof action !== 'string') { return null; } diff --git a/ext/js/language/deinflector.js b/ext/js/language/deinflector.js index 676f45a1..26cc6b18 100644 --- a/ext/js/language/deinflector.js +++ b/ext/js/language/deinflector.js @@ -20,7 +20,7 @@ export class Deinflector { /** * @param {import('deinflector').ReasonsRaw} reasons * @example - * const deinflectionReasons = JSON.parse( + * const deinflectionReasons = parseJson( * readFileSync(path.join('ext/data/deinflect.json')).toString(), * ); * const deinflector = new Deinflector(deinflectionReasons); diff --git a/ext/js/language/dictionary-importer.js b/ext/js/language/dictionary-importer.js index dfbd9590..df9c48f1 100644 --- a/ext/js/language/dictionary-importer.js +++ b/ext/js/language/dictionary-importer.js @@ -26,6 +26,7 @@ import { } from '../../lib/zip.js'; import {stringReverse} from '../core.js'; import {ExtensionError} from '../core/extension-error.js'; +import {parseJson} from '../core/json.js'; import {MediaUtil} from '../media/media-util.js'; const ajvSchemas = /** @type {import('dictionary-importer').CompiledSchemaValidators} */ (/** @type {unknown} */ (ajvSchemas0)); @@ -89,7 +90,7 @@ export class DictionaryImporter { const indexFile2 = /** @type {import('@zip.js/zip.js').Entry} */ (indexFile); const indexContent = await this._getData(indexFile2, new TextWriter()); - const index = /** @type {import('dictionary-data').Index} */ (JSON.parse(indexContent)); + const index = /** @type {import('dictionary-data').Index} */ (parseJson(indexContent)); if (!ajvSchemas.dictionaryIndex(index)) { throw this._formatAjvSchemaError(ajvSchemas.dictionaryIndex, indexFileName); @@ -589,25 +590,6 @@ export class DictionaryImporter { } /** - * @param {string} url - * @returns {Promise<unknown>} - */ - async _fetchJsonAsset(url) { - const response = await fetch(url, { - method: 'GET', - mode: 'no-cors', - cache: 'default', - credentials: 'omit', - redirect: 'follow', - referrerPolicy: 'no-referrer' - }); - if (!response.ok) { - throw new Error(`Failed to fetch ${url}: ${response.status}`); - } - return await response.json(); - } - - /** * @param {import('dictionary-data').TermV1} entry * @param {string} dictionary * @returns {import('dictionary-database').DatabaseTermEntry} @@ -730,7 +712,7 @@ export class DictionaryImporter { const results = []; for (const file of files) { const content = await this._getData(file, new TextWriter()); - const entries = /** @type {unknown} */ (JSON.parse(content)); + const entries = /** @type {unknown} */ (parseJson(content)); startIndex = progressData.index; this._progress(); diff --git a/ext/js/media/audio-downloader.js b/ext/js/media/audio-downloader.js index e041cc67..a9b2133b 100644 --- a/ext/js/media/audio-downloader.js +++ b/ext/js/media/audio-downloader.js @@ -18,6 +18,7 @@ import {RequestBuilder} from '../background/request-builder.js'; import {ExtensionError} from '../core/extension-error.js'; +import {readResponseJson} from '../core/json.js'; import {JsonSchema} from '../data/json-schema.js'; import {ArrayBufferUtil} from '../data/sandbox/array-buffer-util.js'; import {NativeSimpleDOMParser} from '../dom/native-simple-dom-parser.js'; @@ -272,7 +273,8 @@ export class AudioDownloader { throw new Error(`Invalid response: ${response.status}`); } - const responseJson = await response.json(); + /** @type {import('audio-downloader').CustomAudioList} */ + const responseJson = await readResponseJson(response); if (this._customAudioListSchema === null) { const schema = await this._getCustomAudioListSchema(); @@ -425,6 +427,6 @@ export class AudioDownloader { redirect: 'follow', referrerPolicy: 'no-referrer' }); - return await response.json(); + return await readResponseJson(response); } } diff --git a/ext/js/pages/settings/backup-controller.js b/ext/js/pages/settings/backup-controller.js index 85803077..c539bdfe 100644 --- a/ext/js/pages/settings/backup-controller.js +++ b/ext/js/pages/settings/backup-controller.js @@ -18,6 +18,7 @@ import {Dexie} from '../../../lib/dexie.js'; import {isObject, log} from '../../core.js'; +import {parseJson} from '../../core/json.js'; import {OptionsUtil} from '../../data/options-util.js'; import {ArrayBufferUtil} from '../../data/sandbox/array-buffer-util.js'; import {querySelectorNotNull} from '../../dom/query-selector.js'; @@ -427,7 +428,8 @@ export class BackupController { if (this._optionsUtil === null) { throw new Error('OptionsUtil invalid'); } const dataString = ArrayBufferUtil.arrayBufferUtf8Decode(await this._readFileArrayBuffer(file)); - const data = JSON.parse(dataString); + /** @type {import('backup-controller').BackupData} */ + const data = parseJson(dataString); // Type check if (!isObject(data)) { diff --git a/ext/js/pages/settings/generic-setting-controller.js b/ext/js/pages/settings/generic-setting-controller.js index 8666614b..8268f563 100644 --- a/ext/js/pages/settings/generic-setting-controller.js +++ b/ext/js/pages/settings/generic-setting-controller.js @@ -17,6 +17,7 @@ */ import {ExtensionError} from '../../core/extension-error.js'; +import {parseJson} from '../../core/json.js'; import {DocumentUtil} from '../../dom/document-util.js'; import {DOMDataBinder} from '../../dom/dom-data-binder.js'; @@ -265,7 +266,7 @@ export class GenericSettingController { */ _getTransformDataArray(transformRaw) { if (typeof transformRaw === 'string') { - const transforms = JSON.parse(transformRaw); + const transforms = parseJson(transformRaw); return Array.isArray(transforms) ? transforms : [transforms]; } return []; diff --git a/ext/js/templates/sandbox/template-renderer-frame-api.js b/ext/js/templates/sandbox/template-renderer-frame-api.js index 94ebf7fe..388401f2 100644 --- a/ext/js/templates/sandbox/template-renderer-frame-api.js +++ b/ext/js/templates/sandbox/template-renderer-frame-api.js @@ -17,6 +17,7 @@ */ import {ExtensionError} from '../../core/extension-error.js'; +import {parseJson} from '../../core/json.js'; export class TemplateRendererFrameApi { /** @@ -108,7 +109,7 @@ export class TemplateRendererFrameApi { * @returns {T} */ _clone(value) { - return JSON.parse(JSON.stringify(value)); + return parseJson(JSON.stringify(value)); } /** diff --git a/test/anki-note-builder.test.js b/test/anki-note-builder.test.js index cc136957..bdf3f8e4 100644 --- a/test/anki-note-builder.test.js +++ b/test/anki-note-builder.test.js @@ -22,6 +22,7 @@ import {readFileSync} from 'fs'; import {fileURLToPath} from 'node:url'; import path from 'path'; import {describe, vi} from 'vitest'; +import {parseJson} from '../dev/json.js'; import {AnkiNoteBuilder} from '../ext/js/data/anki-note-builder.js'; import {JapaneseUtil} from '../ext/js/language/sandbox/japanese-util.js'; import {createTranslatorTest} from './fixtures/translator-test.js'; @@ -170,11 +171,12 @@ async function getRenderResults(dictionaryEntries, type, mode, template, expect) const testInputsFilePath = path.join(dirname, 'data/translator-test-inputs.json'); -/** @type {import('test/anki-note-builder').TranslatorTestInputs} */ -const {optionsPresets, tests} = JSON.parse(readFileSync(testInputsFilePath, {encoding: 'utf8'})); +/** @type {import('test/translator').TranslatorTestInputs} */ +const {optionsPresets, tests} = parseJson(readFileSync(testInputsFilePath, {encoding: 'utf8'})); const testResults1FilePath = path.join(dirname, 'data/anki-note-builder-test-results.json'); -const expectedResults1 = JSON.parse(readFileSync(testResults1FilePath, {encoding: 'utf8'})); +/** @type {import('test/translator').AnkiNoteBuilderTestResults} */ +const expectedResults1 = parseJson(readFileSync(testResults1FilePath, {encoding: 'utf8'})); const template = readFileSync(path.join(dirname, '../ext/data/templates/default-anki-field-templates.handlebars'), {encoding: 'utf8'}); diff --git a/test/database.test.js b/test/database.test.js index 2fdea99c..702de9f8 100644 --- a/test/database.test.js +++ b/test/database.test.js @@ -20,6 +20,7 @@ import {IDBFactory, IDBKeyRange} from 'fake-indexeddb'; import {fileURLToPath} from 'node:url'; import path from 'path'; import {beforeEach, describe, expect, test, vi} from 'vitest'; +import {parseJson} from '../dev/json.js'; import {createDictionaryArchive} from '../dev/util.js'; import {DictionaryDatabase} from '../ext/js/language/dictionary-database.js'; import {DictionaryImporter} from '../ext/js/language/dictionary-importer.js'; @@ -109,7 +110,8 @@ async function testDatabase1() { // Load dictionary data const testDictionary = createTestDictionaryArchive('valid-dictionary1'); const testDictionarySource = await testDictionary.generateAsync({type: 'arraybuffer'}); - const testDictionaryIndex = JSON.parse(await testDictionary.files['index.json'].async('string')); + /** @type {import('dictionary-data').Index} */ + const testDictionaryIndex = parseJson(await testDictionary.files['index.json'].async('string')); const title = testDictionaryIndex.title; const titles = new Map([ @@ -852,7 +854,8 @@ async function testDatabase2() { // Load dictionary data const testDictionary = createTestDictionaryArchive('valid-dictionary1'); const testDictionarySource = await testDictionary.generateAsync({type: 'arraybuffer'}); - const testDictionaryIndex = JSON.parse(await testDictionary.files['index.json'].async('string')); + /** @type {import('dictionary-data').Index} */ + const testDictionaryIndex = parseJson(await testDictionary.files['index.json'].async('string')); const title = testDictionaryIndex.title; const titles = new Map([ diff --git a/test/deinflector.test.js b/test/deinflector.test.js index adb347f1..1d7a39cf 100644 --- a/test/deinflector.test.js +++ b/test/deinflector.test.js @@ -22,6 +22,7 @@ import fs from 'fs'; import {fileURLToPath} from 'node:url'; import path from 'path'; import {describe, expect, test} from 'vitest'; +import {parseJson} from '../dev/json.js'; import {Deinflector} from '../ext/js/language/deinflector.js'; const dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -929,7 +930,8 @@ function testDeinflections() { } ]; - const deinflectionReasons = JSON.parse(fs.readFileSync(path.join(dirname, '..', 'ext', 'data/deinflect.json'), {encoding: 'utf8'})); + /** @type {import('deinflector').ReasonsRaw} */ + const deinflectionReasons = parseJson(fs.readFileSync(path.join(dirname, '..', 'ext', 'data/deinflect.json'), {encoding: 'utf8'})); const deinflector = new Deinflector(deinflectionReasons); describe('deinflections', () => { diff --git a/test/dom-text-scanner.test.js b/test/dom-text-scanner.test.js index d62e334d..f53e326d 100644 --- a/test/dom-text-scanner.test.js +++ b/test/dom-text-scanner.test.js @@ -19,6 +19,7 @@ import {fileURLToPath} from 'node:url'; import path from 'path'; import {describe, expect} from 'vitest'; +import {parseJson} from '../dev/json.js'; import {DOMTextScanner} from '../ext/js/dom/dom-text-scanner.js'; import {createDomTest} from './fixtures/dom-test.js'; @@ -109,28 +110,33 @@ describe('DOMTextScanner', () => { window.getComputedStyle = createAbsoluteGetComputedStyle(window); for (const testElement of /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('y-test'))) { - let testData = JSON.parse(/** @type {string} */ (testElement.dataset.testData)); + /** @type {import('test/dom-text-scanner').TestData|import('test/dom-text-scanner').TestData[]} */ + let testData = parseJson(/** @type {string} */ (testElement.dataset.testData)); if (!Array.isArray(testData)) { testData = [testData]; } for (const testDataItem of testData) { - let { - node, + const { + node: nodeSelector, offset, length, forcePreserveWhitespace, generateLayoutContent, reversible, expected: { - node: expectedNode, + node: expectedNodeSelector, offset: expectedOffset, content: expectedContent, remainder: expectedRemainder } } = testDataItem; - node = querySelectorTextNode(testElement, node); - expectedNode = querySelectorTextNode(testElement, expectedNode); + const node = querySelectorTextNode(testElement, nodeSelector); + const expectedNode = querySelectorTextNode(testElement, expectedNodeSelector); + + expect(node).not.toEqual(null); + expect(expectedNode).not.toEqual(null); + if (node === null || expectedNode === null) { continue; } // Standard test { diff --git a/test/fixtures/translator-test.js b/test/fixtures/translator-test.js index b17c37d9..cb1a3ef5 100644 --- a/test/fixtures/translator-test.js +++ b/test/fixtures/translator-test.js @@ -21,6 +21,7 @@ import {readFileSync} from 'fs'; import {fileURLToPath, pathToFileURL} from 'node:url'; import {dirname, join, resolve} from 'path'; import {expect, vi} from 'vitest'; +import {parseJson} from '../../dev/json.js'; import {createDictionaryArchive} from '../../dev/util.js'; import {AnkiNoteDataCreator} from '../../ext/js/data/sandbox/anki-note-data-creator.js'; import {DictionaryDatabase} from '../../ext/js/language/dictionary-database.js'; @@ -60,7 +61,7 @@ async function fetch(url) { status: 200, statusText: 'OK', text: async () => content.toString('utf8'), - json: async () => JSON.parse(content.toString('utf8')) + json: async () => parseJson(content.toString('utf8')) }; } @@ -96,7 +97,8 @@ async function createTranslatorContext(dictionaryDirectory, dictionaryName) { // Setup translator const japaneseUtil = new JapaneseUtil(null); const translator = new Translator({japaneseUtil, database: dictionaryDatabase}); - const deinflectionReasons = JSON.parse(readFileSync(deinflectionReasonsPath, {encoding: 'utf8'})); + /** @type {import('deinflector').ReasonsRaw} */ + const deinflectionReasons = parseJson(readFileSync(deinflectionReasonsPath, {encoding: 'utf8'})); translator.prepare(deinflectionReasons); // Assign properties diff --git a/test/json-schema.test.js b/test/json-schema.test.js index a93e5002..e6817d23 100644 --- a/test/json-schema.test.js +++ b/test/json-schema.test.js @@ -19,6 +19,7 @@ /* eslint-disable no-multi-spaces */ import {expect, test} from 'vitest'; +import {parseJson} from '../dev/json.js'; import {JsonSchema} from '../ext/js/data/json-schema.js'; /** @@ -54,7 +55,7 @@ function createProxy(schema, value) { * @returns {T} */ function clone(value) { - return JSON.parse(JSON.stringify(value)); + return parseJson(JSON.stringify(value)); } diff --git a/test/options-util.test.js b/test/options-util.test.js index f2ffa36c..41185756 100644 --- a/test/options-util.test.js +++ b/test/options-util.test.js @@ -22,6 +22,7 @@ import fs from 'fs'; import url, {fileURLToPath} from 'node:url'; import path from 'path'; import {expect, test, vi} from 'vitest'; +import {parseJson} from '../dev/json.js'; import {OptionsUtil} from '../ext/js/data/options-util.js'; import {TemplatePatcher} from '../ext/js/templates/template-patcher.js'; @@ -40,7 +41,7 @@ async function fetch(url2) { status: 200, statusText: 'OK', text: async () => Promise.resolve(content.toString('utf8')), - json: async () => Promise.resolve(JSON.parse(content.toString('utf8'))) + json: async () => Promise.resolve(parseJson(content.toString('utf8'))) }; } /** @type {import('dev/vm').PseudoChrome} */ diff --git a/test/translator.test.js b/test/translator.test.js index 59887d7e..42a9076e 100644 --- a/test/translator.test.js +++ b/test/translator.test.js @@ -20,6 +20,7 @@ import {readFileSync} from 'fs'; import {fileURLToPath} from 'node:url'; import path from 'path'; import {describe} from 'vitest'; +import {parseJson} from '../dev/json.js'; import {createTranslatorTest} from './fixtures/translator-test.js'; import {createTestAnkiNoteData} from './utilities/anki.js'; import {createFindOptions} from './utilities/translator.js'; @@ -27,14 +28,16 @@ import {createFindOptions} from './utilities/translator.js'; const dirname = path.dirname(fileURLToPath(import.meta.url)); const testInputsFilePath = path.join(dirname, 'data/translator-test-inputs.json'); -/** @type {import('test/anki-note-builder').TranslatorTestInputs} */ -const {optionsPresets, tests} = JSON.parse(readFileSync(testInputsFilePath, {encoding: 'utf8'})); +/** @type {import('test/translator').TranslatorTestInputs} */ +const {optionsPresets, tests} = parseJson(readFileSync(testInputsFilePath, {encoding: 'utf8'})); const testResults1FilePath = path.join(dirname, 'data/translator-test-results.json'); -const expectedResults1 = JSON.parse(readFileSync(testResults1FilePath, {encoding: 'utf8'})); +/** @type {import('test/translator').TranslatorTestResults} */ +const expectedResults1 = parseJson(readFileSync(testResults1FilePath, {encoding: 'utf8'})); const testResults2FilePath = path.join(dirname, 'data/translator-test-results-note-data1.json'); -const expectedResults2 = JSON.parse(readFileSync(testResults2FilePath, {encoding: 'utf8'})); +/** @type {import('test/translator').TranslatorTestNoteDataResults} */ +const expectedResults2 = parseJson(readFileSync(testResults2FilePath, {encoding: 'utf8'})); const dictionaryName = 'Test Dictionary 2'; const test = await createTranslatorTest(void 0, path.join(dirname, 'data/dictionaries/valid-dictionary1'), dictionaryName); diff --git a/types/ext/api.d.ts b/types/ext/api.d.ts index 6b7b4b19..2d78cc06 100644 --- a/types/ext/api.d.ts +++ b/types/ext/api.d.ts @@ -467,6 +467,6 @@ export type RequestBackendReadySignalResult = boolean; export type CreateActionPortDetails = Record<string, never>; export type CreateActionPortResult = { - name: string; + name: 'action-port'; id: string; }; diff --git a/types/ext/audio-downloader.d.ts b/types/ext/audio-downloader.d.ts index b8e812f8..dfda8cb9 100644 --- a/types/ext/audio-downloader.d.ts +++ b/types/ext/audio-downloader.d.ts @@ -42,3 +42,13 @@ export type AudioBinaryBase64 = { data: string; contentType: string | null; }; + +export type CustomAudioList = { + type: 'audioSourceList'; + audioSources: CustomAudioListSource[]; +}; + +export type CustomAudioListSource = { + url: string; + name?: string; +}; diff --git a/types/ext/cross-frame-api.d.ts b/types/ext/cross-frame-api.d.ts index 88ce59a7..e31079b7 100644 --- a/types/ext/cross-frame-api.d.ts +++ b/types/ext/cross-frame-api.d.ts @@ -52,3 +52,16 @@ export type Invocation = { ack: boolean; timer: Core.Timeout | null; }; + +export type PortDetails = CrossFrameCommunicationPortDetails | ActionPortDetails; + +export type CrossFrameCommunicationPortDetails = { + name: 'cross-frame-communication-port'; + otherTabId: number; + otherFrameId: number; +}; + +export type ActionPortDetails = { + name: 'action-port'; + id: string; +}; diff --git a/types/test/dom-text-scanner.d.ts b/types/test/dom-text-scanner.d.ts new file mode 100644 index 00000000..362a7eb5 --- /dev/null +++ b/types/test/dom-text-scanner.d.ts @@ -0,0 +1,32 @@ +/* + * 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/>. + */ + +export type TestData = { + node: string; + offset: number; + length: number; + forcePreserveWhitespace?: boolean; + generateLayoutContent?: boolean; + reversible?: boolean; + expected: { + node: string; + offset: number; + content: string; + remainder?: number; + }; +}; + diff --git a/types/test/anki-note-builder.d.ts b/types/test/translator.d.ts index 0ccb25e9..3e4c8b9d 100644 --- a/types/test/anki-note-builder.d.ts +++ b/types/test/translator.d.ts @@ -17,6 +17,9 @@ import type {OptionsPresetObject} from 'dev/vm'; import type {FindTermsMode} from 'translator'; +import type {DictionaryEntry} from 'dictionary'; +import type {NoteData} from 'anki-templates'; +import type {NoteFields} from 'anki'; export type TranslatorTestInputs = { optionsPresets: OptionsPresetObject; @@ -39,3 +42,25 @@ export type TestInputFindTerm = { text: string; options: string; }; + +export type TranslatorTestResults = TranslatorTestResult[]; + +export type TranslatorTestResult = { + name: string; + originalTextLength?: number; + dictionaryEntries: DictionaryEntry[]; +}; + +export type TranslatorTestNoteDataResults = TranslatorTestNoteDataResult[]; + +export type TranslatorTestNoteDataResult = { + name: string; + noteDataList: NoteData[]; +}; + +export type AnkiNoteBuilderTestResults = AnkiNoteBuilderTestResult[]; + +export type AnkiNoteBuilderTestResult = { + name: string; + results: NoteFields[] | null; +}; |