From bc2abe50fa5cc329c343f843c09185b72ba9fca3 Mon Sep 17 00:00:00 2001 From: tecc Date: Tue, 19 Sep 2023 23:47:49 +0200 Subject: [PATCH] feat(rewrite): Start implementing name algorithm --- new/backend/src/locales.ts | 98 ++++++++++++++++++++++++++- new/backend/src/server/v1.ts | 20 ++++-- new/backend/src/server/v1/pronouns.ts | 25 +++++-- 3 files changed, 130 insertions(+), 13 deletions(-) diff --git a/new/backend/src/locales.ts b/new/backend/src/locales.ts index a000058d6..4460f823e 100644 --- a/new/backend/src/locales.ts +++ b/new/backend/src/locales.ts @@ -87,8 +87,8 @@ export function isLocaleActive(localeCode: string): boolean { export interface Pronoun { keys: Array; - description: string; - history: string; + description?: string; + history?: string; source?: string; isNormative: boolean; isPlural: boolean; @@ -99,6 +99,39 @@ export interface Pronoun { smallForm?: string; forms: Record; + + canonicalName: string; +} + +export function parsePronounFromParts( + parts: Array, + forms: Array +): Pronoun | null { + if (parts.length !== forms.length) { + return null; + } + + const pronounForms: Pronoun["forms"] = {}; + for (let i = 0; i < forms.length; i++) { + const form = parts[i].split("|"); + pronounForms[forms[i]] = { + written: form[0], + pronounced: form[1], + }; + } + + return { + forms: pronounForms, + isNormative: false, + keys: [], + isPronounceable: false, + isPlural: false, + isPluralHonorific: false, + canonicalName: `${parts[0]}/${parts[1]}`, + }; +} +export function parsePronounFromString(s: string, forms: Array) { + return parsePronounFromParts(s.split("/"), forms); } export interface PronounExample { @@ -162,7 +195,7 @@ export class Locale { } } - private _config: unknown | null = null; + private _config: any | null = null; private _translations: unknown | null = null; private _pronouns: Array = []; private _pronounsByAlias: Record = {}; @@ -261,6 +294,7 @@ export class Locale { for (const key of obj.keys) { this._pronounsByAlias[key] = i; } + obj.canonicalName = obj.keys[0]; } const examples = await this.readFile( ["pronouns/examples.tsv"], @@ -302,9 +336,67 @@ export class Locale { return this._pronouns[index]; } + public pronounNameVariants(pronoun: Pronoun): Array { + // NOTE(tecc): This is an attempt at decoding the code from classes.js. + // I make no guarantee regarding the accuracy. + const variants: Set = new Set(); + + const firstMorpheme = + pronoun.forms[this.morphemes[0]].written.split("&"); + if (this.morphemes.length === 1) { + return firstMorpheme; + } + + const secondMorpheme = + pronoun.forms[this.morphemes[1]].written.split("&"); + const thirdMorpheme = + this.morphemes.length > 2 + ? pronoun.forms[this.morphemes[2]].written.split("&") + : []; + + let thirdFormMorpheme = pronoun.thirdForm + ? pronoun.forms[pronoun.thirdForm].written.split("&") + : []; + + let thirdMorphemeThreeForms = thirdMorpheme; + if (this.code === "ru" || this.code === "ua") { + thirdMorphemeThreeForms = thirdMorpheme.map((x) => `[-${x}]`); + } + + for (let istr in firstMorpheme) { + const i = istr as unknown as number; + let firstPart = firstMorpheme[i]; + let secondPart = + secondMorpheme[Math.max(i, secondMorpheme.length - 1)]; + if ( + firstPart === secondPart && + thirdMorpheme.length && + !this.config.pronouns.threeForms + ) { + secondPart = + thirdMorpheme[Math.max(i, thirdMorpheme.length - 1)]; + } + let variant = firstPart + "/" + secondPart; + if (this.config.pronouns.threeForms) { + variant += "/" + thirdMorphemeThreeForms[i]; + } else if (pronoun.thirdForm) { + variant += "/" + thirdFormMorpheme[i]; + } + + variants.add(variant); + } + return [...variants]; + } + public pronounName(pronoun: Pronoun, separator: string = ","): string { + return this.pronounNameVariants(pronoun).join(separator); + } + public get examples() { return this._examples; } + public get morphemes() { + return this._morphemes; + } } export function examplesFor( diff --git a/new/backend/src/server/v1.ts b/new/backend/src/server/v1.ts index 80400eddc..5739c8631 100644 --- a/new/backend/src/server/v1.ts +++ b/new/backend/src/server/v1.ts @@ -1,5 +1,11 @@ import { Type } from "@sinclair/typebox"; -import { examplesFor, getLocale, Pronoun, PronounExample } from "#self/locales"; +import { + examplesFor, + getLocale, + Locale, + Pronoun, + PronounExample, +} from "#self/locales"; import pronouns from "#self/server/v1/pronouns"; export type V1AppInstance = AppInstance; @@ -28,7 +34,7 @@ export function error(options: T) { export function replyError( reply: AppReply, - options: T, + options: T ): AppReply { const response = error(options); return reply.status(options.code).send(response); @@ -41,7 +47,8 @@ export const ApiError = { }, UNKNOWN_PRONOUN: { code: 404, - message: "We aren't aware of any such pronoun. Perhaps there's a typo?\nNote that for custom pronouns, you need to specify all necessary forms: https://en.pronouns.page/faq#custom-pronouns", + message: + "We aren't aware of any such pronoun. Perhaps there's a typo?\nNote that for custom pronouns, you need to specify all necessary forms: https://en.pronouns.page/faq#custom-pronouns", }, } satisfies Record; @@ -52,6 +59,8 @@ export const localeSpecific = Type.Object({ export function transformPronoun( pronoun: Pronoun, examples: Array, + locale: Locale, + processName = false ) { const morphemes: Record = {}; const pronunciations: Record = {}; @@ -60,7 +69,7 @@ export function transformPronoun( pronunciations[name] = form.pronounced; } return { - canonicalName: pronoun.keys[0], + canonicalName: pronoun.canonicalName, description: pronoun.description, normative: pronoun.isNormative, morphemes, @@ -74,6 +83,7 @@ export function transformPronoun( smallForm: pronoun.smallForm ?? null, sourcesInfo: pronoun.source ?? null, examples: examplesFor(pronoun, examples), + name: processName ? locale.pronounName(pronoun) : undefined, }; } @@ -83,7 +93,7 @@ export function transformPronoun( * every route is prefixed with the respective locale ID. */ -export const routes = async function(app: AppInstance) { +export const routes = async function (app: AppInstance) { app.register(pronouns); } satisfies AppPluginAsync; export default routes; diff --git a/new/backend/src/server/v1/pronouns.ts b/new/backend/src/server/v1/pronouns.ts index 261a3710f..0567dfec0 100644 --- a/new/backend/src/server/v1/pronouns.ts +++ b/new/backend/src/server/v1/pronouns.ts @@ -4,7 +4,12 @@ import { replyError, transformPronoun, } from "#self/server/v1"; -import { getLocale, PronounExample } from "#self/locales"; +import { + getLocale, + parsePronounFromParts, + parsePronounFromString, + PronounExample, +} from "#self/locales"; import { isNotBlank, parseBool } from "@pronounspage/common/util"; import { Type } from "@sinclair/typebox"; @@ -27,7 +32,8 @@ export const plugin = async function (app: AppInstance) { for (const pronoun of locale.pronouns) { obj[pronoun.keys[0]] = transformPronoun( pronoun, - locale.examples + locale.examples, + locale ); } @@ -64,8 +70,15 @@ export const plugin = async function (app: AppInstance) { return replyError(reply, ApiError.INVALID_LOCALE); } - const key = req.params["*"].split("/").filter(isNotBlank).join("/"); // This is to get rid of any extraneous parts - const found = locale.pronoun(key); + const keyParts: Array = req.params["*"] + .split("/") + .filter(isNotBlank); + const key = keyParts.join("/"); + let found = locale.pronoun(key); + if (found == null) { + found = parsePronounFromParts(keyParts, locale.morphemes); + req.log.info(`${JSON.stringify(keyParts)} - ${found}`); + } if (found == null) { return replyError(reply, ApiError.UNKNOWN_PRONOUN); } @@ -76,7 +89,9 @@ export const plugin = async function (app: AppInstance) { found, examples.length < 1 ? locale.examples - : examples.map(parseExample) + : examples.map(parseExample), + locale, + true ); } );