added adjectives to Nouns and also fixed the the issue with the nouns not saving the gender/number flexibility. Still need to refactor to keep the flexibility for the VerbSelections - (functions arent saved in useStickyState 🤦‍♂️)

This commit is contained in:
lingdocs 2022-04-27 11:38:43 +05:00
parent 3f9b1161d5
commit 06cd3d2f18
18 changed files with 271 additions and 87 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@lingdocs/pashto-inflector",
"version": "2.3.2",
"version": "2.3.3",
"author": "lingdocs.com",
"description": "A Pashto inflection and verb conjugation engine, inculding React components for displaying Pashto text, inflections, and conjugations",
"homepage": "https://verbs.lingdocs.com",

View File

@ -173,18 +173,21 @@ function massageSubjectChange(subject: T.NPSelection | undefined, old: T.EPSelec
}
if (subject.type === "pronoun" && old.predicate.type === "NP" && old.predicate.NP?.type === "noun" && isUnisexNounEntry(old.predicate.NP.entry)) {
const predicate = old.predicate.NP;
const numberAdjusted = predicate.changeNumber
? predicate.changeNumber(personNumber(subject.person))
: predicate;
const fullyAdjusted = numberAdjusted.changeGender
? numberAdjusted.changeGender(personGender(subject.person))
: numberAdjusted;
const adjusted = {
...predicate,
...predicate.numberCanChange ? {
number: personNumber(subject.person),
} : {},
...predicate.genderCanChange ? {
gender: personGender(subject.person),
} : {},
}
return {
...old,
subject,
predicate: {
...old.predicate,
NP: fullyAdjusted,
NP: adjusted,
},
};
}

View File

@ -0,0 +1,84 @@
import * as T from "../../types";
import { useState } from "react";
import AdjectivePicker from "./AdjectivePicker";
function AdjectiveManager(props: {
adjectives: T.AdjectiveSelection[],
entryFeeder: T.EntryFeederSingleType<T.AdjectiveEntry>,
opts: T.TextOptions,
onChange: (adjs: T.AdjectiveSelection[]) => void,
}) {
const [adding, setAdding] = useState<boolean>(false);
function handleChange(i: number) {
return (a: T.AdjectiveSelection | undefined) => {
if (a === undefined) return;
const updated = [...props.adjectives];
updated[i] = a;
props.onChange(
updated.filter((x): x is T.AdjectiveSelection => !!x)
);
};
}
function deleteAdj(i: number) {
return () => {
props.onChange(remove(props.adjectives, i));
};
}
function handleAddNew(a: T.AdjectiveSelection | undefined) {
if (a === undefined) return;
setAdding(false);
props.onChange([
a,
...props.adjectives,
]);
}
// const flippedList = [...props.adjectives];
// flippedList.reverse();
// console.log(props.adjectives);
return <div className="mb-1">
{!!props.adjectives.length && <div className="d-flex flex-row justify-content-between">
<h6>Adjectives</h6>
{!adding ? <h6 onClick={() => setAdding(true)}>+ Adj.</h6> : <div></div>}
</div>}
{adding && <div>
<div className="d-flex flex-row justify-content-between">
<div>Add Adjective</div>
<div onClick={() => setAdding(false)}>Cancel</div>
</div>
<AdjectivePicker
noTitle
adjective={undefined}
entryFeeder={props.entryFeeder}
opts={props.opts}
onChange={handleAddNew}
/>
</div>}
{props.adjectives.map((adj, i) => (
<div className="d-flex flex-row align-items-baseline">
<AdjectivePicker
noTitle
key={`adj${i}`}
adjective={adj}
entryFeeder={props.entryFeeder}
opts={props.opts}
onChange={handleChange(i)}
/>
<div onClick={deleteAdj(i)} className="ml-4">
<div className="fas fa-trash" />
</div>
</div>
))}
{!adding && !props.adjectives.length && <h6 className="clickable" style={{ float: "right" }}>
<div onClick={() => setAdding(true)}>+ Adj.</div>
</h6>}
</div>;
}
function remove<X>(arr: X[], i: number): X[] {
return [
...arr.slice(0, i),
...arr.slice(i + 1),
];
}
export default AdjectiveManager;

View File

@ -6,6 +6,7 @@ function AdjectivePicker(props: {
adjective: T.AdjectiveSelection | undefined,
onChange: (p: T.AdjectiveSelection | undefined) => void,
opts: T.TextOptions,
noTitle?: boolean,
}) {
function onEntrySelect(entry: T.AdjectiveEntry | undefined) {
if (!entry) {
@ -14,7 +15,7 @@ function AdjectivePicker(props: {
props.onChange(makeAdjectiveSelection(entry));
}
return <div style={{ maxWidth: "225px", minWidth: "125px" }}>
<h6>Adjective</h6>
{!props.noTitle && <h6>Adjective</h6>}
<div>
<EntrySelect
value={props.adjective?.entry}

View File

@ -4,9 +4,9 @@ import {
import * as T from "../../types";
import ButtonSelect from "../ButtonSelect";
import InlinePs from "../InlinePs";
// import { useState } from "react";
// import { isFemNounEntry, isPattern1Entry, isPattern2Entry, isPattern3Entry, isPattern4Entry, isPattern5Entry, isPattern6FemEntry } from "../../lib/type-predicates";
import EntrySelect from "../EntrySelect";
import AdjectiveManager from "./AdjectiveManager";
// const filterOptions = [
// {
@ -56,7 +56,7 @@ import EntrySelect from "../EntrySelect";
// }
function NPNounPicker(props: {
entryFeeder: T.EntryFeederSingleType<T.NounEntry>,
entryFeeder: T.EntryFeeder,
noun: T.NounSelection | undefined,
onChange: (p: T.NounSelection | undefined) => void,
opts: T.TextOptions,
@ -76,6 +76,14 @@ function NPNounPicker(props: {
// setPatternFilter(undefined);
// setShowFilter(false);
// }
function handelAdjectivesUpdate(adjectives: T.AdjectiveSelection[]) {
if (props.noun) {
props.onChange({
...props.noun,
adjectives,
});
}
}
return <div style={{ maxWidth: "225px", minWidth: "125px" }}>
{/* {showFilter && <div className="mb-2 text-center">
<div className="d-flex flex-row justify-content-between">
@ -90,11 +98,17 @@ function NPNounPicker(props: {
handleChange={setPatternFilter}
/>
</div>} */}
{props.noun && <AdjectiveManager
adjectives={props.noun?.adjectives}
entryFeeder={props.entryFeeder.adjectives}
opts={props.opts}
onChange={handelAdjectivesUpdate}
/>}
<h6>Noun</h6>
{!(props.noun && props.noun.dynamicComplement) ? <div>
<EntrySelect
value={props.noun?.entry}
entryFeeder={props.entryFeeder}
entryFeeder={props.entryFeeder.nouns}
onChange={onEntrySelect}
name="Noun"
opts={props.opts}
@ -112,30 +126,36 @@ function NPNounPicker(props: {
</div>}
{props.noun && <div className="my-2 d-flex flex-row justify-content-around align-items-center">
<div>
{props.noun.changeGender ? <ButtonSelect
{props.noun.genderCanChange ? <ButtonSelect
small
options={[
{ label: "Masc", value: "masc" },
{ label: "Fem", value: "fem" },
]}
value={props.noun.gender}
handleChange={(g) => {
if (!props.noun || !props.noun.changeGender) return;
props.onChange(props.noun.changeGender(g));
handleChange={(gender) => {
if (!props.noun || !props.noun.genderCanChange) return;
props.onChange({
...props.noun,
gender,
});
}}
/> : props.noun.gender === "masc" ? "Masc." : "Fem."}
</div>
<div>
{props.noun.changeNumber ? <ButtonSelect
{props.noun.numberCanChange ? <ButtonSelect
small
options={[
{ label: "Sing.", value: "singular" },
{ label: "Plur.", value: "plural" },
]}
value={props.noun.number}
handleChange={(n) => {
if (!props.noun || !props.noun.changeNumber) return;
props.onChange(props.noun.changeNumber(n));
handleChange={(number) => {
if (!props.noun || !props.noun.numberCanChange) return;
props.onChange({
...props.noun,
number,
});
}}
/> : props.noun.number === "singular" ? "Sing." : "Plur."}
</div>

View File

@ -93,7 +93,7 @@ function NPPicker(props: {
/>
: npType === "noun"
? <NounPicker
entryFeeder={props.entryFeeder.nouns}
entryFeeder={props.entryFeeder}
noun={(props.np && props.np.type === "noun") ? props.np : undefined}
onChange={props.onChange}
opts={props.opts}

View File

@ -102,23 +102,10 @@ export function makeNounSelection(entry: T.NounEntry, dynamicComplement?: true):
type: "noun",
entry,
gender: isMascNounEntry(entry) ? "masc" : "fem",
genderCanChange: isUnisexNounEntry(entry),
number,
numberCanChange: number === "singular",
adjectives: [],
dynamicComplement,
...isUnisexNounEntry(entry) ? {
changeGender: function(gender: T.Gender): T.NounSelection {
return {
...this,
gender,
};
},
} : {},
...number === "singular" ? {
changeNumber: function(number: T.NounNumber): T.NounSelection {
return {
...this,
number,
};
},
} : {},
};
}

View File

@ -68,7 +68,7 @@ function CompoundDisplay({ info, opts, handleLinkClick }: {
<div className="text-center">{info.type}</div>
<CompoundFormula
a={<div
className={classNames([{ clickable: handleLinkClick }])}
className={classNames([{ clickable: typeof handleLinkClick === "function" }])}
onClick={(handleLinkClick)
// @ts-ignore - thinks there might not be a complement, but there will be
? () => handleLinkClick(info.entry.complement?.ts)

View File

@ -11,10 +11,11 @@ import {
flattenLengths,
} from "./segment";
import { removeAccents } from "../accent-helpers";
import { getEnglishFromRendered, getPashtoFromRendered } from "./np-tools";
export function compileEP(EP: T.EPRendered, form: T.FormVersion): { ps: T.SingleOrLengthOpts<T.PsString[]>, e?: string [] };
export function compileEP(EP: T.EPRendered, form: T.FormVersion, combineLengths: true): { ps: T.PsString[], e?: string [] };
export function compileEP(EP: T.EPRendered, form: T.FormVersion, combineLengths?: true): { ps: T.SingleOrLengthOpts<T.PsString[]>, e?: string [] } {
export function compileEP(EP: T.EPRendered, form: T.FormVersion): { ps: T.SingleOrLengthOpts<T.PsString[]>, e?: string[] };
export function compileEP(EP: T.EPRendered, form: T.FormVersion, combineLengths: true): { ps: T.PsString[], e?: string[] };
export function compileEP(EP: T.EPRendered, form: T.FormVersion, combineLengths?: true): { ps: T.SingleOrLengthOpts<T.PsString[]>, e?: string[] } {
const { kids, NPs } = getSegmentsAndKids(EP, form);
const equative = EP.equative.ps;
const psResult = compilePs({
@ -36,12 +37,12 @@ function getSegmentsAndKids(EP: T.EPRendered, form: T.FormVersion): { kids: Segm
}
return [s];
}
const subject = makeSegment(EP.subject.ps);
const predicate = makeSegment(EP.predicate.ps);
const subject = makeSegment(getPashtoFromRendered(EP.subject));
const predicate = makeSegment(getPashtoFromRendered(EP.predicate));
return {
kids: EP.equative.hasBa
? [makeSegment(grammarUnits.baParticle, ["isBa", "isKid"])]
: [],
? [makeSegment(grammarUnits.baParticle, ["isBa", "isKid"])]
: [],
NPs: [
...ifNotRemoved(subject, "subject"),
...ifNotRemoved(predicate, "predicate"),
@ -77,8 +78,8 @@ function compileEnglish(EP: T.EPRendered): string[] | undefined {
function insertEWords(e: string, { subject, predicate }: { subject: string, predicate: string }): string {
return e.replace("$SUBJ", subject).replace("$PRED", predicate || "");
}
const engSubj = EP.subject.e || undefined;
const engPred = EP.predicate.e || undefined;
const engSubj = getEnglishFromRendered(EP.subject);
const engPred = getEnglishFromRendered(EP.predicate);
// require all English parts for making the English phrase
return (EP.englishBase && engSubj && engPred)
? EP.englishBase.map(e => insertEWords(e, {

View File

@ -20,6 +20,7 @@ import {
removeDuplicates,
} from "./vp-tools";
import { isImperativeTense, isModalTense, isPerfectTense } from "../type-predicates";
import { getEnglishFromRendered, getPashtoFromRendered } from "./np-tools";
type Form = T.FormVersion & { OSV?: boolean };
export function compileVP(VP: T.VPRendered, form: Form): { ps: T.SingleOrLengthOpts<T.PsString[]>, e?: string [] };
@ -79,8 +80,8 @@ function compilePs({ NPs, kids, verb: { head, rest }, VP }: CompilePsInput): T.S
function getSegmentsAndKids(VP: T.VPRendered, form: Form): { kids: Segment[], NPs: Segment[][] } {
const SO = {
subject: VP.subject.ps,
object: typeof VP.object === "object" ? VP.object.ps : undefined,
subject: getPashtoFromRendered(VP.subject),
object: typeof VP.object === "object" ? getPashtoFromRendered(VP.object) : undefined,
}
const removeKing = form.removeKing && !(VP.isCompound === "dynamic" && VP.isPast);
const shrinkServant = form.shrinkServant && !(VP.isCompound === "dynamic" && !VP.isPast);
@ -282,8 +283,10 @@ function compileEnglish(VP: T.VPRendered): string[] | undefined {
function insertEWords(e: string, { subject, object }: { subject: string, object?: string }): string {
return e.replace("$SUBJ", subject).replace("$OBJ", object || "");
}
const engSubj = VP.subject.e || undefined;
const engObj = (typeof VP.object === "object" && VP.object.e) ? VP.object.e : undefined;
const engSubj = getEnglishFromRendered(VP.subject);
const engObj = typeof VP.object === "object"
? getEnglishFromRendered(VP.object)
: undefined;
// require all English parts for making the English phrase
return (VP.englishBase && engSubj && (engObj || typeof VP.object !== "object"))
? VP.englishBase.map(e => insertEWords(e, {

View File

@ -0,0 +1,42 @@
import * as T from "../../types";
import { concatPsString } from "../p-text-helpers";
export function getPashtoFromRendered(np: T.Rendered<T.NPSelection | T.EqCompSelection>): T.PsString[] {
const adjs = np.adjectives;
if (!adjs) {
return np.ps;
}
return np.ps.map(p => (
concatPsString(
adjs.reduce((accum, curr) => (
// TODO: with variations of adjs?
concatPsString(accum, " ", curr.ps[0])
), { p: "", f: "" }),
" ",
p,
)
));
}
export function getEnglishFromRendered(np: T.Rendered<T.NPSelection | T.EqCompSelection>): string | undefined {
if (!np.e) return undefined;
if (np.type === "noun") {
try {
// split out the atricles so adjectives can be stuck inbetween them and the word
const chunks = np.e.split("the)");
const [articles, word] = chunks.length === 1
? ["", np.e]
: [chunks[0] + "the) ", chunks[1]];
const adjs = !np.adjectives
? ""
: np.adjectives.reduce((accum, curr): string => {
if (!curr.e) throw new Error("no english for adjective");
return accum + curr.e + " ";
}, "");
return `${articles}${adjs}${word}`;
} catch (e) {
return undefined;
}
}
return np.e;
}

View File

@ -0,0 +1,47 @@
import * as T from "../../types";
import { inflectWord } from "../../lib/pashto-inflector";
import {
psStringFromEntry,
isUnisexSet,
} from "../../lib/p-text-helpers";
import { getEnglishWord } from "../get-english-word";
import {
personGender,
personIsPlural,
} from "../../lib/misc-helpers";
function chooseInflection(inflections: T.UnisexSet<T.InflectionSet>, pers: T.Person, inflected?: boolean): T.ArrayOneOrMore<T.PsString> {
const gender = personGender(pers);
const plural = personIsPlural(pers);
const infNumber = (plural ? 1 : 0) + (inflected ? 1 : 0);
return inflections[gender][infNumber];
}
export function renderAdjectiveSelection(a: T.AdjectiveSelection, person: T.Person, inflected: boolean): T.Rendered<T.AdjectiveSelection> {
const infs = inflectWord(a.entry);
const eWord = getEnglishWord(a.entry);
const e = !eWord
? undefined
: typeof eWord === "string"
? eWord
: (eWord.singular || undefined);
if (!infs) return {
type: "adjective",
entry: a.entry,
ps: [psStringFromEntry(a.entry)],
e,
inflected: false,
person,
}
if (!infs.inflections || !isUnisexSet(infs.inflections)) {
throw new Error("error getting inflections for adjective, looks like a noun's inflections");
}
return {
type: "adjective",
entry: a.entry,
ps: chooseInflection(infs.inflections, person, inflected),
e,
inflected: false,
person,
};
}

View File

@ -7,10 +7,10 @@ import { renderNPSelection } from "./render-np";
import { getPersonFromVerbForm } from "../../lib/misc-helpers";
import { getVerbBlockPosFromPerson } from "../misc-helpers";
import { getEnglishWord } from "../get-english-word";
import { isUnisexSet, psStringFromEntry } from "../p-text-helpers";
import { inflectWord } from "../pashto-inflector";
import { psStringFromEntry } from "../p-text-helpers";
import { personGender, personIsPlural } from "../../library";
import { isLocativeAdverbEntry } from "../type-predicates";
import { renderAdjectiveSelection } from "./render-adj";
export function renderEP(EP: T.EPSelectionComplete): T.EPRendered {
const kingPerson = (EP.subject.type === "pronoun")
@ -71,26 +71,7 @@ function renderEqCompSelection(s: T.EqCompSelection, person: T.Person): T.Render
};
}
if (s.type === "adjective") {
const infs = inflectWord(s.entry);
if (!infs) return {
type: "adjective",
entry: s.entry,
ps: [psStringFromEntry(s.entry)],
e,
inflected: false,
person,
}
if (!infs.inflections || !isUnisexSet(infs.inflections)) {
throw new Error("error getting inflections for adjective, looks like a noun's inflections");
}
return {
type: "adjective",
entry: s.entry,
ps: chooseInflection(infs.inflections, person),
e,
inflected: false,
person,
};
return renderAdjectiveSelection(s, person, false)
}
throw new Error("invalid EqCompSelection");
}

View File

@ -11,6 +11,7 @@ import {
} from "../p-text-helpers";
import { parseEc } from "../misc-helpers";
import { getEnglishWord } from "../get-english-word";
import { renderAdjectiveSelection } from "./render-adj";
export function renderNPSelection(NP: T.NPSelection, inflected: boolean, inflectEnglish: boolean, role: "subject"): T.Rendered<T.NPSelection>
export function renderNPSelection(NP: T.NPSelection | T.ObjectNP, inflected: boolean, inflectEnglish: boolean, role: "object"): T.Rendered<T.NPSelection> | T.Person.ThirdPlurMale | "none";
@ -48,9 +49,11 @@ function renderNounSelection(n: T.NounSelection, inflected: boolean): T.Rendered
? ps
: [psStringFromEntry(n.entry)];
})();
const person = getPersonNumber(n.gender, n.number);
return {
...n,
person: getPersonNumber(n.gender, n.number),
adjectives: n.adjectives.map(a => renderAdjectiveSelection(a, person, inflected)),
person,
inflected,
ps: pashto,
e: english,

View File

@ -43,7 +43,7 @@ export function renderVP(VP: T.VPSelectionComplete): T.VPRendered {
const inflectSubject = isPast && isTransitive && !isMascSingAnimatePattern4(VP.subject);
const inflectObject = !isPast && isFirstOrSecondPersPronoun(VP.verb.object);
// Render Elements
return {
const b: T.VPRendered = {
type: "VPRendered",
king,
servant,
@ -59,6 +59,7 @@ export function renderVP(VP: T.VPSelectionComplete): T.VPRendered {
vs: VP.verb,
}),
};
return b;
}
function renderVerbSelection(vs: T.VerbSelectionComplete, person: T.Person, objectPerson: T.Person | undefined): T.VerbRendered {

View File

@ -25,10 +25,23 @@ export type Segment = { ps: T.PsString[] } & SegmentDescriptions & {
adjust: (o: { ps?: T.PsString | T.PsString[] | ((ps: T.PsString) => T.PsString), desc?: SDT[] }) => Segment,
};
function addAdjectives(ps: T.PsString[], adjectives?: T.Rendered<T.AdjectiveSelection>[]): T.PsString[] {
if (!adjectives) return ps;
return ps.map(p => {
// TODO: more thorough use of all possible variends?
return concatPsString(...adjectives.map(a => a.ps[0]), p);
});
}
export function makeSegment(
ps: T.PsString | T.PsString[],
input: T.Rendered<T.NounSelection> | T.PsString | T.PsString[],
options?: (keyof SegmentDescriptions)[],
): Segment {
const ps: T.PsString[] = Array.isArray(input)
? input
: "type" in input
? addAdjectives(input.ps, input.adjectives)
: [input];
return {
ps: Array.isArray(ps) ? ps : [ps],
...options && options.reduce((all, curr) => ({
@ -39,6 +52,7 @@ export function makeSegment(
return {
...this,
...o.ps ? {
// TODO: is this ok with the adjectives?
ps: Array.isArray(o.ps)
? o.ps
: "p" in o.ps

View File

@ -41,4 +41,4 @@ export default function useStickyState<T extends string | number | object | bool
}, [key, value]);
return [value, setValue];
}
}

View File

@ -592,16 +592,11 @@ export type NounSelection = {
type: "noun",
entry: NounEntry,
gender: Gender,
genderCanChange: boolean,
number: NounNumber,
numberCanChange: boolean,
dynamicComplement?: boolean,
// TODO: Implement
// adjectives: [],
// TODO: Implement
// possesor: NPSelection | undefined,
/* method only present if it's possible to change gender */
changeGender?: (gender: Gender) => NounSelection,
/* method only present if it's possible to change number */
changeNumber?: (number: NounNumber) => NounSelection,
adjectives: AdjectiveSelection[],
};
export type AdjectiveSelection = {
@ -633,8 +628,8 @@ export type ReplaceKey<T, K extends string, R> = T extends Record<K, unknown> ?
export type FormVersion = { removeKing: boolean, shrinkServant: boolean };
export type Rendered<T extends NPSelection | EqCompSelection> = ReplaceKey<
Omit<T, "changeGender" | "changeNumber" | "changeDistance">,
export type Rendered<T extends NPSelection | EqCompSelection | AdjectiveSelection> = ReplaceKey<
Omit<T, "changeGender" | "changeNumber" | "changeDistance" | "adjectives">,
"e",
string
> & {
@ -642,6 +637,8 @@ export type Rendered<T extends NPSelection | EqCompSelection> = ReplaceKey<
e?: string,
inflected: boolean,
person: Person,
// TODO: better recursive thing
adjectives?: Rendered<AdjectiveSelection>[],
};
// TODO: recursive changing this down into the possesor etc.