From c286d76b4de4607d81b29624ae8b9255fbc0417c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20Oszcz=C4=99da?= Date: Tue, 9 Jul 2024 23:42:19 +0100 Subject: [PATCH 01/17] feat: Create sections in dictionary constants. --- source/constants/dictionaries.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/source/constants/dictionaries.ts b/source/constants/dictionaries.ts index fb950b353..1bd3a13c2 100644 --- a/source/constants/dictionaries.ts +++ b/source/constants/dictionaries.ts @@ -1,5 +1,23 @@ import type { LearningLanguage } from "logos:constants/languages"; +const sections = [ + "partOfSpeech", + "definitions", + "translations", + "relations", + "syllables", + "pronunciation", + "rhymes", + "audio", + "expressions", + "examples", + "frequency", + "inflection", + "etymology", + "notes", +] as const; +type DictionarySection = keyof typeof sections; + type Dictionary = "dexonline" | "dicolink" | "wiktionary" | "wordnik" | "words-api"; const dictionariesByLanguage = Object.freeze({ @@ -25,4 +43,4 @@ const dictionariesByLanguage = Object.freeze({ } satisfies Record as Record); export default Object.freeze({ languages: dictionariesByLanguage }); -export type { Dictionary }; +export type { Dictionary, DictionarySection }; From c2e531749129a87e04547f722b4d46d4b7292d3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20Oszcz=C4=99da?= Date: Tue, 9 Jul 2024 23:42:34 +0100 Subject: [PATCH 02/17] feat: Add emojis for dictionary sections. --- source/constants/emojis.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/source/constants/emojis.ts b/source/constants/emojis.ts index 79574ed03..5ced105fd 100644 --- a/source/constants/emojis.ts +++ b/source/constants/emojis.ts @@ -55,8 +55,13 @@ export default Object.freeze({ word: { word: "πŸ“œ", definitions: "πŸ“š", + translations: "🌐", + relations: "🌳", + pronunciation: "πŸ—£οΈ", expressions: "πŸ’", - etymology: "🌐", + examples: "🏷️", + etymology: "🌱", + notes: "πŸ“", }, music: { song: "🎡", From 99bbfeca197956157ea5d494f1dd1f2655ad322c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20Oszcz=C4=99da?= Date: Tue, 9 Jul 2024 23:50:54 +0100 Subject: [PATCH 03/17] refactor: Move adapters to subdirectory `adapters/`. --- .../adapters/dictionaries/{ => adapters}/dexonline.ts | 0 .../adapters/dictionaries/{ => adapters}/dicolink.ts | 0 .../adapters/dictionaries/{ => adapters}/wiktionary.ts | 0 .../adapters/dictionaries/{ => adapters}/wordnik.ts | 0 .../adapters/dictionaries/{ => adapters}/words-api.ts | 0 source/library/stores/adapters/dictionaries.ts | 10 +++++----- 6 files changed, 5 insertions(+), 5 deletions(-) rename source/library/adapters/dictionaries/{ => adapters}/dexonline.ts (100%) rename source/library/adapters/dictionaries/{ => adapters}/dicolink.ts (100%) rename source/library/adapters/dictionaries/{ => adapters}/wiktionary.ts (100%) rename source/library/adapters/dictionaries/{ => adapters}/wordnik.ts (100%) rename source/library/adapters/dictionaries/{ => adapters}/words-api.ts (100%) diff --git a/source/library/adapters/dictionaries/dexonline.ts b/source/library/adapters/dictionaries/adapters/dexonline.ts similarity index 100% rename from source/library/adapters/dictionaries/dexonline.ts rename to source/library/adapters/dictionaries/adapters/dexonline.ts diff --git a/source/library/adapters/dictionaries/dicolink.ts b/source/library/adapters/dictionaries/adapters/dicolink.ts similarity index 100% rename from source/library/adapters/dictionaries/dicolink.ts rename to source/library/adapters/dictionaries/adapters/dicolink.ts diff --git a/source/library/adapters/dictionaries/wiktionary.ts b/source/library/adapters/dictionaries/adapters/wiktionary.ts similarity index 100% rename from source/library/adapters/dictionaries/wiktionary.ts rename to source/library/adapters/dictionaries/adapters/wiktionary.ts diff --git a/source/library/adapters/dictionaries/wordnik.ts b/source/library/adapters/dictionaries/adapters/wordnik.ts similarity index 100% rename from source/library/adapters/dictionaries/wordnik.ts rename to source/library/adapters/dictionaries/adapters/wordnik.ts diff --git a/source/library/adapters/dictionaries/words-api.ts b/source/library/adapters/dictionaries/adapters/words-api.ts similarity index 100% rename from source/library/adapters/dictionaries/words-api.ts rename to source/library/adapters/dictionaries/adapters/words-api.ts diff --git a/source/library/stores/adapters/dictionaries.ts b/source/library/stores/adapters/dictionaries.ts index 835790ecd..4734d59cd 100644 --- a/source/library/stores/adapters/dictionaries.ts +++ b/source/library/stores/adapters/dictionaries.ts @@ -2,11 +2,11 @@ import type { Dictionary } from "logos:constants/dictionaries"; import type { LearningLanguage } from "logos:constants/languages"; import { isDefined } from "logos:core/utilities"; import type { DictionaryAdapter } from "logos/adapters/dictionaries/adapter"; -import { DexonlineAdapter } from "logos/adapters/dictionaries/dexonline"; -import { DicolinkAdapter } from "logos/adapters/dictionaries/dicolink"; -import { WiktionaryAdapter } from "logos/adapters/dictionaries/wiktionary"; -import { WordnikAdapter } from "logos/adapters/dictionaries/wordnik.ts"; -import { WordsAPIAdapter } from "logos/adapters/dictionaries/words-api"; +import { DexonlineAdapter } from "logos/adapters/dictionaries/adapters/dexonline"; +import { DicolinkAdapter } from "logos/adapters/dictionaries/adapters/dicolink"; +import { WiktionaryAdapter } from "logos/adapters/dictionaries/adapters/wiktionary"; +import { WordnikAdapter } from "logos/adapters/dictionaries/adapters/wordnik.ts"; +import { WordsAPIAdapter } from "logos/adapters/dictionaries/adapters/words-api"; import type { Client } from "logos/client"; import { Logger } from "logos/logger"; From 764fef9c640cd271064bccae2bfe1032c2ed033b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20Oszcz=C4=99da?= Date: Tue, 9 Jul 2024 23:51:30 +0100 Subject: [PATCH 04/17] feat: Create file for dictionary entry data. --- .../adapters/dictionaries/dictionary-entry.ts | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 source/library/adapters/dictionaries/dictionary-entry.ts diff --git a/source/library/adapters/dictionaries/dictionary-entry.ts b/source/library/adapters/dictionaries/dictionary-entry.ts new file mode 100644 index 000000000..ec6bc2ef8 --- /dev/null +++ b/source/library/adapters/dictionaries/dictionary-entry.ts @@ -0,0 +1,132 @@ +import type { DictionarySection } from "logos:constants/dictionaries.ts"; +import type { Licence } from "logos:constants/licences.ts"; +import type { PartOfSpeech } from "logos:constants/parts-of-speech.ts"; + +type LabelledField = { + labels?: string[]; + value: string; +}; + +type LemmaField = LabelledField; +type PartOfSpeechField = LabelledField & + Partial<{ + detected: PartOfSpeech; + }>; +type MeaningField = LabelledField & + Partial<{ + relations: RelationField; + definitions: DefinitionField[]; + expressions: ExpressionField[]; + examples: ExampleField[]; + }>; +type DefinitionField = MeaningField; +type TranslationField = MeaningField; +type RelationField = Partial<{ + synonyms: string[]; + antonyms: string[]; + diminutives: string[]; + augmentatives: string[]; +}>; +type SyllableField = LabelledField; +type PronunciationField = LabelledField; +type AudioField = LabelledField; +type RhymeField = LabelledField; +type ExpressionField = LabelledField & + Partial<{ + relations: RelationField; + expressions: ExpressionField[]; + examples: ExampleField[]; + }>; +type ExampleField = LabelledField & Partial<{ expressions: ExpressionField[] }>; +type FrequencyField = { value: number }; +type InflectionField = { + tabs: Discord.CamelizedDiscordEmbed[]; +}; +type EtymologyField = LabelledField; +type NoteField = LabelledField; + +interface DictionaryEntrySource { + /** Direct link to the lemma page. */ + link: string; + + /** Licence under which information about the lemma was obtained. */ + licence: Licence; +} + +interface DictionaryEntry extends Record { + /** Sources of information about the lemma. */ + sources: DictionaryEntrySource[]; + + /** Topic word of the dictionary entry. */ + lemma: LemmaField; + + /** Part of speech of the lemma. */ + partOfSpeech: PartOfSpeechField; + + /** Definitions belonging to the lemma. */ + definitions: DefinitionField[]; + + /** Translations of the lemma. */ + translations: TranslationField[]; + + /** Relations between the lemma and other words. */ + relations: RelationField; + + /** Syllable composition of the lemma. */ + syllables: SyllableField; + + /** Pronunciation of the lemma. */ + pronunciation: PronunciationField; + + /** Rhythmic composition of the lemma. */ + rhymes: RhymeField; + + /** Audio example of pronunciation of the lemma. */ + audio: AudioField[]; + + /** Expressions featuring the lemma. */ + expressions: ExpressionField[]; + + /** Examples of the lemma used in a sentence. */ + examples: ExampleField[]; + + /** Indication of how frequently the lemma is used. */ + frequency: FrequencyField; + + /** Inflection of the lemma. */ + inflection: InflectionField; + + /** Origin of the lemma. */ + etymology: EtymologyField; + + /** Additional notes on usage, prevalence, etc. */ + notes: NoteField; +} + +// TODO(vxern): Include. +// type DictionaryEntryField = keyof DictionaryEntry; +// +// const requiredDictionaryEntryFields = ["sources", "lemma"] satisfies DictionaryEntryField[]; +// type RequiredDictionaryEntryFields = (typeof requiredDictionaryEntryFields)[number]; + +export type { + LemmaField, + PartOfSpeechField, + MeaningField, + DefinitionField, + TranslationField, + RelationField, + SyllableField, + PronunciationField, + RhymeField, + AudioField, + ExpressionField, + ExampleField, + FrequencyField, + InflectionField, + EtymologyField, + NoteField, + DictionaryEntrySource, + DictionaryEntry, + LabelledField, +}; From 2b1cf455247521c45bcdaacd7e4efec14feabad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20Oszcz=C4=99da?= Date: Wed, 10 Jul 2024 23:18:39 +0100 Subject: [PATCH 05/17] fix: Type signature of `DictionarySection`. --- source/constants/dictionaries.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/constants/dictionaries.ts b/source/constants/dictionaries.ts index 1bd3a13c2..18375e676 100644 --- a/source/constants/dictionaries.ts +++ b/source/constants/dictionaries.ts @@ -16,7 +16,7 @@ const sections = [ "etymology", "notes", ] as const; -type DictionarySection = keyof typeof sections; +type DictionarySection = (typeof sections)[number]; type Dictionary = "dexonline" | "dicolink" | "wiktionary" | "wordnik" | "words-api"; From 36204446747e9c1833279ee74a54e5f0f405e0c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20Oszcz=C4=99da?= Date: Fri, 12 Jul 2024 21:16:44 +0100 Subject: [PATCH 06/17] misc: Update signature of `getPartOfSpeech()`. --- source/constants/parts-of-speech.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/source/constants/parts-of-speech.ts b/source/constants/parts-of-speech.ts index 44192d9f1..28ed03195 100644 --- a/source/constants/parts-of-speech.ts +++ b/source/constants/parts-of-speech.ts @@ -45,19 +45,20 @@ function isUnknownPartOfSpeech(partOfSpeech: PartOfSpeech): partOfSpeech is "unk return partOfSpeech === "unknown"; } +interface PartOfSpeechDetection { + readonly detected: PartOfSpeech; + readonly original: string; +} function getPartOfSpeech({ terms, learningLanguage, -}: { terms: { exact: string; approximate?: string }; learningLanguage: LearningLanguage }): [ - detected: PartOfSpeech, - original: string, -] { +}: { terms: { exact: string; approximate?: string }; learningLanguage: LearningLanguage }): PartOfSpeechDetection { if (isPartOfSpeech(terms.exact)) { - return [terms.exact, terms.exact]; + return { detected: terms.exact, original: terms.exact }; } if (!(learningLanguage in partsOfSpeechByLanguage)) { - return ["unknown", terms.exact]; + return { detected: "unknown", original: terms.exact }; } const partsOfSpeechLocalised = partsOfSpeechByLanguage[ @@ -65,14 +66,14 @@ function getPartOfSpeech({ ] as Record; if (terms.exact in partsOfSpeechLocalised) { - return [partsOfSpeechLocalised[terms.exact]!, terms.exact]; + return { detected: partsOfSpeechLocalised[terms.exact]!, original: terms.exact }; } if (terms.approximate !== undefined && terms.approximate in partsOfSpeechLocalised) { - return [partsOfSpeechLocalised[terms.approximate]!, terms.approximate]; + return { detected: partsOfSpeechLocalised[terms.approximate]!, original: terms.approximate }; } - return ["unknown", terms.exact]; + return { detected: "unknown", original: terms.exact }; } export { getPartOfSpeech, isUnknownPartOfSpeech }; From 43a18d0220dcfc7a66407dfdc6af2360b34c7bdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20Oszcz=C4=99da?= Date: Fri, 12 Jul 2024 21:17:11 +0100 Subject: [PATCH 07/17] misc: Remove moved type definitions. --- .../library/adapters/dictionaries/adapter.ts | 77 ++----------------- 1 file changed, 5 insertions(+), 72 deletions(-) diff --git a/source/library/adapters/dictionaries/adapter.ts b/source/library/adapters/dictionaries/adapter.ts index 67a891a53..c5545c6e8 100644 --- a/source/library/adapters/dictionaries/adapter.ts +++ b/source/library/adapters/dictionaries/adapter.ts @@ -1,81 +1,14 @@ +import type { DictionarySection } from "logos:constants/dictionaries.ts"; import type { LearningLanguage } from "logos:constants/languages"; -import type { Licence } from "logos:constants/licences"; -import type { PartOfSpeech } from "logos:constants/parts-of-speech"; +import type { DictionaryEntry } from "logos/adapters/dictionaries/dictionary-entry"; import type { Client } from "logos/client"; import { Logger } from "logos/logger"; -type DictionaryProvisions = - /** Provides definitions of a lemma. */ - | "definitions" - /** Provides a lemma's etymology. */ - | "etymology" - /** Provides relations between a lemma and other words. */ - | "relations" - /** Provides words that rhyme with a given lemma. */ - | "rhymes"; - -interface TaggedValue { - tags?: string[]; - value: T; -} - -interface Expression extends TaggedValue {} - -interface Definition extends TaggedValue { - definitions?: Definition[]; - expressions?: Expression[]; - relations?: Relations; -} - -interface Relations { - synonyms?: string[]; - antonyms?: string[]; - diminutives?: string[]; - augmentatives?: string[]; -} - -interface Rhymes extends TaggedValue {} - -interface Etymology extends TaggedValue {} - -type InflectionTable = { title: string; fields: Discord.CamelizedDiscordEmbedField[] }[]; - -interface DictionaryEntry { - /** The topic word of an entry. */ - lemma: string; - - /** The part of speech of the lemma. */ - partOfSpeech: [detected: PartOfSpeech, text: string]; - - /** The definitions for the lemma in its native language. */ - nativeDefinitions?: Definition[]; - - /** The definitions for the lemma. */ - definitions?: Definition[]; - - /** Relations between the lemma and other words. */ - relations?: Relations; - - /** Rhythmic composition of the lemma. */ - rhymes?: Rhymes; - - /** The expressions for the lemma. */ - expressions?: Expression[]; - - /** The etymologies for the lemma. */ - etymologies?: Etymology[]; - - /** The inflection of the lemma. */ - inflectionTable?: InflectionTable; - - sources: [link: string, licence: Licence][]; -} - abstract class DictionaryAdapter { readonly log: Logger; readonly client: Client; readonly identifier: string; - readonly provides: DictionaryProvisions[]; + readonly provides: DictionarySection[]; readonly supports: LearningLanguage[]; readonly isFallback: boolean; @@ -86,7 +19,7 @@ abstract class DictionaryAdapter { provides, supports, isFallback = false, - }: { identifier: string; provides: DictionaryProvisions[]; supports: LearningLanguage[]; isFallback?: boolean }, + }: { identifier: string; provides: DictionarySection[]; supports: LearningLanguage[]; isFallback?: boolean }, ) { this.log = Logger.create({ identifier, isDebug: client.environment.isDebug }); this.client = client; @@ -137,4 +70,4 @@ abstract class DictionaryAdapter { } export { DictionaryAdapter }; -export type { Definition, DictionaryEntry, Relations, Rhymes, Expression, Etymology, DictionaryProvisions }; +export type { DictionaryEntry }; From 2a63d5dcaa0f081a5c2ef77a4d0de1a15c5f7d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20Oszcz=C4=99da?= Date: Fri, 12 Jul 2024 21:17:33 +0100 Subject: [PATCH 08/17] misc: Improve signature of `DictionaryEntry`. --- .../adapters/dictionaries/dictionary-entry.ts | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/source/library/adapters/dictionaries/dictionary-entry.ts b/source/library/adapters/dictionaries/dictionary-entry.ts index ec6bc2ef8..c3e37bb41 100644 --- a/source/library/adapters/dictionaries/dictionary-entry.ts +++ b/source/library/adapters/dictionaries/dictionary-entry.ts @@ -8,10 +8,9 @@ type LabelledField = { }; type LemmaField = LabelledField; -type PartOfSpeechField = LabelledField & - Partial<{ - detected: PartOfSpeech; - }>; +type PartOfSpeechField = LabelledField & { + detected: PartOfSpeech; +}; type MeaningField = LabelledField & Partial<{ relations: RelationField; @@ -53,7 +52,7 @@ interface DictionaryEntrySource { licence: Licence; } -interface DictionaryEntry extends Record { +interface DictionaryEntry extends Partial> { /** Sources of information about the lemma. */ sources: DictionaryEntrySource[]; @@ -61,46 +60,46 @@ interface DictionaryEntry extends Record { lemma: LemmaField; /** Part of speech of the lemma. */ - partOfSpeech: PartOfSpeechField; + partOfSpeech?: PartOfSpeechField; /** Definitions belonging to the lemma. */ - definitions: DefinitionField[]; + definitions?: DefinitionField[]; /** Translations of the lemma. */ - translations: TranslationField[]; + translations?: TranslationField[]; /** Relations between the lemma and other words. */ - relations: RelationField; + relations?: RelationField; /** Syllable composition of the lemma. */ - syllables: SyllableField; + syllables?: SyllableField; /** Pronunciation of the lemma. */ - pronunciation: PronunciationField; + pronunciation?: PronunciationField; /** Rhythmic composition of the lemma. */ - rhymes: RhymeField; + rhymes?: RhymeField; /** Audio example of pronunciation of the lemma. */ - audio: AudioField[]; + audio?: AudioField[]; /** Expressions featuring the lemma. */ - expressions: ExpressionField[]; + expressions?: ExpressionField[]; /** Examples of the lemma used in a sentence. */ - examples: ExampleField[]; + examples?: ExampleField[]; /** Indication of how frequently the lemma is used. */ - frequency: FrequencyField; + frequency?: FrequencyField; /** Inflection of the lemma. */ - inflection: InflectionField; + inflection?: InflectionField; /** Origin of the lemma. */ - etymology: EtymologyField; + etymology?: EtymologyField; /** Additional notes on usage, prevalence, etc. */ - notes: NoteField; + notes?: NoteField; } // TODO(vxern): Include. From 87ca632dbe9d34e998f9a64b103bfb0aabb0c93f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20Oszcz=C4=99da?= Date: Fri, 12 Jul 2024 21:18:08 +0100 Subject: [PATCH 09/17] misc: Update the Dexonline adapter to work with the updated types. --- .../dictionaries/adapters/dexonline.ts | 661 ++++++++++-------- 1 file changed, 350 insertions(+), 311 deletions(-) diff --git a/source/library/adapters/dictionaries/adapters/dexonline.ts b/source/library/adapters/dictionaries/adapters/dexonline.ts index 9ad845a62..d09805b8c 100644 --- a/source/library/adapters/dictionaries/adapters/dexonline.ts +++ b/source/library/adapters/dictionaries/adapters/dexonline.ts @@ -3,11 +3,18 @@ import { type PartOfSpeech, getPartOfSpeech } from "logos:constants/parts-of-spe import * as Dexonline from "dexonline-scraper"; import { DictionaryAdapter, type DictionaryEntry } from "logos/adapters/dictionaries/adapter"; import type { Client } from "logos/client"; - -type InflectionTable = NonNullable; +import type { + DefinitionField, + EtymologyField, + ExampleField, + ExpressionField, + InflectionField, + PartOfSpeechField, +} from "logos/adapters/dictionaries/dictionary-entry.ts"; +import { code } from "logos:core/formatting.ts"; class DexonlineAdapter extends DictionaryAdapter { - static readonly #classesWithInflections: PartOfSpeech[] = ["pronoun", "noun", "verb", "adjective", "determiner"]; + static readonly #supportedPartsOfSpeech: PartOfSpeech[] = ["pronoun", "noun", "verb", "adjective", "determiner"]; static readonly #futureAuxiliaryForms = ["voi", "vei", "va", "vom", "veΘ›i", "vor"]; static readonly #presumptiveAuxiliaryForms = ["oi", "Δƒi", "o", "om", "ΔƒΘ›i", "or"]; static readonly #pastAuxiliaryForms = ["am", "ai", "a", "am", "aΘ›i", "au"]; @@ -18,13 +25,21 @@ class DexonlineAdapter extends DictionaryAdapter { constructor(client: Client) { super(client, { identifier: "Dexonline", - provides: ["definitions", "etymology"], + provides: [ + "partOfSpeech", + "definitions", + "relations", + "expressions", + "examples", + "inflection", + "etymology", + ], supports: ["Romanian"], }); } - static #hasInflections(partOfSpeech: PartOfSpeech): boolean { - return DexonlineAdapter.#classesWithInflections.includes(partOfSpeech); + static #isSupported(partOfSpeech: PartOfSpeech): boolean { + return DexonlineAdapter.#supportedPartsOfSpeech.includes(partOfSpeech); } fetch(lemma: string, _: LearningLanguage): Promise { @@ -34,7 +49,7 @@ class DexonlineAdapter extends DictionaryAdapter { parse( interaction: Logos.Interaction, _: string, - __: LearningLanguage, + learningLanguage: LearningLanguage, results: Dexonline.Results, ): DictionaryEntry[] { const entries: DictionaryEntry[] = []; @@ -50,94 +65,144 @@ class DexonlineAdapter extends DictionaryAdapter { }); entries.push({ - lemma: result.lemma, - partOfSpeech, - nativeDefinitions: result.definitions, - etymologies: result.etymology, - expressions: result.expressions, - inflectionTable: undefined, + lemma: { value: result.lemma }, + partOfSpeech: { value: partOfSpeech.detected, detected: partOfSpeech.detected }, + definitions: result.definitions.map(DexonlineAdapter.#transformDefinition), + etymology: DexonlineAdapter.#transformEtymology(result.etymology), + expressions: result.expressions.map(DexonlineAdapter.#transformExpression), + examples: result.examples.map(DexonlineAdapter.#transformExample), + inflection: undefined, sources: [ - [constants.links.dexonlineDefinition(result.lemma), constants.licences.dictionaries.dexonline], + { + link: constants.links.dexonlineDefinition(result.lemma), + licence: constants.licences.dictionaries.dexonline, + }, ], }); } for (const { table } of results.inflection) { - const partsOfSpeechRaw = table.at(0)?.at(0)?.split("(").at(0)?.trim().split(" / "); - if (partsOfSpeechRaw === undefined) { + const partsOfSpeech = table.at(0)?.at(0)?.split("(").at(0)?.trim().split(" / "); + if (partsOfSpeech === undefined) { continue; } - const partsOfSpeech: [detected: PartOfSpeech, original: string][] = []; - for (const partOfSpeechRaw of partsOfSpeechRaw) { - const [topicWord] = partOfSpeechRaw.split(" "); - if (topicWord === undefined) { - continue; - } - - const partOfSpeech = getPartOfSpeech({ - terms: { exact: partOfSpeechRaw, approximate: topicWord }, - learningLanguage: "Romanian", + const partOfSpeechFields: PartOfSpeechField[] = []; + for (const partOfSpeech of partsOfSpeech) { + const [partOfSpeechFuzzy] = partOfSpeech.split(" "); + const detection = getPartOfSpeech({ + terms: { exact: partOfSpeech, approximate: partOfSpeechFuzzy }, + learningLanguage, }); - if (partOfSpeech === undefined) { - continue; - } - - const [detected, _] = partOfSpeech; - if (!DexonlineAdapter.#hasInflections(detected)) { + if (detection === undefined || !DexonlineAdapter.#isSupported(detection.detected)) { continue; } - partsOfSpeech.push(partOfSpeech); + partOfSpeechFields.push({ value: partOfSpeech, detected: detection.detected }); } - if (partsOfSpeech.length === 0) { + if (partOfSpeechFields.length === 0) { continue; } const entry = entries.find((entry) => - partsOfSpeech.some( - (partOfSpeech) => - entry.partOfSpeech[1] === partOfSpeech[1] || entry.partOfSpeech[0] === partOfSpeech[0], + partOfSpeechFields.some( + (partOfSpeechField) => + entry.partOfSpeech !== undefined && + (entry.partOfSpeech.detected === partOfSpeechField.detected || + entry.partOfSpeech.value === partOfSpeechField.value), ), ); - if (entry === undefined) { + if (entry === undefined || entry.partOfSpeech === undefined) { + continue; + } + + const inflectionField = this.#transformInflection(interaction, { + partOfSpeech: entry.partOfSpeech.detected, + table, + }); + if (inflectionField === undefined) { continue; } - entry.inflectionTable = this.tableRowsToFields(interaction, entry.partOfSpeech[0], table); + entry.inflection = inflectionField; } return entries; } - private tableRowsToFields( + static #transformDefinition(definition: Dexonline.Synthesis.Definition): DefinitionField { + return { + labels: definition.tags, + value: definition.value, + definitions: definition.definitions.map((definition) => DexonlineAdapter.#transformDefinition(definition)), + expressions: definition.expressions.map((expression) => DexonlineAdapter.#transformExpression(expression)), + examples: definition.examples.map((example) => DexonlineAdapter.#transformExample(example)), + relations: definition.relations, + }; + } + + static #transformExpression(expression: Dexonline.Synthesis.Expression): ExpressionField { + return { + labels: expression.tags, + value: expression.value, + expressions: expression.expressions.map((expression) => DexonlineAdapter.#transformExpression(expression)), + examples: expression.examples.map((example) => DexonlineAdapter.#transformExample(example)), + relations: expression.relations, + }; + } + + static #transformExample(example: Dexonline.Synthesis.Example): ExampleField { + return { + labels: example.tags, + value: example.value, + }; + } + + static #transformEtymology(etymology: Dexonline.Synthesis.Etymology[]): EtymologyField { + return { + value: etymology + .map((etymology) => { + const labels = etymology.tags.map((tag) => code(tag)).join(" "); + return `${labels} ${etymology.value}`; + }) + .join("\n"), + }; + } + + #transformInflection( interaction: Logos.Interaction, - partOfSpeech: PartOfSpeech, - table: string[][], - ): InflectionTable | undefined { + { + partOfSpeech, + table, + }: { + partOfSpeech: PartOfSpeech; + table: string[][]; + }, + ): InflectionField | undefined { switch (partOfSpeech) { case "pronoun": { - return this.pronounTableToFields(interaction, table); + return this.#pronounTableToFields(interaction, { table }); } case "noun": { - return this.nounTableToFields(interaction, table); + return this.#nounTableToFields(interaction, { table }); } case "verb": { - return this.verbTableToFields(interaction, table); - } - case "adjective": { - return this.adjectiveTableToFields(interaction, table); + return this.#verbTableToFields(interaction, { table }); } + case "adjective": case "determiner": { - return this.determinerTableToFields(interaction, table); + return this.#adjectiveTableToFields(interaction, { table }); } } - return []; + return undefined; } - private pronounTableToFields(interaction: Logos.Interaction, table: string[][]): InflectionTable | undefined { + #pronounTableToFields( + interaction: Logos.Interaction, + { table }: { table: string[][] }, + ): InflectionField | undefined { const [nominativeAccusative, genitiveDative] = table .slice(1) .map((columns) => columns.slice(2).join(", ")) @@ -150,31 +215,33 @@ class DexonlineAdapter extends DictionaryAdapter { localise: this.client.localise.bind(this.client), locale: interaction.parameters.show ? interaction.guildLocale : interaction.locale, }); - return [ - { - title: strings.title, - fields: [ - { - name: constants.special.meta.whitespace, - value: `**${strings.singular}**\n**${strings.plural}**`, - inline: true, - }, - { - name: strings.nominativeAccusative, - value: nominativeAccusative.join("\n"), - inline: true, - }, - { - name: strings.genitiveDative, - value: genitiveDative.map((part) => part.split(", ").at(0)).join("\n"), - inline: true, - }, - ], - }, - ]; + return { + tabs: [ + { + title: strings.title, + fields: [ + { + name: constants.special.meta.whitespace, + value: `**${strings.singular}**\n**${strings.plural}**`, + inline: true, + }, + { + name: strings.nominativeAccusative, + value: nominativeAccusative.join("\n"), + inline: true, + }, + { + name: strings.genitiveDative, + value: genitiveDative.map((part) => part.split(", ").at(0)).join("\n"), + inline: true, + }, + ], + }, + ], + }; } - private nounTableToFields(interaction: Logos.Interaction, table: string[][]): InflectionTable | undefined { + #nounTableToFields(interaction: Logos.Interaction, { table }: { table: string[][] }): InflectionField | undefined { const [nominativeAccusative, genitiveDative, vocative] = table .slice(1) .map((columns) => columns.slice(2)) @@ -203,37 +270,39 @@ class DexonlineAdapter extends DictionaryAdapter { value: `**${strings.singular}**\n**${strings.plural}**`, inline: true, }; - return [ - { - title: strings.title, - fields: [ - numberColumn, - { - name: strings.nominativeAccusative, - value: nominativeAccusative.map((terms) => terms.join(", ")).join("\n"), - inline: true, - }, - { - name: strings.genitiveDative, - value: genitiveDative.map((terms) => terms.join(", ")).join("\n"), - inline: true, - }, - ...(vocative !== undefined - ? [ - numberColumn, - { - name: strings.vocative, - value: vocative.map((terms) => terms.join(", ")).join("\n"), - inline: true, - }, - ] - : []), - ], - }, - ]; + return { + tabs: [ + { + title: strings.title, + fields: [ + numberColumn, + { + name: strings.nominativeAccusative, + value: nominativeAccusative.map((terms) => terms.join(", ")).join("\n"), + inline: true, + }, + { + name: strings.genitiveDative, + value: genitiveDative.map((terms) => terms.join(", ")).join("\n"), + inline: true, + }, + ...(vocative !== undefined + ? [ + numberColumn, + { + name: strings.vocative, + value: vocative.map((terms) => terms.join(", ")).join("\n"), + inline: true, + }, + ] + : []), + ], + }, + ], + }; } - private verbTableToFields(interaction: Logos.Interaction, table: string[][]): InflectionTable | undefined { + #verbTableToFields(interaction: Logos.Interaction, { table }: { table: string[][] }): InflectionField | undefined { const moods = table .slice(2, 3) .map((columns) => columns.slice(2)) @@ -304,156 +373,161 @@ class DexonlineAdapter extends DictionaryAdapter { localise: this.client.localise.bind(this.client), locale: interaction.locale, }); - return [ - { - title: strings.moodsAndParticiples.title, - fields: [ - { - name: strings.moodsAndParticiples.infinitive, - value: `a ${infinitive}`, - inline: true, - }, - { - name: strings.moodsAndParticiples.longInfinitive, - value: longInfinitive, - inline: true, - }, - { - name: strings.moodsAndParticiples.imperative, - value: imperative, - inline: true, - }, - { - name: strings.moodsAndParticiples.supine, - value: supine, - inline: true, - }, - { - name: strings.moodsAndParticiples.present, - value: presentParticiple, - inline: true, - }, - { - name: strings.moodsAndParticiples.past, - value: pastParticiple, - inline: true, - }, - ], - }, - { - title: strings.indicative.title, - fields: [ - { - name: strings.indicative.present, - value: present.join("\n"), - inline: true, - }, - { - name: strings.indicative.preterite, - value: simplePerfect.join("\n"), - inline: true, - }, - { - name: strings.indicative.imperfect, - value: imperfect.join("\n"), - inline: true, - }, - { - name: strings.indicative.pluperfect, - value: pluperfect.join("\n"), - inline: true, - }, - { - name: strings.indicative.perfect, - value: indicativePerfect.join("\n"), - inline: true, - }, - { - name: strings.indicative.futureCertain, - value: indicativeFutureCertain.join("\n"), - inline: true, - }, - { - name: strings.indicative.futurePlanned, - value: indicativeFuturePlanned.join("\n"), - inline: true, - }, - { - name: strings.indicative.futureDecided, - value: indicativeFutureDecided.join("\n"), - inline: true, - }, - { - name: `${strings.indicative.futureIntended} (${strings.indicative.popular})`, - value: indicativeFutureIntended.join("\n"), - inline: true, - }, - { - name: strings.indicative.futureInThePast, - value: indicativeFutureInThePast.join("\n"), - inline: true, - }, - { - name: strings.indicative.futurePerfect, - value: indicativeFuturePerfect.join("\n"), - inline: true, - }, - ], - }, - { - title: strings.subjunctive.title, - fields: [ - { - name: strings.subjunctive.present, - value: subjunctivePresent.join("\n"), - inline: true, - }, - { - name: strings.subjunctive.perfect, - value: subjunctivePerfect, - inline: true, - }, - ], - }, - { - title: strings.conditional.title, - fields: [ - { - name: strings.conditional.present, - value: conditionalPresent.join("\n"), - inline: true, - }, - { - name: strings.conditional.perfect, - value: conditionalPerfect.join("\n"), - inline: true, - }, - ], - }, - { - title: strings.presumptive.title, - fields: [ - { - name: strings.presumptive.present, - value: presumptivePresent.join("\n"), - inline: true, - }, - { - name: strings.presumptive.presentContinuous, - value: presumptivePresentProgressive.join("\n"), - inline: true, - }, - { - name: strings.presumptive.perfect, - value: presumptivePerfect.join("\n"), - inline: true, - }, - ], - }, - ]; + return { + tabs: [ + { + title: strings.moodsAndParticiples.title, + fields: [ + { + name: strings.moodsAndParticiples.infinitive, + value: `a ${infinitive}`, + inline: true, + }, + { + name: strings.moodsAndParticiples.longInfinitive, + value: longInfinitive, + inline: true, + }, + { + name: strings.moodsAndParticiples.imperative, + value: imperative, + inline: true, + }, + { + name: strings.moodsAndParticiples.supine, + value: supine, + inline: true, + }, + { + name: strings.moodsAndParticiples.present, + value: presentParticiple, + inline: true, + }, + { + name: strings.moodsAndParticiples.past, + value: pastParticiple, + inline: true, + }, + ], + }, + { + title: strings.indicative.title, + fields: [ + { + name: strings.indicative.present, + value: present.join("\n"), + inline: true, + }, + { + name: strings.indicative.preterite, + value: simplePerfect.join("\n"), + inline: true, + }, + { + name: strings.indicative.imperfect, + value: imperfect.join("\n"), + inline: true, + }, + { + name: strings.indicative.pluperfect, + value: pluperfect.join("\n"), + inline: true, + }, + { + name: strings.indicative.perfect, + value: indicativePerfect.join("\n"), + inline: true, + }, + { + name: strings.indicative.futureCertain, + value: indicativeFutureCertain.join("\n"), + inline: true, + }, + { + name: strings.indicative.futurePlanned, + value: indicativeFuturePlanned.join("\n"), + inline: true, + }, + { + name: strings.indicative.futureDecided, + value: indicativeFutureDecided.join("\n"), + inline: true, + }, + { + name: `${strings.indicative.futureIntended} (${strings.indicative.popular})`, + value: indicativeFutureIntended.join("\n"), + inline: true, + }, + { + name: strings.indicative.futureInThePast, + value: indicativeFutureInThePast.join("\n"), + inline: true, + }, + { + name: strings.indicative.futurePerfect, + value: indicativeFuturePerfect.join("\n"), + inline: true, + }, + ], + }, + { + title: strings.subjunctive.title, + fields: [ + { + name: strings.subjunctive.present, + value: subjunctivePresent.join("\n"), + inline: true, + }, + { + name: strings.subjunctive.perfect, + value: subjunctivePerfect, + inline: true, + }, + ], + }, + { + title: strings.conditional.title, + fields: [ + { + name: strings.conditional.present, + value: conditionalPresent.join("\n"), + inline: true, + }, + { + name: strings.conditional.perfect, + value: conditionalPerfect.join("\n"), + inline: true, + }, + ], + }, + { + title: strings.presumptive.title, + fields: [ + { + name: strings.presumptive.present, + value: presumptivePresent.join("\n"), + inline: true, + }, + { + name: strings.presumptive.presentContinuous, + value: presumptivePresentProgressive.join("\n"), + inline: true, + }, + { + name: strings.presumptive.perfect, + value: presumptivePerfect.join("\n"), + inline: true, + }, + ], + }, + ], + }; } - private adjectiveTableToFields(interaction: Logos.Interaction, table: string[][]): InflectionTable | undefined { + #adjectiveTableToFields( + interaction: Logos.Interaction, + { table }: { table: string[][] }, + ): InflectionField | undefined { const [nominativeAccusative, genitiveDative] = table .slice(2) .map((columns) => columns.slice(2, 8)) @@ -466,65 +540,30 @@ class DexonlineAdapter extends DictionaryAdapter { localise: this.client.localise.bind(this.client), locale: interaction.locale, }); - return [ - { - title: strings.title, - fields: [ - { - name: constants.special.meta.whitespace, - value: `**${strings.singular}**\n**${strings.plural}**`, - inline: true, - }, - { - name: strings.nominativeAccusative, - value: nominativeAccusative.map((terms) => terms.join(", ")).join("\n"), - inline: true, - }, - { - name: strings.genitiveDative, - value: genitiveDative.map((terms) => terms.join(", ")).join("\n"), - inline: true, - }, - ], - }, - ]; - } - - private determinerTableToFields(interaction: Logos.Interaction, table: string[][]): InflectionTable | undefined { - const [nominativeAccusative, genitiveDative] = table - .slice(2) - .map((columns) => columns.slice(2, 8)) - .toChunked(2); - if (nominativeAccusative === undefined || genitiveDative === undefined) { - return undefined; - } - - const strings = constants.contexts.dexonlineDeterminer({ - localise: this.client.localise.bind(this.client), - locale: interaction.locale, - }); - return [ - { - title: strings.title, - fields: [ - { - name: constants.special.meta.whitespace, - value: `**${strings.singular}**\n**${strings.plural}**`, - inline: true, - }, - { - name: strings.nominativeAccusative, - value: nominativeAccusative.map((terms) => terms.join(", ")).join("\n"), - inline: true, - }, - { - name: strings.genitiveDative, - value: genitiveDative.map((terms) => terms.join(", ")).join("\n"), - inline: true, - }, - ], - }, - ]; + return { + tabs: [ + { + title: strings.title, + fields: [ + { + name: constants.special.meta.whitespace, + value: `**${strings.singular}**\n**${strings.plural}**`, + inline: true, + }, + { + name: strings.nominativeAccusative, + value: nominativeAccusative.map((terms) => terms.join(", ")).join("\n"), + inline: true, + }, + { + name: strings.genitiveDative, + value: genitiveDative.map((terms) => terms.join(", ")).join("\n"), + inline: true, + }, + ], + }, + ], + }; } } From 28960901b60e2142a1815c77a78062b5f87d1cf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20Oszcz=C4=99da?= Date: Fri, 12 Jul 2024 21:26:37 +0100 Subject: [PATCH 10/17] misc: Update the Dicolink adapter to work with the updated types. --- .../dictionaries/adapters/dexonline.ts | 4 +- .../dictionaries/adapters/dicolink.ts | 60 ++++++++----------- 2 files changed, 28 insertions(+), 36 deletions(-) diff --git a/source/library/adapters/dictionaries/adapters/dexonline.ts b/source/library/adapters/dictionaries/adapters/dexonline.ts index d09805b8c..c33367204 100644 --- a/source/library/adapters/dictionaries/adapters/dexonline.ts +++ b/source/library/adapters/dictionaries/adapters/dexonline.ts @@ -1,8 +1,8 @@ import type { LearningLanguage } from "logos:constants/languages"; import { type PartOfSpeech, getPartOfSpeech } from "logos:constants/parts-of-speech"; +import { code } from "logos:core/formatting.ts"; import * as Dexonline from "dexonline-scraper"; import { DictionaryAdapter, type DictionaryEntry } from "logos/adapters/dictionaries/adapter"; -import type { Client } from "logos/client"; import type { DefinitionField, EtymologyField, @@ -11,7 +11,7 @@ import type { InflectionField, PartOfSpeechField, } from "logos/adapters/dictionaries/dictionary-entry.ts"; -import { code } from "logos:core/formatting.ts"; +import type { Client } from "logos/client"; class DexonlineAdapter extends DictionaryAdapter { static readonly #supportedPartsOfSpeech: PartOfSpeech[] = ["pronoun", "noun", "verb", "adjective", "determiner"]; diff --git a/source/library/adapters/dictionaries/adapters/dicolink.ts b/source/library/adapters/dictionaries/adapters/dicolink.ts index bf5243082..04af4dd41 100644 --- a/source/library/adapters/dictionaries/adapters/dicolink.ts +++ b/source/library/adapters/dictionaries/adapters/dicolink.ts @@ -1,6 +1,6 @@ import type { LearningLanguage } from "logos:constants/languages"; import { getPartOfSpeech } from "logos:constants/parts-of-speech"; -import { type Definition, DictionaryAdapter, type DictionaryEntry } from "logos/adapters/dictionaries/adapter"; +import { DictionaryAdapter, type DictionaryEntry } from "logos/adapters/dictionaries/adapter.ts"; import type { Client } from "logos/client"; interface DicolinkResult { @@ -22,7 +22,7 @@ class DicolinkAdapter extends DictionaryAdapter { constructor(client: Client, { token }: { token: string }) { super(client, { identifier: "Dicolink", - provides: ["definitions"], + provides: ["partOfSpeech", "definitions"], supports: ["French"], }); @@ -55,7 +55,7 @@ class DicolinkAdapter extends DictionaryAdapter { } const data = (await response.json()) as Record[]; - const resultsAll = data.map((result: any) => ({ + const results = data.map((result: any) => ({ id: result.id, partOfSpeech: result.nature, source: result.source, @@ -66,58 +66,50 @@ class DicolinkAdapter extends DictionaryAdapter { dicolinkUrl: result.dicolinkUrl, })); - return this.#pickResultsFromBestSource(resultsAll); + return DicolinkAdapter.#pickResultsFromBestSource(results); } parse( _: Logos.Interaction, lemma: string, learningLanguage: LearningLanguage, - resultsAll: DicolinkResult[], + results: DicolinkResult[], ): DictionaryEntry[] { const entries: DictionaryEntry[] = []; - - const sources = resultsAll.map((result) => result.source); - const resultsDistributed = resultsAll.reduce( - (distribution, result) => { - distribution[result.source]?.push(result); - return distribution; - }, - Object.fromEntries(sources.map((source) => [source, []])) as Record, - ); - const results = Object.values(resultsDistributed).reduce((a, b) => { - return a.length > b.length ? a : b; - }); - - for (const result of results) { - const partOfSpeechTopicWord = result.partOfSpeech.split(" ").at(0) ?? result.partOfSpeech; - const partOfSpeech = getPartOfSpeech({ - terms: { exact: result.partOfSpeech, approximate: partOfSpeechTopicWord }, + for (const { partOfSpeech, definition } of results) { + const [partOfSpeechFuzzy] = partOfSpeech.split(" "); + const detection = getPartOfSpeech({ + terms: { exact: partOfSpeech, approximate: partOfSpeechFuzzy }, learningLanguage, }); - const definition: Definition = { value: result.definition }; - const lastEntry = entries.at(-1); - if ( - lastEntry !== undefined && - (lastEntry.partOfSpeech[0] === partOfSpeech[0] || lastEntry.partOfSpeech[1] === partOfSpeech[1]) - ) { - lastEntry.nativeDefinitions?.push(definition); + if (lastEntry !== undefined && lastEntry.partOfSpeech !== undefined) { + if ( + lastEntry.partOfSpeech.detected === detection.detected || + lastEntry.partOfSpeech.value === partOfSpeech + ) { + lastEntry.definitions?.push({ value: definition }); + } continue; } entries.push({ - lemma, - partOfSpeech, - nativeDefinitions: [definition], - sources: [[constants.links.dicolinkDefinition(lemma), constants.licences.dictionaries.dicolink]], + lemma: { value: lemma }, + partOfSpeech: { value: partOfSpeech, detected: detection.detected }, + definitions: [{ value: definition }], + sources: [ + { + link: constants.links.dicolinkDefinition(lemma), + licence: constants.licences.dictionaries.dicolink, + }, + ], }); } return entries; } - #pickResultsFromBestSource(resultsAll: DicolinkResult[]): DicolinkResult[] { + static #pickResultsFromBestSource(resultsAll: DicolinkResult[]): DicolinkResult[] { const sourcesAll = Array.from(new Set(resultsAll.map((result) => result.source)).values()); const sources = sourcesAll.filter((source) => !DicolinkAdapter.#excludedSources.includes(source)); From 8cb66bcb3fc1779c1905605684681959e5e23981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20Oszcz=C4=99da?= Date: Fri, 12 Jul 2024 21:44:57 +0100 Subject: [PATCH 11/17] misc: Update the Wiktionary adapter to work with the updated types. --- .../dictionaries/adapters/wiktionary.ts | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/source/library/adapters/dictionaries/adapters/wiktionary.ts b/source/library/adapters/dictionaries/adapters/wiktionary.ts index 86e308599..eb397945b 100644 --- a/source/library/adapters/dictionaries/adapters/wiktionary.ts +++ b/source/library/adapters/dictionaries/adapters/wiktionary.ts @@ -1,12 +1,7 @@ -import { type LearningLanguage, getFeatureLanguage } from "logos:constants/languages"; +import type { LearningLanguage } from "logos:constants/languages"; import { getWiktionaryLanguageName } from "logos:constants/languages/learning"; import { getPartOfSpeech } from "logos:constants/parts-of-speech"; -import { - type Definition, - DictionaryAdapter, - type DictionaryEntry, - type Etymology, -} from "logos/adapters/dictionaries/adapter"; +import { DictionaryAdapter, type DictionaryEntry } from "logos/adapters/dictionaries/adapter"; import type { Client } from "logos/client"; import * as Wiktionary from "wiktionary-scraper"; @@ -14,7 +9,7 @@ class WiktionaryAdapter extends DictionaryAdapter { constructor(client: Client) { super(client, { identifier: "Wiktionary", - provides: ["definitions", "etymology"], + provides: ["partOfSpeech", "definitions", "translations", "etymology"], supports: [ "Armenian/Eastern", "Armenian/Western", @@ -62,34 +57,36 @@ class WiktionaryAdapter extends DictionaryAdapter { parse( _: Logos.Interaction, - lemma: string, - language: LearningLanguage, + __: string, + learningLanguage: LearningLanguage, results: Wiktionary.Entry[], ): DictionaryEntry[] { + const targetLanguageWiktionary = getWiktionaryLanguageName(learningLanguage); + const entries: DictionaryEntry[] = []; - for (const result of results) { - if (result.partOfSpeech === undefined || result.definitions === undefined) { + for (const { lemma, partOfSpeech, etymology, definitions } of results) { + if (partOfSpeech === undefined || definitions === undefined) { continue; } - const partOfSpeech = getPartOfSpeech({ - terms: { exact: result.partOfSpeech }, - learningLanguage: "English/American", + const [partOfSpeechFuzzy] = partOfSpeech.split(" ").reverse(); + const detection = getPartOfSpeech({ + terms: { exact: partOfSpeech, approximate: partOfSpeechFuzzy }, + learningLanguage, }); - const etymologies: Etymology[] | undefined = - result.etymology !== undefined ? [{ value: result.etymology.paragraphs.join("\n\n") }] : undefined; - const definitions: Definition[] = result.definitions.flatMap((definition) => - definition.fields.map((field) => ({ value: field.value })), - ); + const etymologyContents = etymology !== undefined ? etymology.paragraphs.join("\n\n") : undefined; entries.push({ lemma, - partOfSpeech, - ...(getFeatureLanguage(language) !== "English" ? { definitions } : { nativeDefinitions: definitions }), - etymologies, + partOfSpeech: { value: partOfSpeech, detected: detection.detected }, + definitions: definitions.flatMap(({ fields }) => fields.map((field) => ({ value: field.value }))), + etymology: etymologyContents !== undefined ? { value: etymologyContents } : undefined, sources: [ - [constants.links.wiktionaryDefinition(lemma, language), constants.licences.dictionaries.wiktionary], + { + link: constants.links.wiktionaryDefinition(lemma.value, targetLanguageWiktionary), + licence: constants.licences.dictionaries.wiktionary, + }, ], }); } From 1a242ecf2e2434f0fe21671a6a76266f41025f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20Oszcz=C4=99da?= Date: Fri, 12 Jul 2024 21:53:11 +0100 Subject: [PATCH 12/17] misc: Update the Wordnik adapter to work with the updated types. --- .../adapters/dictionaries/adapters/wordnik.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/source/library/adapters/dictionaries/adapters/wordnik.ts b/source/library/adapters/dictionaries/adapters/wordnik.ts index 816ef1f4a..291f5656a 100644 --- a/source/library/adapters/dictionaries/adapters/wordnik.ts +++ b/source/library/adapters/dictionaries/adapters/wordnik.ts @@ -7,12 +7,12 @@ import { } from "logos/adapters/dictionaries/adapter.ts"; import type { Client } from "logos/client.ts"; -interface Result { +interface WordnikResult { readonly relationshipType: string; readonly words: string[]; } -class WordnikAdapter extends DictionaryAdapter { +class WordnikAdapter extends DictionaryAdapter { readonly token: string; constructor(client: Client, { token }: { token: string }) { @@ -34,7 +34,7 @@ class WordnikAdapter extends DictionaryAdapter { return new WordnikAdapter(client, { token: client.environment.wordnikSecret }); } - async fetch(lemma: string, _: LearningLanguage): Promise { + async fetch(lemma: string, _: LearningLanguage): Promise { const response = await fetch( `${constants.endpoints.wordnik.relatedWords(lemma)}?useCanonical=true&api_key=${this.token}`, { @@ -47,10 +47,10 @@ class WordnikAdapter extends DictionaryAdapter { return undefined; } - return (await response.json()) as Result[]; + return (await response.json()) as WordnikResult[]; } - parse(_: Logos.Interaction, lemma: string, __: LearningLanguage, results: Result[]): DictionaryEntry[] { + parse(_: Logos.Interaction, lemma: string, __: LearningLanguage, results: WordnikResult[]): DictionaryEntry[] { const synonyms: string[] = []; const antonyms: string[] = []; const rhymes: string[] = []; @@ -97,11 +97,15 @@ class WordnikAdapter extends DictionaryAdapter { return [ { - lemma, - partOfSpeech: ["unknown", "unknown"], + lemma: { value: lemma }, relations: relationField, rhymes: rhymeField, - sources: [[constants.links.wordnikDefinitionLink(lemma), constants.licences.dictionaries.wordnik]], + sources: [ + { + link: constants.links.wordnikDefinitionLink(lemma), + licence: constants.licences.dictionaries.wordnik, + }, + ], }, ]; } From 087157943402549b48129cab1ca38e85e7ef7b4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20Oszcz=C4=99da?= Date: Fri, 12 Jul 2024 21:54:45 +0100 Subject: [PATCH 13/17] fix: Type. --- .../adapters/dictionaries/adapters/wordnik.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/source/library/adapters/dictionaries/adapters/wordnik.ts b/source/library/adapters/dictionaries/adapters/wordnik.ts index 291f5656a..a36a3fd6d 100644 --- a/source/library/adapters/dictionaries/adapters/wordnik.ts +++ b/source/library/adapters/dictionaries/adapters/wordnik.ts @@ -1,10 +1,6 @@ import type { LearningLanguage } from "logos:constants/languages.ts"; -import { - DictionaryAdapter, - type DictionaryEntry, - type Relations, - type Rhymes, -} from "logos/adapters/dictionaries/adapter.ts"; +import { DictionaryAdapter, type DictionaryEntry } from "logos/adapters/dictionaries/adapter.ts"; +import type { RelationField, RhymeField } from "logos/adapters/dictionaries/dictionary-entry.ts"; import type { Client } from "logos/client.ts"; interface WordnikResult { @@ -73,7 +69,7 @@ class WordnikAdapter extends DictionaryAdapter { } } - let relationField: Relations | undefined; + let relationField: RelationField | undefined; if (synonyms.length > 0 || antonyms.length > 0) { relationField = {}; @@ -86,7 +82,7 @@ class WordnikAdapter extends DictionaryAdapter { } } - let rhymeField: Rhymes | undefined; + let rhymeField: RhymeField | undefined; if (rhymes.length > 0) { rhymeField = { value: rhymes.join(", ") }; } From 40f347468e42248c0f624e51856c0ff37c942f4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20Oszcz=C4=99da?= Date: Fri, 12 Jul 2024 22:18:28 +0100 Subject: [PATCH 14/17] misc: Update the WordsAPI adapter to work with the updated types. --- .../dictionaries/adapters/words-api.ts | 59 ++++++++++++------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/source/library/adapters/dictionaries/adapters/words-api.ts b/source/library/adapters/dictionaries/adapters/words-api.ts index 0c90f55b6..c893f89d4 100644 --- a/source/library/adapters/dictionaries/adapters/words-api.ts +++ b/source/library/adapters/dictionaries/adapters/words-api.ts @@ -1,23 +1,24 @@ import type { LearningLanguage } from "logos:constants/languages"; import { getPartOfSpeech } from "logos:constants/parts-of-speech"; -import { type Definition, DictionaryAdapter, type DictionaryEntry } from "logos/adapters/dictionaries/adapter"; +import { DictionaryAdapter, type DictionaryEntry } from "logos/adapters/dictionaries/adapter"; import type { Client } from "logos/client"; type SearchResult = { readonly results: { - readonly definition: string; readonly partOfSpeech: string; + readonly definition: string; readonly synonyms?: string[]; readonly typeof?: string[]; readonly derivation?: string[]; }[]; - readonly syllables: { + readonly syllables?: { readonly count: number; readonly list: string[]; }; - readonly pronunciation: { + readonly pronunciation?: Record & { readonly all: string; }; + readonly frequency?: number; }; class WordsAPIAdapter extends DictionaryAdapter { @@ -26,7 +27,7 @@ class WordsAPIAdapter extends DictionaryAdapter { constructor(client: Client, { token }: { token: string }) { super(client, { identifier: "WordsAPI", - provides: ["definitions"], + provides: ["partOfSpeech", "definitions", "relations", "syllables", "pronunciation", "frequency"], supports: ["English/American", "English/British"], isFallback: true, }); @@ -63,32 +64,46 @@ class WordsAPIAdapter extends DictionaryAdapter { learningLanguage: LearningLanguage, searchResult: SearchResult, ): DictionaryEntry[] { + const { results, pronunciation, syllables, frequency } = searchResult; + const entries: DictionaryEntry[] = []; - for (const result of searchResult.results) { - const partOfSpeech = getPartOfSpeech({ - terms: { exact: result.partOfSpeech, approximate: result.partOfSpeech }, + for (const { partOfSpeech, definition, synonyms } of results) { + const [partOfSpeechFuzzy] = partOfSpeech.split(" ").reverse(); + const detection = getPartOfSpeech({ + terms: { exact: partOfSpeech, approximate: partOfSpeechFuzzy }, learningLanguage, }); - const definition: Definition = { value: result.definition }; - if (result.synonyms !== undefined && result.synonyms.length > 0) { - definition.relations = { synonyms: result.synonyms }; - } - const lastEntry = entries.at(-1); - if ( - lastEntry !== undefined && - (lastEntry.partOfSpeech[0] === partOfSpeech[0] || lastEntry.partOfSpeech[1] === partOfSpeech[1]) - ) { - lastEntry.nativeDefinitions?.push(definition); + if (lastEntry !== undefined && lastEntry.partOfSpeech !== undefined) { + if ( + lastEntry.partOfSpeech.detected === detection.detected || + lastEntry.partOfSpeech.value === partOfSpeech + ) { + lastEntry.definitions?.push({ value: definition }); + } continue; } entries.push({ - lemma, - partOfSpeech, - nativeDefinitions: [definition], - sources: [[constants.links.wordsAPIDefinition(), constants.licences.dictionaries.wordsApi]], + lemma: { value: lemma }, + partOfSpeech: { value: partOfSpeech, detected: detection.detected }, + definitions: [{ value: definition, relations: { synonyms } }], + syllables: + syllables !== undefined + ? { labels: [syllables.count.toString()], value: syllables.list.join("|") } + : undefined, + pronunciation: + pronunciation !== undefined && partOfSpeech in pronunciation + ? { labels: ["IPA"], value: pronunciation[partOfSpeech]! } + : undefined, + frequency: frequency !== undefined ? { value: frequency / 5 } : undefined, + sources: [ + { + link: constants.links.wordsAPIDefinition(), + licence: constants.licences.dictionaries.wordsApi, + }, + ], }); } return entries; From 503d96b839f1050ee7981862a16eafcce13cdf5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20Oszcz=C4=99da?= Date: Fri, 12 Jul 2024 22:28:05 +0100 Subject: [PATCH 15/17] misc: Update tests. --- test/source/constants/parts-of-speech.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/source/constants/parts-of-speech.ts b/test/source/constants/parts-of-speech.ts index c8a45a751..9867c6e86 100644 --- a/test/source/constants/parts-of-speech.ts +++ b/test/source/constants/parts-of-speech.ts @@ -14,7 +14,7 @@ describe("isUnknownPartOfSpeech()", () => { describe("getPartOfSpeech()", () => { it("returns the part of speech if the passed exact term is a resolvable part of speech.", () => { - const [detected, original] = getPartOfSpeech({ + const { detected, original } = getPartOfSpeech({ terms: { exact: "proper-noun" satisfies PartOfSpeech }, learningLanguage: "Romanian", }); @@ -23,7 +23,7 @@ describe("getPartOfSpeech()", () => { }); it("returns 'unknown' if the learning language is not supported.", () => { - const [detected, original] = getPartOfSpeech({ + const { detected, original } = getPartOfSpeech({ terms: { exact: "Υ£ΥΈΥ΅Υ‘Υ―Υ‘ΥΆ" }, // 'noun' in Armenian learningLanguage: "Armenian/Eastern", }); @@ -32,7 +32,7 @@ describe("getPartOfSpeech()", () => { }); it("returns the part of speech matched to the exact term in the given language.", () => { - const [detected, original] = getPartOfSpeech({ + const { detected, original } = getPartOfSpeech({ terms: { exact: "rzeczownik" }, learningLanguage: "Polish", }); @@ -44,7 +44,7 @@ describe("getPartOfSpeech()", () => { "returns the part of speech matched to the approximate term in the given language if the exact term has no" + " match.", () => { - const [detected, original] = getPartOfSpeech({ + const { detected, original } = getPartOfSpeech({ terms: { exact: "this.will.not.match", approximate: "proper noun" }, learningLanguage: "English/American", }); @@ -54,7 +54,7 @@ describe("getPartOfSpeech()", () => { ); it("returns 'unknown' if there is no match", () => { - const [detected, original] = getPartOfSpeech({ + const { detected, original } = getPartOfSpeech({ terms: { exact: "this.will.not.match" }, learningLanguage: "English/American", }); From 5a5c846d9f0d78e9fc0c8cb3567c43d599d3e340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20Oszcz=C4=99da?= Date: Sat, 13 Jul 2024 19:36:19 +0100 Subject: [PATCH 16/17] misc: Update `/word` code to use the updated type signatures. --- source/library/commands/handlers/word.ts | 78 ++++++++++++------------ 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/source/library/commands/handlers/word.ts b/source/library/commands/handlers/word.ts index 4023166b4..414904538 100644 --- a/source/library/commands/handlers/word.ts +++ b/source/library/commands/handlers/word.ts @@ -2,10 +2,11 @@ import defaults from "logos:constants/defaults"; import { isLocalisationLanguage } from "logos:constants/languages"; import { type PartOfSpeech, isUnknownPartOfSpeech } from "logos:constants/parts-of-speech"; import { code, trim } from "logos:core/formatting"; -import type { Definition, DictionaryEntry, Expression } from "logos/adapters/dictionaries/adapter"; +import type { DictionaryEntry } from "logos/adapters/dictionaries/adapter"; import type { Client } from "logos/client"; import { InteractionCollector } from "logos/collectors"; import { WordSourceNotice } from "logos/commands/components/source-notices/word-source-notice.ts"; +import type { DefinitionField, ExpressionField } from "logos/adapters/dictionaries/dictionary-entry.ts"; async function handleFindWordAutocomplete( client: Client, @@ -110,7 +111,12 @@ async function handleFindWord( const organised = new Map(); for (const entry of entries) { - const [partOfSpeech, _] = entry.partOfSpeech; + if (entry.partOfSpeech === undefined) { + continue; + } + + const partOfSpeech = entry.partOfSpeech.detected; + if (partOfSpeech === "unknown") { unclassifiedEntries.push(entry); continue; @@ -201,9 +207,11 @@ async function handleFindWord( function sanitiseEntries(entries: DictionaryEntry[]): DictionaryEntry[] { for (const entry of entries) { - for (const etymology of entry.etymologies ?? []) { - etymology.value = etymology.value?.replaceAll("*", "\\*"); + if (entry.etymology === undefined) { + continue; } + + entry.etymology.value = entry.etymology.value?.replaceAll("*", "\\*"); } return entries; } @@ -245,7 +253,7 @@ function generateEmbeds( return entryToEmbeds(client, interaction, entry, data.verbose); } case ContentTabs.Inflection: { - const inflectionTable = entry.inflectionTable?.at(data.inflectionTableIndex); + const inflectionTable = entry.inflection?.tabs?.at(data.inflectionTableIndex); if (inflectionTable === undefined) { return []; } @@ -335,11 +343,11 @@ async function generateButtons( break; } case ContentTabs.Inflection: { - if (entry.inflectionTable === undefined) { + if (entry.inflection === undefined) { return []; } - const rows = entry.inflectionTable.toChunked(5).reverse(); + const rows = entry.inflection.tabs.toChunked(5).reverse(); const button = new InteractionCollector(client, { only: interaction.parameters.show ? [interaction.user.id] : undefined, @@ -348,7 +356,7 @@ async function generateButtons( button.onInteraction(async (buttonPress) => { await client.acknowledge(buttonPress); - if (entry.inflectionTable === undefined) { + if (entry.inflection === undefined) { await displayMenu(client, interaction, data); return; } @@ -361,7 +369,7 @@ async function generateButtons( const [_, indexString] = InteractionCollector.decodeId(customId); const index = Number(indexString); - if (index >= 0 && index <= entry.inflectionTable?.length) { + if (index >= 0 && index <= entry.inflection?.tabs?.length) { data.inflectionTableIndex = index; } @@ -370,7 +378,7 @@ async function generateButtons( await client.registerInteractionCollector(button); - for (const [row, rowIndex] of rows.map<[typeof entry.inflectionTable, number]>((r, i) => [r, i])) { + for (const [row, rowIndex] of rows.map<[typeof entry.inflection.tabs, number]>((r, i) => [r, i])) { const buttons = row.map((table, index) => { const index_ = rowIndex * 5 + index; @@ -434,7 +442,7 @@ async function generateButtons( }); } - if (entry.inflectionTable !== undefined) { + if (entry.inflection !== undefined) { const strings = constants.contexts.inflectionView({ localise: client.localise.bind(client), locale: interaction.displayLocale, @@ -454,7 +462,7 @@ async function generateButtons( const sourceNotice = new WordSourceNotice(client, { interaction, - sources: entry.sources.map(([link, licence]) => `[${licence.name}](${link})`), + sources: entry.sources.map(({ link, licence }) => `[${licence.name}](${link})`), }); await sourceNotice.register(); @@ -485,18 +493,17 @@ function entryToEmbeds( }); partOfSpeechDisplayed = strings.unknown; } else { - const [detected, original] = entry.partOfSpeech; - - if (detected === "unknown") { - partOfSpeechDisplayed = original; + const partOfSpeech = entry.partOfSpeech.detected; + if (partOfSpeech === "unknown") { + partOfSpeechDisplayed = partOfSpeech; } else { const strings = constants.contexts.partOfSpeech({ localise: client.localise.bind(client), locale: interaction.displayLocale, }); - partOfSpeechDisplayed = strings.partOfSpeech(detected); - if (isUnknownPartOfSpeech(detected)) { - partOfSpeechDisplayed += ` β€” '${original}'`; + partOfSpeechDisplayed = strings.partOfSpeech(partOfSpeech); + if (isUnknownPartOfSpeech(partOfSpeech)) { + partOfSpeechDisplayed += ` β€” '${partOfSpeech}'`; } } } @@ -507,8 +514,8 @@ function entryToEmbeds( const embeds: Discord.CamelizedDiscordEmbed[] = []; const fields: Discord.CamelizedDiscordEmbedField[] = []; - if (entry.nativeDefinitions !== undefined && entry.nativeDefinitions.length > 0) { - const definitionsStringified = stringifyEntries(client, interaction, entry.nativeDefinitions, "definitions"); + if (entry.definitions !== undefined && entry.definitions.length > 0) { + const definitionsStringified = stringifyEntries(client, interaction, entry.definitions, "definitions"); const definitionsFitted = fitTextToFieldSize(client, interaction, definitionsStringified, verbose); if (verbose) { @@ -581,20 +588,15 @@ function entryToEmbeds( } } - if (entry.etymologies !== undefined && entry.etymologies.length > 0) { - const etymology = entry.etymologies - .map((etymology) => { - if (etymology.tags === undefined) { - return etymology.value; - } - - if (etymology.value === undefined || etymology.value.length === 0) { - return tagsToString(etymology.tags); - } - - return `${tagsToString(etymology.tags)} ${etymology.value}`; - }) - .join("\n"); + if (entry.etymology !== undefined) { + let etymology: string; + if (entry.etymology.labels === undefined) { + etymology = entry.etymology.value; + } else if (entry.etymology.value === undefined || entry.etymology.value.length === 0) { + etymology = tagsToString(entry.etymology.labels); + } else { + etymology = `${tagsToString(entry.etymology.labels)} ${entry.etymology.value}`; + } const strings = constants.contexts.etymology({ localise: client.localise.bind(client), @@ -634,7 +636,7 @@ function tagsToString(tags: string[]): string { type EntryType = "definitions" | "expressions"; -function isDefinition(_entry: Definition | Expression, entryType: EntryType): _entry is Definition { +function isDefinition(_entry: DefinitionField | ExpressionField, entryType: EntryType): _entry is DefinitionField { return entryType === "definitions"; } @@ -642,7 +644,7 @@ const parenthesesExpression = /\((.+?)\)/g; function stringifyEntries< T extends EntryType, - E extends Definition[] | Expression[] = T extends "definitions" ? Definition[] : Expression[], + E extends DefinitionField[] | ExpressionField[] = T extends "definitions" ? DefinitionField[] : ExpressionField[], >( client: Client, interaction: Logos.Interaction, @@ -671,7 +673,7 @@ function stringifyEntries< entry.value, ); - let anchor = entry.tags === undefined ? value : `${tagsToString(entry.tags)} ${value}`; + let anchor = entry.labels === undefined ? value : `${tagsToString(entry.labels)} ${value}`; if (isDefinition(entry, entryType)) { if (entry.relations !== undefined) { const strings = constants.contexts.wordRelations({ From ed830bd4eecd6dd2b61b49072c1a7c6c4e9a7269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20Oszcz=C4=99da?= Date: Sat, 13 Jul 2024 19:42:04 +0100 Subject: [PATCH 17/17] fix: Duplicated sections and invalid stringification. --- source/library/commands/handlers/word.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/library/commands/handlers/word.ts b/source/library/commands/handlers/word.ts index 414904538..5e2067a63 100644 --- a/source/library/commands/handlers/word.ts +++ b/source/library/commands/handlers/word.ts @@ -509,7 +509,7 @@ function entryToEmbeds( } const partOfSpeechFormatted = `***${partOfSpeechDisplayed}***`; - const word = entry.lemma; + const word = entry.lemma.value; const embeds: Discord.CamelizedDiscordEmbed[] = []; const fields: Discord.CamelizedDiscordEmbedField[] = []; @@ -540,8 +540,8 @@ function entryToEmbeds( } } - if (entry.definitions !== undefined && entry.definitions.length > 0) { - const definitionsStringified = stringifyEntries(client, interaction, entry.definitions, "definitions"); + if (entry.translations !== undefined && entry.translations.length > 0) { + const definitionsStringified = stringifyEntries(client, interaction, entry.translations, "definitions"); const definitionsFitted = fitTextToFieldSize(client, interaction, definitionsStringified, verbose); if (verbose) {