Compare commits

...

2 Commits

Author SHA1 Message Date
adueck c5a238ab05 fixed up determiners for a beta rollout 2024-08-07 16:04:31 -04:00
adueck 6451cee925 new determiners functionality in phrase builder 2024-08-07 14:57:02 -04:00
30 changed files with 1454 additions and 896 deletions

View File

@ -30,6 +30,7 @@ jobs:
yarn install-r yarn install-r
yarn build-library yarn build-library
yarn test --silent yarn test --silent
yarn check-all-inflections
cp .npmrc src/lib cp .npmrc src/lib
cp .npmrc src/components cp .npmrc src/components
cd src/lib cd src/lib

View File

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

View File

@ -1,12 +1,12 @@
{ {
"name": "@lingdocs/ps-react", "name": "@lingdocs/ps-react",
"version": "7.4.1", "version": "7.5.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@lingdocs/ps-react", "name": "@lingdocs/ps-react",
"version": "7.4.1", "version": "7.5.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@formkit/auto-animate": "^1.0.0-beta.3", "@formkit/auto-animate": "^1.0.0-beta.3",

View File

@ -1,6 +1,6 @@
{ {
"name": "@lingdocs/ps-react", "name": "@lingdocs/ps-react",
"version": "7.4.1", "version": "7.5.1",
"description": "Pashto inflector library module with React components", "description": "Pashto inflector library module with React components",
"main": "dist/components/library.js", "main": "dist/components/library.js",
"module": "dist/components/library.js", "module": "dist/components/library.js",

View File

@ -3,105 +3,150 @@ import * as T from "../../types";
import AdjectivePicker from "./np-picker/AdjectivePicker"; import AdjectivePicker from "./np-picker/AdjectivePicker";
import LocativeAdverbPicker from "./ep-explorer/eq-comp-picker/LocativeAdverbPicker"; import LocativeAdverbPicker from "./ep-explorer/eq-comp-picker/LocativeAdverbPicker";
import SandwichPicker from "./np-picker/SandwichPicker"; import SandwichPicker from "./np-picker/SandwichPicker";
const compTypes: T.ComplementType[] = ["adjective", "loc. adv.", "sandwich", "comp. noun"]; const compTypes: T.ComplementType[] = [
"adjective",
"loc. adv.",
"sandwich",
"comp. noun",
];
function selectionTypeToCompType(s: Exclude<T.ComplementType, "comp. noun"> | "noun"): T.ComplementType { function selectionTypeToCompType(
if (s === "noun") return "comp. noun"; s: Exclude<T.ComplementType, "comp. noun"> | "noun"
return s; ): T.ComplementType {
if (s === "noun") return "comp. noun";
return s;
} }
function ComplementPicker(props: { function ComplementPicker(props: {
phraseIsComplete: boolean, phraseIsComplete: boolean;
onChange: (comp: T.ComplementSelection | undefined) => void, onChange: (comp: T.ComplementSelection | undefined) => void;
comp: T.ComplementSelection | undefined, comp: T.ComplementSelection | undefined;
opts: T.TextOptions, opts: T.TextOptions;
cantClear?: boolean, cantClear?: boolean;
heading?: JSX.Element | string, heading?: JSX.Element | string;
entryFeeder: T.EntryFeeder, entryFeeder: T.EntryFeeder;
negative: boolean;
}) { }) {
const [compType, setCompType] = useState<T.ComplementType | undefined>(props.comp const [compType, setCompType] = useState<T.ComplementType | undefined>(
props.comp ? selectionTypeToCompType(props.comp.selection.type) : undefined
);
useEffect(() => {
setCompType(
props.comp
? selectionTypeToCompType(props.comp.selection.type) ? selectionTypeToCompType(props.comp.selection.type)
: undefined); : undefined
useEffect(() => { );
setCompType(props.comp }, [props.comp]);
? selectionTypeToCompType(props.comp.selection.type) function handleClear() {
: undefined); setCompType(undefined);
}, [props.comp]); props.onChange(undefined);
function handleClear() { }
setCompType(undefined); function handleCompTypeChange(ctp: T.ComplementType) {
props.onChange(undefined); props.onChange(undefined);
} setCompType(ctp);
function handleCompTypeChange(ctp: T.ComplementType) { }
props.onChange(undefined); function handleSandwichExit() {
setCompType(ctp); setCompType(undefined);
} props.onChange(undefined);
function handleSandwichExit() { }
setCompType(undefined); const clearButton =
props.onChange(undefined); compType && !props.cantClear ? (
} <button className="btn btn-sm btn-light mb-2" onClick={handleClear}>
const clearButton = (compType && !props.cantClear) X
? <button className="btn btn-sm btn-light mb-2" onClick={handleClear}>X</button> </button>
: <div></div>; ) : (
return <> <div></div>
<div className="d-flex flex-row justify-content-between"> );
<div></div> return (
<div> <>
{typeof props.heading === "string" <div className="d-flex flex-row justify-content-between">
? <div className="h5 text-center">{props.heading}</div> <div></div>
: props.heading} <div>
</div> {typeof props.heading === "string" ? (
<div> <div className="h5 text-center">{props.heading}</div>
{clearButton} ) : (
</div> props.heading
)}
</div> </div>
{!compType && <div className="text-center"> <div>{clearButton}</div>
<div className="h6 mr-3"> </div>
Choose Complement {!compType && (
<div className="text-center">
<div className="h6 mr-3">Choose Complement</div>
{compTypes.map((cpt) => (
<div key={cpt} className="mb-2">
<button
key={cpt}
type="button"
className="mr-2 btn btn-sm btn-outline-secondary"
onClick={() => handleCompTypeChange(cpt)}
>
{cpt}
</button>
</div> </div>
{compTypes.map((cpt) => <div key={cpt} className="mb-2"> ))}
<button
key={cpt}
type="button"
className="mr-2 btn btn-sm btn-outline-secondary"
onClick={() => handleCompTypeChange(cpt)}
>
{cpt}
</button>
</div>)}
</div>}
<div style={{ minWidth: "9rem" }}>
{compType === "adjective" ?
<AdjectivePicker
entryFeeder={props.entryFeeder}
adjective={props.comp?.selection.type === "adjective" ? props.comp.selection : undefined}
opts={props.opts}
onChange={(a) => props.onChange(a ? { type: "complement", selection: a } : undefined)}
phraseIsComplete={props.phraseIsComplete}
/>
: compType === "loc. adv."
? <LocativeAdverbPicker
entryFeeder={props.entryFeeder.locativeAdverbs}
adjective={props.comp?.selection.type === "loc. adv." ? props.comp.selection : undefined}
opts={props.opts}
onChange={(a) => props.onChange(a ? { type: "complement", selection: a } : undefined)}
/>
: compType === "sandwich"
? <SandwichPicker
onChange={(a) => props.onChange(a ? { type: "complement", selection: a } : undefined)}
opts={props.opts}
sandwich={props.comp?.selection.type === "sandwich" ? props.comp.selection : undefined}
entryFeeder={props.entryFeeder}
onExit={handleSandwichExit}
// TODO: get phraseIsComplete working here
phraseIsComplete={props.phraseIsComplete}
/>
: compType === "comp. noun"
? <div style={{ maxWidth: "9rem" }}>
Sorry, can't choose complement nouns yet 🚧
</div>
: null}
</div> </div>
</>; )}
<div style={{ minWidth: "9rem" }}>
{compType === "adjective" ? (
<AdjectivePicker
entryFeeder={props.entryFeeder}
adjective={
props.comp?.selection.type === "adjective"
? props.comp.selection
: undefined
}
opts={props.opts}
onChange={(a) =>
props.onChange(
a ? { type: "complement", selection: a } : undefined
)
}
phraseIsComplete={props.phraseIsComplete}
negative={props.negative}
/>
) : compType === "loc. adv." ? (
<LocativeAdverbPicker
entryFeeder={props.entryFeeder.locativeAdverbs}
adjective={
props.comp?.selection.type === "loc. adv."
? props.comp.selection
: undefined
}
opts={props.opts}
onChange={(a) =>
props.onChange(
a ? { type: "complement", selection: a } : undefined
)
}
/>
) : compType === "sandwich" ? (
<SandwichPicker
onChange={(a) =>
props.onChange(
a ? { type: "complement", selection: a } : undefined
)
}
opts={props.opts}
sandwich={
props.comp?.selection.type === "sandwich"
? props.comp.selection
: undefined
}
entryFeeder={props.entryFeeder}
onExit={handleSandwichExit}
// TODO: get phraseIsComplete working here
phraseIsComplete={props.phraseIsComplete}
negative={props.negative}
/>
) : compType === "comp. noun" ? (
<div style={{ maxWidth: "9rem" }}>
Sorry, can't choose complement nouns yet 🚧
</div>
) : null}
</div>
</>
);
} }
export default ComplementPicker; export default ComplementPicker;

View File

@ -3,183 +3,259 @@ import { StyleHTMLAttributes } from "react";
import Select, { StylesConfig } from "react-select"; import Select, { StylesConfig } from "react-select";
import AsyncSelect from "react-select/async"; import AsyncSelect from "react-select/async";
import { import {
makeSelectOption, makeSelectOption,
makeVerbSelectOption, makeVerbSelectOption,
} from "./np-picker/picker-tools"; } from "./np-picker/picker-tools";
export const customStyles: StylesConfig = { export const customStyles: StylesConfig = {
menuPortal: (base: any) => ({ menuPortal: (base: any) => ({
...base, ...base,
zIndex: 99999, zIndex: 99999,
}), }),
menu: (base: any) => ({ menu: (base: any) => ({
...base, ...base,
zIndex: 999999, zIndex: 999999,
}), }),
option: (provided: any, state: any) => ({ option: (provided: any, state: any) => ({
...provided, ...provided,
padding: "10px 5px", padding: "10px 5px",
color: "#121418", color: "#121418",
}), }),
input: (base: any) => ({ input: (base: any) => ({
...base, ...base,
padding: 0, padding: 0,
}), }),
} };
function EntrySelect<E extends T.DictionaryEntry | T.VerbEntry>(props: { function EntrySelect<E extends T.DictionaryEntry | T.VerbEntry>(props: {
entryFeeder: T.EntryFeederSingleType<E>, entryFeeder: T.EntryFeederSingleType<E>;
value: E | undefined, value: E | undefined;
onChange: (value: E | undefined) => void, onChange: (value: E | undefined) => void;
name: string | undefined, name: string | undefined;
isVerbSelect?: boolean, isVerbSelect?: boolean;
opts: T.TextOptions, opts: T.TextOptions;
style?: StyleHTMLAttributes<HTMLDivElement>, style?: StyleHTMLAttributes<HTMLDivElement>;
placeholder?: string, placeholder?: string;
}) { }) {
const divStyle = props.style || { width: "13rem" }; const divStyle = props.style || { width: "13rem" };
const placeholder = "placeholder" in props const placeholder =
? props.placeholder "placeholder" in props
: "search" in props.entryFeeder ? props.placeholder
? "Search Pashto" : "search" in props.entryFeeder
: "Search…"; ? "Search Pashto"
function makeOption(e: E | T.DictionaryEntry) { : "Search…";
if ("entry" in e) { function makeOption(e: E | T.DictionaryEntry) {
return (props.isVerbSelect ? makeVerbSelectOption : makeSelectOption)(e, props.opts); if ("entry" in e) {
} return (props.isVerbSelect ? makeVerbSelectOption : makeSelectOption)(
return makeSelectOption(e, props.opts); e,
props.opts
);
} }
const value = props.value ? makeOption(props.value) : undefined; return makeSelectOption(e, props.opts);
if ("search" in props.entryFeeder) { }
const search = props.entryFeeder.search; const value = props.value ? makeOption(props.value) : undefined;
const getByTs = props.entryFeeder.getByTs; if ("search" in props.entryFeeder) {
const options = (searchString: string) => const search = props.entryFeeder.search;
new Promise<{ value: string, label: string | JSX.Element }[]>(resolve => { const getByTs = props.entryFeeder.getByTs;
resolve(search(searchString).map(makeOption)); const options = (searchString: string) =>
}); new Promise<{ value: string; label: string | JSX.Element }[]>(
const onChange = (v: { label: string | JSX.Element, value: string } | null) => { (resolve) => {
if (!v) { resolve(search(searchString).map(makeOption));
props.onChange(undefined);
return;
}
const s = getByTs(parseInt(v.value));
if (!s) return;
props.onChange(s);
} }
return <div style={divStyle}> );
<AsyncSelect const onChange = (
styles={customStyles} v: { label: string | JSX.Element; value: string } | null
isSearchable={true} ) => {
className="mb-2" if (!v) {
value={value} props.onChange(undefined);
// @ts-ignore return;
onChange={onChange} }
defaultOptions={[]} const s = getByTs(parseInt(v.value));
loadOptions={options} if (!s) return;
placeholder={placeholder} props.onChange(s);
/> };
</div>; return (
} <div style={divStyle}>
const entries = props.entryFeeder; <AsyncSelect
const options = entries styles={customStyles}
.sort((a, b) => { isSearchable={true}
if ("entry" in a) { className="mb-2"
return a.entry.p.localeCompare("p" in b ? b.p : b.entry.p, "af-PS") value={value}
} // @ts-ignore
return a.p.localeCompare("p" in b ? b.p : b.entry.p, "af-PS"); onChange={onChange}
}) defaultOptions={[]}
.map(makeOption); loadOptions={options}
const onChange = (v: { label: string | JSX.Element, value: string } | null) => { placeholder={placeholder}
if (!v) {
props.onChange(undefined);
return;
}
const s = entries.find(e => (
("entry" in e)
? e.entry.ts.toString() === v.value
: e.ts.toString() === v.value
));
if (!s) return;
props.onChange(s);
}
return <div style={divStyle}>
<Select
styles={customStyles}
isSearchable={true}
value={value || null}
// @ts-ignore - gets messed up when using customStyles
onChange={onChange}
className="mb-2"
options={options}
placeholder={placeholder}
/> />
</div>
);
}
const entries = props.entryFeeder;
const options = entries
.sort((a, b) => {
if ("entry" in a) {
return a.entry.p.localeCompare("p" in b ? b.p : b.entry.p, "af-PS");
}
return a.p.localeCompare("p" in b ? b.p : b.entry.p, "af-PS");
})
.map(makeOption);
const onChange = (
v: { label: string | JSX.Element; value: string } | null
) => {
if (!v) {
props.onChange(undefined);
return;
}
const s = entries.find((e) =>
"entry" in e
? e.entry.ts.toString() === v.value
: e.ts.toString() === v.value
);
if (!s) return;
props.onChange(s);
};
return (
<div style={divStyle}>
<Select
styles={customStyles}
isSearchable={true}
value={value || null}
// @ts-ignore - sadly gets messed up when using customStyles
onChange={onChange}
className="mb-2"
options={options}
placeholder={placeholder}
/>
</div> </div>
);
}
export function DeterminerSelect(props: {
determiners: Readonly<T.Determiner[]>;
value: T.Determiner[];
onChange: (value: T.Determiner[] | undefined) => void;
name: string | undefined;
opts: T.TextOptions;
}) {
const placeholder = "Select determiner…";
const value = props.value ? props.value.map(makeDeterminerOption) : undefined;
const options = props.determiners.map(makeDeterminerOption);
const onChange = (
v: { label: string | JSX.Element; value: string }[] | null
) => {
if (!v) {
props.onChange(undefined);
return;
}
const dets: T.Determiner[] = v.map(
({ value }) => JSON.parse(value) as T.Determiner
);
props.onChange(dets);
};
return (
<Select
name={props.name}
styles={customStyles}
isSearchable={true}
value={value}
isMulti
// @ts-ignore - gets messed up when using customStyles
onChange={onChange}
className="mb-2"
options={options}
placeholder={placeholder}
/>
);
} }
export function SandwichSelect<E extends T.Sandwich>(props: { export function SandwichSelect<E extends T.Sandwich>(props: {
sandwiches: E[], sandwiches: E[];
value: E | undefined, value: E | undefined;
onChange: (value: E | undefined) => void, onChange: (value: E | undefined) => void;
name: string | undefined, name: string | undefined;
opts: T.TextOptions, opts: T.TextOptions;
style?: StyleHTMLAttributes<HTMLDivElement>, style?: StyleHTMLAttributes<HTMLDivElement>;
}) { }) {
const divStyle = props.style || { width: "13rem" }; const divStyle = props.style || { width: "13rem" };
const placeholder = "Select Sandwich…"; const placeholder = "Select Sandwich…";
const value = props.value ? makeSandwichOption(props.value) : undefined; const value = props.value ? makeSandwichOption(props.value) : undefined;
const options = props.sandwiches const options = props.sandwiches
// .sort((a, b) => { // .sort((a, b) => {
// if ("entry" in a) { // if ("entry" in a) {
// return a.before.p.localeCompare("p" in b ? b.p : b.entry.p, "af-PS") // return a.before.p.localeCompare("p" in b ? b.p : b.entry.p, "af-PS")
// } // }
// return a.p.localeCompare("p" in b ? b.p : b.entry.p, "af-PS"); // return a.p.localeCompare("p" in b ? b.p : b.entry.p, "af-PS");
// }) // })
.map(makeSandwichOption); .map(makeSandwichOption);
const onChange = (v: { label: string | JSX.Element, value: string } | null) => { const onChange = (
if (!v) { v: { label: string | JSX.Element; value: string } | null
props.onChange(undefined); ) => {
return; if (!v) {
} props.onChange(undefined);
const s = props.sandwiches.find(e => { return;
const sValue = JSON.parse(v.value) as T.Sandwich;
if (sValue.type !== "sandwich") throw new Error("error converting selected sandwich value to a sandwich");
return sandwichSideEq(e.before, sValue.before)
&& sandwichSideEq(e.after, sValue.after)
&& (e.e === sValue.e);
});
if (!s) return;
props.onChange(s);
} }
return <div style={divStyle}> const s = props.sandwiches.find((e) => {
<div>Sandwich Base</div> const sValue = JSON.parse(v.value) as T.Sandwich;
<Select if (sValue.type !== "sandwich")
styles={customStyles} throw new Error(
isSearchable={true} "error converting selected sandwich value to a sandwich"
value={value} );
// @ts-ignore - gets messed up when using customStyles return (
onChange={onChange} sandwichSideEq(e.before, sValue.before) &&
className="mb-2" sandwichSideEq(e.after, sValue.after) &&
options={options} e.e === sValue.e
placeholder={placeholder} );
/> });
if (!s) return;
props.onChange(s);
};
return (
<div style={divStyle}>
<div>Sandwich Base</div>
<Select
styles={customStyles}
isSearchable={true}
value={value}
// @ts-ignore - gets messed up when using customStyles
onChange={onChange}
className="mb-2"
options={options}
placeholder={placeholder}
/>
</div> </div>
);
} }
function sandwichSideEq(s1: T.PsString | undefined, s2: T.PsString | undefined): boolean { function sandwichSideEq(
if (s1 === undefined && s2 === undefined) { s1: T.PsString | undefined,
return true s2: T.PsString | undefined
} ): boolean {
if (typeof s1 === "object" && typeof s2 === "object" && s1.p === s2.p) { if (s1 === undefined && s2 === undefined) {
return true; return true;
} }
return false; if (typeof s1 === "object" && typeof s2 === "object" && s1.p === s2.p) {
return true;
}
return false;
} }
function makeSandwichOption(s: T.Sandwich): { label: string, value: string } { function makeDeterminerOption(d: T.Determiner): {
return { label: string;
label: `${s.before ? s.before.p : ""} ... ${s.after ? s.after.p : ""} (${s.e})`, value: string;
value: JSON.stringify(s), } {
}; return {
label: `${d.p} - ${d.f}`,
value: JSON.stringify(d),
};
}
function makeSandwichOption(s: T.Sandwich): { label: string; value: string } {
return {
label: `${s.before ? s.before.p : ""} ... ${s.after ? s.after.p : ""} (${
s.e
})`,
value: JSON.stringify(s),
};
} }
export default EntrySelect; export default EntrySelect;

View File

@ -6,89 +6,112 @@ type APType = "adverb" | "sandwich";
const types: APType[] = ["adverb", "sandwich"]; const types: APType[] = ["adverb", "sandwich"];
function APPicker(props: { function APPicker(props: {
phraseIsComplete: boolean, phraseIsComplete: boolean;
onChange: (comp: T.APSelection | undefined) => void, onChange: (comp: T.APSelection | undefined) => void;
AP: T.APSelection | undefined, AP: T.APSelection | undefined;
opts: T.TextOptions, opts: T.TextOptions;
cantClear?: boolean, cantClear?: boolean;
heading?: JSX.Element | string, heading?: JSX.Element | string;
entryFeeder: T.EntryFeeder, entryFeeder: T.EntryFeeder;
onRemove: () => void, onRemove: () => void;
negative: boolean;
}) { }) {
const [type, setType] = useState<APType | undefined>(props.AP const [type, setType] = useState<APType | undefined>(
? props.AP.selection.type props.AP ? props.AP.selection.type : undefined
: undefined); );
useEffect(() => { useEffect(() => {
setType(props.AP setType(props.AP ? props.AP.selection.type : undefined);
? props.AP.selection.type }, [props.AP]);
: undefined); function handleClear() {
}, [props.AP]); setType(undefined);
function handleClear() { props.onChange(undefined);
setType(undefined); }
props.onChange(undefined); function handleCompTypeChange(ctp: APType) {
} props.onChange(undefined);
function handleCompTypeChange(ctp: APType) { setType(ctp);
props.onChange(undefined); }
setType(ctp); function handleSandwichExit() {
} setType(undefined);
function handleSandwichExit() { props.onChange(undefined);
setType(undefined); }
props.onChange(undefined); const clearButton =
} type && !props.cantClear ? (
const clearButton = (type && !props.cantClear) <button className="btn btn-sm btn-light mb-2" onClick={handleClear}>
? <button className="btn btn-sm btn-light mb-2" onClick={handleClear}>X</button> X
: (!props.cantClear) </button>
? <div> ) : !props.cantClear ? (
<div className="clickable" onClick={props.onRemove}><i className="fas fa-trash" /></div> <div>
<div className="clickable" onClick={props.onRemove}>
<i className="fas fa-trash" />
</div> </div>
: <div></div>; </div>
return <> ) : (
<div className="d-flex flex-row justify-content-between"> <div></div>
<div></div> );
<div> return (
{typeof props.heading === "string" <>
? <div className="h5 text-center">{props.heading}</div> <div className="d-flex flex-row justify-content-between">
: props.heading} <div></div>
</div> <div>
<div> {typeof props.heading === "string" ? (
{clearButton} <div className="h5 text-center">{props.heading}</div>
</div> ) : (
props.heading
)}
</div> </div>
{!type && <div className="text-center"> <div>{clearButton}</div>
<div className="h6 mr-3"> </div>
Choose AP {!type && (
<div className="text-center">
<div className="h6 mr-3">Choose AP</div>
{types.map((apt) => (
<div key={apt} className="mb-2">
<button
key={apt}
type="button"
className="mr-2 btn btn-sm btn-outline-secondary"
onClick={() => handleCompTypeChange(apt)}
>
{apt}
</button>
</div> </div>
{types.map((apt) => <div key={apt} className="mb-2"> ))}
<button
key={apt}
type="button"
className="mr-2 btn btn-sm btn-outline-secondary"
onClick={() => handleCompTypeChange(apt)}
>
{apt}
</button>
</div>)}
</div>}
<div style={{ minWidth: "9rem" }}>
{type === "adverb" ?
<AdverbPicker
entryFeeder={props.entryFeeder.adverbs}
adjective={props.AP?.selection.type === "adverb" ? props.AP.selection : undefined}
opts={props.opts}
onChange={(a) => props.onChange(a ? { type: "AP", selection: a } : undefined)}
/>
: type === "sandwich" ?
<SandwichPicker
onChange={(a) => props.onChange(a ? { type: "AP", selection: a } : undefined)}
opts={props.opts}
sandwich={props.AP?.selection.type === "sandwich" ? props.AP.selection : undefined}
entryFeeder={props.entryFeeder}
phraseIsComplete={props.phraseIsComplete}
onExit={handleSandwichExit}
/>
: null}
</div> </div>
</>; )}
<div style={{ minWidth: "9rem" }}>
{type === "adverb" ? (
<AdverbPicker
entryFeeder={props.entryFeeder.adverbs}
adjective={
props.AP?.selection.type === "adverb"
? props.AP.selection
: undefined
}
opts={props.opts}
onChange={(a) =>
props.onChange(a ? { type: "AP", selection: a } : undefined)
}
/>
) : type === "sandwich" ? (
<SandwichPicker
onChange={(a) =>
props.onChange(a ? { type: "AP", selection: a } : undefined)
}
opts={props.opts}
sandwich={
props.AP?.selection.type === "sandwich"
? props.AP.selection
: undefined
}
entryFeeder={props.entryFeeder}
phraseIsComplete={props.phraseIsComplete}
onExit={handleSandwichExit}
negative={props.negative}
/>
) : null}
</div>
</>
);
} }
export default APPicker; export default APPicker;

View File

@ -600,23 +600,29 @@ function Sandwich({
); );
} }
function Demonstrative({ function Determiners({
opts, opts,
script, script,
children, children,
}: { }: {
opts: T.TextOptions; opts: T.TextOptions;
script: "p" | "f"; script: "p" | "f";
children: T.Rendered<T.DemonstrativeSelection> | undefined; children: T.Rendered<T.DeterminersSelection> | undefined;
}) { }) {
if (!children) { if (!children || children.determiners.length === 0) {
return null; return null;
} }
return ( return (
<div className="text-center"> <div className="text-center">
<Border padding={"1rem"}>{children.ps[script]}</Border> <div className={`d-flex flex-row${script === "p" ? "-reverse" : ""}`}>
<div>DEM</div> {children.determiners.map((d) => (
<SubText>{children.e}</SubText> <div className="mx-1">
<Border padding={"1rem"}>{d.ps[0][script]}</Border>
<div>{"demonstrative" in d.determiner ? "DEM" : "DET"}</div>
<SubText>{d.e}</SubText>
</div>
))}
</div>
</div> </div>
); );
} }
@ -663,8 +669,8 @@ export function NPBlock({
np.selection.possesor && np.selection.possesor &&
!np.selection.possesor.shrunken !np.selection.possesor.shrunken
); );
const demWithoutNoun = const detsWithoutNoun =
np.selection.demonstrative && !np.selection.demonstrative.withNoun; np.selection.determiners && !np.selection.determiners.withNoun;
const elements = [ const elements = [
...(!inside ...(!inside
? [ ? [
@ -675,12 +681,12 @@ export function NPBlock({
</Possesors>, </Possesors>,
] ]
: []), : []),
<Demonstrative opts={opts} script={script}> <Determiners opts={opts} script={script}>
{np.selection.demonstrative ? np.selection.demonstrative : undefined} {np.selection.determiners}
</Demonstrative>, </Determiners>,
<div <div
style={{ style={{
opacity: demWithoutNoun ? 0.5 : 1, opacity: detsWithoutNoun ? 0.5 : 1,
}} }}
> >
<Adjectives opts={opts} script={script}> <Adjectives opts={opts} script={script}>
@ -689,7 +695,7 @@ export function NPBlock({
</div>, </div>,
<div <div
style={{ style={{
opacity: demWithoutNoun ? 0.5 : 1, opacity: detsWithoutNoun ? 0.5 : 1,
}} }}
className={np.selection.adjectives?.length ? "mx-1" : ""} className={np.selection.adjectives?.length ? "mx-1" : ""}
> >

View File

@ -3,109 +3,169 @@ import NPPicker from "../np-picker/NPPicker";
import EquativePicker from "./EquativePicker"; import EquativePicker from "./EquativePicker";
import ButtonSelect from "../ButtonSelect"; import ButtonSelect from "../ButtonSelect";
import ComplementPicker from "../ComplementPicker"; import ComplementPicker from "../ComplementPicker";
import epsReducer, { EpsReducerAction } from "../../../lib/src/phrase-building/eps-reducer"; import epsReducer, {
import { EpsReducerAction,
useEffect, } from "../../../lib/src/phrase-building/eps-reducer";
useRef, import { useEffect, useRef } from "react";
} from "react";
import { completeEPSelection } from "../../../lib/src/phrase-building/render-ep"; import { completeEPSelection } from "../../../lib/src/phrase-building/render-ep";
import APPicker from "../../src/ap-picker/APPicker"; import APPicker from "../../src/ap-picker/APPicker";
import autoAnimate from "@formkit/auto-animate"; import autoAnimate from "@formkit/auto-animate";
function EPPicker({ opts, eps, onChange, entryFeeder }: { function EPPicker({
opts: T.TextOptions, opts,
eps: T.EPSelectionState, eps,
onChange: (eps: T.EPSelectionState) => void, onChange,
entryFeeder: T.EntryFeeder, entryFeeder,
}: {
opts: T.TextOptions;
eps: T.EPSelectionState;
onChange: (eps: T.EPSelectionState) => void;
entryFeeder: T.EntryFeeder;
}) { }) {
const parent = useRef<HTMLDivElement>(null); const parent = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
parent.current && autoAnimate(parent.current); parent.current && autoAnimate(parent.current);
}, [parent]); }, [parent]);
function adjustEps(action: EpsReducerAction) { function adjustEps(action: EpsReducerAction) {
onChange(epsReducer(eps, action)); onChange(epsReducer(eps, action));
} }
const phraseIsComplete = !!completeEPSelection(eps); const phraseIsComplete = !!completeEPSelection(eps);
return <div> return (
<div className="clickable h5" onClick={() => adjustEps({ type: "insert new AP" })}>+ AP</div> <div>
<div ref={parent} className="d-flex flex-row justify-content-around flex-wrap" style={{ marginLeft: "-0.5rem", marginRight: "-0.5rem" }}> <div
{eps.blocks.map(({ block, key }, i) => ( className="clickable h5"
<div className="my-2 card block-card p-1 mx-1" key={key}> onClick={() => adjustEps({ type: "insert new AP" })}
<div className="d-flex flex-row justify-content-between mb-1" style={{ height: "1rem" }}> >
{i > 0 ? <div + AP
className="small clickable ml-1" </div>
onClick={() => adjustEps({ type: "shift block", payload: { index: i, direction: "back" }})} <div
> ref={parent}
<i className="fas fa-chevron-left" /> className="d-flex flex-row justify-content-around flex-wrap"
</div> : <div/>} style={{ marginLeft: "-0.5rem", marginRight: "-0.5rem" }}
{i < eps.blocks.length - 1 ? <div >
className="small clickable mr-1" {eps.blocks.map(({ block, key }, i) => (
onClick={() => adjustEps({ type: "shift block", payload: { index: i, direction: "forward" }})} <div className="my-2 card block-card p-1 mx-1" key={key}>
> <div
<i className="fas fa-chevron-right" /> className="d-flex flex-row justify-content-between mb-1"
</div> : <div/>} style={{ height: "1rem" }}
</div> >
{block && block.type === "subjectSelection" {i > 0 ? (
? <NPPicker <div
phraseIsComplete={phraseIsComplete} className="small clickable ml-1"
heading={<div className="h5 text-center">Subject</div>} onClick={() =>
entryFeeder={entryFeeder} adjustEps({
np={block.selection} type: "shift block",
counterPart={undefined} payload: { index: i, direction: "back" },
role="subject" })
onChange={payload => adjustEps({ type: "set subject", payload })} }
opts={opts} >
/> <i className="fas fa-chevron-left" />
: <APPicker
phraseIsComplete={phraseIsComplete}
heading="AP"
entryFeeder={entryFeeder}
AP={block}
opts={opts}
onChange={AP => adjustEps({ type: "set AP", payload: { index: i, AP } })}
onRemove={() => adjustEps({ type: "remove AP", payload: i })}
/>}
</div> </div>
))} ) : (
<div className="my-2 card block-card p-1"> <div />
<div className="h5 text-center">Predicate</div> )}
<div className="mb-2 text-center"> {i < eps.blocks.length - 1 ? (
<ButtonSelect <div
small className="small clickable mr-1"
options={[ onClick={() =>
{ value: "NP", label: "NP" }, adjustEps({
{ value: "Complement", label: "Complement" }, type: "shift block",
]} payload: { index: i, direction: "forward" },
value={eps.predicate.type} })
handleChange={payload => adjustEps({ type: "set predicate type", payload })} }
/> >
<i className="fas fa-chevron-right" />
</div> </div>
{eps.predicate.type === "NP" ? <NPPicker ) : (
phraseIsComplete={phraseIsComplete} <div />
entryFeeder={entryFeeder} )}
np={eps.predicate.type === "NP" ? eps.predicate.NP : undefined}
counterPart={undefined}
role="subject"
onChange={payload => adjustEps({ type: "set predicate NP", payload })}
opts={opts}
/> : <ComplementPicker
phraseIsComplete={phraseIsComplete}
comp={eps.predicate.type === "Complement" ? eps.predicate.Complement : undefined}
onChange={payload => adjustEps({ type: "set predicate complement", payload })}
opts={opts}
entryFeeder={entryFeeder}
/>}
</div>
<div className="my-2">
<div className="h5 text-center clickable">Equative</div>
<EquativePicker
equative={eps.equative}
onChange={payload => adjustEps({ type: "set equative", payload })}
hideNegative={false}
/>
</div> </div>
{block && block.type === "subjectSelection" ? (
<NPPicker
phraseIsComplete={phraseIsComplete}
heading={<div className="h5 text-center">Subject</div>}
entryFeeder={entryFeeder}
np={block.selection}
counterPart={undefined}
role="subject"
onChange={(payload) =>
adjustEps({ type: "set subject", payload })
}
opts={opts}
negative={eps.equative.negative}
/>
) : (
<APPicker
negative={eps.equative.negative}
phraseIsComplete={phraseIsComplete}
heading="AP"
entryFeeder={entryFeeder}
AP={block}
opts={opts}
onChange={(AP) =>
adjustEps({ type: "set AP", payload: { index: i, AP } })
}
onRemove={() => adjustEps({ type: "remove AP", payload: i })}
/>
)}
</div>
))}
<div className="my-2 card block-card p-1">
<div className="h5 text-center">Predicate</div>
<div className="mb-2 text-center">
<ButtonSelect
small
options={[
{ value: "NP", label: "NP" },
{ value: "Complement", label: "Complement" },
]}
value={eps.predicate.type}
handleChange={(payload) =>
adjustEps({ type: "set predicate type", payload })
}
/>
</div>
{eps.predicate.type === "NP" ? (
<NPPicker
phraseIsComplete={phraseIsComplete}
entryFeeder={entryFeeder}
np={eps.predicate.type === "NP" ? eps.predicate.NP : undefined}
counterPart={undefined}
role="subject"
onChange={(payload) =>
adjustEps({ type: "set predicate NP", payload })
}
opts={opts}
negative={eps.equative.negative}
/>
) : (
<ComplementPicker
phraseIsComplete={phraseIsComplete}
comp={
eps.predicate.type === "Complement"
? eps.predicate.Complement
: undefined
}
onChange={(payload) =>
adjustEps({ type: "set predicate complement", payload })
}
opts={opts}
entryFeeder={entryFeeder}
negative={eps.equative.negative}
/>
)}
</div> </div>
</div>; <div className="my-2">
<div className="h5 text-center clickable">Equative</div>
<EquativePicker
equative={eps.equative}
onChange={(payload) => adjustEps({ type: "set equative", payload })}
hideNegative={false}
/>
</div>
</div>
</div>
);
} }
export default EPPicker; export default EPPicker;

View File

@ -8,6 +8,7 @@ function AdjectiveManager(props: {
opts: T.TextOptions; opts: T.TextOptions;
onChange: (adjs: T.AdjectiveSelection[]) => void; onChange: (adjs: T.AdjectiveSelection[]) => void;
phraseIsComplete: boolean; phraseIsComplete: boolean;
negative: boolean;
}) { }) {
const [adding, setAdding] = useState<boolean>(false); const [adding, setAdding] = useState<boolean>(false);
function handleChange(i: number) { function handleChange(i: number) {
@ -48,6 +49,7 @@ function AdjectiveManager(props: {
entryFeeder={props.entryFeeder} entryFeeder={props.entryFeeder}
opts={props.opts} opts={props.opts}
onChange={handleAddNew} onChange={handleAddNew}
negative={props.negative}
/> />
</div> </div>
)} )}
@ -70,6 +72,7 @@ function AdjectiveManager(props: {
</div> </div>
</div> </div>
<AdjectivePicker <AdjectivePicker
negative={props.negative}
phraseIsComplete={props.phraseIsComplete} phraseIsComplete={props.phraseIsComplete}
noTitle noTitle
key={`adj${i}`} key={`adj${i}`}

View File

@ -5,64 +5,74 @@ import EntrySelect from "../EntrySelect";
import SandwichPicker from "./SandwichPicker"; import SandwichPicker from "./SandwichPicker";
function AdjectivePicker(props: { function AdjectivePicker(props: {
entryFeeder: T.EntryFeeder, entryFeeder: T.EntryFeeder;
adjective: T.AdjectiveSelection | undefined, adjective: T.AdjectiveSelection | undefined;
onChange: (p: T.AdjectiveSelection | undefined) => void, onChange: (p: T.AdjectiveSelection | undefined) => void;
opts: T.TextOptions, opts: T.TextOptions;
noTitle?: boolean, noTitle?: boolean;
phraseIsComplete: boolean, phraseIsComplete: boolean;
negative: boolean;
}) { }) {
const [addingSandwich, setAddingSandwich] = useState<boolean>(false); const [addingSandwich, setAddingSandwich] = useState<boolean>(false);
function onEntrySelect(entry: T.AdjectiveEntry | undefined) { function onEntrySelect(entry: T.AdjectiveEntry | undefined) {
if (!entry) { if (!entry) {
return props.onChange(undefined); return props.onChange(undefined);
}
props.onChange(makeAdjectiveSelection(entry));
} }
function handleSandwichChange(s: T.SandwichSelection<T.Sandwich> | undefined) { props.onChange(makeAdjectiveSelection(entry));
if (!props.adjective) return; }
props.onChange({ function handleSandwichChange(
...props.adjective, s: T.SandwichSelection<T.Sandwich> | undefined
sandwich: s, ) {
}); if (!props.adjective) return;
if (!s) { props.onChange({
setAddingSandwich(false); ...props.adjective,
} sandwich: s,
});
if (!s) {
setAddingSandwich(false);
} }
function handleSandwichExit() { }
setAddingSandwich(false); function handleSandwichExit() {
props.onChange(undefined); setAddingSandwich(false);
} props.onChange(undefined);
return <div style={{ maxWidth: "225px", minWidth: "125px" }}> }
{(props.adjective?.sandwich || addingSandwich) && <SandwichPicker return (
onChange={handleSandwichChange} <div style={{ maxWidth: "225px", minWidth: "125px" }}>
opts={props.opts} {(props.adjective?.sandwich || addingSandwich) && (
sandwich={props.adjective?.sandwich} <SandwichPicker
entryFeeder={props.entryFeeder} negative={props.negative}
onExit={handleSandwichExit} onChange={handleSandwichChange}
// TODO: not allowing shrinking any possesisives on sandwiches for now - need to work with the blocks and their special behaviour opts={props.opts}
phraseIsComplete={false} sandwich={props.adjective?.sandwich}
/>} entryFeeder={props.entryFeeder}
<div className="d-flex flex-row justify-content-between align-items-baseline"> onExit={handleSandwichExit}
{!props.noTitle && <div> // TODO: not allowing shrinking any possesisives on sandwiches for now - need to work with the blocks and their special behaviour
<div className="h6">Adjective</div> phraseIsComplete={false}
</div>} />
{/* not ready for sandwiches on adjectives */} )}
{/* {(!addingSandwich && props.adjective && !props.adjective?.sandwich) <div className="d-flex flex-row justify-content-between align-items-baseline">
{!props.noTitle && (
<div>
<div className="h6">Adjective</div>
</div>
)}
{/* not ready for sandwiches on adjectives */}
{/* {(!addingSandwich && props.adjective && !props.adjective?.sandwich)
? <div className="clickable" onClick={() => setAddingSandwich(true)}>+ Sandwich</div> ? <div className="clickable" onClick={() => setAddingSandwich(true)}>+ Sandwich</div>
: <div></div>} */} : <div></div>} */}
<div /> <div />
</div> </div>
<div className="mt-1"> <div className="mt-1">
<EntrySelect <EntrySelect
value={props.adjective?.entry} value={props.adjective?.entry}
entryFeeder={props.entryFeeder.adjectives} entryFeeder={props.entryFeeder.adjectives}
onChange={onEntrySelect} onChange={onEntrySelect}
name="Adjective" name="Adjective"
opts={props.opts} opts={props.opts}
/> />
</div> </div>
</div>; </div>
);
} }
export default AdjectivePicker; export default AdjectivePicker;

View File

@ -1,38 +1,52 @@
import * as T from "../../../types"; import * as T from "../../../types";
import classNames from "classnames"; import { DeterminerSelect } from "../EntrySelect";
export default function DemonstrativePicker({ export default function DeterminersPicker({
demonstrative, determiners,
onChange, onChange,
opts,
negative,
}: { }: {
demonstrative: T.NounSelection["demonstrative"]; determiners: T.NounSelection["determiners"];
onChange: (dem: T.NounSelection["demonstrative"]) => void; onChange: (dem: T.NounSelection["determiners"]) => void;
opts: T.TextOptions;
negative: boolean;
}) { }) {
function handleDChange(d: "daa" | "hagha" | "dagha") { const hasDemonstrative =
if (!demonstrative) { determiners &&
onChange({ determiners.determiners.some((d) => "demonstrative" in d.determiner);
type: "demonstrative", function allowed(d: T.Determiner): boolean {
demonstrative: d, if (d.p === "هیڅ" && !negative) {
withNoun: true, return false;
});
} else {
onChange({
...demonstrative,
demonstrative: d,
});
} }
if (hasDemonstrative && "demonstrative" in d) {
return false;
}
return true;
} }
function handleWithNounChange(e: React.ChangeEvent<HTMLInputElement>) { function handleWithNounChange(e: React.ChangeEvent<HTMLInputElement>) {
if (demonstrative) { if (determiners) {
onChange({ onChange({
...demonstrative, ...determiners,
withNoun: e.target.checked, withNoun: e.target.checked,
}); });
} }
} }
function handleDeterminerChange(value: T.Determiner[] | undefined) {
onChange({
type: "determiners",
withNoun: determiners ? determiners.withNoun : true,
determiners: value
? value.map((d) => ({
type: "determiner",
determiner: d,
}))
: [],
});
}
return ( return (
<div> <div>
<div className="d-flex flex-row justify-content-around py-1"> {/* <div className="d-flex flex-row justify-content-around py-1">
<div> <div>
<button <button
className={classNames("btn", "btn-outline-secondary", { className={classNames("btn", "btn-outline-secondary", {
@ -63,20 +77,29 @@ export default function DemonstrativePicker({
هغه هغه
</button> </button>
</div> </div>
</div> </div> */}
<DeterminerSelect
determiners={T.determiners.filter(allowed)}
value={
determiners ? determiners.determiners.map((x) => x.determiner) : []
}
onChange={handleDeterminerChange}
name="determiner"
opts={opts}
/>
<div <div
className="form-check" className="form-check"
style={{ style={{
opacity: demonstrative ? 1 : 0.5, opacity: determiners ? 1 : 0.5,
}} }}
> >
<input <input
className="form-check-input" className="form-check-input"
type="checkbox" type="checkbox"
checked={demonstrative?.withNoun} checked={determiners?.withNoun}
onChange={handleWithNounChange} onChange={handleWithNounChange}
id="withNoun" id="withNoun"
disabled={!demonstrative} disabled={!hasDemonstrative}
/> />
<label className="form-check-label text-muted" htmlFor="withNoun"> <label className="form-check-label text-muted" htmlFor="withNoun">
with noun with noun

View File

@ -7,7 +7,7 @@ import InlinePs from "../InlinePs";
import EntrySelect from "../EntrySelect"; import EntrySelect from "../EntrySelect";
import AdjectiveManager from "./AdjectiveManager"; import AdjectiveManager from "./AdjectiveManager";
import { useState } from "react"; import { useState } from "react";
import DemonstrativePicker from "./DemonstrativePicker"; import DeterminersPicker from "./DeterminersPicker";
// const filterOptions = [ // const filterOptions = [
// { // {
@ -62,9 +62,9 @@ function NPNounPicker(props: {
onChange: (p: T.NounSelection | undefined) => void; onChange: (p: T.NounSelection | undefined) => void;
opts: T.TextOptions; opts: T.TextOptions;
phraseIsComplete: boolean; phraseIsComplete: boolean;
negative: boolean;
}) { }) {
const [addingDemonstrative, setAddingDemonstrative] = const [addingDeterminers, setAddingDeterminers] = useState<boolean>(false);
useState<boolean>(false);
// const [patternFilter, setPatternFilter] = useState<FilterPattern | undefined>(undefined); // const [patternFilter, setPatternFilter] = useState<FilterPattern | undefined>(undefined);
// const [showFilter, setShowFilter] = useState<boolean>(false) // const [showFilter, setShowFilter] = useState<boolean>(false)
// const nounsFiltered = props.nouns // const nounsFiltered = props.nouns
@ -88,13 +88,13 @@ function NPNounPicker(props: {
}); });
} }
} }
function handleDemonstrativeChange( function handleDeterminersChange(
demonstrative: undefined | T.NounSelection["demonstrative"] determiners: undefined | T.NounSelection["determiners"]
) { ) {
if (props.noun) { if (props.noun) {
props.onChange({ props.onChange({
...props.noun, ...props.noun,
demonstrative, determiners,
}); });
} }
} }
@ -105,32 +105,34 @@ function NPNounPicker(props: {
minWidth: "125px", minWidth: "125px",
}} }}
> >
{!addingDemonstrative && !props.noun?.demonstrative ? ( {!addingDeterminers && !props.noun?.determiners ? (
<div> <div>
<span <span
className="clickable text-muted" className="clickable text-muted"
onClick={() => setAddingDemonstrative(true)} onClick={() => setAddingDeterminers(true)}
> >
+ Demonstrative + Determiners
</span> </span>
</div> </div>
) : ( ) : (
<div> <div>
<div className="d-flex flex-row justify-content-between mb-1"> <div className="d-flex flex-row justify-content-between mb-1">
<div>{!props.noun?.demonstrative ? "Add" : ""} Demonstrative</div> <div>{!props.noun?.determiners ? "Add" : ""} Determiners</div>
<div <div
className="clickable" className="clickable"
onClick={() => { onClick={() => {
handleDemonstrativeChange(undefined); handleDeterminersChange(undefined);
setAddingDemonstrative(false); setAddingDeterminers(false);
}} }}
> >
<i className="fas fa-trash" /> <i className="fas fa-trash" />
</div> </div>
</div> </div>
<DemonstrativePicker <DeterminersPicker
demonstrative={props.noun?.demonstrative} determiners={props.noun?.determiners}
onChange={handleDemonstrativeChange} onChange={handleDeterminersChange}
opts={props.opts}
negative={props.negative}
/> />
</div> </div>
)} )}
@ -151,13 +153,14 @@ function NPNounPicker(props: {
<div <div
style={{ style={{
opacity: opacity:
props.noun?.demonstrative && !props.noun.demonstrative.withNoun props.noun?.determiners && !props.noun.determiners.withNoun
? 0.5 ? 0.5
: 1, : 1,
}} }}
> >
{props.noun && ( {props.noun && (
<AdjectiveManager <AdjectiveManager
negative={props.negative}
phraseIsComplete={props.phraseIsComplete} phraseIsComplete={props.phraseIsComplete}
adjectives={props.noun?.adjectives} adjectives={props.noun?.adjectives}
entryFeeder={props.entryFeeder} entryFeeder={props.entryFeeder}

View File

@ -25,6 +25,7 @@ function NPPicker(props: {
phraseIsComplete: boolean; phraseIsComplete: boolean;
isShrunk?: boolean; isShrunk?: boolean;
isRemoved?: boolean; isRemoved?: boolean;
negative: boolean;
}) { }) {
if ( if (
props.is2ndPersonPicker && props.is2ndPersonPicker &&
@ -223,6 +224,7 @@ function NPPicker(props: {
role="possesor" role="possesor"
opts={props.opts} opts={props.opts}
entryFeeder={props.entryFeeder} entryFeeder={props.entryFeeder}
negative={props.negative}
/> />
</div> </div>
)} )}
@ -250,6 +252,7 @@ function NPPicker(props: {
<NounPicker <NounPicker
phraseIsComplete={props.phraseIsComplete} phraseIsComplete={props.phraseIsComplete}
entryFeeder={props.entryFeeder} entryFeeder={props.entryFeeder}
negative={props.negative}
noun={ noun={
props.np && props.np.selection.type === "noun" props.np && props.np.selection.type === "noun"
? props.np.selection ? props.np.selection

View File

@ -5,74 +5,82 @@ import { useEffect, useState } from "react";
import NPPicker from "./NPPicker"; import NPPicker from "./NPPicker";
function SandwichPicker(props: { function SandwichPicker(props: {
opts: T.TextOptions, opts: T.TextOptions;
onChange: (s: T.SandwichSelection<T.Sandwich> | undefined) => void, onChange: (s: T.SandwichSelection<T.Sandwich> | undefined) => void;
sandwich: T.SandwichSelection<T.Sandwich> | undefined; sandwich: T.SandwichSelection<T.Sandwich> | undefined;
entryFeeder: T.EntryFeeder, entryFeeder: T.EntryFeeder;
phraseIsComplete: boolean, phraseIsComplete: boolean;
onExit: () => void, onExit: () => void;
negative: boolean;
}) { }) {
const [sandwichBase, setSandwichBase] = useState<T.Sandwich | undefined>(props.sandwich); const [sandwichBase, setSandwichBase] = useState<T.Sandwich | undefined>(
useEffect(() => { props.sandwich
setSandwichBase(props.sandwich); );
}, [props.sandwich]); useEffect(() => {
function handleNounChange(n: T.NPSelection | undefined) { setSandwichBase(props.sandwich);
if (!n) { }, [props.sandwich]);
props.onChange(undefined); function handleNounChange(n: T.NPSelection | undefined) {
return; if (!n) {
} props.onChange(undefined);
if (!sandwichBase) return; return;
props.onChange({
...sandwichBase,
inside: n,
});
} }
function handleSandwichChange(s: T.Sandwich | undefined) { if (!sandwichBase) return;
if (!s) { props.onChange({
setSandwichBase(undefined); ...sandwichBase,
props.onChange(undefined); inside: n,
return; });
} }
setSandwichBase(s); function handleSandwichChange(s: T.Sandwich | undefined) {
if (!props.sandwich) return; if (!s) {
props.onChange({ setSandwichBase(undefined);
...props.sandwich, props.onChange(undefined);
...s, return;
});
} }
function handleExit() { setSandwichBase(s);
props.onExit(); if (!props.sandwich) return;
} props.onChange({
return <div> ...props.sandwich,
<div className="d-flex flex-row justify-content-between"> ...s,
<div></div> });
<div className="text-center">🥪 Sandwich</div> }
<div className="clickable" onClick={handleExit}> function handleExit() {
<i className="fas fa-trash" /> props.onExit();
</div> }
return (
<div>
<div className="d-flex flex-row justify-content-between">
<div></div>
<div className="text-center">🥪 Sandwich</div>
<div className="clickable" onClick={handleExit}>
<i className="fas fa-trash" />
</div> </div>
<div style={{ border: "1px #6C757D solid", padding: "3px" }}> </div>
{sandwichBase && <div className="mb-2" style={{ margin: "0 auto" }}> <div style={{ border: "1px #6C757D solid", padding: "3px" }}>
{sandwichBase && (
<div className="mb-2" style={{ margin: "0 auto" }}>
<NPPicker <NPPicker
onChange={handleNounChange} onChange={handleNounChange}
np={props.sandwich ? props.sandwich.inside : undefined} np={props.sandwich ? props.sandwich.inside : undefined}
counterPart={undefined} counterPart={undefined}
opts={props.opts} opts={props.opts}
role="object" role="object"
cantClear={true} cantClear={true}
entryFeeder={props.entryFeeder} entryFeeder={props.entryFeeder}
phraseIsComplete={props.phraseIsComplete} phraseIsComplete={props.phraseIsComplete}
negative={props.negative}
/> />
</div>} </div>
)}
<SandwichSelect <SandwichSelect
name="sandwich" name="sandwich"
opts={props.opts} opts={props.opts}
sandwiches={sandwiches} sandwiches={sandwiches}
value={sandwichBase} value={sandwichBase}
onChange={handleSandwichChange} onChange={handleSandwichChange}
/> />
</div> </div>
</div>; </div>
);
} }
export default SandwichPicker; export default SandwichPicker;

View File

@ -44,7 +44,7 @@ function VPExplorer(props: {
props.loaded props.loaded
? props.loaded ? props.loaded
: (savedVps) => makeVPSelectionState(props.verb, savedVps), : (savedVps) => makeVPSelectionState(props.verb, savedVps),
"vpsState16", "vpsState17",
flashMessage flashMessage
); );
const [mode, setMode] = useStickyState<"charts" | "phrases" | "quiz">( const [mode, setMode] = useStickyState<"charts" | "phrases" | "quiz">(

View File

@ -163,6 +163,7 @@ function VPPicker({
</div> </div>
{!block || block.type === "AP" ? ( {!block || block.type === "AP" ? (
<APPicker <APPicker
negative={vps.verb.negative}
phraseIsComplete={phraseIsComplete} phraseIsComplete={phraseIsComplete}
heading="AP" heading="AP"
entryFeeder={entryFeeder} entryFeeder={entryFeeder}
@ -238,6 +239,7 @@ function VPPicker({
opts={opts} opts={opts}
isShrunk={servantIsShrunk && roles.servant === "subject"} isShrunk={servantIsShrunk && roles.servant === "subject"}
isRemoved={roles.king === "subject" && VPS?.form.removeKing} isRemoved={roles.king === "subject" && VPS?.form.removeKing}
negative={VPS?.verb.negative || false}
/> />
) : vps.verb && ) : vps.verb &&
block?.type === "objectSelection" && block?.type === "objectSelection" &&
@ -350,6 +352,7 @@ function VPPicker({
isRemoved={ isRemoved={
roles.king === "object" && VPS?.form.removeKing roles.king === "object" && VPS?.form.removeKing
} }
negative={VPS?.verb.negative || false}
/> />
)} )}
</div> </div>
@ -372,6 +375,7 @@ function VPPicker({
} }
opts={opts} opts={opts}
entryFeeder={entryFeeder} entryFeeder={entryFeeder}
negative={vps.verb.negative}
/> />
</div> </div>
)} )}

View File

@ -1,6 +1,6 @@
{ {
"name": "@lingdocs/inflect", "name": "@lingdocs/inflect",
"version": "7.4.1", "version": "7.5.1",
"description": "Pashto inflector library", "description": "Pashto inflector library",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/lib/library.d.ts", "types": "dist/lib/library.d.ts",

View File

@ -48,7 +48,7 @@ type Plurals =
// const endingInHayOrAynRegex = /[^ا][هع]$/; // const endingInHayOrAynRegex = /[^ا][هع]$/;
export function getInfsAndVocative( export function getInfsAndVocative(
entryR: T.DictionaryEntryNoFVars, entryR: T.DictionaryEntryNoFVars | T.Determiner,
plurals: Plurals plurals: Plurals
): ):
| { | {

View File

@ -5,6 +5,7 @@ import {
personNumber, personNumber,
personToGenNum, personToGenNum,
} from "../misc-helpers"; } from "../misc-helpers";
import { monoidPsString } from "../fp-ps";
import { applySingleOrLengthOpts, fmapSingleOrLengthOpts } from "../fp-ps"; import { applySingleOrLengthOpts, fmapSingleOrLengthOpts } from "../fp-ps";
import { import {
concatPsString, concatPsString,
@ -448,7 +449,7 @@ function ensure3rdPast(
} }
const abruptEnder = ["د", "ت", "ړ"].includes(rs[0].p.slice(-1)); const abruptEnder = ["د", "ت", "ړ"].includes(rs[0].p.slice(-1));
// short endings like ورسېد // short endings like ورسېد
const ends = abruptEnder ? [{ p: "", f: "" }, ...ending] : ending; const ends = abruptEnder ? [monoidPsString.empty, ...ending] : ending;
return verbEndingConcat(rs, ends); return verbEndingConcat(rs, ends);
} }

View File

@ -23,7 +23,7 @@ export function inflectWord(word: T.DictionaryEntry): T.InflectorOutput {
// If it's a noun/adj, inflect accordingly // If it's a noun/adj, inflect accordingly
// TODO: What about n. f. / adj. that end in ي ?? // TODO: What about n. f. / adj. that end in ي ??
const w = removeFVarients(word); const w = removeFVarients(word);
if (w.c?.includes("doub.")) { if (word.c?.includes("doub.")) {
const words = splitDoubleWord(w); const words = splitDoubleWord(w);
// TODO: Make this work for non-unisex double words // TODO: Make this work for non-unisex double words
// Right now this an extremely bad and complex way to do this // Right now this an extremely bad and complex way to do this

View File

@ -420,3 +420,123 @@ function arrayMove<X>(ar: X[], old_index: number, new_index: number): X[] {
arr.splice(new_i, 0, arr.splice(old_index, 1)[0]); arr.splice(new_i, 0, arr.splice(old_index, 1)[0]);
return arr; return arr;
} }
// TODO: This takes 8 helper functions to recursively go down and check all determiners
// - is this what LENSES would help with?
export function removeHeetsDet<B extends T.VPSBlock[] | T.EPSBlock[]>(
blocks: B
): B {
return blocks.map<T.VPSBlock | T.EPSBlock>((x) => ({
key: x.key,
block: removeHeetsDetFromBlock(x.block),
})) as B;
}
// TODO: Could use lenses for this
function removeHeetsDetFromBlock<
B extends T.VPSBlock["block"] | T.EPSBlock["block"]
>(block: B): B {
if (!block) {
return block;
}
if (block.type === "AP") {
return removeHeetsDetFromAP(block) as B;
}
if (block.type === "complement") {
return removeHeetsFromComp(block) as B;
}
return {
...block,
selection:
typeof block.selection === "object"
? removeHeetsFromNP(block.selection)
: block.selection,
};
}
function removeHeetsDetFromAP(ap: T.APSelection): T.APSelection {
if (ap.selection.type === "adverb") {
return ap;
}
return {
...ap,
selection: removeHeetsFromSandwich(ap.selection),
};
}
function removeHeetsFromSandwich(
sand: T.SandwichSelection<T.Sandwich>
): T.SandwichSelection<T.Sandwich> {
return {
...sand,
inside: removeHeetsFromNP(sand.inside),
};
}
function removeHeetsFromAdjective(
adj: T.AdjectiveSelection
): T.AdjectiveSelection {
return {
...adj,
sandwich: adj.sandwich ? removeHeetsFromSandwich(adj.sandwich) : undefined,
};
}
function removeHeetsFromComp(
comp: T.ComplementSelection
): T.ComplementSelection {
if (comp.selection.type === "adjective") {
return {
...comp,
selection: removeHeetsFromAdjective(comp.selection),
};
}
if (comp.selection.type === "noun") {
return {
...comp,
selection: removeHeetsFromNoun(comp.selection),
};
}
if (comp.selection.type === "sandwich") {
return {
...comp,
selection: removeHeetsFromSandwich(comp.selection),
};
}
// should be only a loc. adv. left
return comp;
}
function removeHeetsFromNoun(n: T.NounSelection): T.NounSelection {
return {
...n,
adjectives: n.adjectives.map(removeHeetsFromAdjective),
...(n.determiners
? {
determiners: removeHeetsFromDets(n.determiners),
}
: {}),
};
}
function removeHeetsFromNP(np: T.NPSelection): T.NPSelection {
if (np.selection.type === "noun") {
return {
...np,
selection: removeHeetsFromNoun(np.selection),
};
}
return np;
}
function removeHeetsFromDets(
dets: T.DeterminersSelection | undefined
): T.DeterminersSelection | undefined {
if (!dets) {
return dets;
}
return {
...dets,
determiners: dets.determiners.filter((d) => d.determiner.p !== "هیڅ"),
};
}

View File

@ -22,7 +22,7 @@ import {
specifyEquativeLength, specifyEquativeLength,
} from "./blocks-utils"; } from "./blocks-utils";
import { blank, kidsBlank } from "../misc-helpers"; import { blank, kidsBlank } from "../misc-helpers";
import { monoidPsStringWVars } from "../fp-ps"; import { monoidPsString, monoidPsStringWVars } from "../fp-ps";
import { concatAll } from "fp-ts/lib/Monoid"; import { concatAll } from "fp-ts/lib/Monoid";
type BlankoutOptions = { type BlankoutOptions = {
@ -275,7 +275,7 @@ function applyBlankOut(
return kidsBlank; return kidsBlank;
} }
if (blankOut?.negative && "block" in x && x.block.type === "negative") { if (blankOut?.negative && "block" in x && x.block.type === "negative") {
return { p: "", f: "" }; return monoidPsString.empty;
} }
return x; return x;
}); });
@ -314,7 +314,7 @@ function getPsFromPiece(
} }
if (piece.block.type === "objectSelection") { if (piece.block.type === "objectSelection") {
if (typeof piece.block.selection !== "object") { if (typeof piece.block.selection !== "object") {
return [{ p: "", f: "" }]; return [monoidPsString.empty];
} }
return getPashtoFromRendered(piece.block.selection, subjectPerson); return getPashtoFromRendered(piece.block.selection, subjectPerson);
} }

View File

@ -1,216 +1,257 @@
import * as T from "../../../types"; import * as T from "../../../types";
import { import { personGender, personNumber } from "../misc-helpers";
personGender,
personNumber,
} from "../misc-helpers";
import { isUnisexNounEntry } from "../type-predicates"; import { isUnisexNounEntry } from "../type-predicates";
import { checkForMiniPronounsError } from "./compile"; import { checkForMiniPronounsError } from "./compile";
import { adjustSubjectSelection, getSubjectSelection, insertNewAP, removeAP, setAP, shiftBlock } from "./blocks-utils"; import {
adjustSubjectSelection,
getSubjectSelection,
insertNewAP,
removeAP,
removeHeetsDet,
setAP,
shiftBlock,
} from "./blocks-utils";
export type EpsReducerAction = { export type EpsReducerAction =
type: "set predicate type", | {
payload: "NP" | "Complement", type: "set predicate type";
} | { payload: "NP" | "Complement";
type: "set subject",
payload: T.NPSelection | undefined,
} | {
type: "set predicate NP",
payload: T.NPSelection | undefined,
} | {
type: "set predicate complement",
payload: T.ComplementSelection | undefined,
} | {
type: "set omitSubject",
payload: "true" | "false",
} | {
type: "set equative",
payload: T.EquativeSelection,
} | {
type: "insert new AP",
} | {
type: "set AP",
payload: {
index: number,
AP: T.APSelection | undefined,
},
} | {
type: "remove AP",
payload: number,
} | {
type: "shift block",
payload: {
index: number,
direction: "back" | "forward",
},
} | {
type: "load EPS",
payload: T.EPSelectionState,
}
export default function epsReducer(eps: T.EPSelectionState, action: EpsReducerAction, sendAlert?: (msg: string) => void): T.EPSelectionState {
if (action.type === "set predicate type") {
return {
...eps,
predicate: {
...eps.predicate,
type: action.payload,
},
};
} }
if (action.type === "set subject") { | {
const subject = action.payload; type: "set subject";
if (!subject) { payload: T.NPSelection | undefined;
return { }
...eps, | {
blocks: adjustSubjectSelection(eps.blocks, subject), type: "set predicate NP";
}; payload: T.NPSelection | undefined;
} }
if (subject.selection.type === "pronoun" && eps.predicate.type === "NP" && eps.predicate.NP?.selection.type === "noun" && isUnisexNounEntry(eps.predicate.NP.selection.entry)) { | {
const predicate = eps.predicate.NP.selection; type: "set predicate complement";
const adjusted = { payload: T.ComplementSelection | undefined;
...predicate, }
...predicate.numberCanChange ? { | {
number: personNumber(subject.selection.person), type: "set omitSubject";
} : {}, payload: "true" | "false";
...predicate.genderCanChange ? { }
gender: personGender(subject.selection.person), | {
} : {}, type: "set equative";
payload: T.EquativeSelection;
}
| {
type: "insert new AP";
}
| {
type: "set AP";
payload: {
index: number;
AP: T.APSelection | undefined;
};
}
| {
type: "remove AP";
payload: number;
}
| {
type: "shift block";
payload: {
index: number;
direction: "back" | "forward";
};
}
| {
type: "load EPS";
payload: T.EPSelectionState;
};
export default function epsReducer(
eps: T.EPSelectionState,
action: EpsReducerAction,
sendAlert?: (msg: string) => void
): T.EPSelectionState {
if (action.type === "set predicate type") {
return {
...eps,
predicate: {
...eps.predicate,
type: action.payload,
},
};
}
if (action.type === "set subject") {
const subject = action.payload;
if (!subject) {
return {
...eps,
blocks: adjustSubjectSelection(eps.blocks, subject),
};
}
if (
subject.selection.type === "pronoun" &&
eps.predicate.type === "NP" &&
eps.predicate.NP?.selection.type === "noun" &&
isUnisexNounEntry(eps.predicate.NP.selection.entry)
) {
const predicate = eps.predicate.NP.selection;
const adjusted = {
...predicate,
...(predicate.numberCanChange
? {
number: personNumber(subject.selection.person),
} }
return { : {}),
...eps, ...(predicate.genderCanChange
blocks: adjustSubjectSelection(eps.blocks, subject), ? {
predicate: { gender: personGender(subject.selection.person),
...eps.predicate, }
NP: { : {}),
type: "NP", };
selection: adjusted, return {
}, ...eps,
}, blocks: adjustSubjectSelection(eps.blocks, subject),
}; predicate: {
} ...eps.predicate,
const n: T.EPSelectionState = { NP: {
...eps, type: "NP",
blocks: adjustSubjectSelection(eps.blocks, subject), selection: adjusted,
}; },
return subject ? ensureMiniPronounsOk(eps, n, sendAlert) : n; },
};
} }
if (action.type === "set predicate NP") { const n: T.EPSelectionState = {
const selection = action.payload; ...eps,
if (!selection) { blocks: adjustSubjectSelection(eps.blocks, subject),
return { };
...eps, return subject ? ensureMiniPronounsOk(eps, n, sendAlert) : n;
predicate: { }
...eps.predicate, if (action.type === "set predicate NP") {
NP: selection, const selection = action.payload;
}, if (!selection) {
}; return {
} ...eps,
const subject = getSubjectSelection(eps.blocks).selection; predicate: {
if (subject?.selection.type === "pronoun" && selection.selection.type === "noun" && isUnisexNounEntry(selection.selection.entry)) { ...eps.predicate,
const { gender, number } = selection.selection; NP: selection,
const pronoun = subject.selection.person; },
const newPronoun = movePersonNumber(movePersonGender(pronoun, gender), number); };
return {
...eps,
blocks: adjustSubjectSelection(eps.blocks, {
type: "NP",
selection: {
...subject.selection,
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 complement") { const subject = getSubjectSelection(eps.blocks).selection;
return { if (
...eps, subject?.selection.type === "pronoun" &&
predicate: { selection.selection.type === "noun" &&
...eps.predicate, isUnisexNounEntry(selection.selection.entry)
Complement: action.payload, ) {
}, const { gender, number } = selection.selection;
}; const pronoun = subject.selection.person;
const newPronoun = movePersonNumber(
movePersonGender(pronoun, gender),
number
);
return {
...eps,
blocks: adjustSubjectSelection(eps.blocks, {
type: "NP",
selection: {
...subject.selection,
person: newPronoun,
},
}),
predicate: {
...eps.predicate,
NP: selection,
},
};
} }
if (action.type === "set omitSubject") { const n: T.EPSelectionState = {
const n: T.EPSelectionState = { ...eps,
...eps, predicate: {
omitSubject: action.payload === "true", ...eps.predicate,
}; NP: selection,
return ensureMiniPronounsOk(eps, n, sendAlert); },
} };
if (action.type === "set equative") { return selection ? ensureMiniPronounsOk(eps, n, sendAlert) : n;
return { }
...eps, if (action.type === "set predicate complement") {
equative: action.payload, return {
} ...eps,
} predicate: {
if (action.type === "insert new AP") { ...eps.predicate,
return { Complement: action.payload,
...eps, },
blocks: insertNewAP(eps.blocks), };
}; }
} if (action.type === "set omitSubject") {
if (action.type === "set AP") { const n: T.EPSelectionState = {
const { index, AP } = action.payload; ...eps,
return { omitSubject: action.payload === "true",
...eps, };
blocks: setAP(eps.blocks, index, AP), return ensureMiniPronounsOk(eps, n, sendAlert);
}; }
} if (action.type === "set equative") {
if (action.type === "remove AP") { return {
return { ...eps,
...eps, blocks: !action.payload.negative
blocks: removeAP(eps.blocks, action.payload), ? removeHeetsDet(eps.blocks)
}; : eps.blocks,
} equative: action.payload,
if (action.type === "shift block") { };
const { index, direction } = action.payload; }
return { if (action.type === "insert new AP") {
...eps, return {
blocks: shiftBlock(eps.blocks, index, direction), ...eps,
}; blocks: insertNewAP(eps.blocks),
} };
if (action.type === "load EPS") { }
return action.payload; if (action.type === "set AP") {
} const { index, AP } = action.payload;
throw new Error("unknown epsReducer action"); return {
...eps,
blocks: setAP(eps.blocks, index, AP),
};
}
if (action.type === "remove AP") {
return {
...eps,
blocks: removeAP(eps.blocks, action.payload),
};
}
if (action.type === "shift block") {
const { index, direction } = action.payload;
return {
...eps,
blocks: shiftBlock(eps.blocks, index, direction),
};
}
if (action.type === "load EPS") {
return action.payload;
}
throw new Error("unknown epsReducer action");
} }
function ensureMiniPronounsOk(old: T.EPSelectionState, eps: T.EPSelectionState, sendAlert?: (msg: string) => void): T.EPSelectionState { function ensureMiniPronounsOk(
const error = checkForMiniPronounsError(eps); old: T.EPSelectionState,
if (error) { eps: T.EPSelectionState,
if (sendAlert) sendAlert(error); sendAlert?: (msg: string) => void
return old; ): T.EPSelectionState {
} const error = checkForMiniPronounsError(eps);
return eps; if (error) {
if (sendAlert) sendAlert(error);
return old;
}
return eps;
} }
function movePersonGender(p: T.Person, gender: T.Gender): T.Person { function movePersonGender(p: T.Person, gender: T.Gender): T.Person {
const pGender = personGender(p); const pGender = personGender(p);
if (gender === pGender) { if (gender === pGender) {
return p; return p;
} }
return (gender === "masc") ? (p - 1) : (p + 1); return gender === "masc" ? p - 1 : p + 1;
} }
function movePersonNumber(p: T.Person, number: T.NounNumber): T.Person { function movePersonNumber(p: T.Person, number: T.NounNumber): T.Person {
const pNumber = personNumber(p); const pNumber = personNumber(p);
if (pNumber === number) { if (pNumber === number) {
return p; return p;
} }
return (number === "plural") return number === "plural" ? p + 6 : p - 6;
? (p + 6)
: (p - 6);
} }

View File

@ -81,6 +81,6 @@ export function makeNounSelection(
possesor: !complementType ? old?.possesor : undefined, possesor: !complementType ? old?.possesor : undefined,
dynamicComplement: complementType === "dynamic", dynamicComplement: complementType === "dynamic",
genStativeComplement: complementType === "generative stative", genStativeComplement: complementType === "generative stative",
demonstrative: old?.demonstrative, determiners: old?.determiners,
}; };
} }

View File

@ -2,8 +2,10 @@ import { isFirstPerson, isSecondPerson } from "../misc-helpers";
import * as T from "../../../types"; import * as T from "../../../types";
import { concatPsString } from "../p-text-helpers"; import { concatPsString } from "../p-text-helpers";
import { flattenLengths } from "./compile"; import { flattenLengths } from "./compile";
import { monoidPsStringWVars } from "../fp-ps";
import { concatAll } from "fp-ts/lib/Monoid";
function getBaseAndAdjectives({ function getBaseWDetsAndAdjs({
selection, selection,
}: T.Rendered< }: T.Rendered<
T.NPSelection | T.ComplementSelection | T.APSelection T.NPSelection | T.ComplementSelection | T.APSelection
@ -11,45 +13,39 @@ function getBaseAndAdjectives({
if (selection.type === "sandwich") { if (selection.type === "sandwich") {
return getSandwichPsBaseAndAdjectives(selection); return getSandwichPsBaseAndAdjectives(selection);
} }
const adjs = "adjectives" in selection && selection.adjectives; const determiners = (
const demonstrativePs = ("demonstrative" in selection && ("determiners" in selection && selection.determiners?.determiners) ||
selection.demonstrative?.ps) || { p: "", f: "" }; []
if (!adjs) { ).map((x) => x.ps);
// TODO: does this ever get used?? const detWOutNoun =
return flattenLengths(selection.ps).map((x) => "determiners" in selection &&
concatPsString(demonstrativePs, x) selection.determiners &&
); !selection.determiners.withNoun;
} const adjs = (("adjectives" in selection && selection.adjectives) || []).map(
(x) => x.ps
if (selection.demonstrative && !selection.demonstrative.withNoun) {
return [demonstrativePs];
}
return flattenLengths(selection.ps).map((p) =>
concatPsString(
demonstrativePs,
// demons ? " " : "",
adjs.reduce(
(accum, curr) => {
// TODO: with variations of adjs? {
return concatPsString(
accum,
accum.p === "" && accum.f === "" ? "" : "", //" ",
curr.ps[0]
);
},
{ p: "", f: "" }
),
" ",
p
)
); );
const base = flattenLengths(selection.ps);
return assemblePsWords([
...determiners,
...(detWOutNoun ? [] : [...adjs, base]),
]);
}
// TODO: perhaps use this for more things (a simple compileIntoText ?)
function assemblePsWords(words: T.PsString[][]): T.PsString[] {
return concatAll(monoidPsStringWVars)([
...intersperse(words, [{ p: " ", f: " " }]),
]);
}
function intersperse<T>(arr: T[], sep: T): T[] {
return arr.reduce((a: T[], v: T) => [...a, v, sep], []).slice(0, -1);
} }
function getSandwichPsBaseAndAdjectives( function getSandwichPsBaseAndAdjectives(
s: T.Rendered<T.SandwichSelection<T.Sandwich>> s: T.Rendered<T.SandwichSelection<T.Sandwich>>
): T.PsString[] { ): T.PsString[] {
const insideBase = getBaseAndAdjectives(s.inside); const insideBase = getBaseWDetsAndAdjs(s.inside);
const willContractWithPronoun = const willContractWithPronoun =
s.before && s.before &&
s.before.p === "د" && s.before.p === "د" &&
@ -118,7 +114,7 @@ export function getPashtoFromRendered(
| T.Rendered<T.APSelection>, | T.Rendered<T.APSelection>,
subjectsPerson: false | T.Person subjectsPerson: false | T.Person
): T.PsString[] { ): T.PsString[] {
const base = getBaseAndAdjectives(b); const base = getBaseWDetsAndAdjs(b);
if (b.selection.type === "loc. adv." || b.selection.type === "adverb") { if (b.selection.type === "loc. adv." || b.selection.type === "adverb") {
return base; return base;
} }
@ -172,7 +168,7 @@ function addPossesor(
); );
} }
const wPossesor = existing.flatMap((ps) => const wPossesor = existing.flatMap((ps) =>
getBaseAndAdjectives(owner).map((v) => getBaseWDetsAndAdjs(owner).map((v) =>
owner.selection.type === "pronoun" && owner.selection.type === "pronoun" &&
subjectsPerson !== false && subjectsPerson !== false &&
willBeReflexive(subjectsPerson, owner.selection.person) willBeReflexive(subjectsPerson, owner.selection.person)
@ -204,7 +200,7 @@ function addArticlesAndAdjs(
const adjs = !np.adjectives const adjs = !np.adjectives
? "" ? ""
: np.adjectives.reduce((accum, curr): string => { : np.adjectives.reduce((accum, curr): string => {
if (!curr.e) throw new Error("no english for adjective"); if (!curr.e) return "ADJ";
return accum + curr.e + " "; return accum + curr.e + " ";
}, ""); }, "");
const genderTag = np.genderCanChange const genderTag = np.genderCanChange
@ -212,10 +208,17 @@ function addArticlesAndAdjs(
? " (f.)" ? " (f.)"
: " (m.)" : " (m.)"
: ""; : "";
const demonstrative = np.demonstrative ? ` ${np.demonstrative.e}` : ""; const moreThanOneDet = (np.determiners?.determiners.length || 0) > 1;
const demWithoutNoun = np.demonstrative && !np.demonstrative.withNoun; const determiners =
return `${np.demonstrative ? "" : articles}${demonstrative}${ np.determiners && np.determiners.determiners
demWithoutNoun ? ` (${(adjs + word).trim()})` : adjs + word ? np.determiners.determiners
// @ts-ignore - weird, ts is not recognizing this as rendered
.map((x) => (moreThanOneDet ? `(${x.e})` : x.e))
.join(" ")
: "";
const detsWithoutNoun = np.determiners && !np.determiners.withNoun;
return `${np.determiners ? "" : articles}${determiners}${
detsWithoutNoun ? ` (${(adjs + word).trim()})` : adjs + word
}${genderTag}`; }${genderTag}`;
} catch (e) { } catch (e) {
return undefined; return undefined;

View File

@ -15,6 +15,7 @@ import { shortVerbEndConsonant } from "../parsing/misc";
import { removeL } from "../new-verb-engine/rs-helpers"; import { removeL } from "../new-verb-engine/rs-helpers";
import { applySingleOrLengthOpts } from "../fp-ps"; import { applySingleOrLengthOpts } from "../fp-ps";
import { accentOnNFromEnd } from "../accent-helpers"; import { accentOnNFromEnd } from "../accent-helpers";
import { getInfsAndVocative } from "../inflections-and-vocative";
// TODO: can have subject and objects in possesors!! // TODO: can have subject and objects in possesors!!
@ -115,6 +116,20 @@ export function renderNounSelection(
return ps.length > 0 ? ps : [psStringFromEntry(n.entry)]; return ps.length > 0 ? ps : [psStringFromEntry(n.entry)];
})(); })();
const person = getPersonNumber(n.gender, n.number); const person = getPersonNumber(n.gender, n.number);
const determiners: T.Rendered<T.DeterminersSelection> | undefined =
n.determiners
? {
...n.determiners,
determiners: n.determiners.determiners.map((determiner) =>
renderDeterminer({
determiner,
inflected,
number: n.number,
gender: n.gender,
})
),
}
: undefined;
return { return {
...n, ...n,
adjectives: n.adjectives.map((a) => adjectives: n.adjectives.map((a) =>
@ -131,64 +146,119 @@ export function renderNounSelection(
ps: pashto, ps: pashto,
e: english, e: english,
possesor: renderPossesor(n.possesor, role), possesor: renderPossesor(n.possesor, role),
demonstrative: renderDemonstrative({ determiners,
demonstrative: n.demonstrative,
inflected,
plural: n.number === "plural",
gender: n.gender,
}),
}; };
} }
function renderDemonstrative({ function renderDeterminer({
demonstrative, determiner: { determiner },
inflected, inflected,
plural, number,
gender, gender,
}: { }: {
demonstrative: T.DemonstrativeSelection | undefined; determiner: T.DeterminerSelection;
inflected: boolean; inflected: boolean;
plural: boolean; number: T.NounNumber;
gender: T.Gender; gender: T.Gender;
}): T.Rendered<T.DemonstrativeSelection> | undefined { }): T.Rendered<T.DeterminerSelection> {
if (!demonstrative) { if (determiner.p === "دا") {
return undefined; const ps = inflected ? { p: "دې", f: "de" } : { p: "دا", f: "daa" };
return {
type: "determiner",
determiner,
inflected,
number,
gender,
ps: [ps],
e: number === "plural" ? "these" : "this",
};
} }
const ps = if (determiner.p === "دغه") {
demonstrative.demonstrative === "daa" const ps = inflected
? inflected ? number === "plural"
? { p: "دې", f: "de" } ? { p: "دغو", f: "dágho" }
: { p: "دا", f: "daa" } : gender === "masc"
: demonstrative.demonstrative === "dagha" ? { p: "دغه", f: "dághu" }
? inflected : { p: "دغې", f: "dághe" }
? plural : { p: "دغه", f: "dágha" };
? { p: "دغو", f: "dágho" } return {
: gender === "masc" type: "determiner",
? { p: "دغه", f: "dághu" } determiner,
: { p: "دغې", f: "dághe" } inflected,
: { p: "دغه", f: "dágha" } number,
: inflected gender,
? plural ps: [ps],
e: number === "plural" ? "these" : "this",
};
}
if (determiner.p === "هغه") {
const ps = inflected
? number === "plural"
? { p: "هغو", f: "hágho" } ? { p: "هغو", f: "hágho" }
: gender === "masc" : gender === "masc"
? { p: "هغه", f: "hághu" } ? { p: "هغه", f: "hághu" }
: { p: "هغې", f: "hághe" } : { p: "هغې", f: "hághe" }
: { p: "هغه", f: "hágha" }; : { p: "هغه", f: "hágha" };
return {
type: "determiner",
determiner,
inflected,
number,
gender,
ps: [ps],
e: number === "plural" ? "those" : "that",
};
}
const e = const e =
demonstrative.demonstrative === "hagha" determiner.f === "Tol"
? plural ? "all/the whole"
? "those" : determiner.f === "bul"
: "that" ? "other/another"
: plural : determiner.f === "har"
? "these" ? "every/each"
: "this"; : determiner.f === "koom"
? "some/which"
: determiner.f === "heets"
? "no"
: determiner.f === "dáase"
? number === "plural"
? "such/like these"
: "such/like this"
: determiner.f === "daghase"
? `just such/just like ${number === "plural" ? "these" : "this"}`
: determiner.f === "hase"
? `such/like ${number === "plural" ? "those" : "that"}`
: number === "plural"
? "just such/just like these"
: "just such/just like this";
return { return {
...demonstrative, type: "determiner",
ps, determiner,
inflected,
number,
gender,
ps: inflectDeterminer(determiner, inflected, gender, number),
e, e,
}; };
} }
function inflectDeterminer(
determiner: T.Determiner,
inflected: boolean,
gender: T.Gender,
number: T.NounNumber
): T.PsString[] {
const infs = getInfsAndVocative(determiner, undefined);
if (!infs || !infs.inflections) {
return [{ p: determiner.p, f: determiner.f }];
}
const inf = getBasicInf(infs.inflections, gender, number, inflected);
if (!inf) {
return [{ p: determiner.p, f: determiner.f }];
}
return inf;
}
function renderPronounSelection( function renderPronounSelection(
p: T.PronounSelection, p: T.PronounSelection,
inflected: boolean, inflected: boolean,
@ -269,6 +339,20 @@ function renderPossesor(
}; };
} }
function getBasicInf(
infs: T.Inflections,
gender: T.Gender,
number: T.NounNumber,
inflected: boolean
): T.PsString[] | false {
const inflectionNumber = (inflected ? 1 : 0) + (number === "plural" ? 1 : 0);
if (gender in infs) {
// @ts-ignore
return infs[gender][inflectionNumber];
}
return false;
}
function getInf( function getInf(
infs: T.InflectorOutput, infs: T.InflectorOutput,
t: "plural" | "arabicPlural" | "inflections", t: "plural" | "arabicPlural" | "inflections",

View File

@ -11,6 +11,7 @@ import {
getSubjectSelection, getSubjectSelection,
insertNewAP, insertNewAP,
removeAP, removeAP,
removeHeetsDet,
setAP, setAP,
shiftBlock, shiftBlock,
} from "./blocks-utils"; } from "./blocks-utils";
@ -19,6 +20,7 @@ import {
changeTransitivity, changeTransitivity,
makeVPSelectionState, makeVPSelectionState,
} from "./verb-selection"; } from "./verb-selection";
import { mapGen } from "../fp-ps";
export type VpsReducerAction = export type VpsReducerAction =
| { | {
@ -203,11 +205,13 @@ export function vpsReducer(
} }
if (action.type === "set negativity") { if (action.type === "set negativity") {
if (!vps.verb) return vps; if (!vps.verb) return vps;
const negative = action.payload === "true";
return { return {
...vps, ...vps,
blocks: !negative ? removeHeetsDet(vps.blocks) : vps.blocks,
verb: { verb: {
...vps.verb, ...vps.verb,
negative: action.payload === "true", negative,
}, },
}; };
} }

View File

@ -62,14 +62,23 @@ export function isNounOrAdjEntry(
} }
export function isInflectableEntry( export function isInflectableEntry(
e: T.Entry | T.DictionaryEntry | T.DictionaryEntryNoFVars e: T.Entry | T.DictionaryEntry | T.DictionaryEntryNoFVars | T.Determiner
): e is T.InflectableEntry { ): e is T.InflectableEntry {
if ("entry" in e) { if ("entry" in e) {
return false; return false;
} }
if (isDeterminer(e)) {
return true;
}
return isNounEntry(e) || isAdjectiveEntry(e) || isNumberEntry(e); return isNounEntry(e) || isAdjectiveEntry(e) || isNumberEntry(e);
} }
export function isDeterminer(
e: T.Entry | T.DictionaryEntry | T.Determiner
): e is T.Determiner {
return "type" in e && e.type === "det";
}
export function isNumberEntry( export function isNumberEntry(
e: T.Entry | T.DictionaryEntry e: T.Entry | T.DictionaryEntry
): e is T.NumberEntry { ): e is T.NumberEntry {

View File

@ -899,13 +899,35 @@ export type NounSelection = {
genStativeComplement?: boolean; genStativeComplement?: boolean;
adjectives: AdjectiveSelection[]; adjectives: AdjectiveSelection[];
possesor: undefined | PossesorSelection; possesor: undefined | PossesorSelection;
demonstrative: undefined | DemonstrativeSelection; determiners?: DeterminersSelection;
}; };
export type DemonstrativeSelection = { export type DeterminersSelection = {
type: "demonstrative"; type: "determiners";
demonstrative: "daa" | "hagha" | "dagha";
withNoun: boolean; withNoun: boolean;
determiners: DeterminerSelection[];
};
export const determiners = [
{ p: "دا", f: "daa", type: "det", demonstrative: true },
{ p: "دغه", f: "dágha", type: "det", demonstrative: true },
{ p: "هغه", f: "hágha", type: "det", demonstrative: true },
{ p: "کوم", f: "koom", type: "det" },
{ p: "داسې", f: "dáase", type: "det" },
{ p: "دغسې", f: "daghase", type: "det" },
{ p: "هسې", f: "hase", type: "det" },
{ p: "هغسې", f: "hagháse", type: "det" },
{ p: "هر", f: "har", type: "det" },
{ p: "ټول", f: "Tol", type: "det" },
{ p: "بل", f: "bul", type: "det" },
{ p: "هیڅ", f: "heets", type: "det", noInf: true },
] as const;
export type Determiner = (typeof determiners)[number];
export type DeterminerSelection = {
type: "determiner";
determiner: Determiner;
}; };
export type AdverbSelection = { export type AdverbSelection = {
@ -970,7 +992,8 @@ export type Rendered<
| AdjectiveSelection | AdjectiveSelection
| SandwichSelection<Sandwich> | SandwichSelection<Sandwich>
| ComplementSelection | ComplementSelection
| DemonstrativeSelection | DeterminersSelection
| DeterminerSelection
| ComplementSelection["selection"] | ComplementSelection["selection"]
| UnselectedComplementSelection | UnselectedComplementSelection
| undefined | undefined
@ -1020,13 +1043,21 @@ export type Rendered<
inflected: boolean; inflected: boolean;
person: Person; person: Person;
} }
: T extends DemonstrativeSelection : T extends DeterminersSelection
? { ? {
type: "demonstrative"; type: "determiners";
demonstrative: DemonstrativeSelection["demonstrative"];
withNoun: boolean; withNoun: boolean;
ps: PsString; determiners: Rendered<DeterminerSelection>[];
}
: T extends DeterminerSelection
? {
type: "determiner";
determiner: DeterminerSelection["determiner"];
ps: PsString[];
e: string; e: string;
inflected: boolean;
number: NounNumber;
gender: Gender;
} }
: T extends ComplementSelection : T extends ComplementSelection
? { ? {
@ -1078,7 +1109,7 @@ export type Rendered<
shrunken: boolean; shrunken: boolean;
np: Rendered<NPSelection>; np: Rendered<NPSelection>;
}; };
demonstrative?: Rendered<DemonstrativeSelection>; determiners?: Rendered<DeterminersSelection>;
}; };
export type EPSelectionState = { export type EPSelectionState = {