Pop-up dictionary browser extension for language learning. Successor to Yomichan. (PERSONAL FORK)
at lambda-fork/main 335 lines 13 kB view raw
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}