diff --git a/package.json b/package.json index d94eea9..f76057c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pashto-inflector", - "version": "7.4.1", + "version": "7.5.0", "author": "lingdocs.com", "description": "A Pashto inflection and verb conjugation engine, inculding React components for displaying Pashto text, inflections, and conjugations", "homepage": "https://verbs.lingdocs.com", diff --git a/src/components/package-lock.json b/src/components/package-lock.json index 08db85e..5b5366e 100644 --- a/src/components/package-lock.json +++ b/src/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@lingdocs/ps-react", - "version": "7.4.1", + "version": "7.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@lingdocs/ps-react", - "version": "7.4.1", + "version": "7.5.0", "license": "MIT", "dependencies": { "@formkit/auto-animate": "^1.0.0-beta.3", diff --git a/src/components/package.json b/src/components/package.json index b5b2344..7e7a59a 100644 --- a/src/components/package.json +++ b/src/components/package.json @@ -1,6 +1,6 @@ { "name": "@lingdocs/ps-react", - "version": "7.4.1", + "version": "7.5.0", "description": "Pashto inflector library module with React components", "main": "dist/components/library.js", "module": "dist/components/library.js", diff --git a/src/components/src/ComplementPicker.tsx b/src/components/src/ComplementPicker.tsx index e1a85cc..5ce4d25 100644 --- a/src/components/src/ComplementPicker.tsx +++ b/src/components/src/ComplementPicker.tsx @@ -3,105 +3,150 @@ import * as T from "../../types"; import AdjectivePicker from "./np-picker/AdjectivePicker"; import LocativeAdverbPicker from "./ep-explorer/eq-comp-picker/LocativeAdverbPicker"; 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 | "noun"): T.ComplementType { - if (s === "noun") return "comp. noun"; - return s; +function selectionTypeToCompType( + s: Exclude | "noun" +): T.ComplementType { + if (s === "noun") return "comp. noun"; + return s; } function ComplementPicker(props: { - phraseIsComplete: boolean, - onChange: (comp: T.ComplementSelection | undefined) => void, - comp: T.ComplementSelection | undefined, - opts: T.TextOptions, - cantClear?: boolean, - heading?: JSX.Element | string, - entryFeeder: T.EntryFeeder, + phraseIsComplete: boolean; + onChange: (comp: T.ComplementSelection | undefined) => void; + comp: T.ComplementSelection | undefined; + opts: T.TextOptions; + cantClear?: boolean; + heading?: JSX.Element | string; + entryFeeder: T.EntryFeeder; + negative: boolean; }) { - const [compType, setCompType] = useState(props.comp + const [compType, setCompType] = useState( + props.comp ? selectionTypeToCompType(props.comp.selection.type) : undefined + ); + useEffect(() => { + setCompType( + props.comp ? selectionTypeToCompType(props.comp.selection.type) - : undefined); - useEffect(() => { - setCompType(props.comp - ? selectionTypeToCompType(props.comp.selection.type) - : undefined); - }, [props.comp]); - function handleClear() { - setCompType(undefined); - props.onChange(undefined); - } - function handleCompTypeChange(ctp: T.ComplementType) { - props.onChange(undefined); - setCompType(ctp); - } - function handleSandwichExit() { - setCompType(undefined); - props.onChange(undefined); - } - const clearButton = (compType && !props.cantClear) - ? - :
; - return <> -
-
-
- {typeof props.heading === "string" - ?
{props.heading}
- : props.heading} -
-
- {clearButton} -
+ : undefined + ); + }, [props.comp]); + function handleClear() { + setCompType(undefined); + props.onChange(undefined); + } + function handleCompTypeChange(ctp: T.ComplementType) { + props.onChange(undefined); + setCompType(ctp); + } + function handleSandwichExit() { + setCompType(undefined); + props.onChange(undefined); + } + const clearButton = + compType && !props.cantClear ? ( + + ) : ( +
+ ); + return ( + <> +
+
+
+ {typeof props.heading === "string" ? ( +
{props.heading}
+ ) : ( + props.heading + )}
- {!compType &&
-
- Choose Complement +
{clearButton}
+
+ {!compType && ( +
+
Choose Complement
+ {compTypes.map((cpt) => ( +
+
- {compTypes.map((cpt) =>
- -
)} -
} -
- {compType === "adjective" ? - props.onChange(a ? { type: "complement", selection: a } : undefined)} - phraseIsComplete={props.phraseIsComplete} - /> - : compType === "loc. adv." - ? props.onChange(a ? { type: "complement", selection: a } : undefined)} - /> - : compType === "sandwich" - ? 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" - ?
- Sorry, can't choose complement nouns yet 🚧 -
- : null} + ))}
- ; + )} +
+ {compType === "adjective" ? ( + + props.onChange( + a ? { type: "complement", selection: a } : undefined + ) + } + phraseIsComplete={props.phraseIsComplete} + negative={props.negative} + /> + ) : compType === "loc. adv." ? ( + + props.onChange( + a ? { type: "complement", selection: a } : undefined + ) + } + /> + ) : compType === "sandwich" ? ( + + 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" ? ( +
+ Sorry, can't choose complement nouns yet 🚧 +
+ ) : null} +
+ + ); } -export default ComplementPicker; \ No newline at end of file +export default ComplementPicker; diff --git a/src/components/src/EntrySelect.tsx b/src/components/src/EntrySelect.tsx index 935139c..2b79e9c 100644 --- a/src/components/src/EntrySelect.tsx +++ b/src/components/src/EntrySelect.tsx @@ -3,183 +3,259 @@ import { StyleHTMLAttributes } from "react"; import Select, { StylesConfig } from "react-select"; import AsyncSelect from "react-select/async"; import { - makeSelectOption, - makeVerbSelectOption, + makeSelectOption, + makeVerbSelectOption, } from "./np-picker/picker-tools"; export const customStyles: StylesConfig = { - menuPortal: (base: any) => ({ - ...base, - zIndex: 99999, - }), - menu: (base: any) => ({ - ...base, - zIndex: 999999, - }), - option: (provided: any, state: any) => ({ - ...provided, - padding: "10px 5px", - color: "#121418", - }), - input: (base: any) => ({ - ...base, - padding: 0, - }), -} + menuPortal: (base: any) => ({ + ...base, + zIndex: 99999, + }), + menu: (base: any) => ({ + ...base, + zIndex: 999999, + }), + option: (provided: any, state: any) => ({ + ...provided, + padding: "10px 5px", + color: "#121418", + }), + input: (base: any) => ({ + ...base, + padding: 0, + }), +}; function EntrySelect(props: { - entryFeeder: T.EntryFeederSingleType, - value: E | undefined, - onChange: (value: E | undefined) => void, - name: string | undefined, - isVerbSelect?: boolean, - opts: T.TextOptions, - style?: StyleHTMLAttributes, - placeholder?: string, + entryFeeder: T.EntryFeederSingleType; + value: E | undefined; + onChange: (value: E | undefined) => void; + name: string | undefined; + isVerbSelect?: boolean; + opts: T.TextOptions; + style?: StyleHTMLAttributes; + placeholder?: string; }) { - const divStyle = props.style || { width: "13rem" }; - const placeholder = "placeholder" in props - ? props.placeholder - : "search" in props.entryFeeder - ? "Search Pashto" - : "Search…"; - function makeOption(e: E | T.DictionaryEntry) { - if ("entry" in e) { - return (props.isVerbSelect ? makeVerbSelectOption : makeSelectOption)(e, props.opts); - } - return makeSelectOption(e, props.opts); + const divStyle = props.style || { width: "13rem" }; + const placeholder = + "placeholder" in props + ? props.placeholder + : "search" in props.entryFeeder + ? "Search Pashto" + : "Search…"; + function makeOption(e: E | T.DictionaryEntry) { + if ("entry" in e) { + return (props.isVerbSelect ? makeVerbSelectOption : makeSelectOption)( + e, + props.opts + ); } - const value = props.value ? makeOption(props.value) : undefined; - if ("search" in props.entryFeeder) { - const search = props.entryFeeder.search; - const getByTs = props.entryFeeder.getByTs; - const options = (searchString: string) => - new Promise<{ value: string, label: string | JSX.Element }[]>(resolve => { - resolve(search(searchString).map(makeOption)); - }); - const onChange = (v: { label: string | JSX.Element, value: string } | null) => { - if (!v) { - props.onChange(undefined); - return; - } - const s = getByTs(parseInt(v.value)); - if (!s) return; - props.onChange(s); + return makeSelectOption(e, props.opts); + } + const value = props.value ? makeOption(props.value) : undefined; + if ("search" in props.entryFeeder) { + const search = props.entryFeeder.search; + const getByTs = props.entryFeeder.getByTs; + const options = (searchString: string) => + new Promise<{ value: string; label: string | JSX.Element }[]>( + (resolve) => { + resolve(search(searchString).map(makeOption)); } - return
- -
; - } - 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
-
+ ); +} + +export function DeterminerSelect(props: { + determiners: Readonly; + 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 ( + + const s = props.sandwiches.find((e) => { + 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 ( +
+
Sandwich Base
+
@@ -372,6 +375,7 @@ function VPPicker({ } opts={opts} entryFeeder={entryFeeder} + negative={vps.verb.negative} />
)} diff --git a/src/lib/package.json b/src/lib/package.json index cc23ce5..229a9f7 100644 --- a/src/lib/package.json +++ b/src/lib/package.json @@ -1,6 +1,6 @@ { "name": "@lingdocs/inflect", - "version": "7.4.1", + "version": "7.5.0", "description": "Pashto inflector library", "main": "dist/index.js", "types": "dist/lib/library.d.ts", diff --git a/src/lib/src/inflections-and-vocative.ts b/src/lib/src/inflections-and-vocative.ts index 8eeb5fc..7a1207b 100644 --- a/src/lib/src/inflections-and-vocative.ts +++ b/src/lib/src/inflections-and-vocative.ts @@ -48,7 +48,7 @@ type Plurals = // const endingInHayOrAynRegex = /[^ا][هع]$/; export function getInfsAndVocative( - entryR: T.DictionaryEntryNoFVars, + entryR: T.DictionaryEntryNoFVars | T.Determiner, plurals: Plurals ): | { diff --git a/src/lib/src/new-verb-engine/render-verb.ts b/src/lib/src/new-verb-engine/render-verb.ts index d53fd68..d08f983 100644 --- a/src/lib/src/new-verb-engine/render-verb.ts +++ b/src/lib/src/new-verb-engine/render-verb.ts @@ -5,6 +5,7 @@ import { personNumber, personToGenNum, } from "../misc-helpers"; +import { monoidPsString } from "../fp-ps"; import { applySingleOrLengthOpts, fmapSingleOrLengthOpts } from "../fp-ps"; import { concatPsString, @@ -448,7 +449,7 @@ function ensure3rdPast( } const abruptEnder = ["د", "ت", "ړ"].includes(rs[0].p.slice(-1)); // short endings like ورسېد - const ends = abruptEnder ? [{ p: "", f: "" }, ...ending] : ending; + const ends = abruptEnder ? [monoidPsString.empty, ...ending] : ending; return verbEndingConcat(rs, ends); } diff --git a/src/lib/src/pashto-inflector.ts b/src/lib/src/pashto-inflector.ts index 68ce4a8..26cfe15 100644 --- a/src/lib/src/pashto-inflector.ts +++ b/src/lib/src/pashto-inflector.ts @@ -23,7 +23,7 @@ export function inflectWord(word: T.DictionaryEntry): T.InflectorOutput { // If it's a noun/adj, inflect accordingly // TODO: What about n. f. / adj. that end in ي ?? const w = removeFVarients(word); - if (w.c?.includes("doub.")) { + if (word.c?.includes("doub.")) { const words = splitDoubleWord(w); // TODO: Make this work for non-unisex double words // Right now this an extremely bad and complex way to do this diff --git a/src/lib/src/phrase-building/blocks-utils.ts b/src/lib/src/phrase-building/blocks-utils.ts index af13c3f..bb3bb27 100644 --- a/src/lib/src/phrase-building/blocks-utils.ts +++ b/src/lib/src/phrase-building/blocks-utils.ts @@ -420,3 +420,122 @@ function arrayMove(ar: X[], old_index: number, new_index: number): X[] { arr.splice(new_i, 0, arr.splice(old_index, 1)[0]); 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( + blocks: B +): B { + return blocks.map((x) => ({ + key: x.key, + block: removeHeetsDetFromBlock(x.block), + })) as B; +} + +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.SandwichSelection { + 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: { + ...n.determiners, + determiners: removeHeetsFromDets(n.determiners.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.DeterminerSelection[] +): T.DeterminerSelection[] { + if (!dets) { + return dets; + } + return dets.filter((d) => d.determiner.p !== "هیڅ"); +} diff --git a/src/lib/src/phrase-building/compile.ts b/src/lib/src/phrase-building/compile.ts index 8a61892..3a61cef 100644 --- a/src/lib/src/phrase-building/compile.ts +++ b/src/lib/src/phrase-building/compile.ts @@ -22,7 +22,7 @@ import { specifyEquativeLength, } from "./blocks-utils"; import { blank, kidsBlank } from "../misc-helpers"; -import { monoidPsStringWVars } from "../fp-ps"; +import { monoidPsString, monoidPsStringWVars } from "../fp-ps"; import { concatAll } from "fp-ts/lib/Monoid"; type BlankoutOptions = { @@ -275,7 +275,7 @@ function applyBlankOut( return kidsBlank; } if (blankOut?.negative && "block" in x && x.block.type === "negative") { - return { p: "", f: "" }; + return monoidPsString.empty; } return x; }); @@ -314,7 +314,7 @@ function getPsFromPiece( } if (piece.block.type === "objectSelection") { if (typeof piece.block.selection !== "object") { - return [{ p: "", f: "" }]; + return [monoidPsString.empty]; } return getPashtoFromRendered(piece.block.selection, subjectPerson); } diff --git a/src/lib/src/phrase-building/eps-reducer.ts b/src/lib/src/phrase-building/eps-reducer.ts index c865cea..b9a207e 100644 --- a/src/lib/src/phrase-building/eps-reducer.ts +++ b/src/lib/src/phrase-building/eps-reducer.ts @@ -1,216 +1,257 @@ import * as T from "../../../types"; -import { - personGender, - personNumber, -} from "../misc-helpers"; +import { personGender, personNumber } from "../misc-helpers"; import { isUnisexNounEntry } from "../type-predicates"; 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 = { - 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, - }, - }; +export type EpsReducerAction = + | { + type: "set predicate type"; + payload: "NP" | "Complement"; } - 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), - } : {}, - ...predicate.genderCanChange ? { - gender: personGender(subject.selection.person), - } : {}, + | { + 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; + 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, - blocks: adjustSubjectSelection(eps.blocks, subject), - predicate: { - ...eps.predicate, - NP: { - type: "NP", - selection: adjusted, - }, - }, - }; - } - const n: T.EPSelectionState = { - ...eps, - blocks: adjustSubjectSelection(eps.blocks, subject), - }; - return subject ? ensureMiniPronounsOk(eps, n, sendAlert) : n; + : {}), + ...(predicate.genderCanChange + ? { + gender: personGender(subject.selection.person), + } + : {}), + }; + return { + ...eps, + blocks: adjustSubjectSelection(eps.blocks, subject), + predicate: { + ...eps.predicate, + NP: { + type: "NP", + selection: adjusted, + }, + }, + }; } - if (action.type === "set predicate NP") { - const selection = action.payload; - if (!selection) { - return { - ...eps, - predicate: { - ...eps.predicate, - NP: selection, - }, - }; - } - const subject = getSubjectSelection(eps.blocks).selection; - if (subject?.selection.type === "pronoun" && selection.selection.type === "noun" && isUnisexNounEntry(selection.selection.entry)) { - 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, - }, - }; - } - const n: T.EPSelectionState = { - ...eps, - predicate: { - ...eps.predicate, - NP: selection, - }, - }; - return selection ? ensureMiniPronounsOk(eps, n, sendAlert) : n; + const n: T.EPSelectionState = { + ...eps, + blocks: adjustSubjectSelection(eps.blocks, subject), + }; + return subject ? ensureMiniPronounsOk(eps, n, sendAlert) : n; + } + if (action.type === "set predicate NP") { + const selection = action.payload; + if (!selection) { + return { + ...eps, + predicate: { + ...eps.predicate, + NP: selection, + }, + }; } - if (action.type === "set predicate complement") { - return { - ...eps, - predicate: { - ...eps.predicate, - Complement: action.payload, - }, - }; + const subject = getSubjectSelection(eps.blocks).selection; + if ( + subject?.selection.type === "pronoun" && + selection.selection.type === "noun" && + isUnisexNounEntry(selection.selection.entry) + ) { + 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 = { - ...eps, - omitSubject: action.payload === "true", - }; - return ensureMiniPronounsOk(eps, n, sendAlert); - } - if (action.type === "set equative") { - return { - ...eps, - equative: action.payload, - } - } - if (action.type === "insert new AP") { - return { - ...eps, - blocks: insertNewAP(eps.blocks), - }; - } - if (action.type === "set AP") { - const { index, AP } = action.payload; - 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"); + const n: T.EPSelectionState = { + ...eps, + predicate: { + ...eps.predicate, + NP: selection, + }, + }; + return selection ? ensureMiniPronounsOk(eps, n, sendAlert) : n; + } + if (action.type === "set predicate complement") { + return { + ...eps, + predicate: { + ...eps.predicate, + Complement: action.payload, + }, + }; + } + if (action.type === "set omitSubject") { + const n: T.EPSelectionState = { + ...eps, + omitSubject: action.payload === "true", + }; + return ensureMiniPronounsOk(eps, n, sendAlert); + } + if (action.type === "set equative") { + return { + ...eps, + blocks: !action.payload.negative + ? removeHeetsDet(eps.blocks) + : eps.blocks, + equative: action.payload, + }; + } + if (action.type === "insert new AP") { + return { + ...eps, + blocks: insertNewAP(eps.blocks), + }; + } + if (action.type === "set AP") { + const { index, AP } = action.payload; + 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 { - const error = checkForMiniPronounsError(eps); - if (error) { - if (sendAlert) sendAlert(error); - return old; - } - return eps; +function ensureMiniPronounsOk( + old: T.EPSelectionState, + eps: T.EPSelectionState, + sendAlert?: (msg: string) => void +): T.EPSelectionState { + const error = checkForMiniPronounsError(eps); + if (error) { + if (sendAlert) sendAlert(error); + return old; + } + return eps; } function movePersonGender(p: T.Person, gender: T.Gender): T.Person { - const pGender = personGender(p); - if (gender === pGender) { - return p; - } - return (gender === "masc") ? (p - 1) : (p + 1); + const pGender = personGender(p); + if (gender === pGender) { + return p; + } + return gender === "masc" ? p - 1 : p + 1; } function movePersonNumber(p: T.Person, number: T.NounNumber): T.Person { - const pNumber = personNumber(p); - if (pNumber === number) { - return p; - } - return (number === "plural") - ? (p + 6) - : (p - 6); -} \ No newline at end of file + const pNumber = personNumber(p); + if (pNumber === number) { + return p; + } + return number === "plural" ? p + 6 : p - 6; +} diff --git a/src/lib/src/phrase-building/make-selections.ts b/src/lib/src/phrase-building/make-selections.ts index b6fd0e4..4f98612 100644 --- a/src/lib/src/phrase-building/make-selections.ts +++ b/src/lib/src/phrase-building/make-selections.ts @@ -81,6 +81,6 @@ export function makeNounSelection( possesor: !complementType ? old?.possesor : undefined, dynamicComplement: complementType === "dynamic", genStativeComplement: complementType === "generative stative", - demonstrative: old?.demonstrative, + determiners: old?.determiners, }; } diff --git a/src/lib/src/phrase-building/np-tools.ts b/src/lib/src/phrase-building/np-tools.ts index 2caf166..42b9091 100644 --- a/src/lib/src/phrase-building/np-tools.ts +++ b/src/lib/src/phrase-building/np-tools.ts @@ -2,8 +2,10 @@ import { isFirstPerson, isSecondPerson } from "../misc-helpers"; import * as T from "../../../types"; import { concatPsString } from "../p-text-helpers"; import { flattenLengths } from "./compile"; +import { monoidPsStringWVars } from "../fp-ps"; +import { concatAll } from "fp-ts/lib/Monoid"; -function getBaseAndAdjectives({ +function getBaseWDetsAndAdjs({ selection, }: T.Rendered< T.NPSelection | T.ComplementSelection | T.APSelection @@ -11,45 +13,39 @@ function getBaseAndAdjectives({ if (selection.type === "sandwich") { return getSandwichPsBaseAndAdjectives(selection); } - const adjs = "adjectives" in selection && selection.adjectives; - const demonstrativePs = ("demonstrative" in selection && - selection.demonstrative?.ps) || { p: "", f: "" }; - if (!adjs) { - // TODO: does this ever get used?? - return flattenLengths(selection.ps).map((x) => - concatPsString(demonstrativePs, x) - ); - } - - 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 determiners = ( + ("determiners" in selection && selection.determiners?.determiners) || + [] + ).map((x) => x.ps); + const detWOutNoun = + "determiners" in selection && + selection.determiners && + !selection.determiners.type; + const adjs = (("adjectives" in selection && selection.adjectives) || []).map( + (x) => x.ps ); + 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(arr: T[], sep: T): T[] { + return arr.reduce((a: T[], v: T) => [...a, v, sep], []).slice(0, -1); } function getSandwichPsBaseAndAdjectives( s: T.Rendered> ): T.PsString[] { - const insideBase = getBaseAndAdjectives(s.inside); + const insideBase = getBaseWDetsAndAdjs(s.inside); const willContractWithPronoun = s.before && s.before.p === "د" && @@ -118,7 +114,7 @@ export function getPashtoFromRendered( | T.Rendered, subjectsPerson: false | T.Person ): T.PsString[] { - const base = getBaseAndAdjectives(b); + const base = getBaseWDetsAndAdjs(b); if (b.selection.type === "loc. adv." || b.selection.type === "adverb") { return base; } @@ -172,7 +168,7 @@ function addPossesor( ); } const wPossesor = existing.flatMap((ps) => - getBaseAndAdjectives(owner).map((v) => + getBaseWDetsAndAdjs(owner).map((v) => owner.selection.type === "pronoun" && subjectsPerson !== false && willBeReflexive(subjectsPerson, owner.selection.person) @@ -204,7 +200,7 @@ function addArticlesAndAdjs( const adjs = !np.adjectives ? "" : 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 + " "; }, ""); const genderTag = np.genderCanChange @@ -212,10 +208,14 @@ function addArticlesAndAdjs( ? " (f.)" : " (m.)" : ""; - const demonstrative = np.demonstrative ? ` ${np.demonstrative.e}` : ""; - const demWithoutNoun = np.demonstrative && !np.demonstrative.withNoun; - return `${np.demonstrative ? "" : articles}${demonstrative}${ - demWithoutNoun ? ` (${(adjs + word).trim()})` : adjs + word + const determiners = + np.determiners && np.determiners.determiners + ? // @ts-ignore - weird, ts is not recognizing this as rendered + np.determiners.determiners.map((x) => `(${x.e})`).join(" ") + : ""; + const detsWithoutNoun = np.determiners && !np.determiners.withNoun; + return `${np.determiners ? "" : articles}${determiners}${ + detsWithoutNoun ? ` (${(adjs + word).trim()})` : adjs + word }${genderTag}`; } catch (e) { return undefined; diff --git a/src/lib/src/phrase-building/render-np.ts b/src/lib/src/phrase-building/render-np.ts index 9c8189e..512d97d 100644 --- a/src/lib/src/phrase-building/render-np.ts +++ b/src/lib/src/phrase-building/render-np.ts @@ -15,6 +15,7 @@ import { shortVerbEndConsonant } from "../parsing/misc"; import { removeL } from "../new-verb-engine/rs-helpers"; import { applySingleOrLengthOpts } from "../fp-ps"; import { accentOnNFromEnd } from "../accent-helpers"; +import { getInfsAndVocative } from "../inflections-and-vocative"; // TODO: can have subject and objects in possesors!! @@ -115,6 +116,20 @@ export function renderNounSelection( return ps.length > 0 ? ps : [psStringFromEntry(n.entry)]; })(); const person = getPersonNumber(n.gender, n.number); + const determiners: T.Rendered | undefined = + n.determiners + ? { + ...n.determiners, + determiners: n.determiners.determiners.map((determiner) => + renderDeterminer({ + determiner, + inflected, + number: n.number, + gender: n.gender, + }) + ), + } + : undefined; return { ...n, adjectives: n.adjectives.map((a) => @@ -131,64 +146,121 @@ export function renderNounSelection( ps: pashto, e: english, possesor: renderPossesor(n.possesor, role), - demonstrative: renderDemonstrative({ - demonstrative: n.demonstrative, - inflected, - plural: n.number === "plural", - gender: n.gender, - }), + determiners, }; } -function renderDemonstrative({ - demonstrative, +function renderDeterminer({ + determiner: { determiner }, inflected, - plural, + number, gender, }: { - demonstrative: T.DemonstrativeSelection | undefined; + determiner: T.DeterminerSelection; inflected: boolean; - plural: boolean; + number: T.NounNumber; gender: T.Gender; -}): T.Rendered | undefined { - if (!demonstrative) { - return undefined; +}): T.Rendered { + if (determiner.p === "دا") { + 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 = - demonstrative.demonstrative === "daa" - ? inflected - ? { p: "دې", f: "de" } - : { p: "دا", f: "daa" } - : demonstrative.demonstrative === "dagha" - ? inflected - ? plural - ? { p: "دغو", f: "dágho" } - : gender === "masc" - ? { p: "دغه", f: "dághu" } - : { p: "دغې", f: "dághe" } - : { p: "دغه", f: "dágha" } - : inflected - ? plural + if (determiner.p === "دغه") { + const ps = inflected + ? number === "plural" + ? { p: "دغو", f: "dágho" } + : gender === "masc" + ? { p: "دغه", f: "dághu" } + : { p: "دغې", f: "dághe" } + : { p: "دغه", f: "dágha" }; + return { + type: "determiner", + determiner, + inflected, + number, + gender, + ps: [ps], + e: number === "plural" ? "these" : "this", + }; + } + if (determiner.p === "هغه") { + const ps = inflected + ? number === "plural" ? { p: "هغو", f: "hágho" } : gender === "masc" ? { p: "هغه", f: "hághu" } : { p: "هغې", f: "hághe" } : { p: "هغه", f: "hágha" }; + return { + type: "determiner", + determiner, + inflected, + number, + gender, + ps: [ps], + e: number === "plural" ? "those" : "that", + }; + } const e = - demonstrative.demonstrative === "hagha" - ? plural - ? "those" - : "that" - : plural - ? "these" - : "this"; + determiner.f === "Tol" + ? "all/the whole" + : determiner.f === "bul" + ? "other/another" + : determiner.f === "har" + ? "every/each" + : determiner.f === "koom" + ? "some/which" + : determiner.f === "heets" + ? "no" + : determiner.f === "dáase" + ? number === "plural" + ? "such/like these" + : "such/like this" + : determiner.f === "daghase" + ? number === "plural" + ? "just such/just like these" + : "just such/just like this" + : determiner.f === "hase" + ? `such/like ${number === "plural"} ? "those" : "that"` + : number === "plural" + ? "just such/just like these" + : "just such/just like this"; return { - ...demonstrative, - ps, + type: "determiner", + determiner, + inflected, + number, + gender, + ps: inflectDeterminer(determiner, inflected, gender, number), 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( p: T.PronounSelection, inflected: boolean, @@ -269,6 +341,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( infs: T.InflectorOutput, t: "plural" | "arabicPlural" | "inflections", diff --git a/src/lib/src/phrase-building/vps-reducer.ts b/src/lib/src/phrase-building/vps-reducer.ts index a196da2..fe30fdc 100644 --- a/src/lib/src/phrase-building/vps-reducer.ts +++ b/src/lib/src/phrase-building/vps-reducer.ts @@ -11,6 +11,7 @@ import { getSubjectSelection, insertNewAP, removeAP, + removeHeetsDet, setAP, shiftBlock, } from "./blocks-utils"; @@ -19,6 +20,7 @@ import { changeTransitivity, makeVPSelectionState, } from "./verb-selection"; +import { mapGen } from "../fp-ps"; export type VpsReducerAction = | { @@ -203,11 +205,13 @@ export function vpsReducer( } if (action.type === "set negativity") { if (!vps.verb) return vps; + const negative = action.payload === "true"; return { ...vps, + blocks: !negative ? removeHeetsDet(vps.blocks) : vps.blocks, verb: { ...vps.verb, - negative: action.payload === "true", + negative, }, }; } diff --git a/src/lib/src/type-predicates.ts b/src/lib/src/type-predicates.ts index b87fe4f..8705a3f 100644 --- a/src/lib/src/type-predicates.ts +++ b/src/lib/src/type-predicates.ts @@ -62,14 +62,23 @@ export function isNounOrAdjEntry( } export function isInflectableEntry( - e: T.Entry | T.DictionaryEntry | T.DictionaryEntryNoFVars + e: T.Entry | T.DictionaryEntry | T.DictionaryEntryNoFVars | T.Determiner ): e is T.InflectableEntry { if ("entry" in e) { return false; } + if (isDeterminer(e)) { + return true; + } 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( e: T.Entry | T.DictionaryEntry ): e is T.NumberEntry { diff --git a/src/types.ts b/src/types.ts index 532bb39..2bdf464 100644 --- a/src/types.ts +++ b/src/types.ts @@ -899,13 +899,35 @@ export type NounSelection = { genStativeComplement?: boolean; adjectives: AdjectiveSelection[]; possesor: undefined | PossesorSelection; - demonstrative: undefined | DemonstrativeSelection; + determiners?: DeterminersSelection; }; -export type DemonstrativeSelection = { - type: "demonstrative"; - demonstrative: "daa" | "hagha" | "dagha"; +export type DeterminersSelection = { + type: "determiners"; withNoun: boolean; + determiners: DeterminerSelection[]; +}; + +export const determiners = [ + { p: "دا", f: "daa", type: "det" }, + { p: "دغه", f: "dágha", type: "det" }, + { p: "هغه", f: "hágha", 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: "koom", type: "det" }, + { p: "هر", f: "har", type: "det" }, + { p: "ټول", f: "Tol", type: "det" }, + { p: "هیڅ", f: "heets", type: "det", noInf: true }, + { p: "بل", f: "bul", type: "det" }, +] as const; + +export type Determiner = (typeof determiners)[number]; + +export type DeterminerSelection = { + type: "determiner"; + determiner: Determiner; }; export type AdverbSelection = { @@ -970,7 +992,8 @@ export type Rendered< | AdjectiveSelection | SandwichSelection | ComplementSelection - | DemonstrativeSelection + | DeterminersSelection + | DeterminerSelection | ComplementSelection["selection"] | UnselectedComplementSelection | undefined @@ -1020,13 +1043,21 @@ export type Rendered< inflected: boolean; person: Person; } - : T extends DemonstrativeSelection + : T extends DeterminersSelection ? { - type: "demonstrative"; - demonstrative: DemonstrativeSelection["demonstrative"]; + type: "determiners"; withNoun: boolean; - ps: PsString; + determiners: Rendered[]; + } + : T extends DeterminerSelection + ? { + type: "determiner"; + determiner: DeterminerSelection["determiner"]; + ps: PsString[]; e: string; + inflected: boolean; + number: NounNumber; + gender: Gender; } : T extends ComplementSelection ? { @@ -1078,7 +1109,7 @@ export type Rendered< shrunken: boolean; np: Rendered; }; - demonstrative?: Rendered; + determiners?: Rendered; }; export type EPSelectionState = {