diff --git a/docs/development/language-features.md b/docs/development/language-features.md index 96ae5d49ca..54c0624fa0 100644 --- a/docs/development/language-features.md +++ b/docs/development/language-features.md @@ -139,18 +139,26 @@ Transforms files should export a `LanguageTransformDescriptor`, which is then im ```js // from language-transformer.d.ts -export type LanguageTransformDescriptor = { +export type LanguageTransformDescriptor = { language: string; - conditions: ConditionMapObject; - transforms: { - [name: string]: Transform; - }; + conditions: ConditionMapObject; + transforms: TransformMapObject; }; + +export type ConditionMapObject = { + [type in TCondition]: Condition; +}; + +export type TransformMapObject = { + [name: string]: Transform; +}; + ``` - `language` is the ISO code of the language - `conditions` are an object containing parts of speech and grammatical forms that are used to check which deinflections make sense. They are referenced by the deinflection rules. - `transforms` are the actual deinflection rules +- `TCondition` is an optional generic parameter that can be passed to `LanguageTransformDescriptor`. You can learn more about it at the end of this section. Let's try and write a bit of deinflection for English, from scratch. @@ -320,6 +328,78 @@ Here, by setting `valid` to `false`, we are telling the test function to fail th You can also optionally pass a `preprocess` helper function to `testLanguageTransformer`. Refer to the language transforms test files for its specific use case. +#### Opting in autocompletion + +If you want additional type-checking and autocompletion when writing your deinflection rules, you can add them with just a few extra lines of code. Due to the limitations of TypeScript and JSDoc annotations, we will have to perform some type magic in our transformations file, but you don't need to understand what they mean in detail. + +Your `english-transforms.js` file should look like this: + +```js +// english-transforms.js +import { suffixInflection } from "../language-transforms.js"; + +/** @type {import('language-transformer').LanguageTransformDescriptor} */ +export const englishTransforms = { + language: "en", + conditions: { + n: { + name: "Noun", + isDictionaryForm: true, + subConditions: ["np", "ns"], + }, + np: { + name: "Noun plural", + isDictionaryForm: true, + }, + ns: { + name: "Noun singular", + isDictionaryForm: true, + }, + }, + transforms: { + // omitted + }, +}; +``` + +To gain type-safety, we have to pass an additional `TCondition` type parameter to `LanguageTransformDescriptor`. (You can revisit its definition [at the top of this section](#deinflection-rules-aka-language-transforms)) + +The passed type value should be the union type of all conditions in our transforms. To find this value, we first need to move the `conditions` object outside of `englishTransforms` and extract its type by adding a `/** @typedef {keyof typeof conditions} Condition */` comment at the start of the file. Then, you just need to pass it to the `LanguageTransformDescriptor` type declaration like so: + +```js +// english-transforms.js +import { suffixInflection } from "../language-transforms.js"; + +/** @typedef {keyof typeof conditions} Condition */ + +const conditions = { + n: { + name: "Noun", + isDictionaryForm: true, + subConditions: ["np", "ns"], + }, + np: { + name: "Noun plural", + isDictionaryForm: true, + }, + ns: { + name: "Noun singular", + isDictionaryForm: true, + }, +}; + +/** @type {import('language-transformer').LanguageTransformDescriptor} */ +export const englishTransforms = { + language: "en", + conditions, + transforms: { + // omitted + }, +}; +``` + +Now you should be able to check for types whenever writing a deinflection rule. + ### Text Postprocessors In special cases, text may need to be modified after deinflection. These work exactly like text preprocessors, but are applied after deinflection. Currently, this is only used for Korean, where the Hangul text is disassembled into jamo during preprocessing, and so must be reassembled after deinflection. diff --git a/ext/css/display.css b/ext/css/display.css index 466c2efe0f..61aafa4764 100644 --- a/ext/css/display.css +++ b/ext/css/display.css @@ -18,8 +18,6 @@ /* Variables */ :root { - /* Fonts */ - --font-family: 'sans-serif'; /* Strings */ --compact-list-separator: ' | '; @@ -250,9 +248,6 @@ /* Fonts */ -* { - font-family: var(--font-family); -} @font-face { font-family: kanji-stroke-orders; src: url('/data/fonts/kanji-stroke-orders.ttf'); diff --git a/ext/css/material.css b/ext/css/material.css index 00265586d4..9013365f6c 100644 --- a/ext/css/material.css +++ b/ext/css/material.css @@ -1241,7 +1241,7 @@ button.icon-button>.icon-button-inner { top: 0; right: 0; bottom: 0; - z-index: 101; + z-index: 1001; outline: none; overflow: hidden; } diff --git a/ext/data/schemas/options-schema.json b/ext/data/schemas/options-schema.json index 91104c6ea2..939ccc0fc1 100644 --- a/ext/data/schemas/options-schema.json +++ b/ext/data/schemas/options-schema.json @@ -143,7 +143,7 @@ }, "fontFamily": { "type": "string", - "default": "sans-serif" + "default": "" }, "fontSize": { "type": "number", diff --git a/ext/js/data/options-util.js b/ext/js/data/options-util.js index 4be913d536..4fb38dc0cb 100644 --- a/ext/js/data/options-util.js +++ b/ext/js/data/options-util.js @@ -293,7 +293,7 @@ export class OptionsUtil { resultOutputMode: 'group', debugInfo: false, maxResults: 32, - fontFamily: 'sans-serif', + fontFamily: '', fontSize: 14, lineHeight: '1.5', showAdvanced: false, @@ -557,6 +557,7 @@ export class OptionsUtil { this._updateVersion43, this._updateVersion44, this._updateVersion45, + this._updateVersion46, ]; /* eslint-enable @typescript-eslint/unbound-method */ if (typeof targetVersion === 'number' && targetVersion < result.length) { @@ -1410,6 +1411,18 @@ export class OptionsUtil { } } + /** + * - Set default font to empty + * @type {import('options-util').UpdateFunction} + */ + async _updateVersion46(options) { + for (const profile of options.profiles) { + if (profile.options.general.fontFamily === 'sans-serif') { + profile.options.general.fontFamily = ''; + } + } + } + /** * @param {string} url * @returns {Promise} diff --git a/ext/js/display/display.js b/ext/js/display/display.js index b2f96e363c..a251d3c5f3 100644 --- a/ext/js/display/display.js +++ b/ext/js/display/display.js @@ -535,9 +535,9 @@ export class Display extends EventDispatcher { * @param {string} lineHeight */ setFontOptions(fontFamily, fontSize, lineHeight) { - document.documentElement.style.setProperty('--font-family', fontFamily); // Setting these directly rather than using the existing CSS variables // minimizes problems and ensures everything scales correctly + document.documentElement.style.fontFamily = fontFamily; document.documentElement.style.fontSize = `${fontSize}px`; document.documentElement.style.lineHeight = lineHeight; } diff --git a/ext/js/language/de/german-transforms.js b/ext/js/language/de/german-transforms.js index 17ba7a4487..19d9eaac6c 100644 --- a/ext/js/language/de/german-transforms.js +++ b/ext/js/language/de/german-transforms.js @@ -17,14 +17,16 @@ import {prefixInflection, suffixInflection} from '../language-transforms.js'; +/** @typedef {keyof typeof conditions} Condition */ + // https://www.dartmouth.edu/~deutsch/Grammatik/Wortbildung/Separables.html const separablePrefixes = ['ab', 'an', 'auf', 'aus', 'auseinander', 'bei', 'da', 'dabei', 'dar', 'daran', 'dazwischen', 'durch', 'ein', 'empor', 'entgegen', 'entlang', 'entzwei', 'fehl', 'fern', 'fest', 'fort', 'frei', 'gegenüber', 'gleich', 'heim', 'her', 'herab', 'heran', 'herauf', 'heraus', 'herbei', 'herein', 'herüber', 'herum', 'herunter', 'hervor', 'hin', 'hinab', 'hinauf', 'hinaus', 'hinein', 'hinterher', 'hinunter', 'hinweg', 'hinzu', 'hoch', 'los', 'mit', 'nach', 'nebenher', 'nieder', 'statt', 'um', 'vor', 'voran', 'voraus', 'vorbei', 'vorüber', 'vorweg', 'weg', 'weiter', 'wieder', 'zu', 'zurecht', 'zurück', 'zusammen']; /** * @param {string} prefix - * @param {string[]} conditionsIn - * @param {string[]} conditionsOut - * @returns {import('language-transformer').Rule} + * @param {Condition[]} conditionsIn + * @param {Condition[]} conditionsOut + * @returns {import('language-transformer').Rule} */ function separatedPrefix(prefix, conditionsIn, conditionsOut) { const germanLetters = 'a-zA-ZäöüßÄÖÜẞ'; @@ -48,22 +50,24 @@ const zuInfinitiveInflections = separablePrefixes.map((prefix) => { return prefixInflection(prefix + 'zu', prefix, [], ['v']); }); +const conditions = { + v: { + name: 'Verb', + isDictionaryForm: true, + }, + n: { + name: 'Noun', + isDictionaryForm: true, + }, + adj: { + name: 'Adjective', + isDictionaryForm: true, + }, +}; + export const germanTransforms = { language: 'de', - conditions: { - v: { - name: 'Verb', - isDictionaryForm: true, - }, - n: { - name: 'Noun', - isDictionaryForm: true, - }, - adj: { - name: 'Adjective', - isDictionaryForm: true, - }, - }, + conditions, transforms: { 'nominalization': { name: 'nominalization', diff --git a/ext/js/language/en/english-transforms.js b/ext/js/language/en/english-transforms.js index 96b30e4f5c..24777bf8c6 100644 --- a/ext/js/language/en/english-transforms.js +++ b/ext/js/language/en/english-transforms.js @@ -17,12 +17,14 @@ import {prefixInflection, suffixInflection} from '../language-transforms.js'; +/** @typedef {keyof typeof conditions} Condition */ + /** * @param {string} consonants * @param {string} suffix - * @param {string[]} conditionsIn - * @param {string[]} conditionsOut - * @returns {import('language-transformer').SuffixRule[]} + * @param {Condition[]} conditionsIn + * @param {Condition[]} conditionsOut + * @returns {import('language-transformer').SuffixRule[]} */ function doubledConsonantInflection(consonants, suffix, conditionsIn, conditionsOut) { const inflections = []; @@ -64,7 +66,9 @@ const phrasalVerbPrepositions = ['aback', 'about', 'above', 'across', 'after', ' const particlesDisjunction = phrasalVerbParticles.join('|'); const phrasalVerbWordSet = new Set([...phrasalVerbParticles, ...phrasalVerbPrepositions]); const phrasalVerbWordDisjunction = [...phrasalVerbWordSet].join('|'); -/** @type {import('language-transformer').Rule} */ +/** + * @type {import('language-transformer').Rule} + */ const phrasalVerbInterposedObjectRule = { type: 'other', isInflected: new RegExp(`^\\w* (?:(?!\\b(${phrasalVerbWordDisjunction})\\b).)+ (?:${particlesDisjunction})`), @@ -78,7 +82,7 @@ const phrasalVerbInterposedObjectRule = { /** * @param {string} inflected * @param {string} deinflected - * @returns {import('language-transformer').Rule} + * @returns {import('language-transformer').Rule} */ function createPhrasalVerbInflection(inflected, deinflected) { return { @@ -93,8 +97,8 @@ function createPhrasalVerbInflection(inflected, deinflected) { } /** - * @param {import('language-transformer').SuffixRule[]} sourceRules - * @returns {import('language-transformer').Rule[]} + * @param {import('language-transformer').SuffixRule[]} sourceRules + * @returns {import('language-transformer').Rule[]} */ function createPhrasalVerbInflectionsFromSuffixInflections(sourceRules) { return sourceRules.flatMap(({isInflected, deinflected}) => { @@ -105,41 +109,43 @@ function createPhrasalVerbInflectionsFromSuffixInflections(sourceRules) { }); } -/** @type {import('language-transformer').LanguageTransformDescriptor} */ +const conditions = { + v: { + name: 'Verb', + isDictionaryForm: true, + subConditions: ['v_phr'], + }, + v_phr: { + name: 'Phrasal verb', + isDictionaryForm: true, + }, + n: { + name: 'Noun', + isDictionaryForm: true, + subConditions: ['np', 'ns'], + }, + np: { + name: 'Noun plural', + isDictionaryForm: true, + }, + ns: { + name: 'Noun singular', + isDictionaryForm: true, + }, + adj: { + name: 'Adjective', + isDictionaryForm: true, + }, + adv: { + name: 'Adverb', + isDictionaryForm: true, + }, +}; + +/** @type {import('language-transformer').LanguageTransformDescriptor} */ export const englishTransforms = { language: 'en', - conditions: { - v: { - name: 'Verb', - isDictionaryForm: true, - subConditions: ['v_phr'], - }, - v_phr: { - name: 'Phrasal verb', - isDictionaryForm: true, - }, - n: { - name: 'Noun', - isDictionaryForm: true, - subConditions: ['np', 'ns'], - }, - np: { - name: 'Noun plural', - isDictionaryForm: true, - }, - ns: { - name: 'Noun singular', - isDictionaryForm: true, - }, - adj: { - name: 'Adjective', - isDictionaryForm: true, - }, - adv: { - name: 'Adverb', - isDictionaryForm: true, - }, - }, + conditions, transforms: { 'plural': { name: 'plural', diff --git a/ext/js/language/es/spanish-transforms.js b/ext/js/language/es/spanish-transforms.js index dc4fc5a522..202f1387a9 100644 --- a/ext/js/language/es/spanish-transforms.js +++ b/ext/js/language/es/spanish-transforms.js @@ -34,45 +34,47 @@ function addAccent(char) { return ACCENTS.get(char) || char; } -/** @type {import('language-transformer').LanguageTransformDescriptor} */ +const conditions = { + n: { + name: 'Noun', + isDictionaryForm: true, + subConditions: ['ns', 'np'], + }, + np: { + name: 'Noun plural', + isDictionaryForm: false, + }, + ns: { + name: 'Noun singular', + isDictionaryForm: false, + }, + v: { + name: 'Verb', + isDictionaryForm: true, + subConditions: ['v_ar', 'v_er', 'v_ir'], + }, + v_ar: { + name: '-ar verb', + isDictionaryForm: false, + }, + v_er: { + name: '-er verb', + isDictionaryForm: false, + }, + v_ir: { + name: '-ir verb', + isDictionaryForm: false, + }, + adj: { + name: 'Adjective', + isDictionaryForm: true, + }, +}; + +/** @type {import('language-transformer').LanguageTransformDescriptor} */ export const spanishTransforms = { language: 'es', - conditions: { - n: { - name: 'Noun', - isDictionaryForm: true, - subConditions: ['ns', 'np'], - }, - np: { - name: 'Noun plural', - isDictionaryForm: false, - }, - ns: { - name: 'Noun singular', - isDictionaryForm: false, - }, - v: { - name: 'Verb', - isDictionaryForm: true, - subConditions: ['v_ar', 'v_er', 'v_ir'], - }, - v_ar: { - name: '-ar verb', - isDictionaryForm: false, - }, - v_er: { - name: '-er verb', - isDictionaryForm: false, - }, - v_ir: { - name: '-ir verb', - isDictionaryForm: false, - }, - adj: { - name: 'Adjective', - isDictionaryForm: true, - }, - }, + conditions, transforms: { 'plural': { name: 'plural', diff --git a/ext/js/language/ja/japanese-transforms.js b/ext/js/language/ja/japanese-transforms.js index 835c53602d..9931087fc0 100644 --- a/ext/js/language/ja/japanese-transforms.js +++ b/ext/js/language/ja/japanese-transforms.js @@ -24,134 +24,136 @@ const shimauEnglishDescription = '1. Shows a sense of regret/surprise when you d const passiveEnglishDescription = '1. Indicates an action received from an action performer.\n' + '2. Expresses respect for the subject of action performer.\n'; -/** @type {import('language-transformer').LanguageTransformDescriptor} */ +const conditions = { + 'v': { + name: 'Verb', + i18n: [ + { + language: 'ja', + name: '動詞', + }, + ], + isDictionaryForm: false, + subConditions: ['v1', 'v5', 'vk', 'vs', 'vz'], + }, + 'v1': { + name: 'Ichidan verb', + i18n: [ + { + language: 'ja', + name: '一段動詞', + }, + ], + isDictionaryForm: true, + subConditions: ['v1d', 'v1p'], + }, + 'v1d': { + name: 'Ichidan verb, dictionary form', + i18n: [ + { + language: 'ja', + name: '一段動詞、辞書形', + }, + ], + isDictionaryForm: false, + }, + 'v1p': { + name: 'Ichidan verb, progressive or perfect form', + i18n: [ + { + language: 'ja', + name: '一段動詞、進行形または完了形', + }, + ], + isDictionaryForm: false, + }, + 'v5': { + name: 'Godan verb', + i18n: [ + { + language: 'ja', + name: '五段動詞', + }, + ], + isDictionaryForm: true, + subConditions: ['v5d', 'v5m'], + }, + 'v5d': { + name: 'Godan verb, dictionary form', + i18n: [ + { + language: 'ja', + name: '五段動詞、辞書形', + }, + ], + isDictionaryForm: false, + }, + 'v5m': { + name: 'Godan verb, polite (masu) form', + isDictionaryForm: false, + }, + 'vk': { + name: 'Kuru verb', + i18n: [ + { + language: 'ja', + name: '来る動詞', + }, + ], + isDictionaryForm: true, + }, + 'vs': { + name: 'Suru verb', + i18n: [ + { + language: 'ja', + name: 'する動詞', + }, + ], + isDictionaryForm: true, + }, + 'vz': { + name: 'Zuru verb', + i18n: [ + { + language: 'ja', + name: 'ずる動詞', + }, + ], + isDictionaryForm: true, + }, + 'adj-i': { + name: 'Adjective with i ending', + i18n: [ + { + language: 'ja', + name: '形容詞', + }, + ], + isDictionaryForm: true, + }, + '-te': { + name: 'Intermediate -te endings for progressive or perfect tense', + isDictionaryForm: false, + }, + '-ba': { + name: 'Intermediate -ba endings for conditional contraction', + isDictionaryForm: false, + }, + 'adv': { + name: 'Intermediate -ku endings for adverbs', + isDictionaryForm: false, + }, + 'past': { + name: '-ta past form ending', + isDictionaryForm: false, + }, +}; + +/** @type {import('language-transformer').LanguageTransformDescriptor} */ export const japaneseTransforms = { language: 'ja', - conditions: { - 'v': { - name: 'Verb', - i18n: [ - { - language: 'ja', - name: '動詞', - }, - ], - isDictionaryForm: false, - subConditions: ['v1', 'v5', 'vk', 'vs', 'vz'], - }, - 'v1': { - name: 'Ichidan verb', - i18n: [ - { - language: 'ja', - name: '一段動詞', - }, - ], - isDictionaryForm: true, - subConditions: ['v1d', 'v1p'], - }, - 'v1d': { - name: 'Ichidan verb, dictionary form', - i18n: [ - { - language: 'ja', - name: '一段動詞、辞書形', - }, - ], - isDictionaryForm: false, - }, - 'v1p': { - name: 'Ichidan verb, progressive or perfect form', - i18n: [ - { - language: 'ja', - name: '一段動詞、進行形または完了形', - }, - ], - isDictionaryForm: false, - }, - 'v5': { - name: 'Godan verb', - i18n: [ - { - language: 'ja', - name: '五段動詞', - }, - ], - isDictionaryForm: true, - subConditions: ['v5d', 'v5m'], - }, - 'v5d': { - name: 'Godan verb, dictionary form', - i18n: [ - { - language: 'ja', - name: '五段動詞、辞書形', - }, - ], - isDictionaryForm: false, - }, - 'v5m': { - name: 'Godan verb, polite (masu) form', - isDictionaryForm: false, - }, - 'vk': { - name: 'Kuru verb', - i18n: [ - { - language: 'ja', - name: '来る動詞', - }, - ], - isDictionaryForm: true, - }, - 'vs': { - name: 'Suru verb', - i18n: [ - { - language: 'ja', - name: 'する動詞', - }, - ], - isDictionaryForm: true, - }, - 'vz': { - name: 'Zuru verb', - i18n: [ - { - language: 'ja', - name: 'ずる動詞', - }, - ], - isDictionaryForm: true, - }, - 'adj-i': { - name: 'Adjective with i ending', - i18n: [ - { - language: 'ja', - name: '形容詞', - }, - ], - isDictionaryForm: true, - }, - '-te': { - name: 'Intermediate -te endings for progressive or perfect tense', - isDictionaryForm: false, - }, - '-ba': { - name: 'Intermediate -ba endings for conditional contraction', - isDictionaryForm: false, - }, - 'adv': { - name: 'Intermediate -ku endings for adverbs', - isDictionaryForm: false, - }, - 'past': { - name: '-ta past form ending', - isDictionaryForm: false, - }, - }, + conditions, transforms: { '-ba': { name: '-ba', diff --git a/ext/js/language/ko/korean-transforms.js b/ext/js/language/ko/korean-transforms.js index 49f4b91fb2..ac74859136 100644 --- a/ext/js/language/ko/korean-transforms.js +++ b/ext/js/language/ko/korean-transforms.js @@ -17,89 +17,91 @@ import {suffixInflection} from '../language-transforms.js'; -/** @type {import('language-transformer').LanguageTransformDescriptor} */ +const conditions = { + v: { + name: 'Verb or Auxiliary Verb', + isDictionaryForm: true, + i18n: [ + { + language: 'ko', + name: '동사 / 보조 동사', + }, + ], + }, + adj: { + name: 'Adjective or Auxiliary Adjective', + isDictionaryForm: true, + i18n: [ + { + language: 'ko', + name: '형용사 / 보조 형용사', + }, + ], + }, + ida: { + name: 'Postpositional particle ida', + isDictionaryForm: true, + i18n: [ + { + language: 'ko', + name: '조사 이다', + }, + ], + }, + p: { + name: 'Intermediate past tense ending', + isDictionaryForm: false, + }, + f: { + name: 'Intermediate future tense ending', + isDictionaryForm: false, + }, + eusi: { + name: 'Intermediate formal ending', + isDictionaryForm: false, + }, + euob: { + name: 'Intermediate formal ending', + isDictionaryForm: false, + }, + euo: { + name: 'Intermediate formal ending', + isDictionaryForm: false, + }, + sao: { + name: 'Intermediate formal ending', + isDictionaryForm: false, + }, + saob: { + name: 'Intermediate formal ending', + isDictionaryForm: false, + }, + sab: { + name: 'Intermediate formal ending', + isDictionaryForm: false, + }, + jaob: { + name: 'Intermediate formal ending', + isDictionaryForm: false, + }, + jao: { + name: 'Intermediate formal ending', + isDictionaryForm: false, + }, + jab: { + name: 'Intermediate formal ending', + isDictionaryForm: false, + }, + do: { + name: 'Intermediate ending', + isDictionaryForm: false, + }, +}; + +/** @type {import('language-transformer').LanguageTransformDescriptor} */ export const koreanTransforms = { language: 'ko', - conditions: { - v: { - name: 'Verb or Auxiliary Verb', - isDictionaryForm: true, - i18n: [ - { - language: 'ko', - name: '동사 / 보조 동사', - }, - ], - }, - adj: { - name: 'Adjective or Auxiliary Adjective', - isDictionaryForm: true, - i18n: [ - { - language: 'ko', - name: '형용사 / 보조 형용사', - }, - ], - }, - ida: { - name: 'Postpositional particle ida', - isDictionaryForm: true, - i18n: [ - { - language: 'ko', - name: '조사 이다', - }, - ], - }, - p: { - name: 'Intermediate past tense ending', - isDictionaryForm: false, - }, - f: { - name: 'Intermediate future tense ending', - isDictionaryForm: false, - }, - eusi: { - name: 'Intermediate formal ending', - isDictionaryForm: false, - }, - euob: { - name: 'Intermediate formal ending', - isDictionaryForm: false, - }, - euo: { - name: 'Intermediate formal ending', - isDictionaryForm: false, - }, - sao: { - name: 'Intermediate formal ending', - isDictionaryForm: false, - }, - saob: { - name: 'Intermediate formal ending', - isDictionaryForm: false, - }, - sab: { - name: 'Intermediate formal ending', - isDictionaryForm: false, - }, - jaob: { - name: 'Intermediate formal ending', - isDictionaryForm: false, - }, - jao: { - name: 'Intermediate formal ending', - isDictionaryForm: false, - }, - jab: { - name: 'Intermediate formal ending', - isDictionaryForm: false, - }, - do: { - name: 'Intermediate ending', - isDictionaryForm: false, - }, - }, + conditions, transforms: { '어간': { name: '어간', diff --git a/ext/js/language/la/latin-transforms.js b/ext/js/language/la/latin-transforms.js index 60a99f4439..20af008a40 100644 --- a/ext/js/language/la/latin-transforms.js +++ b/ext/js/language/la/latin-transforms.js @@ -19,112 +19,114 @@ import {suffixInflection} from '../language-transforms.js'; // TODO: -ne suffix (estne, nonne)? -/** @type {import('language-transformer').LanguageTransformDescriptor} */ +const conditions = { + v: { + name: 'Verb', + isDictionaryForm: true, + }, + n: { + name: 'Noun', + isDictionaryForm: true, + subConditions: ['ns', 'np'], + }, + ns: { + name: 'Noun, singular', + isDictionaryForm: true, + subConditions: ['n1s', 'n2s', 'n3s', 'n4s', 'n5s'], + }, + np: { + name: 'Noun, plural', + isDictionaryForm: true, + subConditions: ['n1p', 'n2p', 'n3p', 'n4p', 'n5p'], + }, + n1: { + name: 'Noun, 1st declension', + isDictionaryForm: true, + subConditions: ['n1s', 'n1p'], + }, + n1p: { + name: 'Noun, 1st declension, plural', + isDictionaryForm: true, + }, + n1s: { + name: 'Noun, 1st declension, singular', + isDictionaryForm: true, + }, + n2: { + name: 'Noun, 2nd declension', + isDictionaryForm: true, + subConditions: ['n2s', 'n2p'], + }, + n2p: { + name: 'Noun, 2nd declension, plural', + isDictionaryForm: true, + }, + n2s: { + name: 'Noun, 2nd declension, singular', + isDictionaryForm: true, + }, + n3: { + name: 'Noun, 3rd declension', + isDictionaryForm: true, + subConditions: ['n3s', 'n3p'], + }, + n3p: { + name: 'Noun, 3rd declension, plural', + isDictionaryForm: true, + }, + n3s: { + name: 'Noun, 3rd declension, singular', + isDictionaryForm: true, + }, + n4: { + name: 'Noun, 4th declension', + isDictionaryForm: true, + subConditions: ['n4s', 'n4p'], + }, + n4p: { + name: 'Noun, 4th declension, plural', + isDictionaryForm: true, + }, + n4s: { + name: 'Noun, 4th declension, singular', + isDictionaryForm: true, + }, + n5: { + name: 'Noun, 5th declension', + isDictionaryForm: true, + subConditions: ['n5s', 'n5p'], + }, + n5p: { + name: 'Noun, 5th declension, plural', + isDictionaryForm: true, + }, + n5s: { + name: 'Noun, 5th declension, singular', + isDictionaryForm: true, + }, + adj: { + name: 'Adjective', + isDictionaryForm: true, + subConditions: ['adj3', 'adj12'], + }, + adj12: { + name: 'Adjective, 1st-2nd declension', + isDictionaryForm: true, + }, + adj3: { + name: 'Adjective, 3rd declension', + isDictionaryForm: true, + }, + adv: { + name: 'Adverb', + isDictionaryForm: true, + }, +}; + +/** @type {import('language-transformer').LanguageTransformDescriptor} */ export const latinTransforms = { language: 'la', - conditions: { - v: { - name: 'Verb', - isDictionaryForm: true, - }, - n: { - name: 'Noun', - isDictionaryForm: true, - subConditions: ['ns', 'np'], - }, - ns: { - name: 'Noun, singular', - isDictionaryForm: true, - subConditions: ['n1s', 'n2s', 'n3s', 'n4s', 'n5s'], - }, - np: { - name: 'Noun, plural', - isDictionaryForm: true, - subConditions: ['n1p', 'n2p', 'n3p', 'n4p', 'n5p'], - }, - n1: { - name: 'Noun, 1st declension', - isDictionaryForm: true, - subConditions: ['n1s', 'n1p'], - }, - n1p: { - name: 'Noun, 1st declension, plural', - isDictionaryForm: true, - }, - n1s: { - name: 'Noun, 1st declension, singular', - isDictionaryForm: true, - }, - n2: { - name: 'Noun, 2nd declension', - isDictionaryForm: true, - subConditions: ['n2s', 'n2p'], - }, - n2p: { - name: 'Noun, 2nd declension, plural', - isDictionaryForm: true, - }, - n2s: { - name: 'Noun, 2nd declension, singular', - isDictionaryForm: true, - }, - n3: { - name: 'Noun, 3rd declension', - isDictionaryForm: true, - subConditions: ['n3s', 'n3p'], - }, - n3p: { - name: 'Noun, 3rd declension, plural', - isDictionaryForm: true, - }, - n3s: { - name: 'Noun, 3rd declension, singular', - isDictionaryForm: true, - }, - n4: { - name: 'Noun, 4th declension', - isDictionaryForm: true, - subConditions: ['n4s', 'n4p'], - }, - n4p: { - name: 'Noun, 4th declension, plural', - isDictionaryForm: true, - }, - n4s: { - name: 'Noun, 4th declension, singular', - isDictionaryForm: true, - }, - n5: { - name: 'Noun, 5th declension', - isDictionaryForm: true, - subConditions: ['n5s', 'n5p'], - }, - n5p: { - name: 'Noun, 5th declension, plural', - isDictionaryForm: true, - }, - n5s: { - name: 'Noun, 5th declension, singular', - isDictionaryForm: true, - }, - adj: { - name: 'Adjective', - isDictionaryForm: true, - subConditions: ['adj3', 'adj12'], - }, - adj12: { - name: 'Adjective, 1st-2nd declension', - isDictionaryForm: true, - }, - adj3: { - name: 'Adjective, 3rd declension', - isDictionaryForm: true, - }, - adv: { - name: 'Adverb', - isDictionaryForm: true, - }, - }, + conditions, transforms: { plural: { name: 'plural', diff --git a/ext/js/language/language-descriptors.js b/ext/js/language/language-descriptors.js index 5aea0572f5..b21772222f 100644 --- a/ext/js/language/language-descriptors.js +++ b/ext/js/language/language-descriptors.js @@ -143,7 +143,10 @@ const languageDescriptors = [ iso639_3: 'ita', name: 'Italian', exampleText: 'leggere', - textPreprocessors: capitalizationPreprocessors, + textPreprocessors: { + ...capitalizationPreprocessors, + removeAlphabeticDiacritics, + }, }, { iso: 'la', diff --git a/ext/js/language/language-transforms.js b/ext/js/language/language-transforms.js index c7bfd8c0d1..cbf5063850 100644 --- a/ext/js/language/language-transforms.js +++ b/ext/js/language/language-transforms.js @@ -17,11 +17,12 @@ /** + * @template {string} TCondition * @param {string} inflectedSuffix * @param {string} deinflectedSuffix - * @param {string[]} conditionsIn - * @param {string[]} conditionsOut - * @returns {import('language-transformer').SuffixRule} + * @param {TCondition[]} conditionsIn + * @param {TCondition[]} conditionsOut + * @returns {import('language-transformer').SuffixRule} */ export function suffixInflection(inflectedSuffix, deinflectedSuffix, conditionsIn, conditionsOut) { const suffixRegExp = new RegExp(inflectedSuffix + '$'); @@ -36,11 +37,12 @@ export function suffixInflection(inflectedSuffix, deinflectedSuffix, conditionsI } /** + * @template {string} TCondition * @param {string} inflectedPrefix * @param {string} deinflectedPrefix - * @param {string[]} conditionsIn - * @param {string[]} conditionsOut - * @returns {import('language-transformer').Rule} + * @param {TCondition[]} conditionsIn + * @param {TCondition[]} conditionsOut + * @returns {import('language-transformer').Rule} */ export function prefixInflection(inflectedPrefix, deinflectedPrefix, conditionsIn, conditionsOut) { const prefixRegExp = new RegExp('^' + inflectedPrefix); @@ -54,11 +56,12 @@ export function prefixInflection(inflectedPrefix, deinflectedPrefix, conditionsI } /** + * @template {string} TCondition * @param {string} inflectedWord * @param {string} deinflectedWord - * @param {string[]} conditionsIn - * @param {string[]} conditionsOut - * @returns {import('language-transformer').Rule} + * @param {TCondition[]} conditionsIn + * @param {TCondition[]} conditionsOut + * @returns {import('language-transformer').Rule} */ export function wholeWordInflection(inflectedWord, deinflectedWord, conditionsIn, conditionsOut) { const regex = new RegExp('^' + inflectedWord + '$'); diff --git a/ext/js/language/sga/old-irish-transforms.js b/ext/js/language/sga/old-irish-transforms.js index 793c39a809..a700b2edc5 100644 --- a/ext/js/language/sga/old-irish-transforms.js +++ b/ext/js/language/sga/old-irish-transforms.js @@ -17,13 +17,15 @@ import {prefixInflection, suffixInflection} from '../language-transforms.js'; +/** @typedef {keyof typeof conditions} Condition */ + /** * @param {boolean} notBeginning * @param {string} originalOrthography * @param {string} alternateOrthography - * @param {string[]} conditionsIn - * @param {string[]} conditionsOut - * @returns {import('language-transformer').Rule} + * @param {Condition[]} conditionsIn + * @param {Condition[]} conditionsOut + * @returns {import('language-transformer').Rule} */ function tryAlternateOrthography(notBeginning, originalOrthography, alternateOrthography, conditionsIn, conditionsOut) { const orthographyRegExp = notBeginning ? new RegExp('(?} */ export const oldIrishTransforms = { language: 'sga', - conditions: {}, + conditions, transforms: { 'nd for nn': { name: 'nd for nn', diff --git a/ext/js/language/sq/albanian-transforms.js b/ext/js/language/sq/albanian-transforms.js index 0c6625c694..c533717808 100644 --- a/ext/js/language/sq/albanian-transforms.js +++ b/ext/js/language/sq/albanian-transforms.js @@ -17,12 +17,14 @@ import {suffixInflection} from '../language-transforms.js'; +/** @typedef {keyof typeof conditions} Condition */ + /** * @param {string} inflectedSuffix * @param {string} deinflectedSuffix - * @param {string[]} conditionsIn - * @param {string[]} conditionsOut - * @returns {import('language-transformer').Rule} + * @param {Condition[]} conditionsIn + * @param {Condition[]} conditionsOut + * @returns {import('language-transformer').Rule} */ function conjugationIISuffixInflection(inflectedSuffix, deinflectedSuffix, conditionsIn, conditionsOut) { return { @@ -32,36 +34,38 @@ function conjugationIISuffixInflection(inflectedSuffix, deinflectedSuffix, condi }; } -/** @type {import('language-transformer').LanguageTransformDescriptor} */ +const conditions = { + v: { + name: 'Verb', + isDictionaryForm: true, + }, + n: { + name: 'Noun', + isDictionaryForm: true, + subConditions: ['np', 'ns'], + }, + np: { + name: 'Noun plural', + isDictionaryForm: true, + }, + ns: { + name: 'Noun singular', + isDictionaryForm: true, + }, + adj: { + name: 'Adjective', + isDictionaryForm: true, + }, + adv: { + name: 'Adverb', + isDictionaryForm: true, + }, +}; + +/** @type {import('language-transformer').LanguageTransformDescriptor} */ export const albanianTransforms = { language: 'sq', - conditions: { - v: { - name: 'Verb', - isDictionaryForm: true, - }, - n: { - name: 'Noun', - isDictionaryForm: true, - subConditions: ['np', 'ns'], - }, - np: { - name: 'Noun plural', - isDictionaryForm: true, - }, - ns: { - name: 'Noun singular', - isDictionaryForm: true, - }, - adj: { - name: 'Adjective', - isDictionaryForm: true, - }, - adv: { - name: 'Adverb', - isDictionaryForm: true, - }, - }, + conditions, transforms: { // Nouns 'definite': { diff --git a/ext/js/pages/settings/dictionary-controller.js b/ext/js/pages/settings/dictionary-controller.js index 5c0e49d409..4dbcf994d9 100644 --- a/ext/js/pages/settings/dictionary-controller.js +++ b/ext/js/pages/settings/dictionary-controller.js @@ -692,6 +692,9 @@ export class DictionaryController { } const hasEnabledDictionary = (enabledDictionaryCountValid > 0); + if (hasEnabledDictionary) { + this._settingsController.trigger('dictionaryEnabled', {}); + } for (const node of /** @type {NodeListOf} */ (this._noDictionariesEnabledWarnings)) { node.hidden = hasEnabledDictionary; } diff --git a/ext/js/pages/settings/popup-preview-controller.js b/ext/js/pages/settings/popup-preview-controller.js index da163a6adf..7e650c55bc 100644 --- a/ext/js/pages/settings/popup-preview-controller.js +++ b/ext/js/pages/settings/popup-preview-controller.js @@ -48,6 +48,7 @@ export class PopupPreviewController { this._frame.addEventListener('load', this._onFrameLoad.bind(this), false); this._settingsController.on('optionsContextChanged', this._onOptionsContextChange.bind(this)); this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); + this._settingsController.on('dictionaryEnabled', this._onOptionsContextChange.bind(this)); const languageSelect = querySelectorNotNull(document, '#language-select'); languageSelect.addEventListener( /** @type {string} */ ('settingChanged'), @@ -86,6 +87,11 @@ export class PopupPreviewController { this._invoke('updateOptionsContext', {optionsContext}); } + /** */ + _onDictionaryEnabled() { + this._invoke('updateSearch', {}); + } + /** * @param {import('settings-controller').EventArgument<'optionsChanged'>} details */ diff --git a/ext/js/pages/settings/popup-preview-frame.js b/ext/js/pages/settings/popup-preview-frame.js index a0e46d8e33..dba5cb1fd6 100644 --- a/ext/js/pages/settings/popup-preview-frame.js +++ b/ext/js/pages/settings/popup-preview-frame.js @@ -69,6 +69,7 @@ export class PopupPreviewFrame { ['setCustomOuterCss', this._setCustomOuterCss.bind(this)], ['updateOptionsContext', this._updateOptionsContext.bind(this)], ['setLanguageExampleText', this._setLanguageExampleText.bind(this)], + ['updateSearch', this._updateSearch.bind(this)], ]); /* eslint-enable @stylistic/no-multi-spaces */ } diff --git a/test/options-util.test.js b/test/options-util.test.js index c6abc1ac4d..9691173809 100644 --- a/test/options-util.test.js +++ b/test/options-util.test.js @@ -258,7 +258,7 @@ function createProfileOptionsUpdatedTestData1() { resultOutputMode: 'group', debugInfo: false, maxResults: 32, - fontFamily: 'sans-serif', + fontFamily: '', fontSize: 14, lineHeight: '1.5', showAdvanced: false, @@ -636,7 +636,7 @@ function createOptionsUpdatedTestData1() { }, ], profileCurrent: 0, - version: 45, + version: 46, global: { database: { prefixWildcardsSupported: false, diff --git a/test/playwright/visual.spec.js b/test/playwright/visual.spec.js index e5833f71f9..1e29a89866 100644 --- a/test/playwright/visual.spec.js +++ b/test/playwright/visual.spec.js @@ -48,6 +48,20 @@ test('visual', async ({page, extensionId}) => { // Wait for the advanced settings to be visible await page.locator('input#advanced-checkbox').evaluate((/** @type {HTMLInputElement} */ element) => element.click()); + // Import jmdict_swedish.zip from a URL + await page.locator('.settings-item[data-modal-action="show,dictionaries"]').click(); + await page.locator('button[id="dictionary-import-button"]').click(); + await page.locator('textarea[id="dictionary-import-url-text"]').fill('https://github.com/themoeway/yomitan/raw/dictionaries/jmdict_swedish.zip'); + await page.locator('button[id="dictionary-import-url-button"]').click(); + await expect(page.locator('id=dictionaries')).toHaveText('Dictionaries (2 installed, 2 enabled)', {timeout: 5 * 60 * 1000}); + + // Delete the jmdict_swedish dictionary + await page.locator('button.dictionary-menu-button').nth(1).click(); + await page.locator('button.popup-menu-item[data-menu-action="delete"]').click(); + await page.locator('#dictionary-confirm-delete-button').click(); + await page.locator('#dictionaries-modal button[data-modal-action="hide"]').getByText('Close').click(); + await expect(page.locator('id=dictionaries')).toHaveText('Dictionaries (1 installed, 1 enabled)', {timeout: 5 * 60 * 1000}); + // Get page height by getting the footer and adding height and y position as other methods of calculation don't work for some reason const footer = /** @type {import('@playwright/test').ElementHandle} */ (await page.locator('.footer-padding').elementHandle()); expect(footer).not.toBe(null); diff --git a/types/ext/language-descriptors.d.ts b/types/ext/language-descriptors.d.ts index a88f0f6611..426da54202 100644 --- a/types/ext/language-descriptors.d.ts +++ b/types/ext/language-descriptors.d.ts @@ -62,6 +62,10 @@ type CapitalizationPreprocessors = { decapitalize: TextProcessor; }; +type AlphabeticDiacriticsProcessor = { + removeAlphabeticDiacritics: TextProcessor; +}; + /** * This is a mapping of the iso tag to all of the text processors for that language. * Any new language should be added to this object. @@ -98,9 +102,7 @@ type AllTextProcessors = { pre: CapitalizationPreprocessors; }; grc: { - pre: CapitalizationPreprocessors & { - removeAlphabeticDiacritics: TextProcessor; - }; + pre: CapitalizationPreprocessors & AlphabeticDiacriticsProcessor; }; hu: { pre: CapitalizationPreprocessors; @@ -109,12 +111,10 @@ type AllTextProcessors = { pre: CapitalizationPreprocessors; }; it: { - pre: CapitalizationPreprocessors; + pre: CapitalizationPreprocessors & AlphabeticDiacriticsProcessor; }; la: { - pre: CapitalizationPreprocessors & { - removeAlphabeticDiacritics: TextProcessor; - }; + pre: CapitalizationPreprocessors & AlphabeticDiacriticsProcessor; }; lo: Record; ja: { @@ -149,9 +149,7 @@ type AllTextProcessors = { pre: CapitalizationPreprocessors; }; ro: { - pre: CapitalizationPreprocessors & { - removeAlphabeticDiacritics: TextProcessor; - }; + pre: CapitalizationPreprocessors & AlphabeticDiacriticsProcessor; }; ru: { pre: CapitalizationPreprocessors & { @@ -160,9 +158,7 @@ type AllTextProcessors = { }; }; sga: { - pre: CapitalizationPreprocessors & { - removeAlphabeticDiacritics: TextProcessor; - }; + pre: CapitalizationPreprocessors & AlphabeticDiacriticsProcessor; }; sh: { pre: CapitalizationPreprocessors & { @@ -177,9 +173,7 @@ type AllTextProcessors = { }; th: Record; tl: { - pre: CapitalizationPreprocessors & { - removeAlphabeticDiacritics: TextProcessor; - }; + pre: CapitalizationPreprocessors & AlphabeticDiacriticsProcessor; }; tr: { pre: CapitalizationPreprocessors; diff --git a/types/ext/language-transformer.d.ts b/types/ext/language-transformer.d.ts index cf5a004106..2bf57ec699 100644 --- a/types/ext/language-transformer.d.ts +++ b/types/ext/language-transformer.d.ts @@ -15,16 +15,18 @@ * along with this program. If not, see . */ -export type LanguageTransformDescriptor = { +export type LanguageTransformDescriptor = { language: string; - conditions: ConditionMapObject; - transforms: { - [name: string]: Transform; - }; + conditions: ConditionMapObject; + transforms: TransformMapObject; }; -export type ConditionMapObject = { - [type: string]: Condition; +export type ConditionMapObject = { + [type in TCondition]: Condition; +}; + +export type TransformMapObject = { + [name: string]: Transform; }; export type ConditionMapEntry = [type: string, condition: Condition]; @@ -43,11 +45,11 @@ export type RuleI18n = { name: string; }; -export type Transform = { +export type Transform = { name: string; description?: string; i18n?: TransformI18n[]; - rules: Rule[]; + rules: Rule[]; }; export type TransformI18n = { @@ -56,19 +58,19 @@ export type TransformI18n = { description?: string; }; -export type Rule = { +export type Rule = { type: 'suffix' | 'prefix' | 'wholeWord' | 'other'; isInflected: RegExp; deinflect: (inflectedWord: string) => string; - conditionsIn: string[]; - conditionsOut: string[]; + conditionsIn: TCondition[]; + conditionsOut: TCondition[]; }; -export type SuffixRule = { +export type SuffixRule = { type: 'suffix'; isInflected: RegExp; deinflected: string; deinflect: (inflectedWord: string) => string; - conditionsIn: string[]; - conditionsOut: string[]; + conditionsIn: TCondition[]; + conditionsOut: TCondition[]; }; diff --git a/types/ext/popup-preview-frame.d.ts b/types/ext/popup-preview-frame.d.ts index 4b4f3009df..51f7747ac8 100644 --- a/types/ext/popup-preview-frame.d.ts +++ b/types/ext/popup-preview-frame.d.ts @@ -55,6 +55,10 @@ export type ApiSurface = { }; return: void; }; + updateSearch: { + params: Record; + return: void; + }; }; export type ApiParams = BaseApiParams; diff --git a/types/ext/settings-controller.d.ts b/types/ext/settings-controller.d.ts index aa6d29499d..7292c49000 100644 --- a/types/ext/settings-controller.d.ts +++ b/types/ext/settings-controller.d.ts @@ -39,6 +39,7 @@ export type Events = { dictionarySettingsReordered: { source: DictionaryController; }; + dictionaryEnabled: Record; scanInputsChanged: { source: ScanInputsController | ScanInputsSimpleController; };