Pop-up dictionary browser extension for language learning. Successor to Yomichan. (PERSONAL FORK)
at lambda-fork/main 172 lines 6.5 kB view raw
1/* 2 * Copyright (C) 2024-2025 Yomitan Authors 3 * 4 * This program is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 3 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <https://www.gnu.org/licenses/>. 16 */ 17 18import {describe, test} from 'vitest'; 19import {LanguageTransformer} from '../ext/js/language/language-transformer.js'; 20import {getAllLanguageTransformDescriptors} from '../ext/js/language/languages.js'; 21 22class DeinflectionNode { 23 /** 24 * @param {string} text 25 * @param {string[]} ruleNames 26 * @param {?RuleNode} ruleNode 27 * @param {?DeinflectionNode} previous 28 */ 29 constructor(text, ruleNames, ruleNode, previous) { 30 /** @type {string} */ 31 this.text = text; 32 /** @type {string[]} */ 33 this.ruleNames = ruleNames; 34 /** @type {?RuleNode} */ 35 this.ruleNode = ruleNode; 36 /** @type {?DeinflectionNode} */ 37 this.previous = previous; 38 } 39 40 /** 41 * @param {DeinflectionNode} other 42 * @returns {boolean} 43 */ 44 historyIncludes(other) { 45 /** @type {?DeinflectionNode} */ 46 // eslint-disable-next-line @typescript-eslint/no-this-alias 47 let node = this; 48 for (; node !== null; node = node.previous) { 49 if ( 50 node.ruleNode === other.ruleNode && 51 node.text === other.text && 52 arraysAreEqual(node.ruleNames, other.ruleNames) 53 ) { 54 return true; 55 } 56 } 57 return false; 58 } 59 60 /** 61 * @returns {DeinflectionNode[]} 62 */ 63 getHistory() { 64 /** @type {DeinflectionNode[]} */ 65 const results = []; 66 /** @type {?DeinflectionNode} */ 67 // eslint-disable-next-line @typescript-eslint/no-this-alias 68 let node = this; 69 for (; node !== null; node = node.previous) { 70 results.unshift(node); 71 } 72 return results; 73 } 74} 75 76class RuleNode { 77 /** 78 * @param {string} groupName 79 * @param {import('language-transformer').SuffixRule} rule 80 */ 81 constructor(groupName, rule) { 82 /** @type {string} */ 83 this.groupName = groupName; 84 /** @type {import('language-transformer').SuffixRule} */ 85 this.rule = rule; 86 } 87} 88 89/** 90 * @template [T=unknown] 91 * @param {T[]} rules1 92 * @param {T[]} rules2 93 * @returns {boolean} 94 */ 95function arraysAreEqual(rules1, rules2) { 96 if (rules1.length !== rules2.length) { return false; } 97 for (const rule1 of rules1) { 98 if (!rules2.includes(rule1)) { return false; } 99 } 100 return true; 101} 102 103const languagesWithTransforms = getAllLanguageTransformDescriptors(); 104 105describe.each(languagesWithTransforms)('Cycles Test $iso', ({languageTransforms}) => { 106 test('Check for cycles', ({expect}) => { 107 const languageTransformer = new LanguageTransformer(); 108 languageTransformer.addDescriptor(languageTransforms); 109 110 /** @type {RuleNode[]} */ 111 const ruleNodes = []; 112 for (const [groupName, reasonInfo] of Object.entries(languageTransforms.transforms)) { 113 for (const rule of reasonInfo.rules) { 114 if (rule.type === 'suffix') { 115 ruleNodes.push(new RuleNode(groupName, /** @type {import('language-transformer').SuffixRule}*/ (rule))); 116 } 117 } 118 } 119 120 /** @type {DeinflectionNode[]} */ 121 const deinflectionNodes = []; 122 for (const {rule: {isInflected}} of ruleNodes) { 123 const suffixIn = isInflected.source.substring(0, isInflected.source.length - 1); 124 deinflectionNodes.push(new DeinflectionNode(`?${suffixIn}`, [], null, null)); 125 } 126 127 for (let i = 0; i < deinflectionNodes.length; ++i) { 128 const deinflectionNode = deinflectionNodes[i]; 129 const {text, ruleNames} = deinflectionNode; 130 for (const ruleNode of ruleNodes) { 131 const {isInflected, deinflected: suffixOut, conditionsIn, conditionsOut} = ruleNode.rule; 132 const suffixIn = isInflected.source.substring(0, isInflected.source.length - 1); 133 if ( 134 !LanguageTransformer.conditionsMatch( 135 languageTransformer.getConditionFlagsFromConditionTypes(ruleNames), 136 languageTransformer.getConditionFlagsFromConditionTypes(conditionsIn), 137 ) || 138 !text.endsWith(suffixIn) || 139 (text.length - suffixIn.length + suffixOut.length) <= 0 140 ) { 141 continue; 142 } 143 144 const newDeinflectionNode = new DeinflectionNode( 145 text.substring(0, text.length - suffixIn.length) + suffixOut, 146 conditionsOut, 147 ruleNode, 148 deinflectionNode, 149 ); 150 151 // Cycle check 152 if (deinflectionNode.historyIncludes(newDeinflectionNode)) { 153 const stack = []; 154 for (const {text: itemText, ruleNode: itemNode} of newDeinflectionNode.getHistory()) { 155 if (itemNode !== null) { 156 const itemSuffixIn = itemNode.rule.isInflected.source.substring(0, itemNode.rule.isInflected.source.length - 1); 157 const itemSuffixOut = itemNode.rule.deinflected; 158 stack.push(`${itemText} (${itemNode.groupName}, ${itemNode.rule.conditionsIn.join(',')}=>${itemNode.rule.conditionsOut.join(',')}, ${itemSuffixIn}=>${itemSuffixOut})`); 159 } else { 160 stack.push(`${itemText} (start)`); 161 } 162 } 163 const message = `Cycle detected:\n ${stack.join('\n ')}`; 164 expect.soft(true, message).toEqual(false); 165 continue; 166 } 167 168 deinflectionNodes.push(newDeinflectionNode); 169 } 170 } 171 }, {timeout: 30 * 1000}); 172});