Skip to content

Commit

Permalink
Improve translator test inputs typing (#601)
Browse files Browse the repository at this point in the history
* Add type identifier

* Improve type correctness of translator test utilities
  • Loading branch information
toasted-nutbread authored Feb 3, 2024
1 parent 5a2bc4e commit c4f248b
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 48 deletions.
13 changes: 13 additions & 0 deletions test/data/translator-test-inputs.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"optionsPresets": {
"kanji": {
"type": "kanji",
"enabledDictionaryMap": [
[
"${title}",
Expand All @@ -13,6 +14,7 @@
"removeNonJapaneseCharacters": false
},
"default": {
"type": "terms",
"matchType": "exact",
"deinflect": true,
"mainDictionary": "${title}",
Expand Down Expand Up @@ -188,6 +190,7 @@
"options": [
"default",
{
"type": "terms",
"removeNonJapaneseCharacters": false,
"textReplacements": [
null,
Expand All @@ -210,6 +213,7 @@
"options": [
"default",
{
"type": "terms",
"removeNonJapaneseCharacters": false,
"textReplacements": [
null,
Expand All @@ -232,6 +236,7 @@
"options": [
"default",
{
"type": "terms",
"removeNonJapaneseCharacters": false,
"textReplacements": [
null,
Expand All @@ -254,6 +259,7 @@
"options": [
"default",
{
"type": "terms",
"removeNonJapaneseCharacters": false,
"textReplacements": [
null,
Expand All @@ -276,6 +282,7 @@
"options": [
"default",
{
"type": "terms",
"removeNonJapaneseCharacters": false,
"textReplacements": [
null,
Expand Down Expand Up @@ -361,6 +368,7 @@
"options": [
"default",
{
"type": "terms",
"convertNumericCharacters": "true",
"removeNonJapaneseCharacters": false
}
Expand All @@ -374,6 +382,7 @@
"options": [
"default",
{
"type": "terms",
"convertAlphabeticCharacters": "true",
"removeNonJapaneseCharacters": false
}
Expand All @@ -387,6 +396,7 @@
"options": [
"default",
{
"type": "terms",
"convertKatakanaToHiragana": "true"
}
]
Expand All @@ -399,6 +409,7 @@
"options": [
"default",
{
"type": "terms",
"convertHiraganaToKatakana": "true"
}
]
Expand All @@ -411,6 +422,7 @@
"options": [
"default",
{
"type": "terms",
"convertHalfWidthCharacters": "true",
"convertKatakanaToHiragana": "true"
}
Expand All @@ -424,6 +436,7 @@
"options": [
"default",
{
"type": "terms",
"collapseEmphaticSequences": "full"
}
]
Expand Down
8 changes: 3 additions & 5 deletions test/dictionary-data.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {describe} from 'vitest';
import {parseJson} from '../dev/json.js';
import {createTranslatorTest} from './fixtures/translator-test.js';
import {createTestAnkiNoteData, getTemplateRenderResults} from './utilities/anki.js';
import {createFindOptions} from './utilities/translator.js';
import {createFindKanjiOptions, createFindTermsOptions} from './utilities/translator.js';

const dirname = path.dirname(fileURLToPath(import.meta.url));
const dictionaryName = 'Test Dictionary 2';
Expand Down Expand Up @@ -61,8 +61,7 @@ describe('Dictionary data', () => {
case 'findTerms':
{
const {mode, text} = data;
/** @type {import('translation').FindTermsOptions} */
const options = createFindOptions(dictionaryName, optionsPresets, data.options);
const options = createFindTermsOptions(dictionaryName, optionsPresets, data.options);
const {dictionaryEntries, originalTextLength} = await translator.findTerms(mode, text, options);
const renderResults = mode !== 'simple' ? await getTemplateRenderResults(dictionaryEntries, 'terms', mode, template, expect) : null;
const noteDataList = mode !== 'simple' ? dictionaryEntries.map((dictionaryEntry) => createTestAnkiNoteData(dictionaryEntry, mode)) : null;
Expand All @@ -75,8 +74,7 @@ describe('Dictionary data', () => {
case 'findKanji':
{
const {text} = data;
/** @type {import('translation').FindKanjiOptions} */
const options = createFindOptions(dictionaryName, optionsPresets, data.options);
const options = createFindKanjiOptions(dictionaryName, optionsPresets, data.options);
const dictionaryEntries = await translator.findKanji(text, options);
const renderResults = await getTemplateRenderResults(dictionaryEntries, 'kanji', 'split', template, expect);
const noteDataList = dictionaryEntries.map((dictionaryEntry) => createTestAnkiNoteData(dictionaryEntry, 'split'));
Expand Down
8 changes: 3 additions & 5 deletions test/dictionary-data.write.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import path from 'path';
import {parseJson} from '../dev/json.js';
import {createTranslatorTest} from './fixtures/translator-test.js';
import {createTestAnkiNoteData, getTemplateRenderResults} from './utilities/anki.js';
import {createFindOptions} from './utilities/translator.js';
import {createFindKanjiOptions, createFindTermsOptions} from './utilities/translator.js';

/**
* @param {string} fileName
Expand Down Expand Up @@ -62,8 +62,7 @@ test('Write dictionary data expected data', async ({window, translator, expect})
case 'findTerms':
{
const {mode, text} = data;
/** @type {import('translation').FindTermsOptions} */
const options = createFindOptions(dictionaryName, optionsPresets, data.options);
const options = createFindTermsOptions(dictionaryName, optionsPresets, data.options);
const {dictionaryEntries, originalTextLength} = await translator.findTerms(mode, text, options);
const renderResults = mode !== 'simple' ? await getTemplateRenderResults(dictionaryEntries, 'terms', mode, template, null) : null;
const noteDataList = mode !== 'simple' ? dictionaryEntries.map((dictionaryEntry) => createTestAnkiNoteData(dictionaryEntry, mode)) : null;
Expand All @@ -75,8 +74,7 @@ test('Write dictionary data expected data', async ({window, translator, expect})
case 'findKanji':
{
const {text} = data;
/** @type {import('translation').FindKanjiOptions} */
const options = createFindOptions(dictionaryName, optionsPresets, data.options);
const options = createFindKanjiOptions(dictionaryName, optionsPresets, data.options);
const dictionaryEntries = await translator.findKanji(text, options);
const renderResults = await getTemplateRenderResults(dictionaryEntries, 'kanji', 'split', template, null);
const noteDataList = dictionaryEntries.map((dictionaryEntry) => createTestAnkiNoteData(dictionaryEntry, 'split'));
Expand Down
146 changes: 109 additions & 37 deletions test/utilities/translator.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,67 +16,139 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

const placeholder = '${title}';

/**
* TODO : This function is not very type safe at the moment, could be improved.
* @template {import('translation').FindTermsOptions|import('translation').FindKanjiOptions} T
* @param {string} dictionaryName
* @template {import('test/translator').OptionsType} T
* @param {T} type
* @param {import('test/translator').OptionsPresetObject} optionsPresets
* @param {import('test/translator').OptionsList} optionsArray
* @returns {T}
* @returns {import('test/translator').OptionsPresetGeneric<T>}
* @throws {Error}
*/
export function createFindOptions(dictionaryName, optionsPresets, optionsArray) {
/** @type {import('core').UnknownObject} */
const options = {};
function getCompositePreset(type, optionsPresets, optionsArray) {
const preset = /** @type {import('test/translator').OptionsPresetGeneric<T>} */ ({type});
if (!Array.isArray(optionsArray)) { optionsArray = [optionsArray]; }
for (const entry of optionsArray) {
switch (typeof entry) {
case 'string':
if (!Object.prototype.hasOwnProperty.call(optionsPresets, entry)) {
throw new Error('Invalid options preset');
{
if (!Object.prototype.hasOwnProperty.call(optionsPresets, entry)) {
throw new Error('Options preset not found');
}
const preset2 = optionsPresets[entry];
if (preset2.type !== type) {
throw new Error('Invalid options preset type');
}
Object.assign(preset, structuredClone(preset2));
}
Object.assign(options, structuredClone(optionsPresets[entry]));
break;
case 'object':
Object.assign(options, structuredClone(entry));
if (entry.type !== type) {
throw new Error('Invalid options preset type');
}
Object.assign(preset, structuredClone(entry));
break;
default:
throw new Error('Invalid options type');
}
}
return preset;
}

// Construct regex
if (Array.isArray(options.textReplacements)) {
options.textReplacements = options.textReplacements.map((value) => {
if (Array.isArray(value)) {
value = value.map(({pattern, flags, replacement}) => ({pattern: new RegExp(pattern, flags), replacement}));
}
return value;
});

/**
* @param {string} dictionaryName
* @param {import('test/translator').OptionsPresetObject} optionsPresets
* @param {import('test/translator').OptionsList} optionsArray
* @returns {import('translation').FindKanjiOptions}
*/
export function createFindKanjiOptions(dictionaryName, optionsPresets, optionsArray) {
const preset = getCompositePreset('kanji', optionsPresets, optionsArray);

/** @type {import('translation').KanjiEnabledDictionaryMap} */
const enabledDictionaryMap = new Map();
const presetEnabledDictionaryMap = preset.enabledDictionaryMap;
if (Array.isArray(presetEnabledDictionaryMap)) {
for (const [key, value] of presetEnabledDictionaryMap) {
enabledDictionaryMap.set(key === placeholder ? dictionaryName : key, value);
}
}

// Update structure
const placeholder = '${title}';
if (options.mainDictionary === placeholder) {
options.mainDictionary = dictionaryName;
return {
enabledDictionaryMap,
removeNonJapaneseCharacters: !!preset.removeNonJapaneseCharacters
};
}

/**
* @param {string} dictionaryName
* @param {import('test/translator').OptionsPresetObject} optionsPresets
* @param {import('test/translator').OptionsList} optionsArray
* @returns {import('translation').FindTermsOptions}
*/
export function createFindTermsOptions(dictionaryName, optionsPresets, optionsArray) {
const preset = getCompositePreset('terms', optionsPresets, optionsArray);

/** @type {import('translation').TermEnabledDictionaryMap} */
const enabledDictionaryMap = new Map();
const presetEnabledDictionaryMap = preset.enabledDictionaryMap;
if (Array.isArray(presetEnabledDictionaryMap)) {
for (const [key, value] of presetEnabledDictionaryMap) {
enabledDictionaryMap.set(key === placeholder ? dictionaryName : key, value);
}
}
let {enabledDictionaryMap} = options;
if (Array.isArray(enabledDictionaryMap)) {
for (const entry of enabledDictionaryMap) {
if (entry[0] === placeholder) {
entry[0] = dictionaryName;

/** @type {import('translation').FindTermsTextReplacements} */
const textReplacements = [];
if (Array.isArray(preset.textReplacements)) {
for (const value of preset.textReplacements) {
if (Array.isArray(value)) {
const array = [];
for (const {pattern, flags, replacement} of value) {
array.push({pattern: new RegExp(pattern, flags), replacement});
}
textReplacements.push(array);
} else {
// Null
textReplacements.push(value);
}
}
enabledDictionaryMap = new Map(enabledDictionaryMap);
options.enabledDictionaryMap = enabledDictionaryMap;
}
const {excludeDictionaryDefinitions} = options;
options.excludeDictionaryDefinitions = (
Array.isArray(excludeDictionaryDefinitions) ?
new Set(excludeDictionaryDefinitions) :
null
);

return /** @type {T} */ (options);
const {
matchType,
deinflect,
mainDictionary,
sortFrequencyDictionary,
sortFrequencyDictionaryOrder,
removeNonJapaneseCharacters,
convertHalfWidthCharacters,
convertNumericCharacters,
convertAlphabeticCharacters,
convertHiraganaToKatakana,
convertKatakanaToHiragana,
collapseEmphaticSequences,
excludeDictionaryDefinitions,
searchResolution
} = preset;

return {
matchType: typeof matchType !== 'undefined' ? matchType : 'exact',
deinflect: typeof deinflect !== 'undefined' ? deinflect : true,
mainDictionary: typeof mainDictionary !== 'undefined' && mainDictionary !== placeholder ? mainDictionary : dictionaryName,
sortFrequencyDictionary: typeof sortFrequencyDictionary !== 'undefined' ? sortFrequencyDictionary : null,
sortFrequencyDictionaryOrder: typeof sortFrequencyDictionaryOrder !== 'undefined' ? sortFrequencyDictionaryOrder : 'ascending',
removeNonJapaneseCharacters: typeof removeNonJapaneseCharacters !== 'undefined' ? removeNonJapaneseCharacters : false,
convertHalfWidthCharacters: typeof convertHalfWidthCharacters !== 'undefined' ? convertHalfWidthCharacters : 'false',
convertNumericCharacters: typeof convertNumericCharacters !== 'undefined' ? convertNumericCharacters : 'false',
convertAlphabeticCharacters: typeof convertAlphabeticCharacters !== 'undefined' ? convertAlphabeticCharacters : 'false',
convertHiraganaToKatakana: typeof convertHiraganaToKatakana !== 'undefined' ? convertHiraganaToKatakana : 'false',
convertKatakanaToHiragana: typeof convertKatakanaToHiragana !== 'undefined' ? convertKatakanaToHiragana : 'false',
collapseEmphaticSequences: typeof collapseEmphaticSequences !== 'undefined' ? collapseEmphaticSequences : 'false',
textReplacements,
enabledDictionaryMap,
excludeDictionaryDefinitions: Array.isArray(excludeDictionaryDefinitions) ? new Set(excludeDictionaryDefinitions) : null,
searchResolution: typeof searchResolution !== 'undefined' ? searchResolution : 'letter'
};
}
7 changes: 6 additions & 1 deletion types/ext/translation.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export type FindTermsOptions = {
/**
* An iterable sequence of text replacements to be applied during the term lookup process.
*/
textReplacements: (FindTermsTextReplacement[] | null)[];
textReplacements: FindTermsTextReplacements;
/**
* The mapping of dictionaries to search for terms in.
* The key is the dictionary name.
Expand Down Expand Up @@ -157,6 +157,11 @@ export type FindTermsTextReplacement = {
replacement: string;
};

/**
* Multiple text replacements.
*/
export type FindTermsTextReplacements = (FindTermsTextReplacement[] | null)[];

/**
* Details about a dictionary.
*/
Expand Down
11 changes: 11 additions & 0 deletions types/test/translator.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

import type {FindTermsMatchType, FindTermsSortOrder, FindTermsVariantMode, FindTermsEmphaticSequencesMode, FindKanjiDictionary, FindTermDictionary} from '../ext/translation';
import type {SearchResolution} from 'settings';
import type {FindTermsMode} from 'translator';
import type {DictionaryEntry} from 'dictionary';
import type {NoteData} from 'anki-templates';
Expand All @@ -30,11 +31,13 @@ export type OptionsList = string | (string | OptionsPreset)[];
export type OptionsPreset = FindKanjiOptionsPreset | FindTermsOptionsPreset;

export type FindKanjiOptionsPreset = {
type: 'kanji';
enabledDictionaryMap?: [key: string, value: FindKanjiDictionary][];
removeNonJapaneseCharacters?: boolean;
};

export type FindTermsOptionsPreset = {
type: 'terms';
matchType?: FindTermsMatchType;
deinflect?: boolean;
mainDictionary?: string;
Expand All @@ -50,8 +53,16 @@ export type FindTermsOptionsPreset = {
textReplacements?: (FindTermsTextReplacement[] | null)[];
enabledDictionaryMap?: [key: string, value: FindTermDictionary][];
excludeDictionaryDefinitions?: string[] | null;
searchResolution?: SearchResolution;
};

export type OptionsType = OptionsPreset['type'];

export type OptionsPresetGeneric<T extends OptionsType> = {
kanji: FindKanjiOptionsPreset;
terms: FindTermsOptionsPreset;
}[T];

export type FindTermsTextReplacement = {
pattern: string;
flags: string;
Expand Down

0 comments on commit c4f248b

Please sign in to comment.