diff --git a/src/core/services/tokenizer/yomitan-parser-runtime.test.ts b/src/core/services/tokenizer/yomitan-parser-runtime.test.ts index 352ecc0..b332313 100644 --- a/src/core/services/tokenizer/yomitan-parser-runtime.test.ts +++ b/src/core/services/tokenizer/yomitan-parser-runtime.test.ts @@ -656,7 +656,7 @@ test('getYomitanDictionaryInfo requests dictionary info via backend action', asy assert.match(scriptValue, /getDictionaryInfo/); }); -test('dictionary settings helpers upsert and remove dictionary entries', async () => { +test('dictionary settings helpers upsert and remove dictionary entries without reordering', async () => { const scripts: string[] = []; const optionsFull = { profileCurrent: 0, diff --git a/src/core/services/tokenizer/yomitan-parser-runtime.ts b/src/core/services/tokenizer/yomitan-parser-runtime.ts index 6bf280c..dad4930 100644 --- a/src/core/services/tokenizer/yomitan-parser-runtime.ts +++ b/src/core/services/tokenizer/yomitan-parser-runtime.ts @@ -1676,7 +1676,7 @@ export async function upsertYomitanDictionarySettings( continue; } - dictionaries.unshift(createDefaultDictionarySettings(normalizedTitle, true)); + dictionaries.push(createDefaultDictionarySettings(normalizedTitle, true)); changed = true; } diff --git a/src/yomitan-translator-sort.test.ts b/src/yomitan-translator-sort.test.ts new file mode 100644 index 0000000..38cae63 --- /dev/null +++ b/src/yomitan-translator-sort.test.ts @@ -0,0 +1,93 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +// @ts-expect-error Vendored Yomitan translator has no local TypeScript declarations. +import { Translator } from '../vendor/yomitan/js/language/translator.js'; + +type SortableTermEntry = { + matchPrimaryReading: boolean; + maxOriginalTextLength: number; + textProcessorRuleChainCandidates: unknown[]; + inflectionRuleChainCandidates: unknown[]; + sourceTermExactMatchCount: number; + frequencyOrder: number; + dictionaryIndex: number; + score: number; + dictionaryAlias: string; + headwords: Array<{ term: string }>; + definitions: Array<{ dictionary: string }>; +}; + +type SortableDefinition = { + dictionary: string; + dictionaryAlias: string; + frequencyOrder: number; + dictionaryIndex: number; + score: number; + headwordIndices: number[]; + index: number; +}; + +test('Translator prioritizes SubMiner term entries without changing dictionary index order', () => { + const translator = new Translator({}); + const entries: SortableTermEntry[] = [ + { + matchPrimaryReading: true, + maxOriginalTextLength: 4, + textProcessorRuleChainCandidates: [], + inflectionRuleChainCandidates: [], + sourceTermExactMatchCount: 1, + frequencyOrder: 0, + dictionaryIndex: 0, + score: 10, + dictionaryAlias: 'JMdict', + headwords: [{ term: 'アイリス' }], + definitions: [{ dictionary: 'JMdict' }], + }, + { + matchPrimaryReading: true, + maxOriginalTextLength: 4, + textProcessorRuleChainCandidates: [], + inflectionRuleChainCandidates: [], + sourceTermExactMatchCount: 1, + frequencyOrder: 99, + dictionaryIndex: 99, + score: 1, + dictionaryAlias: 'SubMiner Character Dictionary', + headwords: [{ term: 'アイリス' }], + definitions: [{ dictionary: 'SubMiner Character Dictionary' }], + }, + ]; + + translator._sortTermDictionaryEntries(entries as unknown[]); + + assert.equal(entries[0]?.dictionaryAlias, 'SubMiner Character Dictionary'); +}); + +test('Translator prioritizes SubMiner definitions without changing dictionary index order', () => { + const translator = new Translator({}); + const definitions: SortableDefinition[] = [ + { + dictionary: 'JMdict', + dictionaryAlias: 'JMdict', + frequencyOrder: 0, + dictionaryIndex: 0, + score: 10, + headwordIndices: [0], + index: 0, + }, + { + dictionary: 'SubMiner Character Dictionary', + dictionaryAlias: 'SubMiner Character Dictionary', + frequencyOrder: 99, + dictionaryIndex: 99, + score: 1, + headwordIndices: [0], + index: 1, + }, + ]; + + translator._sortTermDictionaryEntryDefinitions(definitions as unknown[]); + + assert.equal(definitions[0]?.dictionaryAlias, 'SubMiner Character Dictionary'); +}); diff --git a/vendor/yomitan/js/language/translator.js b/vendor/yomitan/js/language/translator.js index 90051f1..a0f620b 100644 --- a/vendor/yomitan/js/language/translator.js +++ b/vendor/yomitan/js/language/translator.js @@ -25,6 +25,8 @@ import {getAllLanguageReadingNormalizers, getAllLanguageTextProcessors} from './ import {MultiLanguageTransformer} from './multi-language-transformer.js'; import {isCodePointChinese} from './zh/chinese.js'; +const SUBMINER_DICTIONARY_TITLE_PREFIX = 'SubMiner Character Dictionary'; + /** * Class which finds term and kanji dictionary entries for text. */ @@ -1531,6 +1533,23 @@ export class Translator { return Array.isArray(value) ? value : (typeof value === 'number' ? [value] : []); } + /** + * @param {string|undefined} value + * @returns {boolean} + */ + _isSubMinerDictionary(value) { + return typeof value === 'string' && value.startsWith(SUBMINER_DICTIONARY_TITLE_PREFIX); + } + + /** + * @param {string|undefined} dictionary + * @param {string|undefined} dictionaryAlias + * @returns {number} + */ + _getSubMinerDictionarySortBoost(dictionary, dictionaryAlias) { + return (this._isSubMinerDictionary(dictionary) || this._isSubMinerDictionary(dictionaryAlias)) ? 1 : 0; + } + // Kanji data /** @@ -2162,6 +2181,10 @@ export class Translator { i = v2.sourceTermExactMatchCount - v1.sourceTermExactMatchCount; if (i !== 0) { return i; } + // Prefer SubMiner character dictionary entries without changing user dictionary order. + i = this._getSubMinerDictionarySortBoost(v2.definitions[0]?.dictionary, v2.dictionaryAlias) - this._getSubMinerDictionarySortBoost(v1.definitions[0]?.dictionary, v1.dictionaryAlias); + if (i !== 0) { return i; } + // Sort by frequency order i = v1.frequencyOrder - v2.frequencyOrder; if (i !== 0) { return i; } @@ -2205,8 +2228,12 @@ export class Translator { * @returns {number} */ const compareFunction = (v1, v2) => { + // Prefer SubMiner character dictionary definitions without changing user dictionary order. + let i = this._getSubMinerDictionarySortBoost(v2.dictionary, v2.dictionaryAlias) - this._getSubMinerDictionarySortBoost(v1.dictionary, v1.dictionaryAlias); + if (i !== 0) { return i; } + // Sort by frequency order - let i = v1.frequencyOrder - v2.frequencyOrder; + i = v1.frequencyOrder - v2.frequencyOrder; if (i !== 0) { return i; } // Sort by dictionary order