PROPER SHRINKNIG IN PHRASE BUILDERS!

This commit is contained in:
lingdocs 2022-05-05 13:42:52 -05:00
parent 240fd11ed8
commit 9974eba176
24 changed files with 743 additions and 632 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@lingdocs/pashto-inflector",
"version": "2.3.9",
"version": "2.4.0",
"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

@ -1,41 +0,0 @@
/**
* Copyright (c) 2021 lingdocs.com
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
type Props = {
label: string,
options: { label: any, name: string, disabled?: boolean }[],
checked: string[],
handleChange: (p: { name: string, checked: boolean }) => void;
}
export default function (props: Props) {
function handleCheck(e: any) {
props.handleChange({
name: e.target.name as string,
checked: e.target.checked as boolean,
});
}
return (
<div className="text-center">
<span className="mr-2">{props.label}</span>
{props.options.map((option) => (
<div className="form-check form-check-inline">
<input
className="form-check-input"
type="checkbox"
name={option.name}
onChange={handleCheck}
checked={props.checked.includes(option.name)}
disabled={option.disabled}
/>
<label className="form-check-label">{option.label}</label>
</div>
))}
</div>
);
}

View File

@ -1,6 +1,6 @@
import * as T from "../../types";
import { renderEP } from "../../lib/phrase-building/render-ep";
import { compileEP } from "../../lib/phrase-building/compile-ep";
import { completeEPSelection, renderEP } from "../../lib/phrase-building/render-ep";
import { compileEP } from "../../lib/phrase-building/compile";
import Examples from "../Examples";
import ButtonSelect from "../ButtonSelect";
@ -61,33 +61,4 @@ function VariationLayer({ vs, opts }: { vs: T.PsString[], opts: T.TextOptions })
</div>;
}
function completeEPSelection(eps: T.EPSelectionState): T.EPSelectionComplete | undefined {
if (!eps.subject) {
return undefined;
}
if (eps.predicate.type === "Complement") {
const selection = eps.predicate.Complement;
if (!selection) return undefined;
return {
...eps,
subject: eps.subject,
predicate: {
type: "Complement",
selection,
},
};
}
// predicate is NP
const selection = eps.predicate.NP;
if (!selection) return undefined;
return {
...eps,
subject: eps.subject,
predicate: {
type: "NP",
selection,
},
};
}
export default EPDisplay;

View File

@ -8,6 +8,8 @@ import EqCompPicker from "./eq-comp-picker/EqCompPicker";
import { roleIcon } from "../vp-explorer/VPExplorerExplanationModal";
import EqChartsDisplay from "./EqChartsDisplay";
import epsReducer from "./eps-reducer";
import { useState } from "react";
import { completeEPSelection } from "../../lib/phrase-building/render-ep";
const blankEps: T.EPSelectionState = {
subject: undefined,
predicate: {
@ -19,7 +21,6 @@ const blankEps: T.EPSelectionState = {
tense: "present",
negative: false,
},
shrunkenPossesive: undefined,
omitSubject: false,
};
@ -30,12 +31,20 @@ function EPExplorer(props: {
entryFeeder: T.EntryFeeder,
}) {
const [mode, setMode] = useStickyState<"charts" | "phrases">("charts", "EPExplorerMode");
const [eps, adjustEps] = useStickyReducer(epsReducer, blankEps, "EPSelectionState10");
const [eps, adjustEps] = useStickyReducer(epsReducer, blankEps, "EPState", flashMessage);
const [alert, setAlert] = useState<string | undefined>(undefined);
const king = eps.subject?.type === "pronoun"
? "subject"
: eps.predicate.type === "Complement"
? "subject"
: "predicate";
function flashMessage(msg: string) {
setAlert(msg);
setTimeout(() => {
setAlert(undefined);
}, 1500);
}
const phraseIsComplete = !!completeEPSelection(eps);
return <div>
<div className="mt-2 mb-3 text-center">
<ButtonSelect
@ -51,8 +60,7 @@ function EPExplorer(props: {
{mode === "phrases" && <>
<div className="my-2">
<NPPicker
shrunkenPossesiveInPhrase={eps.shrunkenPossesive}
handleShrinkPossesive={payload => adjustEps({ type: "shrink possesive", payload })}
phraseIsComplete={phraseIsComplete}
heading={<div className="h5 text-center">Subject {king === "subject" ? roleIcon.king : ""}</div>}
entryFeeder={props.entryFeeder}
np={eps.subject}
@ -76,8 +84,7 @@ function EPExplorer(props: {
/>
</div>
{eps.predicate.type === "NP" ? <NPPicker
shrunkenPossesiveInPhrase={eps.shrunkenPossesive}
handleShrinkPossesive={payload => adjustEps({ type: "shrink possesive", payload })}
phraseIsComplete={phraseIsComplete}
entryFeeder={props.entryFeeder}
np={eps.predicate.type === "NP" ? eps.predicate.NP : undefined}
counterPart={undefined}
@ -107,6 +114,15 @@ function EPExplorer(props: {
eps={eps}
setOmitSubject={payload => adjustEps({ type: "set omitSubject", payload })}
/>}
{alert && <div className="alert alert-warning text-center" role="alert" style={{
position: "fixed",
top: "30%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: 9999999999999,
}}>
{alert}
</div>}
</div>;
}

View File

@ -2,7 +2,7 @@ import * as T from "../../types"
import Select from "react-select";
import ButtonSelect from "../ButtonSelect";
export const zIndexProps = {
const zIndexProps = {
menuPortalTarget: document.body,
styles: { menuPortal: (base: any) => ({ ...base, zIndex: 9999 }) },
};

View File

@ -4,8 +4,9 @@ import {
personNumber,
} from "../../lib/misc-helpers";
import { isUnisexNounEntry } from "../../lib/type-predicates";
import { checkForMiniPronounsError } from "../../lib/phrase-building/compile";
export type EpsReducerAction = {
type EpsReducerAction = {
type: "set predicate type",
payload: "NP" | "Complement",
} | {
@ -17,9 +18,6 @@ export type EpsReducerAction = {
} | {
type: "set predicate comp",
payload: T.EqCompSelection | undefined,
} | {
type: "shrink possesive",
payload: number | undefined,
} | {
type: "set omitSubject",
payload: "true" | "false",
@ -28,7 +26,7 @@ export type EpsReducerAction = {
payload: T.EquativeSelection,
};
export default function epsReducer(eps: T.EPSelectionState, action: EpsReducerAction): T.EPSelectionState {
export default function epsReducer(eps: T.EPSelectionState, action: EpsReducerAction, sendAlert?: (msg: string) => void): T.EPSelectionState {
if (action.type === "set predicate type") {
return {
...eps,
@ -39,10 +37,74 @@ export default function epsReducer(eps: T.EPSelectionState, action: EpsReducerAc
};
}
if (action.type === "set subject") {
return massageSubjectChange(action.payload, eps);
const subject = action.payload;
if (!subject) {
return {
...eps,
subject,
};
}
if (subject.type === "pronoun" && eps.predicate.type === "NP" && eps.predicate.NP?.type === "noun" && isUnisexNounEntry(eps.predicate.NP.entry)) {
const predicate = eps.predicate.NP;
const adjusted = {
...predicate,
...predicate.numberCanChange ? {
number: personNumber(subject.person),
} : {},
...predicate.genderCanChange ? {
gender: personGender(subject.person),
} : {},
}
return {
...eps,
subject,
predicate: {
...eps.predicate,
NP: adjusted,
},
};
}
const n: T.EPSelectionState = {
...eps,
subject,
};
return subject ? ensureMiniPronounsOk(eps, n, sendAlert) : n;
}
if (action.type === "set predicate NP") {
return massageNPPredicateChange(action.payload, eps);
const selection = action.payload;
if (!selection) {
return {
...eps,
predicate: {
...eps.predicate,
NP: selection,
},
};
}
if (eps.subject?.type === "pronoun" && selection.type === "noun" && isUnisexNounEntry(selection.entry)) {
const { gender, number } = selection;
const pronoun = eps.subject.person;
const newPronoun = movePersonNumber(movePersonGender(pronoun, gender), number);
return {
...eps,
subject: {
...eps.subject,
person: newPronoun,
},
predicate: {
...eps.predicate,
NP: selection,
},
};
}
const n: T.EPSelectionState = {
...eps,
predicate: {
...eps.predicate,
NP: selection,
},
};
return selection ? ensureMiniPronounsOk(eps, n, sendAlert) : n;
}
if (action.type === "set predicate comp") {
return {
@ -53,17 +115,12 @@ export default function epsReducer(eps: T.EPSelectionState, action: EpsReducerAc
},
};
}
if (action.type === "shrink possesive") {
return {
...eps,
shrunkenPossesive: action.payload,
};
}
if (action.type === "set omitSubject") {
return {
const n: T.EPSelectionState = {
...eps,
omitSubject: action.payload === "true",
};
return ensureMiniPronounsOk(eps, n, sendAlert);
}
// if (action.type === "set equative") {
return {
@ -73,72 +130,13 @@ export default function epsReducer(eps: T.EPSelectionState, action: EpsReducerAc
// }
}
function massageSubjectChange(subject: T.NPSelection | undefined, old: T.EPSelectionState): T.EPSelectionState {
if (!subject) {
return {
...old,
subject,
};
function ensureMiniPronounsOk(old: T.EPSelectionState, eps: T.EPSelectionState, sendAlert?: (msg: string) => void): T.EPSelectionState {
const error = checkForMiniPronounsError(eps);
if (error) {
if (sendAlert) sendAlert(error);
return old;
}
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,
},
};
return eps;
}
function movePersonGender(p: T.Person, gender: T.Gender): T.Person {

View File

@ -10,6 +10,8 @@ import { isSecondPerson } from "../../lib/phrase-building/vp-tools";
const npTypes: T.NPType[] = ["pronoun", "noun", "participle"];
export const shrunkenBackground = "rgba(255, 206, 43, 0.15)";
function NPPicker(props: {
heading?: JSX.Element | string,
onChange: (nps: T.NPSelection | undefined) => void,
@ -17,24 +19,27 @@ function NPPicker(props: {
counterPart: T.NPSelection | T.VerbObject | undefined,
role: "subject" | "object" | "ergative" | "possesor",
opts: T.TextOptions,
handleShrinkPossesive: (uid: number | undefined) => void,
shrunkenPossesiveInPhrase: number | undefined,
cantClear?: boolean,
is2ndPersonPicker?: boolean,
entryFeeder: T.EntryFeeder,
phraseIsComplete: boolean,
isShrunk?: boolean,
}) {
if (props.is2ndPersonPicker && ((props.np?.type !== "pronoun") || !isSecondPerson(props.np.person))) {
throw new Error("can't use 2ndPerson NPPicker without a pronoun");
}
}
const [addingPoss, setAddingPoss] = useState<boolean>(false);
const [npType, setNpType] = useState<T.NPType | undefined>(props.np ? props.np.type : undefined);
const onChange = (np: T.NPSelection | undefined) => {
props.onChange(ensureSingleShrink(props.np, np))
}
useEffect(() => {
setNpType(props.np ? props.np.type : undefined);
}, [props.np]);
function handleClear() {
if (props.np && props.np.type === "noun" && props.np.dynamicComplement) return;
setNpType(undefined);
props.onChange(undefined);
onChange(undefined);
}
function handleNPTypeChange(ntp: T.NPType) {
if (ntp === "pronoun") {
@ -45,38 +50,46 @@ function NPPicker(props: {
distance: "far",
};
setNpType(ntp);
props.onChange(pronoun);
onChange(pronoun);
} else {
props.onChange(undefined);
onChange(undefined);
setNpType(ntp);
}
}
// TODO: REMOVE
function handlePossesiveChange(p: T.NPSelection | undefined) {
if (!props.np || props.np.type === "pronoun") return;
if (!p) {
props.onChange({
onChange({
...props.np,
possesor: undefined,
});
return;
}
const isNewPosesser = checkForNewPossesor(p, props.np.possesor);
const possesor = {
const possesor: T.PossesorSelection = {
np: p,
uid: (!isNewPosesser && props.np.possesor) ? props.np.possesor.uid : makeUID(),
shrunken: (!isNewPosesser && props.np.possesor) ? props.np.possesor.shrunken : false,
};
props.onChange({
onChange({
...props.np,
possesor,
});
}
function handleToggleShrunken() {
if (!props.np || props.np.type === "pronoun" || !props.np.possesor || !props.phraseIsComplete) return;
onChange({
...props.np,
possesor: {
...props.np.possesor,
shrunken: !props.np.possesor.shrunken,
},
});
}
const isDynamicComplement = props.np && props.np.type === "noun" && props.np.dynamicComplement;
const clearButton = (!props.cantClear && !props.is2ndPersonPicker && !isDynamicComplement)
? <button className="btn btn-sm btn-light mb-2" onClick={handleClear}>X</button>
: <div></div>;
const possesiveUid = (props.np && props.np.type !== "pronoun" && props.np.possesor)
? props.np.possesor.uid
: undefined;
return <>
<div className="d-flex flex-row justify-content-between">
<div></div>
@ -105,16 +118,15 @@ function NPPicker(props: {
</button>
</div>)}
</div>}
{(props.np && props.np.type !== "pronoun" && (props.np.possesor || addingPoss)) && <div className="mb-3" style={{ paddingLeft: "0.5rem", borderLeft: "1px solid grey" }}>
{(props.np && props.np.type !== "pronoun" && (props.np.possesor || addingPoss)) && <div className="mb-3" style={{
paddingLeft: "0.65rem",
borderLeft: "2px solid grey",
background: (props.np.possesor?.shrunken && !props.isShrunk) ? shrunkenBackground : "inherit",
}}>
<div className="d-flex flex-row text-muted mb-2">
<div>Possesive:</div>
{props.np.possesor && <div className="clickable mx-2" onClick={() => {
props.handleShrinkPossesive(possesiveUid === props.shrunkenPossesiveInPhrase
? undefined
: possesiveUid
);
}}>
{possesiveUid === props.shrunkenPossesiveInPhrase ? "👶 Shrunken" : "Shrink"}
{(props.np.possesor && !props.isShrunk) && <div className="clickable ml-3 mr-2" onClick={handleToggleShrunken}>
{!props.np.possesor.shrunken ? "🪄" : "👶"}
</div>}
<div className="clickable ml-2" onClick={() => {
setAddingPoss(false);
@ -124,12 +136,11 @@ function NPPicker(props: {
</div>
</div>
<NPPicker
phraseIsComplete={props.phraseIsComplete}
onChange={handlePossesiveChange}
counterPart={undefined}
cantClear
np={props.np.possesor ? props.np.possesor.np : undefined}
handleShrinkPossesive={props.handleShrinkPossesive}
shrunkenPossesiveInPhrase={props.shrunkenPossesiveInPhrase}
role="possesor"
opts={props.opts}
entryFeeder={props.entryFeeder}
@ -142,7 +153,7 @@ function NPPicker(props: {
? <PronounPicker
role={props.role}
pronoun={props.np}
onChange={props.onChange}
onChange={onChange}
is2ndPersonPicker={props.is2ndPersonPicker}
opts={props.opts}
/>
@ -150,14 +161,14 @@ function NPPicker(props: {
? <NounPicker
entryFeeder={props.entryFeeder}
noun={(props.np && props.np.type === "noun") ? props.np : undefined}
onChange={props.onChange}
onChange={onChange}
opts={props.opts}
/>
: npType === "participle"
? <ParticiplePicker
entryFeeder={props.entryFeeder.verbs}
participle={(props.np && props.np.type === "participle") ? props.np : undefined}
onChange={props.onChange}
onChange={onChange}
opts={props.opts}
/>
: null
@ -166,6 +177,52 @@ function NPPicker(props: {
</>;
}
function ensureSingleShrink(old: T.NPSelection | undefined, s: T.NPSelection | undefined): T.NPSelection | undefined {
if (!s) return s;
function countShrinks(np: T.NPSelection): number {
if (np.type === "pronoun") return 0;
if (!np.possesor) return 0;
return (np.possesor.shrunken ? 1 : 0) + countShrinks(np.possesor.np);
}
function keepNewShrink(old: T.NPSelection, n: T.NPSelection): T.NPSelection {
if (n.type === "pronoun") return n;
if (old.type === "pronoun" || !n.possesor || !old.possesor) return n;
if (n.possesor.shrunken && !old.possesor.shrunken) {
return {
...n,
possesor: {
...n.possesor,
np: removeShrinks(n.possesor.np),
},
};
}
return {
...n,
possesor: {
shrunken: false,
np: keepNewShrink(old.possesor.np, n.possesor.np),
},
}
}
function removeShrinks(n: T.NPSelection): T.NPSelection {
if (n.type === "pronoun") return n;
if (!n.possesor) return n;
return {
...n,
possesor: {
shrunken: false,
np: removeShrinks(n.possesor.np),
},
};
}
if (!old) return s;
if (s.type === "pronoun") return s;
if (!s.possesor) return s;
const numOfShrinks = countShrinks(s);
if (numOfShrinks < 2) return s;
return keepNewShrink(old, s);
}
function checkForNewPossesor(n: T.NPSelection | undefined, old: T.PossesorSelection | undefined): boolean {
if (!old || !n) {
return true;
@ -183,8 +240,4 @@ function checkForNewPossesor(n: T.NPSelection | undefined, old: T.PossesorSelect
return false;
}
function makeUID() {
return Math.floor(Math.random() * 10000000);
}
export default NPPicker;

View File

@ -1,21 +1,16 @@
import { renderVP, compileVP } from "../../lib/phrase-building/index";
import { compileVP } from "../../lib/phrase-building/compile";
import * as T from "../../types";
import AbbreviationFormSelector from "./AbbreviationFormSelector";
import {
isPastTense,
completeVPSelection,
} from "../../lib/phrase-building/vp-tools";
import useStickyState from "../../lib/useStickyState";
import Examples from "../Examples";
function VPDisplay({ VP, opts, setForm }: {
VP: T.VPSelectionState,
VP: T.VPSelectionState | T.VPRendered,
opts: T.TextOptions,
setForm: (form: T.FormVersion) => void,
}) {
const [OSV, setOSV] = useStickyState<boolean>(false, "includeOSV");
const VPComplete = completeVPSelection(VP);
if (!VPComplete) {
if (!("type" in VP)) {
return <div className="lead text-muted text-center mt-4">
{(() => {
const twoNPs = (VP.subject === undefined) && (VP.verb.object === undefined);
@ -23,7 +18,7 @@ function VPDisplay({ VP, opts, setForm }: {
})()}
</div>;
}
const result = compileVP(renderVP(VPComplete), { ...VP.form, OSV });
const result = compileVP(VP, { ...VP.form, OSV });
return <div className="text-center mt-1">
{VP.verb.transitivity === "transitive" && <div className="form-check mb-2">
<input
@ -38,7 +33,7 @@ function VPDisplay({ VP, opts, setForm }: {
</label>
</div>}
<AbbreviationFormSelector
adjustable={whatsAdjustable(VPComplete)}
adjustable={VP.whatsAdjustable}
form={VP.form}
onChange={setForm}
/>
@ -61,20 +56,6 @@ function VPDisplay({ VP, opts, setForm }: {
</div>
}
function whatsAdjustable(VP: T.VPSelectionComplete): "both" | "king" | "servant" {
// TODO: intransitive dynamic compounds?
return (VP.verb.isCompound === "dynamic" && VP.verb.transitivity === "transitive")
? (isPastTense(VP.verb.tense) ? "servant" : "king")
: VP.verb.transitivity === "transitive"
? "both"
: VP.verb.transitivity === "intransitive"
? "king"
// grammTrans
: isPastTense(VP.verb.tense)
? "servant"
: "king";
}
function VariationLayer({ vs, opts }: { vs: T.PsString[], opts: T.TextOptions }) {
return <div className="mb-2">
<Examples opts={opts} lineHeight={0}>{vs}</Examples>

View File

@ -1,4 +1,4 @@
import NPPicker from "../np-picker/NPPicker";
import NPPicker, { shrunkenBackground } from "../np-picker/NPPicker";
import VerbPicker from "./VerbPicker";
import TensePicker from "./TensePicker";
import VPDisplay from "./VPDisplay";
@ -8,13 +8,14 @@ import ChartDisplay from "./VPChartDisplay";
import useStickyState, { useStickyReducer } from "../../lib/useStickyState";
import { makeVPSelectionState } from "./verb-selection";
import { useEffect, useState } from "react";
import { getKingAndServant } from "../../lib/phrase-building/render-vp";
import { isPastTense } from "../../lib/phrase-building/vp-tools";
import { getKingAndServant, renderVP } from "../../lib/phrase-building/render-vp";
import { completeVPSelection, isPastTense } from "../../lib/phrase-building/vp-tools";
import VPExplorerQuiz from "./VPExplorerQuiz";
import VPExplorerExplanationModal, { roleIcon } from "./VPExplorerExplanationModal";
// @ts-ignore
import LZString from "lz-string";
import { vpsReducer } from "./vps-reducer";
import { getShrunkenServant } from "../../lib/phrase-building/compile";
const phraseURLParam = "VPPhrase";
@ -28,7 +29,7 @@ const phraseURLParam = "VPPhrase";
// TODO: error handling on error with rendering etc
export function VPExplorer(props: {
function VPExplorer(props: {
loaded?: T.VPSelectionState,
verb: T.VerbEntry,
opts: T.TextOptions,
@ -40,7 +41,8 @@ export function VPExplorer(props: {
props.loaded
? props.loaded
: savedVps => makeVPSelectionState(props.verb, savedVps),
"vpsState8",
"vpsState8",
flashMessage,
);
const [mode, setMode] = useStickyState<"charts" | "phrases" | "quiz">(
savedMode => {
@ -51,12 +53,19 @@ export function VPExplorer(props: {
"verbExplorerMode2",
);
const [showShareClipped, setShowShareClipped] = useState<boolean>(false);
const [alert, setAlert] = useState<string | undefined>(undefined);
const [showingExplanation, setShowingExplanation] = useState<{ role: "servant" | "king", item: "subject" | "object" } | false>(false);
const isPast = isPastTense(vps.verb.tenseCategory === "perfect" ? vps.verb.perfectTense : vps.verb.verbTense);
const roles = getKingAndServant(
isPast,
vps.verb.transitivity !== "intransitive",
);
function flashMessage(msg: string) {
setAlert(msg);
setTimeout(() => {
setAlert(undefined);
}, 1500);
}
useEffect(() => {
const VPSFromUrl = getVPSFromUrl();
if (VPSFromUrl) {
@ -90,16 +99,10 @@ export function VPExplorer(props: {
function handleSubjObjSwap() {
adjustVps({ type: "swap subj/obj" });
}
function handleShrinkPossesive(shrunkenPossesive: number | undefined) {
adjustVps({
type: "shrink possesive",
payload: shrunkenPossesive,
});
}
function quizLock<T>(f: T) {
if (mode === "quiz") {
return () => {
alert("to adjust this, get out of quiz mode");
flashMessage("to adjust this, get out of quiz mode");
return null;
};
}
@ -120,6 +123,15 @@ export function VPExplorer(props: {
setShowShareClipped(false);
}, 1000);
}
const VPS = completeVPSelection(vps);
const phraseIsComplete = !!VPS;
const rendered = VPS ? renderVP(VPS) : undefined;
const servantIsShrunk = !!(rendered ? getShrunkenServant(rendered) : undefined);
function toggleServantShrink() {
adjustVps({
type: "toggle servant shrink",
});
}
return <div className="mt-3" style={{ maxWidth: "950px"}}>
<VerbPicker
vps={vps}
@ -155,40 +167,60 @@ export function VPExplorer(props: {
</div>}
{mode !== "quiz" && <div className="d-flex flex-row justify-content-around flex-wrap" style={{ marginLeft: "-0.5rem", marginRight: "-0.5rem" }}>
{mode === "phrases" && <>
<div className="my-2">
<div className="my-2" style={{ background: (servantIsShrunk && roles.servant === "subject") ? shrunkenBackground : "inherit" }}>
<NPPicker
phraseIsComplete={phraseIsComplete}
heading={roles.king === "subject"
? <div className="h5 text-center clickable" onClick={() => setShowingExplanation({ role: "king", item: "subject" })}>Subject {roleIcon.king}</div>
: <div className="h5 text-center clickable" onClick={() => setShowingExplanation({ role: "servant", item: "subject" })}>Subject {roleIcon.servant}</div>}
? <div className="h5 text-center" onClick={() => setShowingExplanation({ role: "king", item: "subject" })}>Subject {roleIcon.king}</div>
: <div className="h5 text-center">
Subject
{` `}
<span className="clickable" onClick={() => setShowingExplanation({ role: "servant", item: "subject" })}>{roleIcon.servant}</span>
{` `}
{(rendered && rendered.whatsAdjustable !== "king") &&
<span onClick={toggleServantShrink} className="ml-3">
{!servantIsShrunk ? "🪄" : "👶"}
</span>
}
</div>}
entryFeeder={props.entryFeeder}
role={(isPast && vps.verb.transitivity !== "intransitive")
? "ergative"
: "subject"
}
shrunkenPossesiveInPhrase={vps.shrunkenPossesive}
is2ndPersonPicker={vps.verb.tenseCategory === "imperative"}
np={vps.subject}
counterPart={vps.verb ? vps.verb.object : undefined}
onChange={handleSubjectChange}
handleShrinkPossesive={handleShrinkPossesive}
opts={props.opts}
isShrunk={(servantIsShrunk && roles.servant === "subject")}
/>
</div>
{vps.verb && (vps.verb.object !== "none") && <div className="my-2">
{vps.verb && (vps.verb.object !== "none") && <div className="my-2" style={{ background: (servantIsShrunk && roles.servant === "object") ? shrunkenBackground : "inherit" }}>
{(typeof vps.verb.object === "number")
? <div className="text-muted">Unspoken 3rd Pers. Masc. Plur.</div>
: <NPPicker
shrunkenPossesiveInPhrase={vps.shrunkenPossesive}
handleShrinkPossesive={handleShrinkPossesive}
phraseIsComplete={phraseIsComplete}
heading={roles.king === "object"
? <div className="h5 text-center clickable" onClick={() => setShowingExplanation({ role: "king", item: "object" })}>Object {roleIcon.king}</div>
: <div className="h5 text-center clickable" onClick={() => setShowingExplanation({ role: "servant", item: "object" })}>Object {roleIcon.servant}</div>}
: <div className="h5 text-center">
Object
{` `}
<span className="clickable" onClick={() => setShowingExplanation({ role: "servant", item: "object" })}>{roleIcon.servant}</span>
{` `}
{(rendered && rendered.whatsAdjustable !== "king") &&
<span onClick={toggleServantShrink} className="ml-3">
{!servantIsShrunk ? "🪄" : "👶"}
</span>
}
</div>}
entryFeeder={props.entryFeeder}
role="object"
np={vps.verb.object}
counterPart={vps.subject}
onChange={handleObjectChange}
opts={props.opts}
isShrunk={(servantIsShrunk && roles.servant === "object")}
/>}
</div>}
</>}
@ -201,7 +233,7 @@ export function VPExplorer(props: {
</div>
</div>}
{mode === "phrases" && <VPDisplay
VP={vps}
VP={rendered ? rendered : vps}
opts={props.opts}
setForm={handleSetForm}
/>}
@ -220,6 +252,15 @@ export function VPExplorer(props: {
}}>
Phrase URL copied to clipboard
</div>}
{alert && <div className="alert alert-warning text-center" role="alert" style={{
position: "fixed",
top: "30%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: 9999999999999,
}}>
{alert}
</div>}
</div>
}

View File

@ -7,7 +7,8 @@ import { standardizePashto } from "../../lib/standardize-pashto";
import shuffleArray from "../../lib/shuffle-array";
import InlinePs from "../InlinePs";
import { psStringEquals } from "../../lib/p-text-helpers";
import { renderVP, compileVP } from "../../lib/phrase-building/index";
import { renderVP } from "../../lib/phrase-building/render-vp";
import { compileVP } from "../../lib/phrase-building/compile";
import { getRandomTense } from "./TensePicker";
import { getTenseFromVerbSelection, removeBa, switchSubjObj } from "../../lib/phrase-building/vp-tools";
import playAudio from "../../lib/play-audio";
@ -445,7 +446,6 @@ function getRandomVPSelection(mix: MixType = "both") {
return {
subject: subject !== undefined ? subject : randSubj,
verb: randomizeTense(verb, true),
shrunkenPossesive: undefined,
form: { removeKing: false, shrinkServant: false },
}
}
@ -462,7 +462,6 @@ function getRandomVPSelection(mix: MixType = "both") {
return {
subject: randSubj,
verb: randomizeTense(v, true),
shrunkenPossesive: undefined,
form: { removeKing: false, shrinkServant: false },
};
};

View File

@ -67,15 +67,6 @@ export function makeVPSelectionState(
canChangeStatDyn: "stative" in info,
},
form: os ? os.form : { removeKing: false, shrinkServant: false },
shrunkenPossesive: os ? os.shrunkenPossesive : undefined,
};
}
export function changeVoice(v: T.VerbSelection, voice: "active" | "passive", s: T.NPSelection | undefined): T.VerbSelection {
return {
...v,
voice,
object: voice === "active" ? s : "none",
};
}

View File

@ -3,12 +3,13 @@ import {
isInvalidSubjObjCombo,
} from "../../lib/phrase-building/vp-tools";
import { switchSubjObj } from "../../lib/phrase-building/vp-tools";
import { changeStatDyn, changeTransitivity, changeVoice } from "./verb-selection";
import { changeStatDyn, changeTransitivity } from "./verb-selection";
import { ensure2ndPersSubjPronounAndNoConflict } from "../../lib/phrase-building/vp-tools";
import {
isPerfectTense,
isImperativeTense,
} from "../../lib/type-predicates";
import { checkForMiniPronounsError } from "../../lib/phrase-building/compile";
export type VpsReducerAction = {
type: "load vps",
@ -24,9 +25,6 @@ export type VpsReducerAction = {
payload: T.NPSelection | undefined,
} | {
type: "swap subj/obj",
} | {
type: "shrink possesive",
payload: number | undefined,
} | {
type: "set form",
payload: T.FormVersion,
@ -48,163 +46,189 @@ export type VpsReducerAction = {
} | {
type: "set tense category",
payload: "basic" | "modal" | "perfect" | "imperative",
}
} | {
type: "toggle servant shrink",
};
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;
export function vpsReducer(vps: T.VPSelectionState, action: VpsReducerAction, sendAlert?: (msg: string) => void): T.VPSelectionState {
return ensureMiniPronounsOk(vps, doReduce());
function ensureMiniPronounsOk(old: T.VPSelectionState, vps: T.VPSelectionState): T.VPSelectionState {
const error = checkForMiniPronounsError(vps);
if (error) {
if (sendAlert) sendAlert(error);
return old;
}
return {
...vps,
subject: action.payload.subject,
};
return vps;
}
if (action.type === "set object") {
if (!vps.verb) return vps;
if ((vps.verb.object === "none") || (typeof vps.verb.object === "number")) {
return vps;
function doReduce(): T.VPSelectionState {
if (action.type === "load vps") {
return action.payload;
}
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") {
if (action.type === "set subject") {
const { subject, skipPronounConflictCheck } = action.payload;
if (
!skipPronounConflictCheck
&&
hasPronounConflict(subject, vps.verb?.object)
) {
if (sendAlert) sendAlert("That combination of pronouns is not allowed");
return vps;
}
if (voice === "passive" && (typeof vps.verb.object === "object")) {
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)) {
if (sendAlert) sendAlert("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 === "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") {
return {
...vps,
subject: typeof vps.verb.object === "object" ? vps.verb.object : undefined,
verb: {
...vps.verb,
object: "none",
voice,
},
};
} else {
return {
...vps,
subject: undefined,
verb: {
...vps.verb,
// TODO: is this ok??
object: typeof vps.subject === "object" ? vps.subject : undefined,
voice,
},
};
}
} 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,
subject: vps.verb.object,
verb: changeVoice(vps.verb, voice, vps.verb.object),
verb: {
...vps.verb,
perfectTense: tense,
tenseCategory: "perfect",
},
};
} else if (isImperativeTense(tense)) {
return {
...vps,
verb: {
...vps.verb,
imperativeTense: tense,
tenseCategory: "imperative",
},
};
} else {
return {
...vps,
verb: changeVoice(vps.verb, voice, voice === "active" ? vps.subject : undefined),
verb: {
...vps.verb,
verbTense: tense,
tenseCategory: vps.verb.tenseCategory === "perfect"
? "basic"
: vps.verb.tenseCategory,
},
};
}
} 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)) {
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,
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,
},
});
};
}
// if (action.type === "toggle servant shrink") {
return {
...vps,
verb: {
...vps.verb,
tenseCategory: category,
form: {
...vps.form,
shrinkServant: !vps.form.shrinkServant,
},
};
// }
// }
}
}
function hasPronounConflict(subject: T.NPSelection | undefined, object: undefined | T.VerbObject): boolean {

View File

@ -1,97 +0,0 @@
import * as T from "../../types";
import * as grammarUnits from "../grammar-units";
import {
removeDuplicates,
} from "./vp-tools";
import {
combineSegments,
makeSegment,
putKidsInKidsSection,
Segment,
flattenLengths,
} from "./segment";
import { removeAccents } from "../accent-helpers";
import { getEnglishFromRendered, getPashtoFromRendered } from "./np-tools";
import {
orderKidsSection,
findPossesiveToShrinkInEP,
shrinkNP,
} from "./compile-tools";
export function compileEP(EP: T.EPRendered): { ps: T.SingleOrLengthOpts<T.PsString[]>, e?: string[] };
export function compileEP(EP: T.EPRendered, combineLengths: true): { ps: T.PsString[], e?: string[] };
export function compileEP(EP: T.EPRendered, combineLengths?: true): { ps: T.SingleOrLengthOpts<T.PsString[]>, e?: string[] } {
const { kids, NPs } = getSegmentsAndKids(EP);
const equative = EP.equative.ps;
const psResult = compilePs({
NPs,
kids,
equative,
negative: EP.equative.negative,
});
return {
ps: combineLengths ? flattenLengths(psResult) : psResult,
e: compileEnglish(EP),
};
}
function getSegmentsAndKids(EP: T.EPRendered): { kids: Segment[], NPs: Segment[] } {
const possToShrink = findPossesiveToShrinkInEP(EP);
const shrunkenPossAllowed = !((possToShrink?.from === "subject") && EP.omitSubject);
const possUid = shrunkenPossAllowed ? EP.shrunkenPossesive : undefined;
const subject = makeSegment(getPashtoFromRendered(EP.subject, possUid, false));
const predicate = makeSegment(getPashtoFromRendered(EP.predicate, possUid, false));
return {
kids: orderKidsSection([
...EP.equative.hasBa
? [makeSegment(grammarUnits.baParticle, ["isBa", "isKid"])]
: [],
...(possToShrink && shrunkenPossAllowed)
? [shrinkNP(possToShrink.np)]
: [],
]),
NPs: [
...EP.omitSubject ? [] : [subject],
predicate
],
};
}
function compilePs({ NPs, kids, equative, negative }: {
NPs: Segment[],
kids: Segment[],
equative: T.SingleOrLengthOpts<T.PsString[]>,
negative: boolean,
}): T.SingleOrLengthOpts<T.PsString[]> {
if ("long" in equative) {
return {
long: compilePs({ NPs, kids, equative: equative.long, negative }) as T.PsString[],
short: compilePs({ NPs, kids, equative: equative.short, negative }) as T.PsString[],
};
}
const allSegments = putKidsInKidsSection([
...NPs,
...negative ? [
makeSegment({ p: "نه", f: "nú" }),
makeSegment(removeAccents(equative))
] : [
makeSegment(equative),
],
], kids);
return removeDuplicates(combineSegments(allSegments, "spaces"));
}
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 = 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, {
subject: engSubj,
predicate: engPred,
}))
: undefined;
}

View File

@ -1,90 +0,0 @@
import * as T from "../../types";
import {
Segment,
makeSegment,
} from "./segment";
import { getVerbBlockPosFromPerson } from "../misc-helpers";
import { pronouns } from "../grammar-units";
export function orderKidsSection(kids: Segment[]): Segment[] {
const sorted = [...kids];
return sorted.sort((a, b) => {
// ba first
if (a.isBa) return -1;
// kinds lined up 1st 2nd 3rd person
if (a.isMiniPronoun && b.isMiniPronoun) {
if (a.isMiniPronoun < b.isMiniPronoun) {
return -1;
}
if (a.isMiniPronoun > b.isMiniPronoun) {
return 1;
}
// TODO: is this enough?
return 0;
}
return 0;
});
}
function findPossesiveInNP(NP: T.Rendered<T.NPSelection> | T.ObjectNP | undefined, uid: number): T.Rendered<T.NPSelection> | undefined {
if (NP === undefined) return undefined;
if (typeof NP !== "object") return undefined;
if (!NP.possesor) return undefined;
if (NP.possesor.uid === uid) {
return 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;
const inSubject = findPossesiveInNP(EP.subject, uid);
if (inSubject) {
return {
np: inSubject,
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;
return (
findPossesiveInNP(VP.subject, uid)
||
findPossesiveInNP(obj, uid)
);
}
export function shrinkNP(np: T.Rendered<T.NPSelection>): Segment {
function getFirstSecThird(): 1 | 2 | 3 {
if ([0, 1, 6, 7].includes(np.person)) return 1;
if ([2, 3, 8, 9].includes(np.person)) return 2;
return 3;
}
const [row, col] = getVerbBlockPosFromPerson(np.person);
return makeSegment(pronouns.mini[row][col], ["isKid", getFirstSecThird()]);
}

View File

@ -1,6 +1,6 @@
import * as T from "../../types";
import {
concatPsString, psStringEquals,
concatPsString,
} from "../p-text-helpers";
import {
Segment,
@ -20,18 +20,18 @@ import {
} from "./vp-tools";
import { isImperativeTense, isModalTense, isPerfectTense } from "../type-predicates";
import { getEnglishFromRendered, getPashtoFromRendered } from "./np-tools";
import {
orderKidsSection,
findPossesiveToShrinkInVP,
shrinkNP,
} from "./compile-tools";
import { getVerbBlockPosFromPerson } from "../misc-helpers";
import { pronouns } from "../grammar-units";
import { completeEPSelection, renderEP } from "./render-ep";
import { completeVPSelection } from "./vp-tools";
import { renderVP } from "./render-vp";
type Form = T.FormVersion & { OSV?: boolean };
export function compileVP(VP: T.VPRendered, form: Form): { ps: T.SingleOrLengthOpts<T.PsString[]>, e?: string [] };
export function compileVP(VP: T.VPRendered, form: Form, combineLengths: true): { ps: T.PsString[], e?: string [] };
export function compileVP(VP: T.VPRendered, form: Form, combineLengths?: true): { ps: T.SingleOrLengthOpts<T.PsString[]>, e?: string [] } {
const verb = VP.verb.ps;
const { kids, NPs } = getSegmentsAndKids(VP, form);
const { kids, NPs } = getVPSegmentsAndKids(VP, form);
const psResult = compilePs({
NPs,
kids,
@ -82,9 +82,8 @@ function compilePs({ NPs, kids, verb: { head, rest }, VP }: CompilePsInput): T.S
)));
}
function getSegmentsAndKids(VP: T.VPRendered, form: Form): { kids: Segment[], NPs: Segment[][] } {
const removeKing = form.removeKing && !(VP.isCompound === "dynamic" && VP.isPast);
const shrinkServant = form.shrinkServant && !(VP.isCompound === "dynamic" && !VP.isPast);
export function getShrunkenServant(VP: T.VPRendered): Segment | undefined {
const shrinkServant = VP.form.shrinkServant && !(VP.isCompound === "dynamic" && !VP.isPast);
const toShrinkServant = (() => {
if (!shrinkServant) return undefined;
if (!VP.servant) return undefined;
@ -92,27 +91,23 @@ function getSegmentsAndKids(VP: T.VPRendered, form: Form): { kids: Segment[], NP
if (typeof servant !== "object") return undefined;
return servant;
})();
const shrunkenServant = toShrinkServant ? shrinkNP(toShrinkServant) : undefined;
const possToShrink = findPossesiveToShrinkInVP(VP);
const shrunkenPossesive = possToShrink ? shrinkNP(possToShrink) : undefined;
const shrunkenPossAllowed = possToShrink && shrunkenPossesive && (
!shrunkenServant || !psStringEquals(shrunkenPossesive.ps[0], shrunkenServant.ps[0])
) && (
// can only shrink the possesive if the parent of the possesive is still in full form
!(possToShrink.role === "king" && removeKing)
&&
!(possToShrink.role === "servant" && shrinkServant)
);
const shrinkPossUid = shrunkenPossAllowed
? VP.shrunkenPossesive
: undefined;
return toShrinkServant ? shrinkNP(toShrinkServant) : undefined;
}
export function getVPSegmentsAndKids(VP: T.VPRendered, form?: Form): { kids: Segment[], NPs: Segment[][] } {
const removeKing = VP.form.removeKing && !(VP.isCompound === "dynamic" && VP.isPast);
const shrunkenServant = getShrunkenServant(VP);
const possToShrink = findPossesivesToShrinkInVP(VP, {
shrunkServant: !!shrunkenServant,
removedKing: removeKing,
});
const SO = {
subject: getPashtoFromRendered(VP.subject, shrinkPossUid, false),
object: typeof VP.object === "object" ? getPashtoFromRendered(VP.object, shrinkPossUid, VP.subject.person) : undefined,
subject: getPashtoFromRendered(VP.subject, false),
object: typeof VP.object === "object" ? getPashtoFromRendered(VP.object, VP.subject.person) : undefined,
};
function getSegment(t: "subject" | "object"): Segment | undefined {
const word = (VP.servant === t)
? (!shrinkServant ? SO[t] : undefined)
? (!shrunkenServant ? SO[t] : undefined)
: (VP.king === t)
? (!removeKing ? SO[t] : undefined)
: undefined;
@ -128,8 +123,7 @@ function getSegmentsAndKids(VP: T.VPRendered, form: Form): { kids: Segment[], NP
? [makeSegment(grammarUnits.baParticle, ["isBa", "isKid"])] : [],
...shrunkenServant
? [shrunkenServant] : [],
...(shrunkenPossesive && shrunkenPossAllowed)
? [shrunkenPossesive] : [],
...possToShrink.map(shrinkNP),
]),
NPs: [
[
@ -138,7 +132,7 @@ function getSegmentsAndKids(VP: T.VPRendered, form: Form): { kids: Segment[], NP
],
// TODO: make this an option to also include O S V order ??
// also show O S V if both are showing
...(subject && object && form.OSV) ? [[object, subject]] : [],
...(subject && object && (form && form.OSV)) ? [[object, subject]] : [],
],
};
}
@ -255,6 +249,82 @@ function arrangeVerbWNegative(head: T.PsString | undefined, restRaw: T.PsString[
];
}
export function compileEP(EP: T.EPRendered): { ps: T.SingleOrLengthOpts<T.PsString[]>, e?: string[] };
export function compileEP(EP: T.EPRendered, combineLengths: true): { ps: T.PsString[], e?: string[] };
export function compileEP(EP: T.EPRendered, combineLengths?: true): { ps: T.SingleOrLengthOpts<T.PsString[]>, e?: string[] } {
const { kids, NPs } = getEPSegmentsAndKids(EP);
const equative = EP.equative.ps;
const psResult = compileEPPs({
NPs,
kids,
equative,
negative: EP.equative.negative,
});
return {
ps: combineLengths ? flattenLengths(psResult) : psResult,
e: compileEPEnglish(EP),
};
}
export function getEPSegmentsAndKids(EP: T.EPRendered): { kids: Segment[], NPs: Segment[] } {
const possToShrink = findPossesivesToShrinkInEP(EP);
const subject = makeSegment(getPashtoFromRendered(EP.subject, false));
const predicate = makeSegment(getPashtoFromRendered(EP.predicate, false));
return {
kids: orderKidsSection([
...EP.equative.hasBa
? [makeSegment(grammarUnits.baParticle, ["isBa", "isKid"])]
: [],
...possToShrink.map(a => shrinkNP(a.np)),
]),
NPs: [
...EP.omitSubject ? [] : [subject],
predicate
],
};
}
function compileEPPs({ NPs, kids, equative, negative }: {
NPs: Segment[],
kids: Segment[],
equative: T.SingleOrLengthOpts<T.PsString[]>,
negative: boolean,
}): T.SingleOrLengthOpts<T.PsString[]> {
if ("long" in equative) {
return {
long: compileEPPs({ NPs, kids, equative: equative.long, negative }) as T.PsString[],
short: compileEPPs({ NPs, kids, equative: equative.short, negative }) as T.PsString[],
};
}
const allSegments = putKidsInKidsSection([
...NPs,
...negative ? [
makeSegment({ p: "نه", f: "nú" }),
makeSegment(removeAccents(equative))
] : [
makeSegment(equative),
],
], kids);
return removeDuplicates(combineSegments(allSegments, "spaces"));
}
function compileEPEnglish(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 = 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, {
subject: engSubj,
predicate: engPred,
}))
: undefined;
}
function mergeSegments(s1: Segment, s2: Segment, noSpace?: "no space"): Segment {
if (noSpace) {
return s2.adjust({ ps: (p) => concatPsString(s1.ps[0], p) });
@ -309,3 +379,126 @@ function compileEnglish(VP: T.VPRendered): string[] | undefined {
}))
: undefined;
}
export function orderKidsSection(kids: Segment[]): Segment[] {
const sorted = [...kids];
return sorted.sort((a, b) => {
// ba first
if (a.isBa) return -1;
// kinds lined up 1st 2nd 3rd person
if (a.isMiniPronoun && b.isMiniPronoun) {
if (a.isMiniPronoun < b.isMiniPronoun) {
return -1;
}
if (a.isMiniPronoun > b.isMiniPronoun) {
return 1;
}
// TODO: is this enough?
return 0;
}
return 0;
});
}
export function checkForMiniPronounsError(s: T.EPSelectionState | T.VPSelectionState): undefined | string {
function findDuplicateMiniPronoun(mp: Segment[]): Segment | undefined {
const duplicates = mp.filter((item, index) => (
mp.findIndex(m => item.ps[0].p === m.ps[0].p) !== index
));
if (duplicates.length === 0) return undefined;
return duplicates[0];
}
const kids = (() => {
if ("predicate" in s) {
const EPS = completeEPSelection(s);
if (!EPS) return undefined;
const { kids } = getEPSegmentsAndKids(renderEP(EPS));
return kids;
}
const VPS = completeVPSelection(s);
if (!VPS) return undefined;
const { kids } = getVPSegmentsAndKids(renderVP(VPS));
return kids;
})();
if (!kids) return undefined;
const miniPronouns = kids.filter(x => x.isMiniPronoun);
if (miniPronouns.length > 2) {
return "can't add another mini-pronoun, there are alread two";
}
const duplicateMiniPronoun = findDuplicateMiniPronoun(miniPronouns);
if (duplicateMiniPronoun) {
return `there's already a ${duplicateMiniPronoun.ps[0].p} - ${duplicateMiniPronoun.ps[0].f} mini-pronoun in use, can't have two of those`;
}
return undefined;
}
export function findPossesivesToShrinkInVP(VP: T.VPRendered, f: {
shrunkServant: boolean,
removedKing: boolean,
}): T.Rendered<T.NPSelection>[] {
return findPossesives(VP.subject, VP.object).filter(x => (
// only give the possesive to shrink if it's not alread in a shrunken servant
!(f.shrunkServant && x.role === "servant")
&& // or in a removed king
!(f.removedKing && x.role === "king")
));
}
function findPossesives(...nps: (T.Rendered<T.NPSelection> | T.ObjectNP | undefined)[]): T.Rendered<T.NPSelection>[] {
return nps.reduce((accum, curr) => {
const res = findPossesiveInNP(curr);
if (res) return [...accum, res];
return accum;
}, [] as T.Rendered<T.NPSelection>[]);
}
function findPossesiveInNP(NP: T.Rendered<T.NPSelection> | T.ObjectNP | undefined): T.Rendered<T.NPSelection> | undefined {
if (NP === undefined) return undefined;
if (typeof NP !== "object") return undefined;
if (!NP.possesor) return undefined;
if (NP.possesor.shrunken) {
return NP.possesor.np;
}
return findPossesiveInNP(NP.possesor.np);
}
type FoundNP = {
np: T.Rendered<T.NPSelection>,
from: "subject" | "predicate",
};
export function findPossesivesToShrinkInEP(EP: T.EPRendered): FoundNP[] {
const inSubject = findPossesiveInNP(EP.subject);
const inPredicate = (EP.predicate.type === "adjective" || EP.predicate.type === "loc. adv.")
? undefined
: findPossesiveInNP(
// @ts-ignore - ts being dumb
EP.predicate as T.NPSelection
);
return [
...inSubject ? [{ np: inSubject, from: "subject"} as FoundNP] : [],
...inPredicate ? [{ np: inPredicate, from: "predicate" } as FoundNP] : [],
].filter(found => !(found.from === "subject" && EP.omitSubject));
}
export function findPossesiveToShrinkInVP(VP: T.VPRendered): T.Rendered<T.NPSelection> | undefined {
const obj: T.Rendered<T.NPSelection> | undefined = ("object" in VP && typeof VP.object === "object")
? VP.object
: undefined;
return (
findPossesiveInNP(VP.subject)
||
findPossesiveInNP(obj)
);
}
export function shrinkNP(np: T.Rendered<T.NPSelection>): Segment {
function getFirstSecThird(): 1 | 2 | 3 {
if ([0, 1, 6, 7].includes(np.person)) return 1;
if ([2, 3, 8, 9].includes(np.person)) return 2;
return 3;
}
const [row, col] = getVerbBlockPosFromPerson(np.person);
return makeSegment(pronouns.mini[row][col], ["isKid", getFirstSecThird()]);
}

View File

@ -1,7 +0,0 @@
import { renderVP } from "./render-vp";
import { compileVP } from "./compile-vp";
export {
renderVP,
compileVP,
};

View File

@ -22,14 +22,39 @@ function getBaseAndAdjectives(np: T.Rendered<T.NPSelection | T.EqCompSelection>)
));
}
export function getPashtoFromRendered(np: T.Rendered<T.NPSelection | T.EqCompSelection>, shrunkenPossesive: number | undefined, subjectsPerson: false | T.Person): T.PsString[] {
const base = getBaseAndAdjectives(np);
if (!np.possesor || np.possesor.uid === shrunkenPossesive) {
return base;
function trimOffShrunkenPossesive(p: T.Rendered<T.NPSelection>): T.Rendered<T.NPSelection> {
if (!("possesor" in p)) {
return p;
}
if (!p.possesor) {
return p;
}
if (p.possesor.shrunken) {
return {
...p,
possesor: undefined,
};
}
return {
...p,
possesor: {
...p.possesor,
np: trimOffShrunkenPossesive(p.possesor.np),
}
}
return addPossesor(np.possesor.np, base, subjectsPerson);
}
export function getPashtoFromRendered(np: T.Rendered<T.NPSelection> | T.Rendered<T.EqCompSelection>, subjectsPerson: false | T.Person): T.PsString[] {
const base = getBaseAndAdjectives(np);
if (np.type !== "loc. adv." && np.type !== "adjective") {
// ts being dumb
const trimmed = trimOffShrunkenPossesive(np as T.Rendered<T.NPSelection>);
if (trimmed.possesor) {
return addPossesor(trimmed.possesor.np, base, subjectsPerson);
}
}
return base;
}
function addPossesor(owner: T.Rendered<T.NPSelection>, existing: T.PsString[], subjectsPerson: false | T.Person): T.PsString[] {
function willBeReflexive(subj: T.Person, obj: T.Person): boolean {

View File

@ -32,7 +32,6 @@ export function renderEP(EP: T.EPSelectionComplete): T.EPRendered {
: renderEqCompSelection(EP.predicate.selection, kingPerson),
equative: renderEquative(EP.equative, kingPerson),
englishBase: equativeBuilders[EP.equative.tense](kingPerson, EP.equative.negative),
shrunkenPossesive: EP.shrunkenPossesive,
omitSubject: EP.omitSubject,
};
}
@ -150,3 +149,32 @@ function getEnglishConj(p: T.Person, e: string | T.EnglishBlock): string {
// return inflections[gender][plural ? 1 : 0];
// }
export function completeEPSelection(eps: T.EPSelectionState): T.EPSelectionComplete | undefined {
if (!eps.subject) {
return undefined;
}
if (eps.predicate.type === "Complement") {
const selection = eps.predicate.Complement;
if (!selection) return undefined;
return {
...eps,
subject: eps.subject,
predicate: {
type: "Complement",
selection,
},
};
}
// predicate is NP
const selection = eps.predicate.NP;
if (!selection) return undefined;
return {
...eps,
subject: eps.subject,
predicate: {
type: "NP",
selection,
},
};
}

View File

@ -12,7 +12,7 @@ import {
import { parseEc } from "../misc-helpers";
import { getEnglishWord } from "../get-english-word";
import { renderAdjectiveSelection } from "./render-adj";
import { isPattern5Entry, isUnisexNounEntry } from "../type-predicates";
import { isPattern5Entry, isUnisexAnimNounEntry } from "../type-predicates";
export function renderNPSelection(NP: T.NPSelection, inflected: boolean, inflectEnglish: boolean, role: "subject", soRole: "servant" | "king" | "none"): T.Rendered<T.NPSelection>;
export function renderNPSelection(NP: T.NPSelection | T.ObjectNP, inflected: boolean, inflectEnglish: boolean, role: "object", soRole: "servant" | "king" | "none"): T.Rendered<T.NPSelection> | T.Person.ThirdPlurMale | "none";
@ -87,13 +87,18 @@ function renderParticipleSelection(p: T.ParticipleSelection, inflected: boolean,
};
}
function renderPossesor(possesor: { np: T.NPSelection, uid: number } | undefined, possesorRole: "servant" | "king" | "none"): { np: T.Rendered<T.NPSelection>, uid: number } | undefined {
function renderPossesor(possesor: T.PossesorSelection | undefined, possesorRole: "servant" | "king" | "none"): T.RenderedPossesorSelection | undefined {
if (!possesor) return undefined;
const isSingUnisexAnim5PatternNoun = (possesor.np.type === "noun"
&& possesor.np.number === "singular"
&& isUnisexAnimNounEntry(possesor.np.entry)
&& isPattern5Entry(possesor.np.entry)
);
return {
uid: possesor.uid,
shrunken: possesor.shrunken,
np: renderNPSelection(
possesor.np,
!(possesor.np.type === "noun" && isUnisexNounEntry(possesor.np.entry) && isPattern5Entry(possesor.np.entry)),
!isSingUnisexAnim5PatternNoun,
false,
"subject",
possesorRole,

View File

@ -47,7 +47,6 @@ export function renderVP(VP: T.VPSelectionComplete): T.VPRendered {
type: "VPRendered",
king,
servant,
shrunkenPossesive: VP.shrunkenPossesive,
isPast,
isTransitive,
isCompound: VP.verb.isCompound,
@ -60,10 +59,25 @@ export function renderVP(VP: T.VPSelectionComplete): T.VPRendered {
vs: VP.verb,
}),
form: VP.form,
whatsAdjustable: whatsAdjustable(VP),
};
return b;
}
function whatsAdjustable(VP: T.VPSelectionComplete): "both" | "king" | "servant" {
// TODO: intransitive dynamic compounds?
return (VP.verb.isCompound === "dynamic" && VP.verb.transitivity === "transitive")
? (isPastTense(VP.verb.tense) ? "servant" : "king")
: VP.verb.transitivity === "transitive"
? (VP.verb.voice === "active" ? "both" : "king")
: VP.verb.transitivity === "intransitive"
? "king"
// grammTrans
: isPastTense(VP.verb.tense)
? "servant"
: "king";
}
function renderVerbSelection(vs: T.VerbSelectionComplete, person: T.Person, objectPerson: T.Person | undefined): T.VerbRendered {
const v = vs.dynAuxVerb || vs.verb;
const conjugations = conjugateVerb(v.entry, v.complement);

View File

@ -49,6 +49,10 @@ export function isUnisexNounEntry(e: T.NounEntry | T.AdjectiveEntry): e is T.Uni
return isNounEntry(e) && e.c.includes("unisex");
}
export function isUnisexAnimNounEntry(e: T.NounEntry | T.AdjectiveEntry): e is T.UnisexAnimNounEntry {
return isUnisexNounEntry(e) && e.c.includes("anim.");
}
export function isAdjOrUnisexNounEntry(e: T.Entry): e is (T.AdjectiveEntry | T.UnisexNounEntry) {
return isAdjectiveEntry(e) || (
isNounEntry(e) && isUnisexNounEntry(e)
@ -136,9 +140,7 @@ export function isPattern5Entry<T extends (T.NounEntry | T.AdjectiveEntry)>(e: T
return (
!!(e.infap && e.infaf && e.infbp && e.infbf)
&&
(e.infaf.slice(-1) === "u")
&&
!e.infap.slice(1).includes("ا")
e.infap.includes("ا")
);
}

View File

@ -46,15 +46,16 @@ export default function useStickyState<T extends SaveableData>(defaultValue: T |
}
export function useStickyReducer<T extends SaveableData, A>(
reducer: (state: T, dispatch: A) => T,
reducer: (state: T, dispatch: A, sendAlert?: (msg: string) => void) => T,
defaultValue: T | ((old: T | undefined) => T),
key: string,
): [T, (action: A) => void] {
sendAlert?: (msg: string) => void,
): [T, (action: A) => void, ((msg: string) => void) | undefined] {
const [state, unsafeSetState] = useStickyState<T>(defaultValue, key);
function adjustState(action: A) {
unsafeSetState(oldState => {
return reducer(oldState, action);
return reducer(oldState, action, sendAlert);
});
}
return [state, adjustState];
return [state, adjustState, sendAlert];
}

File diff suppressed because one or more lines are too long

View File

@ -473,6 +473,7 @@ export type NounEntry = DictionaryEntry & { c: string } & { __brand: "a noun ent
export type MascNounEntry = NounEntry & { __brand2: "a masc noun entry" };
export type FemNounEntry = NounEntry & { __brand2: "a fem noun entry" };
export type UnisexNounEntry = MascNounEntry & { __brand3: "a unisex noun entry" };
export type UnisexAnimNounEntry = UnisexNounEntry & { __brand4: "an anim unisex noun entry" };
export type AdverbEntry = DictionaryEntry & { c: string } & { __brand: "an adverb entry" };
export type LocativeAdverbEntry = AdverbEntry & { __brand2: "a locative adverb entry" };
export type AdjectiveEntry = DictionaryEntry & { c: string } & { __brand: "an adjective entry" };
@ -501,7 +502,6 @@ export type VPRendered = {
type: "VPRendered",
king: "subject" | "object",
servant: "subject" | "object" | undefined,
shrunkenPossesive: number | undefined,
isPast: boolean,
isTransitive: boolean,
isCompound: "stative" | "dynamic" | false,
@ -510,6 +510,7 @@ export type VPRendered = {
verb: VerbRendered,
englishBase?: string[],
form: FormVersion,
whatsAdjustable: "both" | "king" | "servant",
}
export type VerbTense = "presentVerb"
@ -531,14 +532,12 @@ export type Tense = EquativeTense | VerbTense | PerfectTense | ModalTense | Impe
export type VPSelectionState = {
subject: NPSelection | undefined,
verb: VerbSelection,
shrunkenPossesive: undefined | number,
form: FormVersion,
};
export type VPSelectionComplete = {
subject: NPSelection,
verb: VerbSelectionComplete,
shrunkenPossesive: undefined | number,
form: FormVersion,
};
@ -592,7 +591,7 @@ export type NPType = "noun" | "pronoun" | "participle";
export type ObjectNP = "none" | Person.ThirdPlurMale;
export type PossesorSelection = {
uid: number,
shrunken: boolean,
np: NPSelection,
}
@ -639,6 +638,13 @@ export type ReplaceKey<T, K extends string, R> = T extends Record<K, unknown> ?
export type FormVersion = { removeKing: boolean, shrinkServant: boolean };
// TODO: rendered should would for rendering T.PossesorSelection etc
// look recursively down on something
export type RenderedPossesorSelection = {
np: Rendered<NPSelection>,
shrunken: boolean,
};
export type Rendered<T extends NPSelection | EqCompSelection | AdjectiveSelection> = ReplaceKey<
Omit<T, "changeGender" | "changeNumber" | "changeDistance" | "adjectives" | "possesor">,
"e",
@ -652,7 +658,7 @@ export type Rendered<T extends NPSelection | EqCompSelection | AdjectiveSelectio
// TODO: better recursive thing
adjectives?: Rendered<AdjectiveSelection>[],
possesor?: {
uid: number,
shrunken: boolean,
np: Rendered<NPSelection>,
},
};
@ -666,7 +672,6 @@ export type EPSelectionState = {
Complement: EqCompSelection | undefined,
},
equative: EquativeSelection,
shrunkenPossesive: undefined | number,
omitSubject: boolean,
};
@ -703,7 +708,6 @@ export type EPRendered = {
predicate: Rendered<NPSelection> | Rendered<EqCompSelection>,
equative: EquativeRendered,
englishBase?: string[],
shrunkenPossesive: undefined | number,
omitSubject: boolean,
}