PROPER SHRINKNIG IN PHRASE BUILDERS!
This commit is contained in:
parent
240fd11ed8
commit
9974eba176
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@lingdocs/pashto-inflector",
|
"name": "@lingdocs/pashto-inflector",
|
||||||
"version": "2.3.9",
|
"version": "2.4.0",
|
||||||
"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",
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as T from "../../types";
|
import * as T from "../../types";
|
||||||
import { renderEP } from "../../lib/phrase-building/render-ep";
|
import { completeEPSelection, renderEP } from "../../lib/phrase-building/render-ep";
|
||||||
import { compileEP } from "../../lib/phrase-building/compile-ep";
|
import { compileEP } from "../../lib/phrase-building/compile";
|
||||||
import Examples from "../Examples";
|
import Examples from "../Examples";
|
||||||
import ButtonSelect from "../ButtonSelect";
|
import ButtonSelect from "../ButtonSelect";
|
||||||
|
|
||||||
|
@ -61,33 +61,4 @@ function VariationLayer({ vs, opts }: { vs: T.PsString[], opts: T.TextOptions })
|
||||||
</div>;
|
</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;
|
export default EPDisplay;
|
|
@ -8,6 +8,8 @@ import EqCompPicker from "./eq-comp-picker/EqCompPicker";
|
||||||
import { roleIcon } from "../vp-explorer/VPExplorerExplanationModal";
|
import { roleIcon } from "../vp-explorer/VPExplorerExplanationModal";
|
||||||
import EqChartsDisplay from "./EqChartsDisplay";
|
import EqChartsDisplay from "./EqChartsDisplay";
|
||||||
import epsReducer from "./eps-reducer";
|
import epsReducer from "./eps-reducer";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { completeEPSelection } from "../../lib/phrase-building/render-ep";
|
||||||
const blankEps: T.EPSelectionState = {
|
const blankEps: T.EPSelectionState = {
|
||||||
subject: undefined,
|
subject: undefined,
|
||||||
predicate: {
|
predicate: {
|
||||||
|
@ -19,7 +21,6 @@ const blankEps: T.EPSelectionState = {
|
||||||
tense: "present",
|
tense: "present",
|
||||||
negative: false,
|
negative: false,
|
||||||
},
|
},
|
||||||
shrunkenPossesive: undefined,
|
|
||||||
omitSubject: false,
|
omitSubject: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -30,12 +31,20 @@ function EPExplorer(props: {
|
||||||
entryFeeder: T.EntryFeeder,
|
entryFeeder: T.EntryFeeder,
|
||||||
}) {
|
}) {
|
||||||
const [mode, setMode] = useStickyState<"charts" | "phrases">("charts", "EPExplorerMode");
|
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"
|
const king = eps.subject?.type === "pronoun"
|
||||||
? "subject"
|
? "subject"
|
||||||
: eps.predicate.type === "Complement"
|
: eps.predicate.type === "Complement"
|
||||||
? "subject"
|
? "subject"
|
||||||
: "predicate";
|
: "predicate";
|
||||||
|
function flashMessage(msg: string) {
|
||||||
|
setAlert(msg);
|
||||||
|
setTimeout(() => {
|
||||||
|
setAlert(undefined);
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
const phraseIsComplete = !!completeEPSelection(eps);
|
||||||
return <div>
|
return <div>
|
||||||
<div className="mt-2 mb-3 text-center">
|
<div className="mt-2 mb-3 text-center">
|
||||||
<ButtonSelect
|
<ButtonSelect
|
||||||
|
@ -51,8 +60,7 @@ function EPExplorer(props: {
|
||||||
{mode === "phrases" && <>
|
{mode === "phrases" && <>
|
||||||
<div className="my-2">
|
<div className="my-2">
|
||||||
<NPPicker
|
<NPPicker
|
||||||
shrunkenPossesiveInPhrase={eps.shrunkenPossesive}
|
phraseIsComplete={phraseIsComplete}
|
||||||
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}
|
||||||
|
@ -76,8 +84,7 @@ function EPExplorer(props: {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{eps.predicate.type === "NP" ? <NPPicker
|
{eps.predicate.type === "NP" ? <NPPicker
|
||||||
shrunkenPossesiveInPhrase={eps.shrunkenPossesive}
|
phraseIsComplete={phraseIsComplete}
|
||||||
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}
|
||||||
|
@ -107,6 +114,15 @@ function EPExplorer(props: {
|
||||||
eps={eps}
|
eps={eps}
|
||||||
setOmitSubject={payload => adjustEps({ type: "set omitSubject", payload })}
|
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>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as T from "../../types"
|
||||||
import Select from "react-select";
|
import Select from "react-select";
|
||||||
import ButtonSelect from "../ButtonSelect";
|
import ButtonSelect from "../ButtonSelect";
|
||||||
|
|
||||||
export const zIndexProps = {
|
const zIndexProps = {
|
||||||
menuPortalTarget: document.body,
|
menuPortalTarget: document.body,
|
||||||
styles: { menuPortal: (base: any) => ({ ...base, zIndex: 9999 }) },
|
styles: { menuPortal: (base: any) => ({ ...base, zIndex: 9999 }) },
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,8 +4,9 @@ import {
|
||||||
personNumber,
|
personNumber,
|
||||||
} from "../../lib/misc-helpers";
|
} from "../../lib/misc-helpers";
|
||||||
import { isUnisexNounEntry } from "../../lib/type-predicates";
|
import { isUnisexNounEntry } from "../../lib/type-predicates";
|
||||||
|
import { checkForMiniPronounsError } from "../../lib/phrase-building/compile";
|
||||||
|
|
||||||
export type EpsReducerAction = {
|
type EpsReducerAction = {
|
||||||
type: "set predicate type",
|
type: "set predicate type",
|
||||||
payload: "NP" | "Complement",
|
payload: "NP" | "Complement",
|
||||||
} | {
|
} | {
|
||||||
|
@ -17,9 +18,6 @@ export type EpsReducerAction = {
|
||||||
} | {
|
} | {
|
||||||
type: "set predicate comp",
|
type: "set predicate comp",
|
||||||
payload: T.EqCompSelection | undefined,
|
payload: T.EqCompSelection | undefined,
|
||||||
} | {
|
|
||||||
type: "shrink possesive",
|
|
||||||
payload: number | undefined,
|
|
||||||
} | {
|
} | {
|
||||||
type: "set omitSubject",
|
type: "set omitSubject",
|
||||||
payload: "true" | "false",
|
payload: "true" | "false",
|
||||||
|
@ -28,7 +26,7 @@ export type EpsReducerAction = {
|
||||||
payload: T.EquativeSelection,
|
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") {
|
if (action.type === "set predicate type") {
|
||||||
return {
|
return {
|
||||||
...eps,
|
...eps,
|
||||||
|
@ -39,10 +37,74 @@ export default function epsReducer(eps: T.EPSelectionState, action: EpsReducerAc
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (action.type === "set subject") {
|
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") {
|
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") {
|
if (action.type === "set predicate comp") {
|
||||||
return {
|
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") {
|
if (action.type === "set omitSubject") {
|
||||||
return {
|
const n: T.EPSelectionState = {
|
||||||
...eps,
|
...eps,
|
||||||
omitSubject: action.payload === "true",
|
omitSubject: action.payload === "true",
|
||||||
};
|
};
|
||||||
|
return ensureMiniPronounsOk(eps, n, sendAlert);
|
||||||
}
|
}
|
||||||
// if (action.type === "set equative") {
|
// if (action.type === "set equative") {
|
||||||
return {
|
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 {
|
function ensureMiniPronounsOk(old: T.EPSelectionState, eps: T.EPSelectionState, sendAlert?: (msg: string) => void): T.EPSelectionState {
|
||||||
if (!subject) {
|
const error = checkForMiniPronounsError(eps);
|
||||||
return {
|
if (error) {
|
||||||
...old,
|
if (sendAlert) sendAlert(error);
|
||||||
subject,
|
return old;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (subject.type === "pronoun" && old.predicate.type === "NP" && old.predicate.NP?.type === "noun" && isUnisexNounEntry(old.predicate.NP.entry)) {
|
return eps;
|
||||||
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 {
|
function movePersonGender(p: T.Person, gender: T.Gender): T.Person {
|
||||||
|
|
|
@ -10,6 +10,8 @@ import { isSecondPerson } from "../../lib/phrase-building/vp-tools";
|
||||||
|
|
||||||
const npTypes: T.NPType[] = ["pronoun", "noun", "participle"];
|
const npTypes: T.NPType[] = ["pronoun", "noun", "participle"];
|
||||||
|
|
||||||
|
export const shrunkenBackground = "rgba(255, 206, 43, 0.15)";
|
||||||
|
|
||||||
function NPPicker(props: {
|
function NPPicker(props: {
|
||||||
heading?: JSX.Element | string,
|
heading?: JSX.Element | string,
|
||||||
onChange: (nps: T.NPSelection | undefined) => void,
|
onChange: (nps: T.NPSelection | undefined) => void,
|
||||||
|
@ -17,24 +19,27 @@ function NPPicker(props: {
|
||||||
counterPart: T.NPSelection | T.VerbObject | undefined,
|
counterPart: T.NPSelection | T.VerbObject | undefined,
|
||||||
role: "subject" | "object" | "ergative" | "possesor",
|
role: "subject" | "object" | "ergative" | "possesor",
|
||||||
opts: T.TextOptions,
|
opts: T.TextOptions,
|
||||||
handleShrinkPossesive: (uid: number | undefined) => void,
|
|
||||||
shrunkenPossesiveInPhrase: number | undefined,
|
|
||||||
cantClear?: boolean,
|
cantClear?: boolean,
|
||||||
is2ndPersonPicker?: boolean,
|
is2ndPersonPicker?: boolean,
|
||||||
entryFeeder: T.EntryFeeder,
|
entryFeeder: T.EntryFeeder,
|
||||||
|
phraseIsComplete: boolean,
|
||||||
|
isShrunk?: boolean,
|
||||||
}) {
|
}) {
|
||||||
if (props.is2ndPersonPicker && ((props.np?.type !== "pronoun") || !isSecondPerson(props.np.person))) {
|
if (props.is2ndPersonPicker && ((props.np?.type !== "pronoun") || !isSecondPerson(props.np.person))) {
|
||||||
throw new Error("can't use 2ndPerson NPPicker without a pronoun");
|
throw new Error("can't use 2ndPerson NPPicker without a pronoun");
|
||||||
}
|
}
|
||||||
const [addingPoss, setAddingPoss] = useState<boolean>(false);
|
const [addingPoss, setAddingPoss] = useState<boolean>(false);
|
||||||
const [npType, setNpType] = useState<T.NPType | undefined>(props.np ? props.np.type : undefined);
|
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(() => {
|
useEffect(() => {
|
||||||
setNpType(props.np ? props.np.type : undefined);
|
setNpType(props.np ? props.np.type : undefined);
|
||||||
}, [props.np]);
|
}, [props.np]);
|
||||||
function handleClear() {
|
function handleClear() {
|
||||||
if (props.np && props.np.type === "noun" && props.np.dynamicComplement) return;
|
if (props.np && props.np.type === "noun" && props.np.dynamicComplement) return;
|
||||||
setNpType(undefined);
|
setNpType(undefined);
|
||||||
props.onChange(undefined);
|
onChange(undefined);
|
||||||
}
|
}
|
||||||
function handleNPTypeChange(ntp: T.NPType) {
|
function handleNPTypeChange(ntp: T.NPType) {
|
||||||
if (ntp === "pronoun") {
|
if (ntp === "pronoun") {
|
||||||
|
@ -45,38 +50,46 @@ function NPPicker(props: {
|
||||||
distance: "far",
|
distance: "far",
|
||||||
};
|
};
|
||||||
setNpType(ntp);
|
setNpType(ntp);
|
||||||
props.onChange(pronoun);
|
onChange(pronoun);
|
||||||
} else {
|
} else {
|
||||||
props.onChange(undefined);
|
onChange(undefined);
|
||||||
setNpType(ntp);
|
setNpType(ntp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// TODO: REMOVE
|
||||||
function handlePossesiveChange(p: T.NPSelection | undefined) {
|
function handlePossesiveChange(p: T.NPSelection | undefined) {
|
||||||
if (!props.np || props.np.type === "pronoun") return;
|
if (!props.np || props.np.type === "pronoun") return;
|
||||||
if (!p) {
|
if (!p) {
|
||||||
props.onChange({
|
onChange({
|
||||||
...props.np,
|
...props.np,
|
||||||
possesor: undefined,
|
possesor: undefined,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const isNewPosesser = checkForNewPossesor(p, props.np.possesor);
|
const isNewPosesser = checkForNewPossesor(p, props.np.possesor);
|
||||||
const possesor = {
|
const possesor: T.PossesorSelection = {
|
||||||
np: p,
|
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,
|
...props.np,
|
||||||
possesor,
|
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 isDynamicComplement = props.np && props.np.type === "noun" && props.np.dynamicComplement;
|
||||||
const clearButton = (!props.cantClear && !props.is2ndPersonPicker && !isDynamicComplement)
|
const clearButton = (!props.cantClear && !props.is2ndPersonPicker && !isDynamicComplement)
|
||||||
? <button className="btn btn-sm btn-light mb-2" onClick={handleClear}>X</button>
|
? <button className="btn btn-sm btn-light mb-2" onClick={handleClear}>X</button>
|
||||||
: <div></div>;
|
: <div></div>;
|
||||||
const possesiveUid = (props.np && props.np.type !== "pronoun" && props.np.possesor)
|
|
||||||
? props.np.possesor.uid
|
|
||||||
: undefined;
|
|
||||||
return <>
|
return <>
|
||||||
<div className="d-flex flex-row justify-content-between">
|
<div className="d-flex flex-row justify-content-between">
|
||||||
<div></div>
|
<div></div>
|
||||||
|
@ -105,16 +118,15 @@ function NPPicker(props: {
|
||||||
</button>
|
</button>
|
||||||
</div>)}
|
</div>)}
|
||||||
</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 className="d-flex flex-row text-muted mb-2">
|
||||||
<div>Possesive:</div>
|
<div>Possesive:</div>
|
||||||
{props.np.possesor && <div className="clickable mx-2" onClick={() => {
|
{(props.np.possesor && !props.isShrunk) && <div className="clickable ml-3 mr-2" onClick={handleToggleShrunken}>
|
||||||
props.handleShrinkPossesive(possesiveUid === props.shrunkenPossesiveInPhrase
|
{!props.np.possesor.shrunken ? "🪄" : "👶"}
|
||||||
? undefined
|
|
||||||
: possesiveUid
|
|
||||||
);
|
|
||||||
}}>
|
|
||||||
{possesiveUid === props.shrunkenPossesiveInPhrase ? "👶 Shrunken" : "Shrink"}
|
|
||||||
</div>}
|
</div>}
|
||||||
<div className="clickable ml-2" onClick={() => {
|
<div className="clickable ml-2" onClick={() => {
|
||||||
setAddingPoss(false);
|
setAddingPoss(false);
|
||||||
|
@ -124,12 +136,11 @@ function NPPicker(props: {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NPPicker
|
<NPPicker
|
||||||
|
phraseIsComplete={props.phraseIsComplete}
|
||||||
onChange={handlePossesiveChange}
|
onChange={handlePossesiveChange}
|
||||||
counterPart={undefined}
|
counterPart={undefined}
|
||||||
cantClear
|
cantClear
|
||||||
np={props.np.possesor ? props.np.possesor.np : undefined}
|
np={props.np.possesor ? props.np.possesor.np : undefined}
|
||||||
handleShrinkPossesive={props.handleShrinkPossesive}
|
|
||||||
shrunkenPossesiveInPhrase={props.shrunkenPossesiveInPhrase}
|
|
||||||
role="possesor"
|
role="possesor"
|
||||||
opts={props.opts}
|
opts={props.opts}
|
||||||
entryFeeder={props.entryFeeder}
|
entryFeeder={props.entryFeeder}
|
||||||
|
@ -142,7 +153,7 @@ function NPPicker(props: {
|
||||||
? <PronounPicker
|
? <PronounPicker
|
||||||
role={props.role}
|
role={props.role}
|
||||||
pronoun={props.np}
|
pronoun={props.np}
|
||||||
onChange={props.onChange}
|
onChange={onChange}
|
||||||
is2ndPersonPicker={props.is2ndPersonPicker}
|
is2ndPersonPicker={props.is2ndPersonPicker}
|
||||||
opts={props.opts}
|
opts={props.opts}
|
||||||
/>
|
/>
|
||||||
|
@ -150,14 +161,14 @@ function NPPicker(props: {
|
||||||
? <NounPicker
|
? <NounPicker
|
||||||
entryFeeder={props.entryFeeder}
|
entryFeeder={props.entryFeeder}
|
||||||
noun={(props.np && props.np.type === "noun") ? props.np : undefined}
|
noun={(props.np && props.np.type === "noun") ? props.np : undefined}
|
||||||
onChange={props.onChange}
|
onChange={onChange}
|
||||||
opts={props.opts}
|
opts={props.opts}
|
||||||
/>
|
/>
|
||||||
: npType === "participle"
|
: npType === "participle"
|
||||||
? <ParticiplePicker
|
? <ParticiplePicker
|
||||||
entryFeeder={props.entryFeeder.verbs}
|
entryFeeder={props.entryFeeder.verbs}
|
||||||
participle={(props.np && props.np.type === "participle") ? props.np : undefined}
|
participle={(props.np && props.np.type === "participle") ? props.np : undefined}
|
||||||
onChange={props.onChange}
|
onChange={onChange}
|
||||||
opts={props.opts}
|
opts={props.opts}
|
||||||
/>
|
/>
|
||||||
: null
|
: 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 {
|
function checkForNewPossesor(n: T.NPSelection | undefined, old: T.PossesorSelection | undefined): boolean {
|
||||||
if (!old || !n) {
|
if (!old || !n) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -183,8 +240,4 @@ function checkForNewPossesor(n: T.NPSelection | undefined, old: T.PossesorSelect
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeUID() {
|
|
||||||
return Math.floor(Math.random() * 10000000);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default NPPicker;
|
export default NPPicker;
|
|
@ -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 * as T from "../../types";
|
||||||
import AbbreviationFormSelector from "./AbbreviationFormSelector";
|
import AbbreviationFormSelector from "./AbbreviationFormSelector";
|
||||||
import {
|
|
||||||
isPastTense,
|
|
||||||
completeVPSelection,
|
|
||||||
} from "../../lib/phrase-building/vp-tools";
|
|
||||||
import useStickyState from "../../lib/useStickyState";
|
import useStickyState from "../../lib/useStickyState";
|
||||||
import Examples from "../Examples";
|
import Examples from "../Examples";
|
||||||
|
|
||||||
function VPDisplay({ VP, opts, setForm }: {
|
function VPDisplay({ VP, opts, setForm }: {
|
||||||
VP: T.VPSelectionState,
|
VP: T.VPSelectionState | T.VPRendered,
|
||||||
opts: T.TextOptions,
|
opts: T.TextOptions,
|
||||||
setForm: (form: T.FormVersion) => void,
|
setForm: (form: T.FormVersion) => void,
|
||||||
}) {
|
}) {
|
||||||
const [OSV, setOSV] = useStickyState<boolean>(false, "includeOSV");
|
const [OSV, setOSV] = useStickyState<boolean>(false, "includeOSV");
|
||||||
const VPComplete = completeVPSelection(VP);
|
if (!("type" in VP)) {
|
||||||
if (!VPComplete) {
|
|
||||||
return <div className="lead text-muted text-center mt-4">
|
return <div className="lead text-muted text-center mt-4">
|
||||||
{(() => {
|
{(() => {
|
||||||
const twoNPs = (VP.subject === undefined) && (VP.verb.object === undefined);
|
const twoNPs = (VP.subject === undefined) && (VP.verb.object === undefined);
|
||||||
|
@ -23,7 +18,7 @@ function VPDisplay({ VP, opts, setForm }: {
|
||||||
})()}
|
})()}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
const result = compileVP(renderVP(VPComplete), { ...VP.form, OSV });
|
const result = compileVP(VP, { ...VP.form, OSV });
|
||||||
return <div className="text-center mt-1">
|
return <div className="text-center mt-1">
|
||||||
{VP.verb.transitivity === "transitive" && <div className="form-check mb-2">
|
{VP.verb.transitivity === "transitive" && <div className="form-check mb-2">
|
||||||
<input
|
<input
|
||||||
|
@ -38,7 +33,7 @@ function VPDisplay({ VP, opts, setForm }: {
|
||||||
</label>
|
</label>
|
||||||
</div>}
|
</div>}
|
||||||
<AbbreviationFormSelector
|
<AbbreviationFormSelector
|
||||||
adjustable={whatsAdjustable(VPComplete)}
|
adjustable={VP.whatsAdjustable}
|
||||||
form={VP.form}
|
form={VP.form}
|
||||||
onChange={setForm}
|
onChange={setForm}
|
||||||
/>
|
/>
|
||||||
|
@ -61,20 +56,6 @@ function VPDisplay({ VP, opts, setForm }: {
|
||||||
</div>
|
</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 }) {
|
function VariationLayer({ vs, opts }: { vs: T.PsString[], opts: T.TextOptions }) {
|
||||||
return <div className="mb-2">
|
return <div className="mb-2">
|
||||||
<Examples opts={opts} lineHeight={0}>{vs}</Examples>
|
<Examples opts={opts} lineHeight={0}>{vs}</Examples>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import NPPicker from "../np-picker/NPPicker";
|
import NPPicker, { shrunkenBackground } from "../np-picker/NPPicker";
|
||||||
import VerbPicker from "./VerbPicker";
|
import VerbPicker from "./VerbPicker";
|
||||||
import TensePicker from "./TensePicker";
|
import TensePicker from "./TensePicker";
|
||||||
import VPDisplay from "./VPDisplay";
|
import VPDisplay from "./VPDisplay";
|
||||||
|
@ -8,13 +8,14 @@ import ChartDisplay from "./VPChartDisplay";
|
||||||
import useStickyState, { useStickyReducer } 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, renderVP } from "../../lib/phrase-building/render-vp";
|
||||||
import { isPastTense } from "../../lib/phrase-building/vp-tools";
|
import { completeVPSelection, isPastTense } from "../../lib/phrase-building/vp-tools";
|
||||||
import VPExplorerQuiz from "./VPExplorerQuiz";
|
import VPExplorerQuiz from "./VPExplorerQuiz";
|
||||||
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";
|
import { vpsReducer } from "./vps-reducer";
|
||||||
|
import { getShrunkenServant } from "../../lib/phrase-building/compile";
|
||||||
|
|
||||||
const phraseURLParam = "VPPhrase";
|
const phraseURLParam = "VPPhrase";
|
||||||
|
|
||||||
|
@ -28,7 +29,7 @@ const phraseURLParam = "VPPhrase";
|
||||||
|
|
||||||
// TODO: error handling on error with rendering etc
|
// TODO: error handling on error with rendering etc
|
||||||
|
|
||||||
export function VPExplorer(props: {
|
function VPExplorer(props: {
|
||||||
loaded?: T.VPSelectionState,
|
loaded?: T.VPSelectionState,
|
||||||
verb: T.VerbEntry,
|
verb: T.VerbEntry,
|
||||||
opts: T.TextOptions,
|
opts: T.TextOptions,
|
||||||
|
@ -40,7 +41,8 @@ export function VPExplorer(props: {
|
||||||
props.loaded
|
props.loaded
|
||||||
? props.loaded
|
? props.loaded
|
||||||
: savedVps => makeVPSelectionState(props.verb, savedVps),
|
: savedVps => makeVPSelectionState(props.verb, savedVps),
|
||||||
"vpsState8",
|
"vpsState8",
|
||||||
|
flashMessage,
|
||||||
);
|
);
|
||||||
const [mode, setMode] = useStickyState<"charts" | "phrases" | "quiz">(
|
const [mode, setMode] = useStickyState<"charts" | "phrases" | "quiz">(
|
||||||
savedMode => {
|
savedMode => {
|
||||||
|
@ -51,12 +53,19 @@ export function VPExplorer(props: {
|
||||||
"verbExplorerMode2",
|
"verbExplorerMode2",
|
||||||
);
|
);
|
||||||
const [showShareClipped, setShowShareClipped] = useState<boolean>(false);
|
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 [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 isPast = isPastTense(vps.verb.tenseCategory === "perfect" ? vps.verb.perfectTense : vps.verb.verbTense);
|
||||||
const roles = getKingAndServant(
|
const roles = getKingAndServant(
|
||||||
isPast,
|
isPast,
|
||||||
vps.verb.transitivity !== "intransitive",
|
vps.verb.transitivity !== "intransitive",
|
||||||
);
|
);
|
||||||
|
function flashMessage(msg: string) {
|
||||||
|
setAlert(msg);
|
||||||
|
setTimeout(() => {
|
||||||
|
setAlert(undefined);
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const VPSFromUrl = getVPSFromUrl();
|
const VPSFromUrl = getVPSFromUrl();
|
||||||
if (VPSFromUrl) {
|
if (VPSFromUrl) {
|
||||||
|
@ -90,16 +99,10 @@ export function VPExplorer(props: {
|
||||||
function handleSubjObjSwap() {
|
function handleSubjObjSwap() {
|
||||||
adjustVps({ type: "swap subj/obj" });
|
adjustVps({ type: "swap subj/obj" });
|
||||||
}
|
}
|
||||||
function handleShrinkPossesive(shrunkenPossesive: number | undefined) {
|
|
||||||
adjustVps({
|
|
||||||
type: "shrink possesive",
|
|
||||||
payload: shrunkenPossesive,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function quizLock<T>(f: T) {
|
function quizLock<T>(f: T) {
|
||||||
if (mode === "quiz") {
|
if (mode === "quiz") {
|
||||||
return () => {
|
return () => {
|
||||||
alert("to adjust this, get out of quiz mode");
|
flashMessage("to adjust this, get out of quiz mode");
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -120,6 +123,15 @@ export function VPExplorer(props: {
|
||||||
setShowShareClipped(false);
|
setShowShareClipped(false);
|
||||||
}, 1000);
|
}, 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"}}>
|
return <div className="mt-3" style={{ maxWidth: "950px"}}>
|
||||||
<VerbPicker
|
<VerbPicker
|
||||||
vps={vps}
|
vps={vps}
|
||||||
|
@ -155,40 +167,60 @@ export function VPExplorer(props: {
|
||||||
</div>}
|
</div>}
|
||||||
{mode !== "quiz" && <div className="d-flex flex-row justify-content-around flex-wrap" style={{ marginLeft: "-0.5rem", marginRight: "-0.5rem" }}>
|
{mode !== "quiz" && <div className="d-flex flex-row justify-content-around flex-wrap" style={{ marginLeft: "-0.5rem", marginRight: "-0.5rem" }}>
|
||||||
{mode === "phrases" && <>
|
{mode === "phrases" && <>
|
||||||
<div className="my-2">
|
<div className="my-2" style={{ background: (servantIsShrunk && roles.servant === "subject") ? shrunkenBackground : "inherit" }}>
|
||||||
<NPPicker
|
<NPPicker
|
||||||
|
phraseIsComplete={phraseIsComplete}
|
||||||
heading={roles.king === "subject"
|
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" 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">
|
||||||
|
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}
|
entryFeeder={props.entryFeeder}
|
||||||
role={(isPast && vps.verb.transitivity !== "intransitive")
|
role={(isPast && vps.verb.transitivity !== "intransitive")
|
||||||
? "ergative"
|
? "ergative"
|
||||||
: "subject"
|
: "subject"
|
||||||
}
|
}
|
||||||
shrunkenPossesiveInPhrase={vps.shrunkenPossesive}
|
|
||||||
is2ndPersonPicker={vps.verb.tenseCategory === "imperative"}
|
is2ndPersonPicker={vps.verb.tenseCategory === "imperative"}
|
||||||
np={vps.subject}
|
np={vps.subject}
|
||||||
counterPart={vps.verb ? vps.verb.object : undefined}
|
counterPart={vps.verb ? vps.verb.object : undefined}
|
||||||
onChange={handleSubjectChange}
|
onChange={handleSubjectChange}
|
||||||
handleShrinkPossesive={handleShrinkPossesive}
|
|
||||||
opts={props.opts}
|
opts={props.opts}
|
||||||
|
isShrunk={(servantIsShrunk && roles.servant === "subject")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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")
|
{(typeof vps.verb.object === "number")
|
||||||
? <div className="text-muted">Unspoken 3rd Pers. Masc. Plur.</div>
|
? <div className="text-muted">Unspoken 3rd Pers. Masc. Plur.</div>
|
||||||
: <NPPicker
|
: <NPPicker
|
||||||
shrunkenPossesiveInPhrase={vps.shrunkenPossesive}
|
phraseIsComplete={phraseIsComplete}
|
||||||
handleShrinkPossesive={handleShrinkPossesive}
|
|
||||||
heading={roles.king === "object"
|
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: "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}
|
entryFeeder={props.entryFeeder}
|
||||||
role="object"
|
role="object"
|
||||||
np={vps.verb.object}
|
np={vps.verb.object}
|
||||||
counterPart={vps.subject}
|
counterPart={vps.subject}
|
||||||
onChange={handleObjectChange}
|
onChange={handleObjectChange}
|
||||||
opts={props.opts}
|
opts={props.opts}
|
||||||
|
isShrunk={(servantIsShrunk && roles.servant === "object")}
|
||||||
/>}
|
/>}
|
||||||
</div>}
|
</div>}
|
||||||
</>}
|
</>}
|
||||||
|
@ -201,7 +233,7 @@ export function VPExplorer(props: {
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
{mode === "phrases" && <VPDisplay
|
{mode === "phrases" && <VPDisplay
|
||||||
VP={vps}
|
VP={rendered ? rendered : vps}
|
||||||
opts={props.opts}
|
opts={props.opts}
|
||||||
setForm={handleSetForm}
|
setForm={handleSetForm}
|
||||||
/>}
|
/>}
|
||||||
|
@ -220,6 +252,15 @@ export function VPExplorer(props: {
|
||||||
}}>
|
}}>
|
||||||
Phrase URL copied to clipboard
|
Phrase URL copied to clipboard
|
||||||
</div>}
|
</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>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,8 @@ import { standardizePashto } from "../../lib/standardize-pashto";
|
||||||
import shuffleArray from "../../lib/shuffle-array";
|
import shuffleArray from "../../lib/shuffle-array";
|
||||||
import InlinePs from "../InlinePs";
|
import InlinePs from "../InlinePs";
|
||||||
import { psStringEquals } from "../../lib/p-text-helpers";
|
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 { getRandomTense } from "./TensePicker";
|
||||||
import { getTenseFromVerbSelection, removeBa, switchSubjObj } from "../../lib/phrase-building/vp-tools";
|
import { getTenseFromVerbSelection, removeBa, switchSubjObj } from "../../lib/phrase-building/vp-tools";
|
||||||
import playAudio from "../../lib/play-audio";
|
import playAudio from "../../lib/play-audio";
|
||||||
|
@ -445,7 +446,6 @@ function getRandomVPSelection(mix: MixType = "both") {
|
||||||
return {
|
return {
|
||||||
subject: subject !== undefined ? subject : randSubj,
|
subject: subject !== undefined ? subject : randSubj,
|
||||||
verb: randomizeTense(verb, true),
|
verb: randomizeTense(verb, true),
|
||||||
shrunkenPossesive: undefined,
|
|
||||||
form: { removeKing: false, shrinkServant: false },
|
form: { removeKing: false, shrinkServant: false },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -462,7 +462,6 @@ function getRandomVPSelection(mix: MixType = "both") {
|
||||||
return {
|
return {
|
||||||
subject: randSubj,
|
subject: randSubj,
|
||||||
verb: randomizeTense(v, true),
|
verb: randomizeTense(v, true),
|
||||||
shrunkenPossesive: undefined,
|
|
||||||
form: { removeKing: false, shrinkServant: false },
|
form: { removeKing: false, shrinkServant: false },
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -67,15 +67,6 @@ export function makeVPSelectionState(
|
||||||
canChangeStatDyn: "stative" in info,
|
canChangeStatDyn: "stative" in info,
|
||||||
},
|
},
|
||||||
form: os ? os.form : { removeKing: false, shrinkServant: false },
|
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",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,13 @@ import {
|
||||||
isInvalidSubjObjCombo,
|
isInvalidSubjObjCombo,
|
||||||
} from "../../lib/phrase-building/vp-tools";
|
} from "../../lib/phrase-building/vp-tools";
|
||||||
import { switchSubjObj } 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 { ensure2ndPersSubjPronounAndNoConflict } from "../../lib/phrase-building/vp-tools";
|
||||||
import {
|
import {
|
||||||
isPerfectTense,
|
isPerfectTense,
|
||||||
isImperativeTense,
|
isImperativeTense,
|
||||||
} from "../../lib/type-predicates";
|
} from "../../lib/type-predicates";
|
||||||
|
import { checkForMiniPronounsError } from "../../lib/phrase-building/compile";
|
||||||
|
|
||||||
export type VpsReducerAction = {
|
export type VpsReducerAction = {
|
||||||
type: "load vps",
|
type: "load vps",
|
||||||
|
@ -24,9 +25,6 @@ export type VpsReducerAction = {
|
||||||
payload: T.NPSelection | undefined,
|
payload: T.NPSelection | undefined,
|
||||||
} | {
|
} | {
|
||||||
type: "swap subj/obj",
|
type: "swap subj/obj",
|
||||||
} | {
|
|
||||||
type: "shrink possesive",
|
|
||||||
payload: number | undefined,
|
|
||||||
} | {
|
} | {
|
||||||
type: "set form",
|
type: "set form",
|
||||||
payload: T.FormVersion,
|
payload: T.FormVersion,
|
||||||
|
@ -48,163 +46,189 @@ export type VpsReducerAction = {
|
||||||
} | {
|
} | {
|
||||||
type: "set tense category",
|
type: "set tense category",
|
||||||
payload: "basic" | "modal" | "perfect" | "imperative",
|
payload: "basic" | "modal" | "perfect" | "imperative",
|
||||||
}
|
} | {
|
||||||
|
type: "toggle servant shrink",
|
||||||
|
};
|
||||||
|
|
||||||
export function vpsReducer(vps: T.VPSelectionState, action: VpsReducerAction): T.VPSelectionState {
|
export function vpsReducer(vps: T.VPSelectionState, action: VpsReducerAction, sendAlert?: (msg: string) => void): T.VPSelectionState {
|
||||||
if (action.type === "load vps") {
|
return ensureMiniPronounsOk(vps, doReduce());
|
||||||
return action.payload;
|
function ensureMiniPronounsOk(old: T.VPSelectionState, vps: T.VPSelectionState): T.VPSelectionState {
|
||||||
}
|
const error = checkForMiniPronounsError(vps);
|
||||||
if (action.type === "set subject") {
|
if (error) {
|
||||||
const { subject, skipPronounConflictCheck } = action.payload;
|
if (sendAlert) sendAlert(error);
|
||||||
if (
|
return old;
|
||||||
!skipPronounConflictCheck
|
|
||||||
&&
|
|
||||||
hasPronounConflict(subject, vps.verb?.object)
|
|
||||||
) {
|
|
||||||
alert("That combination of pronouns is not allowed");
|
|
||||||
return vps;
|
|
||||||
}
|
}
|
||||||
return {
|
return vps;
|
||||||
...vps,
|
|
||||||
subject: action.payload.subject,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (action.type === "set object") {
|
function doReduce(): T.VPSelectionState {
|
||||||
if (!vps.verb) return vps;
|
if (action.type === "load vps") {
|
||||||
if ((vps.verb.object === "none") || (typeof vps.verb.object === "number")) {
|
return action.payload;
|
||||||
return vps;
|
|
||||||
}
|
}
|
||||||
const object = action.payload;
|
if (action.type === "set subject") {
|
||||||
// check for pronoun conflict
|
const { subject, skipPronounConflictCheck } = action.payload;
|
||||||
if (hasPronounConflict(vps.subject, object)) {
|
if (
|
||||||
alert("That combination of pronouns is not allowed");
|
!skipPronounConflictCheck
|
||||||
return vps;
|
&&
|
||||||
}
|
hasPronounConflict(subject, vps.verb?.object)
|
||||||
return {
|
) {
|
||||||
...vps,
|
if (sendAlert) sendAlert("That combination of pronouns is not allowed");
|
||||||
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;
|
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 {
|
return {
|
||||||
...vps,
|
...vps,
|
||||||
subject: vps.verb.object,
|
verb: {
|
||||||
verb: changeVoice(vps.verb, voice, vps.verb.object),
|
...vps.verb,
|
||||||
|
perfectTense: tense,
|
||||||
|
tenseCategory: "perfect",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (isImperativeTense(tense)) {
|
||||||
|
return {
|
||||||
|
...vps,
|
||||||
|
verb: {
|
||||||
|
...vps.verb,
|
||||||
|
imperativeTense: tense,
|
||||||
|
tenseCategory: "imperative",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
...vps,
|
...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 tense category") {
|
||||||
if (action.type === "set transitivity") {
|
if (!vps.verb) return vps;
|
||||||
if (!(vps.verb && vps.verb.canChangeTransitivity)) return vps;
|
const category = action.payload;
|
||||||
return {
|
if (category === "imperative") {
|
||||||
...vps,
|
return ensure2ndPersSubjPronounAndNoConflict({
|
||||||
verb: changeTransitivity(vps.verb, action.payload),
|
...vps,
|
||||||
};
|
verb: {
|
||||||
}
|
...vps.verb,
|
||||||
if (action.type === "set statDyn") {
|
voice: "active",
|
||||||
if (!(vps.verb && vps.verb.canChangeStatDyn)) return vps;
|
tenseCategory: category,
|
||||||
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 {
|
return {
|
||||||
...vps,
|
...vps,
|
||||||
verb: {
|
verb: {
|
||||||
...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,
|
tenseCategory: category,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
// if (action.type === "toggle servant shrink") {
|
||||||
return {
|
return {
|
||||||
...vps,
|
...vps,
|
||||||
verb: {
|
form: {
|
||||||
...vps.verb,
|
...vps.form,
|
||||||
tenseCategory: category,
|
shrinkServant: !vps.form.shrinkServant,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
// }
|
// }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasPronounConflict(subject: T.NPSelection | undefined, object: undefined | T.VerbObject): boolean {
|
function hasPronounConflict(subject: T.NPSelection | undefined, object: undefined | T.VerbObject): boolean {
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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()]);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as T from "../../types";
|
import * as T from "../../types";
|
||||||
import {
|
import {
|
||||||
concatPsString, psStringEquals,
|
concatPsString,
|
||||||
} from "../p-text-helpers";
|
} from "../p-text-helpers";
|
||||||
import {
|
import {
|
||||||
Segment,
|
Segment,
|
||||||
|
@ -20,18 +20,18 @@ import {
|
||||||
} from "./vp-tools";
|
} from "./vp-tools";
|
||||||
import { isImperativeTense, isModalTense, isPerfectTense } from "../type-predicates";
|
import { isImperativeTense, isModalTense, isPerfectTense } from "../type-predicates";
|
||||||
import { getEnglishFromRendered, getPashtoFromRendered } from "./np-tools";
|
import { getEnglishFromRendered, getPashtoFromRendered } from "./np-tools";
|
||||||
import {
|
import { getVerbBlockPosFromPerson } from "../misc-helpers";
|
||||||
orderKidsSection,
|
import { pronouns } from "../grammar-units";
|
||||||
findPossesiveToShrinkInVP,
|
import { completeEPSelection, renderEP } from "./render-ep";
|
||||||
shrinkNP,
|
import { completeVPSelection } from "./vp-tools";
|
||||||
} from "./compile-tools";
|
import { renderVP } from "./render-vp";
|
||||||
|
|
||||||
type Form = T.FormVersion & { OSV?: boolean };
|
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): { 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.PsString[], e?: string [] };
|
||||||
export function compileVP(VP: T.VPRendered, form: Form, combineLengths?: true): { ps: T.SingleOrLengthOpts<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 verb = VP.verb.ps;
|
||||||
const { kids, NPs } = getSegmentsAndKids(VP, form);
|
const { kids, NPs } = getVPSegmentsAndKids(VP, form);
|
||||||
const psResult = compilePs({
|
const psResult = compilePs({
|
||||||
NPs,
|
NPs,
|
||||||
kids,
|
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[][] } {
|
export function getShrunkenServant(VP: T.VPRendered): Segment | undefined {
|
||||||
const removeKing = form.removeKing && !(VP.isCompound === "dynamic" && VP.isPast);
|
const shrinkServant = VP.form.shrinkServant && !(VP.isCompound === "dynamic" && !VP.isPast);
|
||||||
const shrinkServant = form.shrinkServant && !(VP.isCompound === "dynamic" && !VP.isPast);
|
|
||||||
const toShrinkServant = (() => {
|
const toShrinkServant = (() => {
|
||||||
if (!shrinkServant) return undefined;
|
if (!shrinkServant) return undefined;
|
||||||
if (!VP.servant) 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;
|
if (typeof servant !== "object") return undefined;
|
||||||
return servant;
|
return servant;
|
||||||
})();
|
})();
|
||||||
const shrunkenServant = toShrinkServant ? shrinkNP(toShrinkServant) : undefined;
|
return toShrinkServant ? shrinkNP(toShrinkServant) : undefined;
|
||||||
const possToShrink = findPossesiveToShrinkInVP(VP);
|
}
|
||||||
const shrunkenPossesive = possToShrink ? shrinkNP(possToShrink) : undefined;
|
|
||||||
const shrunkenPossAllowed = possToShrink && shrunkenPossesive && (
|
export function getVPSegmentsAndKids(VP: T.VPRendered, form?: Form): { kids: Segment[], NPs: Segment[][] } {
|
||||||
!shrunkenServant || !psStringEquals(shrunkenPossesive.ps[0], shrunkenServant.ps[0])
|
const removeKing = VP.form.removeKing && !(VP.isCompound === "dynamic" && VP.isPast);
|
||||||
) && (
|
const shrunkenServant = getShrunkenServant(VP);
|
||||||
// can only shrink the possesive if the parent of the possesive is still in full form
|
const possToShrink = findPossesivesToShrinkInVP(VP, {
|
||||||
!(possToShrink.role === "king" && removeKing)
|
shrunkServant: !!shrunkenServant,
|
||||||
&&
|
removedKing: removeKing,
|
||||||
!(possToShrink.role === "servant" && shrinkServant)
|
});
|
||||||
);
|
|
||||||
const shrinkPossUid = shrunkenPossAllowed
|
|
||||||
? VP.shrunkenPossesive
|
|
||||||
: undefined;
|
|
||||||
const SO = {
|
const SO = {
|
||||||
subject: getPashtoFromRendered(VP.subject, shrinkPossUid, false),
|
subject: getPashtoFromRendered(VP.subject, false),
|
||||||
object: typeof VP.object === "object" ? getPashtoFromRendered(VP.object, shrinkPossUid, VP.subject.person) : undefined,
|
object: typeof VP.object === "object" ? getPashtoFromRendered(VP.object, VP.subject.person) : undefined,
|
||||||
};
|
};
|
||||||
function getSegment(t: "subject" | "object"): Segment | undefined {
|
function getSegment(t: "subject" | "object"): Segment | undefined {
|
||||||
const word = (VP.servant === t)
|
const word = (VP.servant === t)
|
||||||
? (!shrinkServant ? SO[t] : undefined)
|
? (!shrunkenServant ? SO[t] : undefined)
|
||||||
: (VP.king === t)
|
: (VP.king === t)
|
||||||
? (!removeKing ? SO[t] : undefined)
|
? (!removeKing ? SO[t] : undefined)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
@ -128,8 +123,7 @@ function getSegmentsAndKids(VP: T.VPRendered, form: Form): { kids: Segment[], NP
|
||||||
? [makeSegment(grammarUnits.baParticle, ["isBa", "isKid"])] : [],
|
? [makeSegment(grammarUnits.baParticle, ["isBa", "isKid"])] : [],
|
||||||
...shrunkenServant
|
...shrunkenServant
|
||||||
? [shrunkenServant] : [],
|
? [shrunkenServant] : [],
|
||||||
...(shrunkenPossesive && shrunkenPossAllowed)
|
...possToShrink.map(shrinkNP),
|
||||||
? [shrunkenPossesive] : [],
|
|
||||||
]),
|
]),
|
||||||
NPs: [
|
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 ??
|
// TODO: make this an option to also include O S V order ??
|
||||||
// also show O S V if both are showing
|
// 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 {
|
function mergeSegments(s1: Segment, s2: Segment, noSpace?: "no space"): Segment {
|
||||||
if (noSpace) {
|
if (noSpace) {
|
||||||
return s2.adjust({ ps: (p) => concatPsString(s1.ps[0], p) });
|
return s2.adjust({ ps: (p) => concatPsString(s1.ps[0], p) });
|
||||||
|
@ -309,3 +379,126 @@ function compileEnglish(VP: T.VPRendered): string[] | undefined {
|
||||||
}))
|
}))
|
||||||
: 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()]);
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { renderVP } from "./render-vp";
|
|
||||||
import { compileVP } from "./compile-vp";
|
|
||||||
|
|
||||||
export {
|
|
||||||
renderVP,
|
|
||||||
compileVP,
|
|
||||||
};
|
|
|
@ -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[] {
|
function trimOffShrunkenPossesive(p: T.Rendered<T.NPSelection>): T.Rendered<T.NPSelection> {
|
||||||
const base = getBaseAndAdjectives(np);
|
if (!("possesor" in p)) {
|
||||||
if (!np.possesor || np.possesor.uid === shrunkenPossesive) {
|
return p;
|
||||||
return base;
|
}
|
||||||
|
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 addPossesor(owner: T.Rendered<T.NPSelection>, existing: T.PsString[], subjectsPerson: false | T.Person): T.PsString[] {
|
||||||
function willBeReflexive(subj: T.Person, obj: T.Person): boolean {
|
function willBeReflexive(subj: T.Person, obj: T.Person): boolean {
|
||||||
|
|
|
@ -32,7 +32,6 @@ export function renderEP(EP: T.EPSelectionComplete): T.EPRendered {
|
||||||
: renderEqCompSelection(EP.predicate.selection, kingPerson),
|
: renderEqCompSelection(EP.predicate.selection, kingPerson),
|
||||||
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,
|
|
||||||
omitSubject: EP.omitSubject,
|
omitSubject: EP.omitSubject,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -150,3 +149,32 @@ function getEnglishConj(p: T.Person, e: string | T.EnglishBlock): string {
|
||||||
// return inflections[gender][plural ? 1 : 0];
|
// 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
import { parseEc } from "../misc-helpers";
|
import { parseEc } from "../misc-helpers";
|
||||||
import { getEnglishWord } from "../get-english-word";
|
import { getEnglishWord } from "../get-english-word";
|
||||||
import { renderAdjectiveSelection } from "./render-adj";
|
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, 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";
|
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;
|
if (!possesor) return undefined;
|
||||||
|
const isSingUnisexAnim5PatternNoun = (possesor.np.type === "noun"
|
||||||
|
&& possesor.np.number === "singular"
|
||||||
|
&& isUnisexAnimNounEntry(possesor.np.entry)
|
||||||
|
&& isPattern5Entry(possesor.np.entry)
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
uid: possesor.uid,
|
shrunken: possesor.shrunken,
|
||||||
np: renderNPSelection(
|
np: renderNPSelection(
|
||||||
possesor.np,
|
possesor.np,
|
||||||
!(possesor.np.type === "noun" && isUnisexNounEntry(possesor.np.entry) && isPattern5Entry(possesor.np.entry)),
|
!isSingUnisexAnim5PatternNoun,
|
||||||
false,
|
false,
|
||||||
"subject",
|
"subject",
|
||||||
possesorRole,
|
possesorRole,
|
||||||
|
|
|
@ -47,7 +47,6 @@ export function renderVP(VP: T.VPSelectionComplete): T.VPRendered {
|
||||||
type: "VPRendered",
|
type: "VPRendered",
|
||||||
king,
|
king,
|
||||||
servant,
|
servant,
|
||||||
shrunkenPossesive: VP.shrunkenPossesive,
|
|
||||||
isPast,
|
isPast,
|
||||||
isTransitive,
|
isTransitive,
|
||||||
isCompound: VP.verb.isCompound,
|
isCompound: VP.verb.isCompound,
|
||||||
|
@ -60,10 +59,25 @@ export function renderVP(VP: T.VPSelectionComplete): T.VPRendered {
|
||||||
vs: VP.verb,
|
vs: VP.verb,
|
||||||
}),
|
}),
|
||||||
form: VP.form,
|
form: VP.form,
|
||||||
|
whatsAdjustable: whatsAdjustable(VP),
|
||||||
};
|
};
|
||||||
return b;
|
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 {
|
function renderVerbSelection(vs: T.VerbSelectionComplete, person: T.Person, objectPerson: T.Person | undefined): T.VerbRendered {
|
||||||
const v = vs.dynAuxVerb || vs.verb;
|
const v = vs.dynAuxVerb || vs.verb;
|
||||||
const conjugations = conjugateVerb(v.entry, v.complement);
|
const conjugations = conjugateVerb(v.entry, v.complement);
|
||||||
|
|
|
@ -49,6 +49,10 @@ export function isUnisexNounEntry(e: T.NounEntry | T.AdjectiveEntry): e is T.Uni
|
||||||
return isNounEntry(e) && e.c.includes("unisex");
|
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) {
|
export function isAdjOrUnisexNounEntry(e: T.Entry): e is (T.AdjectiveEntry | T.UnisexNounEntry) {
|
||||||
return isAdjectiveEntry(e) || (
|
return isAdjectiveEntry(e) || (
|
||||||
isNounEntry(e) && isUnisexNounEntry(e)
|
isNounEntry(e) && isUnisexNounEntry(e)
|
||||||
|
@ -136,9 +140,7 @@ export function isPattern5Entry<T extends (T.NounEntry | T.AdjectiveEntry)>(e: T
|
||||||
return (
|
return (
|
||||||
!!(e.infap && e.infaf && e.infbp && e.infbf)
|
!!(e.infap && e.infaf && e.infbp && e.infbf)
|
||||||
&&
|
&&
|
||||||
(e.infaf.slice(-1) === "u")
|
e.infap.includes("ا")
|
||||||
&&
|
|
||||||
!e.infap.slice(1).includes("ا")
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,15 +46,16 @@ export default function useStickyState<T extends SaveableData>(defaultValue: T |
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useStickyReducer<T extends SaveableData, A>(
|
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),
|
defaultValue: T | ((old: T | undefined) => T),
|
||||||
key: string,
|
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);
|
const [state, unsafeSetState] = useStickyState<T>(defaultValue, key);
|
||||||
function adjustState(action: A) {
|
function adjustState(action: A) {
|
||||||
unsafeSetState(oldState => {
|
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
18
src/types.ts
18
src/types.ts
|
@ -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 MascNounEntry = NounEntry & { __brand2: "a masc noun entry" };
|
||||||
export type FemNounEntry = NounEntry & { __brand2: "a fem noun entry" };
|
export type FemNounEntry = NounEntry & { __brand2: "a fem noun entry" };
|
||||||
export type UnisexNounEntry = MascNounEntry & { __brand3: "a unisex 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 AdverbEntry = DictionaryEntry & { c: string } & { __brand: "an adverb entry" };
|
||||||
export type LocativeAdverbEntry = AdverbEntry & { __brand2: "a locative adverb entry" };
|
export type LocativeAdverbEntry = AdverbEntry & { __brand2: "a locative adverb entry" };
|
||||||
export type AdjectiveEntry = DictionaryEntry & { c: string } & { __brand: "an adjective entry" };
|
export type AdjectiveEntry = DictionaryEntry & { c: string } & { __brand: "an adjective entry" };
|
||||||
|
@ -501,7 +502,6 @@ export type VPRendered = {
|
||||||
type: "VPRendered",
|
type: "VPRendered",
|
||||||
king: "subject" | "object",
|
king: "subject" | "object",
|
||||||
servant: "subject" | "object" | undefined,
|
servant: "subject" | "object" | undefined,
|
||||||
shrunkenPossesive: number | undefined,
|
|
||||||
isPast: boolean,
|
isPast: boolean,
|
||||||
isTransitive: boolean,
|
isTransitive: boolean,
|
||||||
isCompound: "stative" | "dynamic" | false,
|
isCompound: "stative" | "dynamic" | false,
|
||||||
|
@ -510,6 +510,7 @@ export type VPRendered = {
|
||||||
verb: VerbRendered,
|
verb: VerbRendered,
|
||||||
englishBase?: string[],
|
englishBase?: string[],
|
||||||
form: FormVersion,
|
form: FormVersion,
|
||||||
|
whatsAdjustable: "both" | "king" | "servant",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type VerbTense = "presentVerb"
|
export type VerbTense = "presentVerb"
|
||||||
|
@ -531,14 +532,12 @@ export type Tense = EquativeTense | VerbTense | PerfectTense | ModalTense | Impe
|
||||||
export type VPSelectionState = {
|
export type VPSelectionState = {
|
||||||
subject: NPSelection | undefined,
|
subject: NPSelection | undefined,
|
||||||
verb: VerbSelection,
|
verb: VerbSelection,
|
||||||
shrunkenPossesive: undefined | number,
|
|
||||||
form: FormVersion,
|
form: FormVersion,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VPSelectionComplete = {
|
export type VPSelectionComplete = {
|
||||||
subject: NPSelection,
|
subject: NPSelection,
|
||||||
verb: VerbSelectionComplete,
|
verb: VerbSelectionComplete,
|
||||||
shrunkenPossesive: undefined | number,
|
|
||||||
form: FormVersion,
|
form: FormVersion,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -592,7 +591,7 @@ export type NPType = "noun" | "pronoun" | "participle";
|
||||||
export type ObjectNP = "none" | Person.ThirdPlurMale;
|
export type ObjectNP = "none" | Person.ThirdPlurMale;
|
||||||
|
|
||||||
export type PossesorSelection = {
|
export type PossesorSelection = {
|
||||||
uid: number,
|
shrunken: boolean,
|
||||||
np: NPSelection,
|
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 };
|
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<
|
export type Rendered<T extends NPSelection | EqCompSelection | AdjectiveSelection> = ReplaceKey<
|
||||||
Omit<T, "changeGender" | "changeNumber" | "changeDistance" | "adjectives" | "possesor">,
|
Omit<T, "changeGender" | "changeNumber" | "changeDistance" | "adjectives" | "possesor">,
|
||||||
"e",
|
"e",
|
||||||
|
@ -652,7 +658,7 @@ export type Rendered<T extends NPSelection | EqCompSelection | AdjectiveSelectio
|
||||||
// TODO: better recursive thing
|
// TODO: better recursive thing
|
||||||
adjectives?: Rendered<AdjectiveSelection>[],
|
adjectives?: Rendered<AdjectiveSelection>[],
|
||||||
possesor?: {
|
possesor?: {
|
||||||
uid: number,
|
shrunken: boolean,
|
||||||
np: Rendered<NPSelection>,
|
np: Rendered<NPSelection>,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -666,7 +672,6 @@ export type EPSelectionState = {
|
||||||
Complement: EqCompSelection | undefined,
|
Complement: EqCompSelection | undefined,
|
||||||
},
|
},
|
||||||
equative: EquativeSelection,
|
equative: EquativeSelection,
|
||||||
shrunkenPossesive: undefined | number,
|
|
||||||
omitSubject: boolean,
|
omitSubject: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -703,7 +708,6 @@ export type EPRendered = {
|
||||||
predicate: Rendered<NPSelection> | Rendered<EqCompSelection>,
|
predicate: Rendered<NPSelection> | Rendered<EqCompSelection>,
|
||||||
equative: EquativeRendered,
|
equative: EquativeRendered,
|
||||||
englishBase?: string[],
|
englishBase?: string[],
|
||||||
shrunkenPossesive: undefined | number,
|
|
||||||
omitSubject: boolean,
|
omitSubject: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue