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; +}; |