Pop-up dictionary browser extension for language learning. Successor to Yomichan. (PERSONAL FORK)
1/*
2 * Copyright (C) 2023-2025 Yomitan Authors
3 * Copyright (C) 2020-2022 Yomichan Authors
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
19import fs from 'fs';
20import path from 'path';
21import {performance} from 'perf_hooks';
22import {fileURLToPath} from 'url';
23import {getDictionaryArchiveEntries, getDictionaryArchiveJson, getIndexFileName, readArchiveEntryDataJson} from './dictionary-archive-util.js';
24import {parseJson} from './json.js';
25import {createJsonSchema} from './schema-validate.js';
26import {toError} from './to-error.js';
27
28const dirname = path.dirname(fileURLToPath(import.meta.url));
29
30/**
31 * @param {string} relativeFileName
32 * @returns {import('dev/dictionary-validate').Schema}
33 */
34function readSchema(relativeFileName) {
35 const fileName = path.join(dirname, relativeFileName);
36 const source = fs.readFileSync(fileName, {encoding: 'utf8'});
37 return parseJson(source);
38}
39
40/**
41 * @param {import('dev/schema-validate').ValidateMode} mode
42 * @param {import('@zip.js/zip.js').Entry[]} entries
43 * @param {import('dev/dictionary-validate').SchemasDetails} schemasDetails
44 */
45async function validateDictionaryBanks(mode, entries, schemasDetails) {
46 for (const entry of entries) {
47 const {filename} = entry;
48 for (const [fileNameFormat, schema] of schemasDetails) {
49 if (!fileNameFormat.test(filename)) { continue; }
50
51 let jsonSchema;
52 try {
53 jsonSchema = createJsonSchema(mode, schema);
54 } catch (e) {
55 const e2 = toError(e);
56 e2.message += `\n(in file ${filename})`;
57 throw e2;
58 }
59
60 const data = await readArchiveEntryDataJson(entry);
61
62 try {
63 jsonSchema.validate(data);
64 } catch (e) {
65 const e2 = toError(e);
66 e2.message += `\n(in file ${filename})`;
67 throw e2;
68 }
69 break;
70 }
71 }
72}
73
74/**
75 * Validates a dictionary from its zip archive.
76 * @param {import('dev/schema-validate').ValidateMode} mode
77 * @param {ArrayBuffer} archiveData
78 * @param {import('dev/dictionary-validate').Schemas} schemas
79 */
80export async function validateDictionary(mode, archiveData, schemas) {
81 const entries = await getDictionaryArchiveEntries(archiveData);
82 const indexFileName = getIndexFileName();
83 /** @type {import('dictionary-data').Index} */
84 const index = await getDictionaryArchiveJson(entries, indexFileName);
85 const version = index.format || index.version;
86
87 try {
88 const jsonSchema = createJsonSchema(mode, schemas.index);
89 jsonSchema.validate(index);
90 } catch (e) {
91 const e2 = toError(e);
92 e2.message += `\n(in file ${indexFileName})`;
93 throw e2;
94 }
95
96 /** @type {import('dev/dictionary-validate').SchemasDetails} */
97 const schemasDetails = [
98 [/^term_bank_(\d+)\.json$/, version === 1 ? schemas.termBankV1 : schemas.termBankV3],
99 [/^term_meta_bank_(\d+)\.json$/, schemas.termMetaBankV3],
100 [/^kanji_bank_(\d+)\.json$/, version === 1 ? schemas.kanjiBankV1 : schemas.kanjiBankV3],
101 [/^kanji_meta_bank_(\d+)\.json$/, schemas.kanjiMetaBankV3],
102 [/^tag_bank_(\d+)\.json$/, schemas.tagBankV3],
103 ];
104
105 await validateDictionaryBanks(mode, entries, schemasDetails);
106}
107
108/**
109 * Returns a Schemas object from ext/data/schemas/*.
110 * @returns {import('dev/dictionary-validate').Schemas}
111 */
112export function getSchemas() {
113 return {
114 index: readSchema('../ext/data/schemas/dictionary-index-schema.json'),
115 kanjiBankV1: readSchema('../ext/data/schemas/dictionary-kanji-bank-v1-schema.json'),
116 kanjiBankV3: readSchema('../ext/data/schemas/dictionary-kanji-bank-v3-schema.json'),
117 kanjiMetaBankV3: readSchema('../ext/data/schemas/dictionary-kanji-meta-bank-v3-schema.json'),
118 tagBankV3: readSchema('../ext/data/schemas/dictionary-tag-bank-v3-schema.json'),
119 termBankV1: readSchema('../ext/data/schemas/dictionary-term-bank-v1-schema.json'),
120 termBankV3: readSchema('../ext/data/schemas/dictionary-term-bank-v3-schema.json'),
121 termMetaBankV3: readSchema('../ext/data/schemas/dictionary-term-meta-bank-v3-schema.json'),
122 };
123}
124
125/**
126 * Validates dictionary files and logs the results to the console.
127 * @param {import('dev/schema-validate').ValidateMode} mode
128 * @param {string[]} dictionaryFileNames
129 */
130export async function testDictionaryFiles(mode, dictionaryFileNames) {
131 const schemas = getSchemas();
132
133 for (const dictionaryFileName of dictionaryFileNames) {
134 // eslint-disable-next-line no-restricted-syntax
135 const start = performance.now();
136 try {
137 console.log(`Validating ${dictionaryFileName}...`);
138 const source = fs.readFileSync(dictionaryFileName);
139 await validateDictionary(mode, source.buffer, schemas);
140 // eslint-disable-next-line no-restricted-syntax
141 const end = performance.now();
142 console.log(`No issues detected (${((end - start) / 1000).toFixed(2)}s)`);
143 } catch (e) {
144 // eslint-disable-next-line no-restricted-syntax
145 const end = performance.now();
146 console.log(`Encountered an error (${((end - start) / 1000).toFixed(2)}s)`);
147 console.warn(e);
148 }
149 }
150}