Pop-up dictionary browser extension for language learning. Successor to Yomichan. (PERSONAL FORK)
1/*
2 * Copyright (C) 2023-2025 Yomitan Authors
3 * Copyright (C) 2021-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 childProcess from 'child_process';
20import fs from 'fs';
21import {fileURLToPath} from 'node:url';
22import path from 'path';
23import {parseJson} from './json.js';
24
25const dirname = path.dirname(fileURLToPath(import.meta.url));
26
27
28export class ManifestUtil {
29 constructor() {
30 const fileName = path.join(dirname, 'data', 'manifest-variants.json');
31 const {manifest, variants, defaultVariant} = /** @type {import('dev/manifest').ManifestConfig} */ (parseJson(fs.readFileSync(fileName, {encoding: 'utf8'})));
32 /** @type {import('dev/manifest').Manifest} */
33 this._manifest = manifest;
34 /** @type {import('dev/manifest').ManifestVariant[]} */
35 this._variants = variants;
36 /** @type {string} */
37 this._defaultVariant = defaultVariant;
38 /** @type {Map<string, import('dev/manifest').ManifestVariant>} */
39 this._variantMap = new Map();
40 for (const variant of variants) {
41 this._variantMap.set(variant.name, variant);
42 }
43 }
44
45 /**
46 * @param {?string} [variantName]
47 * @returns {import('dev/manifest').Manifest}
48 */
49 getManifest(variantName) {
50 if (typeof variantName === 'string') {
51 const variant = this._variantMap.get(variantName);
52 if (typeof variant !== 'undefined') {
53 return this._createVariantManifest(this._manifest, variant);
54 }
55 }
56
57 if (typeof this._defaultVariant === 'string') {
58 const variant = this._variantMap.get(this._defaultVariant);
59 if (typeof variant !== 'undefined') {
60 return this._createVariantManifest(this._manifest, variant);
61 }
62 }
63
64 return structuredClone(this._manifest);
65 }
66
67 /**
68 * @returns {import('dev/manifest').ManifestVariant[]}
69 */
70 getVariants() {
71 return [...this._variants];
72 }
73
74 /**
75 * @param {string} name
76 * @returns {import('dev/manifest').ManifestVariant|undefined}
77 */
78 getVariant(name) {
79 return this._variantMap.get(name);
80 }
81
82 /**
83 * @param {import('dev/manifest').Manifest} manifest
84 * @returns {string}
85 */
86 static createManifestString(manifest) {
87 return JSON.stringify(manifest, null, 4) + '\n';
88 }
89
90 // Private
91
92 /**
93 * @param {import('dev/manifest').Command} data
94 * @returns {string}
95 * @throws {Error}
96 */
97 _evaluateModificationCommand(data) {
98 const {command, args, trim} = data;
99 const {stdout, stderr, status} = childProcess.spawnSync(command, args, {
100 cwd: dirname,
101 stdio: 'pipe',
102 shell: false,
103 });
104 if (status !== 0) {
105 const message = stderr.toString('utf8').trim();
106 throw new Error(`Failed to execute ${command} ${args.join(' ')}\nstatus=${status}\n${message}`);
107 }
108 let result = stdout.toString('utf8');
109 if (trim) { result = result.trim(); }
110 return result;
111 }
112
113 /**
114 * @param {import('dev/manifest').Manifest} manifest
115 * @param {import('dev/manifest').Modification[]|undefined} modifications
116 * @returns {import('dev/manifest').Manifest}
117 */
118 _applyModifications(manifest, modifications) {
119 if (Array.isArray(modifications)) {
120 for (const modification of modifications) {
121 // Rename to path2 to avoid clashing with imported `node:path` module.
122 const {action, path: path2} = modification;
123 switch (action) {
124 case 'set':
125 {
126 let {value, before, after, command} = modification;
127 /** @type {import('core').UnknownObject} */
128 const object = this._getObjectProperties(manifest, path2, path2.length - 1);
129 const key = path2[path2.length - 1];
130
131 let {index} = modification;
132 if (typeof index !== 'number') {
133 index = -1;
134 }
135 if (typeof before === 'string') {
136 index = this._getObjectKeyIndex(object, before);
137 }
138 if (typeof after === 'string') {
139 index = this._getObjectKeyIndex(object, after);
140 if (index >= 0) { ++index; }
141 }
142 if (typeof command === 'object' && command !== null) {
143 value = this._evaluateModificationCommand(command);
144 }
145
146 this._setObjectKeyAtIndex(object, key, value, index);
147 }
148 break;
149 case 'replace':
150 {
151 const {pattern, patternFlags, replacement} = modification;
152 /** @type {import('core').UnknownObject} */
153 const value = this._getObjectProperties(manifest, path2, path2.length - 1);
154 const regex = new RegExp(pattern, patternFlags);
155 const last = path2[path2.length - 1];
156 let value2 = value[last];
157 value2 = `${value2}`.replace(regex, replacement);
158 value[last] = value2;
159 }
160 break;
161 case 'delete':
162 {
163 /** @type {import('core').UnknownObject} */
164 const value = this._getObjectProperties(manifest, path2, path2.length - 1);
165 const last = path2[path2.length - 1];
166 delete value[last];
167 }
168 break;
169 case 'remove':
170 {
171 const {item} = modification;
172 /** @type {unknown[]} */
173 const value = this._getObjectProperties(manifest, path2, path2.length);
174 const index = value.indexOf(item);
175 if (index >= 0) { value.splice(index, 1); }
176 }
177 break;
178 case 'splice':
179 {
180 const {start, deleteCount, items} = modification;
181 /** @type {unknown[]} */
182 const value = this._getObjectProperties(manifest, path2, path2.length);
183 const itemsNew = items.map((v) => structuredClone(v));
184 value.splice(start, deleteCount, ...itemsNew);
185 }
186 break;
187 case 'copy':
188 case 'move':
189 {
190 const {newPath, before, after} = modification;
191 const oldKey = path2[path2.length - 1];
192 const newKey = newPath[newPath.length - 1];
193 /** @type {import('core').UnknownObject} */
194 const oldObject = this._getObjectProperties(manifest, path2, path2.length - 1);
195 /** @type {import('core').UnknownObject} */
196 const newObject = this._getObjectProperties(manifest, newPath, newPath.length - 1);
197 const oldObjectIsNewObject = this._arraysAreSame(path2, newPath, -1);
198 const value = oldObject[oldKey];
199
200 let {index} = modification;
201 if (typeof index !== 'number' || index < 0) {
202 index = (oldObjectIsNewObject && action !== 'copy') ? this._getObjectKeyIndex(oldObject, oldKey) : -1;
203 }
204 if (typeof before === 'string') {
205 index = this._getObjectKeyIndex(newObject, before);
206 }
207 if (typeof after === 'string') {
208 index = this._getObjectKeyIndex(newObject, after);
209 if (index >= 0) { ++index; }
210 }
211
212 this._setObjectKeyAtIndex(newObject, newKey, value, index);
213 if (action !== 'copy' && (!oldObjectIsNewObject || oldKey !== newKey)) {
214 delete oldObject[oldKey];
215 }
216 }
217 break;
218 case 'add':
219 {
220 const {items} = modification;
221 /** @type {unknown[]} */
222 const value = this._getObjectProperties(manifest, path2, path2.length);
223 const itemsNew = items.map((v) => structuredClone(v));
224 value.push(...itemsNew);
225 }
226 break;
227 }
228 }
229 }
230
231 return manifest;
232 }
233
234 /**
235 * @template [T=unknown]
236 * @param {T[]} array1
237 * @param {T[]} array2
238 * @param {number} lengthOffset
239 * @returns {boolean}
240 */
241 _arraysAreSame(array1, array2, lengthOffset) {
242 let ii = array1.length;
243 if (ii !== array2.length) { return false; }
244 ii += lengthOffset;
245 for (let i = 0; i < ii; ++i) {
246 if (array1[i] !== array2[i]) { return false; }
247 }
248 return true;
249 }
250
251 /**
252 * @param {import('core').UnknownObject} object
253 * @param {string|number} key
254 * @returns {number}
255 */
256 _getObjectKeyIndex(object, key) {
257 return Object.keys(object).indexOf(typeof key === 'string' ? key : `${key}`);
258 }
259
260 /**
261 * @param {import('core').UnknownObject} object
262 * @param {string|number} key
263 * @param {unknown} value
264 * @param {number} index
265 */
266 _setObjectKeyAtIndex(object, key, value, index) {
267 if (index < 0 || typeof key === 'number' || Object.prototype.hasOwnProperty.call(object, key)) {
268 object[key] = value;
269 return;
270 }
271
272 const entries = Object.entries(object);
273 index = Math.min(index, entries.length);
274 for (let i = index, ii = entries.length; i < ii; ++i) {
275 const [key2] = entries[i];
276 delete object[key2];
277 }
278 entries.splice(index, 0, [key, value]);
279 for (let i = index, ii = entries.length; i < ii; ++i) {
280 const [key2, value2] = entries[i];
281 object[key2] = value2;
282 }
283 }
284
285 /**
286 * @template [TReturn=unknown]
287 * @param {unknown} object
288 * @param {import('dev/manifest').PropertyPath} path2
289 * @param {number} count
290 * @returns {TReturn}
291 */
292 _getObjectProperties(object, path2, count) {
293 for (let i = 0; i < count; ++i) {
294 object = /** @type {import('core').UnknownObject} */ (object)[path2[i]];
295 }
296 return /** @type {TReturn} */ (object);
297 }
298
299 /**
300 * @param {import('dev/manifest').ManifestVariant} variant
301 * @returns {import('dev/manifest').ManifestVariant[]}
302 */
303 _getInheritanceChain(variant) {
304 const visited = new Set();
305 const inheritance = [];
306 while (true) {
307 const {name, inherit} = variant;
308 if (visited.has(name)) { break; }
309
310 visited.add(name);
311 inheritance.unshift(variant);
312
313 if (typeof inherit !== 'string') { break; }
314
315 const nextVariant = this._variantMap.get(inherit);
316 if (typeof nextVariant === 'undefined') { break; }
317
318 variant = nextVariant;
319 }
320 return inheritance;
321 }
322
323 /**
324 * @param {import('dev/manifest').Manifest} manifest
325 * @param {import('dev/manifest').ManifestVariant} variant
326 * @returns {import('dev/manifest').Manifest}
327 */
328 _createVariantManifest(manifest, variant) {
329 let modifiedManifest = structuredClone(manifest);
330 for (const {modifications} of this._getInheritanceChain(variant)) {
331 modifiedManifest = this._applyModifications(modifiedManifest, modifications);
332 }
333 return modifiedManifest;
334 }
335}