refactoring the EP and VP explorers - hoping to get proper robust mini pronoun stuff happening soon. also fixed a problem with the EPExplorers, in EPs you omit the SUBJECT always not the king

This commit is contained in:
lingdocs 2022-05-04 10:47:27 -05:00
parent b75ddb9689
commit 240fd11ed8
14 changed files with 564 additions and 352 deletions

View File

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

View File

@ -1,13 +1,13 @@
import * as T from "../../types"; import * as T from "../../types";
import { renderEP } from "../../lib/phrase-building/render-ep"; import { renderEP } from "../../lib/phrase-building/render-ep";
import { compileEP } from "../../lib/phrase-building/compile-ep"; import { compileEP } from "../../lib/phrase-building/compile-ep";
import AbbreviationFormSelector from "../vp-explorer/AbbreviationFormSelector";
import Examples from "../Examples"; import Examples from "../Examples";
import ButtonSelect from "../ButtonSelect";
function EPDisplay({ eps, opts, setForm }: { function EPDisplay({ eps, opts, setOmitSubject }: {
eps: T.EPSelectionState, eps: T.EPSelectionState,
opts: T.TextOptions, opts: T.TextOptions,
setForm: (form: T.FormVersion) => void, setOmitSubject: (value: "true" | "false") => void,
}) { }) {
const EP = completeEPSelection(eps); const EP = completeEPSelection(eps);
if (!EP) { if (!EP) {
@ -22,13 +22,19 @@ function EPDisplay({ eps, opts, setForm }: {
</div> </div>
} }
const rendered = renderEP(EP); const rendered = renderEP(EP);
const result = compileEP(rendered, EP.form); const result = compileEP(rendered);
return <div className="text-center pt-3"> return <div className="text-center pt-3">
<AbbreviationFormSelector <div className="mb-2">
adjustable="king" <ButtonSelect
form={EP.form} small
onChange={setForm} value={(eps.omitSubject ? "true" : "false") as "true" | "false"}
options={[
{ value: "false", label: "Full"},
{ value: "true", label: "No Subj."},
]}
handleChange={setOmitSubject}
/> />
</div>
{"long" in result.ps ? {"long" in result.ps ?
<div> <div>
<VariationLayer vs={result.ps.long} opts={opts} /> <VariationLayer vs={result.ps.long} opts={opts} />

View File

@ -1,26 +1,14 @@
import * as T from "../../types"; import * as T from "../../types";
import useStickyState from "../../lib/useStickyState"; import useStickyState, { useStickyReducer } from "../../lib/useStickyState";
import NPPicker from "../np-picker/NPPicker"; import NPPicker from "../np-picker/NPPicker";
import EquativePicker from "./EquativePicker"; import EquativePicker from "./EquativePicker";
import EPDisplay from "./EPDisplay"; import EPDisplay from "./EPDisplay";
import ButtonSelect from "../ButtonSelect"; import ButtonSelect from "../ButtonSelect";
import EqCompPicker from "./eq-comp-picker/EqCompPicker"; import EqCompPicker from "./eq-comp-picker/EqCompPicker";
import { roleIcon } from "../vp-explorer/VPExplorerExplanationModal"; import { roleIcon } from "../vp-explorer/VPExplorerExplanationModal";
import { isUnisexNounEntry } from "../../lib/type-predicates";
import {
personGender,
personNumber,
} from "../../lib/misc-helpers";
import EqChartsDisplay from "./EqChartsDisplay"; import EqChartsDisplay from "./EqChartsDisplay";
import epsReducer from "./eps-reducer";
// TODO: put the clear button beside the title in the predicate picker? const blankEps: T.EPSelectionState = {
function EPExplorer(props: {
opts: T.TextOptions,
entryFeeder: T.EntryFeeder,
}) {
const [mode, setMode] = useStickyState<"charts" | "phrases">("charts", "EPExplorerMode");
const [eps, setEps] = useStickyState<T.EPSelectionState>({
subject: undefined, subject: undefined,
predicate: { predicate: {
type: "Complement", type: "Complement",
@ -32,44 +20,17 @@ function EPExplorer(props: {
negative: false, negative: false,
}, },
shrunkenPossesive: undefined, shrunkenPossesive: undefined,
form: { removeKing: false, shrinkServant: false }, omitSubject: false,
}, "EPSelectionState10"); };
function handlePredicateTypeChange(type: "NP" | "Complement") {
setEps(o => ({ // TODO: put the clear button beside the title in the predicate picker?
...o,
predicate: { function EPExplorer(props: {
...o.predicate, opts: T.TextOptions,
type, entryFeeder: T.EntryFeeder,
}, }) {
})); const [mode, setMode] = useStickyState<"charts" | "phrases">("charts", "EPExplorerMode");
} const [eps, adjustEps] = useStickyReducer(epsReducer, blankEps, "EPSelectionState10");
function handleSetSubject(subject: T.NPSelection | undefined) {
setEps(old => massageSubjectChange(subject, old));
}
function setPredicateNP(selection: T.NPSelection | undefined) {
setEps(o => massageNPPredicateChange(selection, o))
}
function setPredicateComp(selection: T.EqCompSelection | undefined) {
setEps(o => ({
...o,
predicate: {
...o.predicate,
Complement: selection
},
}));
}
function handleShrinkPossesive(shrunkenPossesive: number | undefined) {
setEps(o => ({
...o,
shrunkenPossesive,
}));
}
function handleSetForm(form: T.FormVersion) {
setEps(o => ({
...o,
form,
}));
}
const king = eps.subject?.type === "pronoun" const king = eps.subject?.type === "pronoun"
? "subject" ? "subject"
: eps.predicate.type === "Complement" : eps.predicate.type === "Complement"
@ -91,13 +52,13 @@ function EPExplorer(props: {
<div className="my-2"> <div className="my-2">
<NPPicker <NPPicker
shrunkenPossesiveInPhrase={eps.shrunkenPossesive} shrunkenPossesiveInPhrase={eps.shrunkenPossesive}
handleShrinkPossesive={handleShrinkPossesive} handleShrinkPossesive={payload => adjustEps({ type: "shrink possesive", payload })}
heading={<div className="h5 text-center">Subject {king === "subject" ? roleIcon.king : ""}</div>} heading={<div className="h5 text-center">Subject {king === "subject" ? roleIcon.king : ""}</div>}
entryFeeder={props.entryFeeder} entryFeeder={props.entryFeeder}
np={eps.subject} np={eps.subject}
counterPart={undefined} counterPart={undefined}
role="subject" role="subject"
onChange={handleSetSubject} onChange={payload => adjustEps({ type: "set subject", payload })}
opts={props.opts} opts={props.opts}
/> />
</div> </div>
@ -111,21 +72,21 @@ function EPExplorer(props: {
{ value: "Complement", label: "Complement" }, { value: "Complement", label: "Complement" },
]} ]}
value={eps.predicate.type} value={eps.predicate.type}
handleChange={handlePredicateTypeChange} handleChange={payload => adjustEps({ type: "set predicate type", payload })}
/> />
</div> </div>
{eps.predicate.type === "NP" ? <NPPicker {eps.predicate.type === "NP" ? <NPPicker
shrunkenPossesiveInPhrase={eps.shrunkenPossesive} shrunkenPossesiveInPhrase={eps.shrunkenPossesive}
handleShrinkPossesive={handleShrinkPossesive} handleShrinkPossesive={payload => adjustEps({ type: "shrink possesive", payload })}
entryFeeder={props.entryFeeder} entryFeeder={props.entryFeeder}
np={eps.predicate.type === "NP" ? eps.predicate.NP : undefined} np={eps.predicate.type === "NP" ? eps.predicate.NP : undefined}
counterPart={undefined} counterPart={undefined}
role="subject" role="subject"
onChange={setPredicateNP} onChange={payload => adjustEps({ type: "set predicate NP", payload })}
opts={props.opts} opts={props.opts}
/> : <EqCompPicker /> : <EqCompPicker
comp={eps.predicate.type === "Complement" ? eps.predicate.Complement : undefined} comp={eps.predicate.type === "Complement" ? eps.predicate.Complement : undefined}
onChange={setPredicateComp} onChange={payload => adjustEps({ type: "set predicate comp", payload })}
opts={props.opts} opts={props.opts}
entryFeeder={props.entryFeeder} entryFeeder={props.entryFeeder}
/>} />}
@ -135,7 +96,7 @@ function EPExplorer(props: {
<div className="h5 text-center clickable">Equative</div> <div className="h5 text-center clickable">Equative</div>
<EquativePicker <EquativePicker
equative={eps.equative} equative={eps.equative}
onChange={(equative) => setEps(o => ({ ...o, equative }))} onChange={payload => adjustEps({ type: "set equative", payload })}
hideNegative={mode === "charts"} hideNegative={mode === "charts"}
/> />
</div> </div>
@ -144,95 +105,9 @@ function EPExplorer(props: {
{mode === "phrases" && <EPDisplay {mode === "phrases" && <EPDisplay
opts={props.opts} opts={props.opts}
eps={eps} eps={eps}
setForm={handleSetForm} setOmitSubject={payload => adjustEps({ type: "set omitSubject", payload })}
/>} />}
</div>; </div>;
} }
export default EPExplorer; export default EPExplorer;
function massageNPPredicateChange(selection: T.NPSelection | undefined, old: T.EPSelectionState): T.EPSelectionState {
if (!selection) {
return {
...old,
predicate: {
...old.predicate,
NP: selection,
},
};
}
if (old.subject?.type === "pronoun" && selection.type === "noun" && isUnisexNounEntry(selection.entry)) {
const { gender, number } = selection;
const pronoun = old.subject.person;
const newPronoun = movePersonNumber(movePersonGender(pronoun, gender), number);
return {
...old,
subject: {
...old.subject,
person: newPronoun,
},
predicate: {
...old.predicate,
NP: selection,
},
};
}
return {
...old,
predicate: {
...old.predicate,
NP: selection,
},
};
}
function massageSubjectChange(subject: T.NPSelection | undefined, old: T.EPSelectionState): T.EPSelectionState {
if (!subject) {
return {
...old,
subject,
};
}
if (subject.type === "pronoun" && old.predicate.type === "NP" && old.predicate.NP?.type === "noun" && isUnisexNounEntry(old.predicate.NP.entry)) {
const predicate = old.predicate.NP;
const adjusted = {
...predicate,
...predicate.numberCanChange ? {
number: personNumber(subject.person),
} : {},
...predicate.genderCanChange ? {
gender: personGender(subject.person),
} : {},
}
return {
...old,
subject,
predicate: {
...old.predicate,
NP: adjusted,
},
};
}
return {
...old,
subject,
};
}
function movePersonGender(p: T.Person, gender: T.Gender): T.Person {
const pGender = personGender(p);
if (gender === pGender) {
return p;
}
return (gender === "masc") ? (p - 1) : (p + 1);
}
function movePersonNumber(p: T.Person, number: T.NounNumber): T.Person {
const pNumber = personNumber(p);
if (pNumber === number) {
return p;
}
return (number === "plural")
? (p + 6)
: (p - 6);
}

View File

@ -0,0 +1,160 @@
import * as T from "../../types";
import {
personGender,
personNumber,
} from "../../lib/misc-helpers";
import { isUnisexNounEntry } from "../../lib/type-predicates";
export type EpsReducerAction = {
type: "set predicate type",
payload: "NP" | "Complement",
} | {
type: "set subject",
payload: T.NPSelection | undefined,
} | {
type: "set predicate NP",
payload: T.NPSelection | undefined,
} | {
type: "set predicate comp",
payload: T.EqCompSelection | undefined,
} | {
type: "shrink possesive",
payload: number | undefined,
} | {
type: "set omitSubject",
payload: "true" | "false",
} | {
type: "set equative",
payload: T.EquativeSelection,
};
export default function epsReducer(eps: T.EPSelectionState, action: EpsReducerAction): T.EPSelectionState {
if (action.type === "set predicate type") {
return {
...eps,
predicate: {
...eps.predicate,
type: action.payload,
},
};
}
if (action.type === "set subject") {
return massageSubjectChange(action.payload, eps);
}
if (action.type === "set predicate NP") {
return massageNPPredicateChange(action.payload, eps);
}
if (action.type === "set predicate comp") {
return {
...eps,
predicate: {
...eps.predicate,
Complement: action.payload,
},
};
}
if (action.type === "shrink possesive") {
return {
...eps,
shrunkenPossesive: action.payload,
};
}
if (action.type === "set omitSubject") {
return {
...eps,
omitSubject: action.payload === "true",
};
}
// if (action.type === "set equative") {
return {
...eps,
equative: action.payload,
}
// }
}
function massageSubjectChange(subject: T.NPSelection | undefined, old: T.EPSelectionState): T.EPSelectionState {
if (!subject) {
return {
...old,
subject,
};
}
if (subject.type === "pronoun" && old.predicate.type === "NP" && old.predicate.NP?.type === "noun" && isUnisexNounEntry(old.predicate.NP.entry)) {
const predicate = old.predicate.NP;
const adjusted = {
...predicate,
...predicate.numberCanChange ? {
number: personNumber(subject.person),
} : {},
...predicate.genderCanChange ? {
gender: personGender(subject.person),
} : {},
}
return {
...old,
subject,
predicate: {
...old.predicate,
NP: adjusted,
},
};
}
return {
...old,
subject,
};
}
function massageNPPredicateChange(selection: T.NPSelection | undefined, old: T.EPSelectionState): T.EPSelectionState {
if (!selection) {
return {
...old,
predicate: {
...old.predicate,
NP: selection,
},
};
}
if (old.subject?.type === "pronoun" && selection.type === "noun" && isUnisexNounEntry(selection.entry)) {
const { gender, number } = selection;
const pronoun = old.subject.person;
const newPronoun = movePersonNumber(movePersonGender(pronoun, gender), number);
return {
...old,
subject: {
...old.subject,
person: newPronoun,
},
predicate: {
...old.predicate,
NP: selection,
},
};
}
return {
...old,
predicate: {
...old.predicate,
NP: selection,
},
};
}
function movePersonGender(p: T.Person, gender: T.Gender): T.Person {
const pGender = personGender(p);
if (gender === pGender) {
return p;
}
return (gender === "masc") ? (p - 1) : (p + 1);
}
function movePersonNumber(p: T.Person, number: T.NounNumber): T.Person {
const pNumber = personNumber(p);
if (pNumber === number) {
return p;
}
return (number === "plural")
? (p + 6)
: (p - 6);
}

View File

@ -2,9 +2,11 @@ import Select from "react-select";
import * as T from "../../types"; import * as T from "../../types";
import ButtonSelect from "../ButtonSelect"; import ButtonSelect from "../ButtonSelect";
import { isImperativeTense, isModalTense, isPerfectTense, isVerbTense } from "../../lib/type-predicates"; import { isImperativeTense, isModalTense, isPerfectTense, isVerbTense } from "../../lib/type-predicates";
import { ensure2ndPersSubjPronounAndNoConflict } from "../../lib/phrase-building/vp-tools";
import useStickyState from "../../lib/useStickyState"; import useStickyState from "../../lib/useStickyState";
import { customStyles } from "../EntrySelect"; import { customStyles } from "../EntrySelect";
import {
VpsReducerAction
} from "./vps-reducer";
const verbTenseOptions: { label: string | JSX.Element, value: T.VerbTense, formula: string, modalFormula: string, }[] = [{ const verbTenseOptions: { label: string | JSX.Element, value: T.VerbTense, formula: string, modalFormula: string, }[] = [{
label: <div><i className="fas fa-video mr-2" />present</div>, label: <div><i className="fas fa-video mr-2" />present</div>,
@ -113,45 +115,17 @@ function TensePicker(props: ({
} | { } | {
vpsComplete: T.VPSelectionComplete, vpsComplete: T.VPSelectionComplete,
}) & { }) & {
onChange: (p: T.VPSelectionState) => void, onChange: (p: VpsReducerAction) => void,
mode: "charts" | "phrases" | "quiz", mode: "charts" | "phrases" | "quiz",
}) { }) {
const [showFormula, setShowFormula] = useStickyState<boolean>(false, "showFormula"); const [showFormula, setShowFormula] = useStickyState<boolean>(false, "showFormula");
function onTenseSelect(o: { value: T.VerbTense | T.PerfectTense | T.ImperativeTense } | null) { function onTenseSelect(o: { value: T.VerbTense | T.PerfectTense | T.ImperativeTense } | null) {
if ("vpsComplete" in props) return; if ("vpsComplete" in props) return;
const value = o?.value ? o.value : undefined; const tense = o?.value ? o.value : undefined;
if (props.vps.verb && value) {
if (isPerfectTense(value)) {
props.onChange({ props.onChange({
...props.vps, type: "set tense",
verb: { payload: tense,
...props.vps.verb,
perfectTense: value,
tenseCategory: "perfect",
},
}); });
} else if (isImperativeTense(value)) {
props.onChange({
...props.vps,
verb: {
...props.vps.verb,
imperativeTense: value,
tenseCategory: "imperative",
},
});
} else {
props.onChange({
...props.vps,
verb: {
...props.vps.verb,
verbTense: value,
tenseCategory: props.vps.verb.tenseCategory === "perfect"
? "basic"
: props.vps.verb.tenseCategory,
},
});
}
}
} }
function moveTense(dir: "forward" | "back") { function moveTense(dir: "forward" | "back") {
if ("vpsComplete" in props) return; if ("vpsComplete" in props) return;
@ -182,41 +156,20 @@ function TensePicker(props: ({
onTenseSelect(newTense); onTenseSelect(newTense);
}; };
} }
function onPosNegSelect(value: string) { function onPosNegSelect(payload: "true" | "false") {
if ("vpsComplete" in props) return; if ("vpsComplete" in props) return;
if (props.vps.verb) {
props.onChange({ props.onChange({
...props.vps, type: "set negativity",
verb: { payload,
...props.vps.verb,
negative: value === "true",
},
}); });
} }
} function onTenseCategorySelect(payload: "basic" | "modal" | "perfect" | "imperative") {
function onTenseCategorySelect(value: "basic" | "modal" | "perfect" | "imperative") {
if ("vpsComplete" in props) return; if ("vpsComplete" in props) return;
if (props.vps.verb) {
if (value === "imperative") {
props.onChange(ensure2ndPersSubjPronounAndNoConflict({
...props.vps,
verb: {
...props.vps.verb,
voice: "active",
tenseCategory: value,
},
}));
return;
}
props.onChange({ props.onChange({
...props.vps, type: "set tense category",
verb: { payload,
...props.vps.verb,
tenseCategory: value,
},
}); });
} }
}
const tOptions = ("vps" in props && (props.vps.verb?.tenseCategory === "perfect")) const tOptions = ("vps" in props && (props.vps.verb?.tenseCategory === "perfect"))
? perfectTenseOptions ? perfectTenseOptions
: ("vps" in props && (props.vps.verb?.tenseCategory === "imperative")) : ("vps" in props && (props.vps.verb?.tenseCategory === "imperative"))
@ -292,7 +245,7 @@ function TensePicker(props: ({
</div> </div>
{props.mode === "phrases" && <ButtonSelect {props.mode === "phrases" && <ButtonSelect
small small
value={props.vps.verb.negative.toString()} value={props.vps.verb.negative.toString() as "true" | "false"}
options={[{ options={[{
label: "Pos.", label: "Pos.",
value: "false", value: "false",

View File

@ -3,21 +3,18 @@ import VerbPicker from "./VerbPicker";
import TensePicker from "./TensePicker"; import TensePicker from "./TensePicker";
import VPDisplay from "./VPDisplay"; import VPDisplay from "./VPDisplay";
import ButtonSelect from "../ButtonSelect"; import ButtonSelect from "../ButtonSelect";
import {
isInvalidSubjObjCombo,
} from "../../lib/phrase-building/vp-tools";
import * as T from "../../types"; import * as T from "../../types";
import ChartDisplay from "./VPChartDisplay"; import ChartDisplay from "./VPChartDisplay";
import useStickyState from "../../lib/useStickyState"; import useStickyState, { useStickyReducer } from "../../lib/useStickyState";
import { makeVPSelectionState } from "./verb-selection"; import { makeVPSelectionState } from "./verb-selection";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { getKingAndServant } from "../../lib/phrase-building/render-vp"; import { getKingAndServant } from "../../lib/phrase-building/render-vp";
import { isPastTense } from "../../lib/phrase-building/vp-tools"; import { isPastTense } from "../../lib/phrase-building/vp-tools";
import VPExplorerQuiz from "./VPExplorerQuiz"; import VPExplorerQuiz from "./VPExplorerQuiz";
import { switchSubjObj } from "../../lib/phrase-building/vp-tools";
import VPExplorerExplanationModal, { roleIcon } from "./VPExplorerExplanationModal"; import VPExplorerExplanationModal, { roleIcon } from "./VPExplorerExplanationModal";
// @ts-ignore // @ts-ignore
import LZString from "lz-string"; import LZString from "lz-string";
import { vpsReducer } from "./vps-reducer";
const phraseURLParam = "VPPhrase"; const phraseURLParam = "VPPhrase";
@ -38,7 +35,8 @@ export function VPExplorer(props: {
handleLinkClick: ((ts: number) => void) | "none", handleLinkClick: ((ts: number) => void) | "none",
entryFeeder: T.EntryFeeder, entryFeeder: T.EntryFeeder,
}) { }) {
const [vps, setVps] = useStickyState<T.VPSelectionState>( const [vps, adjustVps] = useStickyReducer(
vpsReducer,
props.loaded props.loaded
? props.loaded ? props.loaded
: savedVps => makeVPSelectionState(props.verb, savedVps), : savedVps => makeVPSelectionState(props.verb, savedVps),
@ -62,51 +60,41 @@ export function VPExplorer(props: {
useEffect(() => { useEffect(() => {
const VPSFromUrl = getVPSFromUrl(); const VPSFromUrl = getVPSFromUrl();
if (VPSFromUrl) { if (VPSFromUrl) {
setVps(VPSFromUrl); adjustVps({
type: "load vps",
payload: VPSFromUrl
});
} }
// eslint-disable-next-line // eslint-disable-next-line
}, []); }, []);
useEffect(() => { useEffect(() => {
setVps(oldVps => { const newVps = makeVPSelectionState(props.verb, vps);
if (mode === "quiz") { adjustVps({
setMode("phrases"); type: "load vps",
} payload: newVps,
return makeVPSelectionState(props.verb, oldVps);
}); });
// eslint-disable-next-line // eslint-disable-next-line
}, [props.verb]); }, [props.verb]);
function handleSubjectChange(subject: T.NPSelection | undefined, skipPronounConflictCheck?: boolean) { function handleSubjectChange(subject: T.NPSelection | undefined, skipPronounConflictCheck?: boolean) {
if (!skipPronounConflictCheck && hasPronounConflict(subject, vps.verb?.object)) { adjustVps({
alert("That combination of pronouns is not allowed"); type: "set subject",
return; payload: { subject, skipPronounConflictCheck },
} });
setVps(o => ({ ...o, subject }));
} }
function handleObjectChange(object: T.NPSelection | undefined) { function handleObjectChange(object: T.NPSelection | undefined) {
if (!vps.verb) return; adjustVps({
if ((vps.verb.object === "none") || (typeof vps.verb.object === "number")) return; type: "set object",
// check for pronoun conflict payload: object,
if (hasPronounConflict(vps.subject, object)) { });
alert("That combination of pronouns is not allowed");
return;
}
setVps(o => ({
...o,
verb: {
...o.verb,
object,
},
}));
} }
function handleSubjObjSwap() { function handleSubjObjSwap() {
if (vps.verb?.isCompound === "dynamic") return; adjustVps({ type: "swap subj/obj" });
setVps(switchSubjObj)
} }
function handleShrinkPossesive(shrunkenPossesive: number | undefined) { function handleShrinkPossesive(shrunkenPossesive: number | undefined) {
setVps(o => ({ adjustVps({
...o, type: "shrink possesive",
shrunkenPossesive, payload: shrunkenPossesive,
})); });
} }
function quizLock<T>(f: T) { function quizLock<T>(f: T) {
if (mode === "quiz") { if (mode === "quiz") {
@ -118,10 +106,10 @@ export function VPExplorer(props: {
return f; return f;
} }
function handleSetForm(form: T.FormVersion) { function handleSetForm(form: T.FormVersion) {
setVps(o => ({ adjustVps({
...o, type: "set form",
form, payload: form,
})); });
} }
// for some crazy reason I can't get the URI share thing to encode and decode properly // for some crazy reason I can't get the URI share thing to encode and decode properly
function handleCopyShareLink() { function handleCopyShareLink() {
@ -135,7 +123,7 @@ export function VPExplorer(props: {
return <div className="mt-3" style={{ maxWidth: "950px"}}> return <div className="mt-3" style={{ maxWidth: "950px"}}>
<VerbPicker <VerbPicker
vps={vps} vps={vps}
onChange={quizLock(setVps)} onChange={quizLock(adjustVps)}
opts={props.opts} opts={props.opts}
handleLinkClick={props.handleLinkClick} handleLinkClick={props.handleLinkClick}
/> />
@ -207,7 +195,7 @@ export function VPExplorer(props: {
<div className="my-2"> <div className="my-2">
<TensePicker <TensePicker
vps={vps} vps={vps}
onChange={quizLock(setVps)} onChange={quizLock(adjustVps)}
mode={mode} mode={mode}
/> />
</div> </div>
@ -237,13 +225,6 @@ export function VPExplorer(props: {
export default VPExplorer; export default VPExplorer;
function hasPronounConflict(subject: T.NPSelection | undefined, object: undefined | T.VerbObject): boolean {
const subjPronoun = (subject && subject.type === "pronoun") ? subject : undefined;
const objPronoun = (object && typeof object === "object" && object.type === "pronoun") ? object : undefined;
if (!subjPronoun || !objPronoun) return false;
return isInvalidSubjObjCombo(subjPronoun.person, objPronoun.person);
}
function getShareUrl(vps: T.VPSelectionState): string { function getShareUrl(vps: T.VPSelectionState): string {
const stringJSON = JSON.stringify(vps); const stringJSON = JSON.stringify(vps);
const encoded = LZString.compressToEncodedURIComponent(stringJSON); const encoded = LZString.compressToEncodedURIComponent(stringJSON);

View File

@ -5,13 +5,15 @@ import { getVerbInfo } from "../../lib/verb-info";
import Hider from "../Hider"; import Hider from "../Hider";
import useStickyState from "../../lib/useStickyState"; import useStickyState from "../../lib/useStickyState";
import CompoundDisplay from "./CompoundDisplay"; import CompoundDisplay from "./CompoundDisplay";
import { changeStatDyn, changeTransitivity, changeVoice } from "./verb-selection"; import {
VpsReducerAction
} from "./vps-reducer";
// TODO: dark on past tense selecitons // TODO: dark on past tense selecitons
function VerbPicker(props: { function VerbPicker(props: {
vps: T.VPSelectionState, vps: T.VPSelectionState,
onChange: (p: T.VPSelectionState) => void, onChange: (a: VpsReducerAction) => void,
opts: T.TextOptions, opts: T.TextOptions,
handleLinkClick: ((ts: number) => void) | "none", handleLinkClick: ((ts: number) => void) | "none",
}) { }) {
@ -28,43 +30,26 @@ function VerbPicker(props: {
return <div>ERROR: Verb version should be select first</div>; return <div>ERROR: Verb version should be select first</div>;
} }
function onVoiceSelect(value: "active" | "passive") { function onVoiceSelect(value: "active" | "passive") {
if (props.vps.verb && props.vps.verb.canChangeVoice) {
if (value === "passive" && props.vps.verb.tenseCategory === "imperative") {
return;
}
if (value === "passive" && (typeof props.vps.verb.object === "object")) {
props.onChange({ props.onChange({
...props.vps, type: "set voice",
subject: props.vps.verb.object, payload: value,
verb: changeVoice(props.vps.verb, value, props.vps.verb.object),
}); });
} else {
props.onChange({
...props.vps,
verb: changeVoice(props.vps.verb, value, value === "active" ? props.vps.subject : undefined),
});
}
}
} }
function notInstransitive(t: "transitive" | "intransitive" | "grammatically transitive"): "transitive" | "grammatically transitive" { function notInstransitive(t: "transitive" | "intransitive" | "grammatically transitive"): "transitive" | "grammatically transitive" {
return t === "intransitive" ? "transitive" : t; return t === "intransitive" ? "transitive" : t;
} }
function handleChangeTransitivity(t: "transitive" | "grammatically transitive") { function handleChangeTransitivity(payload: "transitive" | "grammatically transitive") {
if (props.vps.verb && props.vps.verb.canChangeTransitivity) {
props.onChange({ props.onChange({
...props.vps, type: "set transitivity",
verb: changeTransitivity(props.vps.verb, t), payload,
}); });
} }
} function handleChangeStatDyn(payload: "stative" | "dynamic") {
function handleChangeStatDyn(c: "stative" | "dynamic") {
if (props.vps.verb && props.vps.verb.canChangeStatDyn) {
props.onChange({ props.onChange({
...props.vps, type: "set statDyn",
verb: changeStatDyn(props.vps.verb, c), payload,
}); });
} }
}
return <div className="mb-3"> return <div className="mb-3">
{info && <CompoundDisplay {info && <CompoundDisplay
info={info} info={info}

View File

@ -0,0 +1,215 @@
import * as T from "../../types";
import {
isInvalidSubjObjCombo,
} from "../../lib/phrase-building/vp-tools";
import { switchSubjObj } from "../../lib/phrase-building/vp-tools";
import { changeStatDyn, changeTransitivity, changeVoice } from "./verb-selection";
import { ensure2ndPersSubjPronounAndNoConflict } from "../../lib/phrase-building/vp-tools";
import {
isPerfectTense,
isImperativeTense,
} from "../../lib/type-predicates";
export type VpsReducerAction = {
type: "load vps",
payload: T.VPSelectionState,
} | {
type: "set subject",
payload: {
subject: T.NPSelection | undefined,
skipPronounConflictCheck?: boolean,
},
} | {
type: "set object",
payload: T.NPSelection | undefined,
} | {
type: "swap subj/obj",
} | {
type: "shrink possesive",
payload: number | undefined,
} | {
type: "set form",
payload: T.FormVersion,
} | {
type: "set voice",
payload: "active" | "passive",
} | {
type: "set transitivity",
payload: "transitive" | "grammatically transitive",
} | {
type: "set statDyn",
payload: "stative" | "dynamic",
} | {
type: "set negativity",
payload: "true" | "false",
} | {
type: "set tense",
payload: T.VerbTense | T.PerfectTense | T.ImperativeTense | undefined,
} | {
type: "set tense category",
payload: "basic" | "modal" | "perfect" | "imperative",
}
export function vpsReducer(vps: T.VPSelectionState, action: VpsReducerAction): T.VPSelectionState {
if (action.type === "load vps") {
return action.payload;
}
if (action.type === "set subject") {
const { subject, skipPronounConflictCheck } = action.payload;
if (
!skipPronounConflictCheck
&&
hasPronounConflict(subject, vps.verb?.object)
) {
alert("That combination of pronouns is not allowed");
return vps;
}
return {
...vps,
subject: action.payload.subject,
};
}
if (action.type === "set object") {
if (!vps.verb) return vps;
if ((vps.verb.object === "none") || (typeof vps.verb.object === "number")) {
return vps;
}
const object = action.payload;
// check for pronoun conflict
if (hasPronounConflict(vps.subject, object)) {
alert("That combination of pronouns is not allowed");
return vps;
}
return {
...vps,
verb: {
...vps.verb,
object,
},
};
}
if (action.type === "swap subj/obj") {
if (vps.verb?.isCompound === "dynamic") return vps;
return switchSubjObj(vps);
}
if (action.type === "shrink possesive") {
return {
...vps,
shrunkenPossesive: action.payload,
};
}
if (action.type === "set form") {
return {
...vps,
form: action.payload,
};
}
if (action.type === "set voice") {
if (vps.verb && vps.verb.canChangeVoice) {
const voice = action.payload;
if (voice === "passive" && vps.verb.tenseCategory === "imperative") {
return vps;
}
if (voice === "passive" && (typeof vps.verb.object === "object")) {
return {
...vps,
subject: vps.verb.object,
verb: changeVoice(vps.verb, voice, vps.verb.object),
};
} else {
return {
...vps,
verb: changeVoice(vps.verb, voice, voice === "active" ? vps.subject : undefined),
};
}
} else {
return vps;
}
}
if (action.type === "set transitivity") {
if (!(vps.verb && vps.verb.canChangeTransitivity)) return vps;
return {
...vps,
verb: changeTransitivity(vps.verb, action.payload),
};
}
if (action.type === "set statDyn") {
if (!(vps.verb && vps.verb.canChangeStatDyn)) return vps;
return {
...vps,
verb: changeStatDyn(vps.verb, action.payload),
};
}
if (action.type === "set negativity") {
if (!vps.verb) return vps;
return {
...vps,
verb: {
...vps.verb,
negative: action.payload === "true",
},
};
}
if (action.type === "set tense") {
const tense = action.payload;
if (!(vps.verb && tense)) return vps;
if (isPerfectTense(tense)) {
return {
...vps,
verb: {
...vps.verb,
perfectTense: tense,
tenseCategory: "perfect",
},
};
} else if (isImperativeTense(tense)) {
return {
...vps,
verb: {
...vps.verb,
imperativeTense: tense,
tenseCategory: "imperative",
},
};
} else {
return {
...vps,
verb: {
...vps.verb,
verbTense: tense,
tenseCategory: vps.verb.tenseCategory === "perfect"
? "basic"
: vps.verb.tenseCategory,
},
};
}
}
// if (action.type === "set tense category") {
if (!vps.verb) return vps;
const category = action.payload;
if (category === "imperative") {
return ensure2ndPersSubjPronounAndNoConflict({
...vps,
verb: {
...vps.verb,
voice: "active",
tenseCategory: category,
},
});
}
return {
...vps,
verb: {
...vps.verb,
tenseCategory: category,
},
};
// }
}
function hasPronounConflict(subject: T.NPSelection | undefined, object: undefined | T.VerbObject): boolean {
const subjPronoun = (subject && subject.type === "pronoun") ? subject : undefined;
const objPronoun = (object && typeof object === "object" && object.type === "pronoun") ? object : undefined;
if (!subjPronoun || !objPronoun) return false;
return isInvalidSubjObjCombo(subjPronoun.person, objPronoun.person);
}

View File

@ -14,14 +14,14 @@ import { removeAccents } from "../accent-helpers";
import { getEnglishFromRendered, getPashtoFromRendered } from "./np-tools"; import { getEnglishFromRendered, getPashtoFromRendered } from "./np-tools";
import { import {
orderKidsSection, orderKidsSection,
findPossesiveToShrink, findPossesiveToShrinkInEP,
shrinkNP, shrinkNP,
} from "./compile-tools"; } from "./compile-tools";
export function compileEP(EP: T.EPRendered, form: T.FormVersion): { ps: T.SingleOrLengthOpts<T.PsString[]>, e?: string[] }; export function compileEP(EP: T.EPRendered): { 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, 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, combineLengths?: true): { ps: T.SingleOrLengthOpts<T.PsString[]>, e?: string[] } {
const { kids, NPs } = getSegmentsAndKids(EP, form); const { kids, NPs } = getSegmentsAndKids(EP);
const equative = EP.equative.ps; const equative = EP.equative.ps;
const psResult = compilePs({ const psResult = compilePs({
NPs, NPs,
@ -35,15 +35,9 @@ export function compileEP(EP: T.EPRendered, form: T.FormVersion, combineLengths?
}; };
} }
function getSegmentsAndKids(EP: T.EPRendered, form: Omit<T.FormVersion, "shrinkServant">): { kids: Segment[], NPs: Segment[] } { function getSegmentsAndKids(EP: T.EPRendered): { kids: Segment[], NPs: Segment[] } {
function ifNotRemoved(s: Segment, role: "subject" | "predicate"): Segment[] { const possToShrink = findPossesiveToShrinkInEP(EP);
if (form.removeKing && EP.king === role) { const shrunkenPossAllowed = !((possToShrink?.from === "subject") && EP.omitSubject);
return [];
}
return [s];
}
const possToShrink = findPossesiveToShrink(EP);
const shrunkenPossAllowed = !(form.removeKing && possToShrink?.role === "king");
const possUid = shrunkenPossAllowed ? EP.shrunkenPossesive : undefined; const possUid = shrunkenPossAllowed ? EP.shrunkenPossesive : undefined;
const subject = makeSegment(getPashtoFromRendered(EP.subject, possUid, false)); const subject = makeSegment(getPashtoFromRendered(EP.subject, possUid, false));
const predicate = makeSegment(getPashtoFromRendered(EP.predicate, possUid, false)); const predicate = makeSegment(getPashtoFromRendered(EP.predicate, possUid, false));
@ -53,12 +47,12 @@ function getSegmentsAndKids(EP: T.EPRendered, form: Omit<T.FormVersion, "shrinkS
? [makeSegment(grammarUnits.baParticle, ["isBa", "isKid"])] ? [makeSegment(grammarUnits.baParticle, ["isBa", "isKid"])]
: [], : [],
...(possToShrink && shrunkenPossAllowed) ...(possToShrink && shrunkenPossAllowed)
? [shrinkNP(possToShrink)] ? [shrinkNP(possToShrink.np)]
: [], : [],
]), ]),
NPs: [ NPs: [
...ifNotRemoved(subject, "subject"), ...EP.omitSubject ? [] : [subject],
...ifNotRemoved(predicate, "predicate"), predicate
], ],
}; };
} }

View File

@ -26,28 +26,55 @@ export function orderKidsSection(kids: Segment[]): Segment[] {
}); });
} }
export function findPossesiveToShrink(VP: T.VPRendered | T.EPRendered): T.Rendered<T.NPSelection> | undefined { function findPossesiveInNP(NP: T.Rendered<T.NPSelection> | T.ObjectNP | undefined, uid: number): T.Rendered<T.NPSelection> | undefined {
const uid = VP.shrunkenPossesive;
function findPossesiveInNP(NP: T.Rendered<T.NPSelection> | T.ObjectNP | undefined): T.Rendered<T.NPSelection> | undefined {
if (NP === undefined) return undefined; if (NP === undefined) return undefined;
if (typeof NP !== "object") return undefined; if (typeof NP !== "object") return undefined;
if (!NP.possesor) return undefined; if (!NP.possesor) return undefined;
if (NP.possesor.uid === uid) { if (NP.possesor.uid === uid) {
return NP.possesor.np; return NP.possesor.np;
} }
return findPossesiveInNP(NP.possesor.np); return findPossesiveInNP(NP.possesor.np, uid);
} }
export function findPossesiveToShrinkInEP(EP: T.EPRendered): {
np: T.Rendered<T.NPSelection>,
from: "subject" | "predicate",
} | undefined {
const uid = EP.shrunkenPossesive;
if (uid === undefined) return undefined; if (uid === undefined) return undefined;
const objPred: T.Rendered<T.NPSelection> | undefined = ("object" in VP) const inSubject = findPossesiveInNP(EP.subject, uid);
? (typeof VP.object === "object" ? VP.object : undefined) if (inSubject) {
: (VP.predicate.type === "noun" || VP.predicate.type === "participle" || VP.predicate.type === "pronoun") return {
// typescript is dumb here; np: inSubject,
? VP.predicate as T.Rendered<T.NPSelection> from: "subject",
};
}
if (EP.predicate.type === "adjective" || EP.predicate.type === "loc. adv.") {
return undefined;
}
// ts being stupid
const predicate = EP.predicate as T.Rendered<T.NPSelection>;
const inPredicate = findPossesiveInNP(predicate, uid);
if (inPredicate) {
return {
np: inPredicate,
from: "predicate",
};
}
return undefined;
}
export function findPossesiveToShrinkInVP(VP: T.VPRendered): T.Rendered<T.NPSelection> | undefined {
const uid = VP.shrunkenPossesive;
if (uid === undefined) return undefined;
const obj: T.Rendered<T.NPSelection> | undefined = ("object" in VP && typeof VP.object === "object")
? VP.object
: undefined; : undefined;
return ( return (
findPossesiveInNP(VP.subject) findPossesiveInNP(VP.subject, uid)
|| ||
findPossesiveInNP(objPred) findPossesiveInNP(obj, uid)
); );
} }

View File

@ -22,7 +22,7 @@ import { isImperativeTense, isModalTense, isPerfectTense } from "../type-predica
import { getEnglishFromRendered, getPashtoFromRendered } from "./np-tools"; import { getEnglishFromRendered, getPashtoFromRendered } from "./np-tools";
import { import {
orderKidsSection, orderKidsSection,
findPossesiveToShrink, findPossesiveToShrinkInVP,
shrinkNP, shrinkNP,
} from "./compile-tools"; } from "./compile-tools";
@ -93,7 +93,7 @@ function getSegmentsAndKids(VP: T.VPRendered, form: Form): { kids: Segment[], NP
return servant; return servant;
})(); })();
const shrunkenServant = toShrinkServant ? shrinkNP(toShrinkServant) : undefined; const shrunkenServant = toShrinkServant ? shrinkNP(toShrinkServant) : undefined;
const possToShrink = findPossesiveToShrink(VP); const possToShrink = findPossesiveToShrinkInVP(VP);
const shrunkenPossesive = possToShrink ? shrinkNP(possToShrink) : undefined; const shrunkenPossesive = possToShrink ? shrinkNP(possToShrink) : undefined;
const shrunkenPossAllowed = possToShrink && shrunkenPossesive && ( const shrunkenPossAllowed = possToShrink && shrunkenPossesive && (
!shrunkenServant || !psStringEquals(shrunkenPossesive.ps[0], shrunkenServant.ps[0]) !shrunkenServant || !psStringEquals(shrunkenPossesive.ps[0], shrunkenServant.ps[0])

View File

@ -33,7 +33,7 @@ export function renderEP(EP: T.EPSelectionComplete): T.EPRendered {
equative: renderEquative(EP.equative, kingPerson), equative: renderEquative(EP.equative, kingPerson),
englishBase: equativeBuilders[EP.equative.tense](kingPerson, EP.equative.negative), englishBase: equativeBuilders[EP.equative.tense](kingPerson, EP.equative.negative),
shrunkenPossesive: EP.shrunkenPossesive, shrunkenPossesive: EP.shrunkenPossesive,
form: EP.form, omitSubject: EP.omitSubject,
}; };
} }

View File

@ -1,5 +1,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
type SaveableData = string | number | object | boolean | undefined | null
/** /**
* replacement from the React useState hook that will persist the state in local storage * replacement from the React useState hook that will persist the state in local storage
* *
@ -8,7 +10,7 @@ import { useEffect, useState } from "react";
* @param key a key for saving the state in locolStorage * @param key a key for saving the state in locolStorage
* @returns * @returns
*/ */
export default function useStickyState<T extends string | number | object | boolean | undefined | null>(defaultValue: T | ((old: T | undefined) => T), key: string): [ export default function useStickyState<T extends SaveableData>(defaultValue: T | ((old: T | undefined) => T), key: string): [
value: T, value: T,
setValue: React.Dispatch<React.SetStateAction<T>>, setValue: React.Dispatch<React.SetStateAction<T>>,
] { ] {
@ -42,3 +44,17 @@ export default function useStickyState<T extends string | number | object | bool
return [value, setValue]; return [value, setValue];
} }
export function useStickyReducer<T extends SaveableData, A>(
reducer: (state: T, dispatch: A) => T,
defaultValue: T | ((old: T | undefined) => T),
key: string,
): [T, (action: A) => void] {
const [state, unsafeSetState] = useStickyState<T>(defaultValue, key);
function adjustState(action: A) {
unsafeSetState(oldState => {
return reducer(oldState, action);
});
}
return [state, adjustState];
}

View File

@ -667,7 +667,7 @@ export type EPSelectionState = {
}, },
equative: EquativeSelection, equative: EquativeSelection,
shrunkenPossesive: undefined | number, shrunkenPossesive: undefined | number,
form: FormVersion, omitSubject: boolean,
}; };
export type EPSelectionComplete = Omit<EPSelectionState, "subject" | "predicate"> & { export type EPSelectionComplete = Omit<EPSelectionState, "subject" | "predicate"> & {
@ -679,7 +679,7 @@ export type EPSelectionComplete = Omit<EPSelectionState, "subject" | "predicate"
type: "Complement", type: "Complement",
selection: EqCompSelection, selection: EqCompSelection,
}, },
form: FormVersion, omitSubject: boolean,
}; };
export type EqCompType = "adjective" | "loc. adv."; // TODO: - more export type EqCompType = "adjective" | "loc. adv."; // TODO: - more
@ -700,11 +700,11 @@ export type EPRendered = {
type: "EPRendered", type: "EPRendered",
king: "subject" | "predicate", king: "subject" | "predicate",
subject: Rendered<NPSelection>, subject: Rendered<NPSelection>,
predicate: Rendered<NPSelection | EqCompSelection>, predicate: Rendered<NPSelection> | Rendered<EqCompSelection>,
equative: EquativeRendered, equative: EquativeRendered,
englishBase?: string[], englishBase?: string[],
shrunkenPossesive: undefined | number, shrunkenPossesive: undefined | number,
form: FormVersion, omitSubject: boolean,
} }
export type EntryFeeder = { export type EntryFeeder = {