This commit is contained in:
lingdocs 2022-06-22 18:52:09 -05:00
parent acd7b7b504
commit 346a89df29
6 changed files with 217 additions and 160 deletions

View File

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

@ -177,7 +177,7 @@ function TensePicker(props: ({
: verbTenseOptions; : verbTenseOptions;
const showImperativeOption = ("vps" in props && props.vps.verb.voice === "active") const showImperativeOption = ("vps" in props && props.vps.verb.voice === "active")
|| ("vpsComplete" in props && props.vpsComplete.verb.voice !== "active"); || ("vpsComplete" in props && props.vpsComplete.verb.voice !== "active");
const canHaveFormula = "vps" in props; const canHaveFormula = "vps" in props && props.mode !== "quiz";
return <div> return <div>
<div style={{ maxWidth: "300px", minWidth: "250px", margin: "0 auto" }}> <div style={{ maxWidth: "300px", minWidth: "250px", margin: "0 auto" }}>
<div className="d-flex flex-row justify-content-between align-items-center"> <div className="d-flex flex-row justify-content-between align-items-center">

View File

@ -1,4 +1,3 @@
import NPPicker, { shrunkenBackground } from "../np-picker/NPPicker";
import VerbPicker from "./VerbPicker"; import VerbPicker from "./VerbPicker";
import TensePicker from "./TensePicker"; import TensePicker from "./TensePicker";
import VPDisplay from "./VPDisplay"; import VPDisplay from "./VPDisplay";
@ -7,17 +6,14 @@ import * as T from "../../types";
import ChartDisplay from "./VPChartDisplay"; import ChartDisplay from "./VPChartDisplay";
import useStickyState, { useStickyReducer } from "../../lib/useStickyState"; import useStickyState, { useStickyReducer } from "../../lib/useStickyState";
import { makeVPSelectionState } from "./verb-selection"; import { makeVPSelectionState } from "./verb-selection";
import { useEffect, useRef, useState } from "react"; import { useEffect, useState } from "react";
import { getKingAndServant, renderVP } from "../../lib/phrase-building/render-vp"; import { completeVPSelection } from "../../lib/phrase-building/vp-tools";
import { completeVPSelection, isPastTense } from "../../lib/phrase-building/vp-tools";
import VPExplorerQuiz from "./VPExplorerQuiz"; import VPExplorerQuiz from "./VPExplorerQuiz";
import VPExplorerExplanationModal, { roleIcon } from "./VPExplorerExplanationModal";
// @ts-ignore // @ts-ignore
import LZString from "lz-string"; import LZString from "lz-string";
import { vpsReducer } from "./vps-reducer"; import { vpsReducer } from "./vps-reducer";
import APPicker from "../ap-picker/APPicker"; import { getObjectSelection } from "../../lib/phrase-building/blocks-utils";
import autoAnimate from "@formkit/auto-animate"; import VPPicker from "./VPPicker";
import { getObjectSelection, getSubjectSelection, isNoObject } from "../../lib/phrase-building/blocks-utils";
const phraseURLParam = "VPhrase"; const phraseURLParam = "VPhrase";
@ -56,16 +52,6 @@ function VPExplorer(props: {
); );
const [showClipped, setShowClipped] = useState<string>(""); const [showClipped, setShowClipped] = useState<string>("");
const [alert, setAlert] = useState<string | undefined>(undefined); const [alert, setAlert] = useState<string | undefined>(undefined);
const [showingExplanation, setShowingExplanation] = useState<{ role: "servant" | "king", item: "subject" | "object" } | false>(false);
const parent = useRef<HTMLDivElement>(null);
useEffect(() => {
parent.current && autoAnimate(parent.current);
}, [parent]);
const isPast = isPastTense(vps.verb.tenseCategory === "perfect" ? vps.verb.perfectTense : vps.verb.verbTense);
const roles = getKingAndServant(
isPast,
vps.verb.transitivity !== "intransitive",
);
function flashMessage(msg: string) { function flashMessage(msg: string) {
setAlert(msg); setAlert(msg);
setTimeout(() => { setTimeout(() => {
@ -90,18 +76,6 @@ function VPExplorer(props: {
} }
// eslint-disable-next-line // eslint-disable-next-line
}, []); }, []);
function handleSubjectChange(subject: T.NPSelection | undefined, skipPronounConflictCheck?: boolean) {
adjustVps({
type: "set subject",
payload: { subject, skipPronounConflictCheck },
});
}
function handleObjectChange(object: T.NPSelection | undefined) {
adjustVps({
type: "set object",
payload: object,
});
}
function handleSubjObjSwap() { function handleSubjObjSwap() {
adjustVps({ type: "swap subj/obj" }); adjustVps({ type: "swap subj/obj" });
} }
@ -138,16 +112,8 @@ function VPExplorer(props: {
flashClippedMessage("Copied phrase code to clipboard"); flashClippedMessage("Copied phrase code to clipboard");
} }
const object = getObjectSelection(vps.blocks).selection; const object = getObjectSelection(vps.blocks).selection;
const subject = getSubjectSelection(vps.blocks).selection;
const VPS = completeVPSelection(vps); const VPS = completeVPSelection(vps);
const phraseIsComplete = !!VPS; const phraseIsComplete = !!VPS;
const rendered = VPS ? renderVP(VPS) : undefined;
const servantIsShrunk = includesShrunkenServant(rendered?.kids);
function toggleServantShrink() {
adjustVps({
type: "toggle servant shrink",
});
}
return <div className="mt-3" style={{ maxWidth: "950px"}}> return <div className="mt-3" style={{ maxWidth: "950px"}}>
<VerbPicker <VerbPicker
vps={vps} vps={vps}
@ -190,113 +156,18 @@ function VPExplorer(props: {
<i className="fas fa-exchange-alt mr-2" /> subj/obj <i className="fas fa-exchange-alt mr-2" /> subj/obj
</button> </button>
</div>} </div>}
{mode === "phrases" && <div className="clickable h5" onClick={() => adjustVps({ type: "insert new AP" })}>+ AP</div>} {mode === "phrases" && <VPPicker
{mode !== "quiz" && <div ref={parent} className="d-flex flex-row justify-content-around flex-wrap" style={{ marginLeft: "-0.5rem", marginRight: "-0.5rem" }}> opts={props.opts}
{mode === "phrases" && <> entryFeeder={props.entryFeeder}
{vps.blocks.map(({ block, key }, i, blocks) => { onChange={(vps) => adjustVps({ type: "load vps", payload: vps })}
if (isNoObject(block)) return null; vps={vps}
return <div className="my-2 card block-card p-1 mx-1" key={key} style={{ />}
background: (servantIsShrunk && ( {mode !== "phrases" && <div className="my-2">
(roles.servant === "subject" && block?.type === "subjectSelection") <TensePicker
|| vps={vps}
(roles.servant === "object" && block?.type === "objectSelection") onChange={adjustVps}
)) ? shrunkenBackground : "inherit", mode={mode}
}}> />
<div className="d-flex flex-row justify-content-between mb-1" style={{ height: "1rem" }}>
{(i > 0 && !isNoObject(blocks[i - 1].block)) ? <div
className="small clickable ml-1"
onClick={() => adjustVps({ type: "shift block", payload: { index: i, direction: "back" }})}
>
<i className="fas fa-chevron-left" />
</div> : <div/>}
{(i < vps.blocks.length - 1 && !isNoObject(blocks[i + 1].block)) ? <div
className="small clickable mr-1"
onClick={() => adjustVps({ type: "shift block", payload: { index: i, direction: "forward" }})}
>
<i className="fas fa-chevron-right" />
</div> : <div/>}
</div>
{(!block || block.type === "AP")
? <APPicker
phraseIsComplete={phraseIsComplete}
heading="AP"
entryFeeder={props.entryFeeder}
AP={block}
opts={props.opts}
onChange={AP => adjustVps({ type: "set AP", payload: { index: i, AP } })}
onRemove={() => adjustVps({ type: "remove AP", payload: i })}
/>
: (block?.type === "subjectSelection")
? <NPPicker
phraseIsComplete={phraseIsComplete}
heading={roles.king === "subject"
? <div className="h5 text-center" onClick={() => setShowingExplanation({ role: "king", item: "subject" })}>Subject {roleIcon.king}</div>
: <div className="h5 text-center">
Subject
{` `}
<span className="clickable" onClick={() => setShowingExplanation({ role: "servant", item: "subject" })}>{roleIcon.servant}</span>
{` `}
{(rendered && rendered.whatsAdjustable !== "king") &&
<span onClick={toggleServantShrink} className="mx-2 clickable">
{!servantIsShrunk ? "🪄" : "👶"}
</span>
}
</div>}
entryFeeder={props.entryFeeder}
np={block.selection}
counterPart={vps.verb ? object : undefined}
role={(isPast && vps.verb.transitivity !== "intransitive")
? "ergative"
: "subject"
}
onChange={handleSubjectChange}
opts={props.opts}
isShrunk={(servantIsShrunk && roles.servant === "subject")}
/>
: (vps.verb && block?.type === "objectSelection" && block.selection !== "none")
? <div className="my-2" style={{ background: (servantIsShrunk && roles.servant === "object") ? shrunkenBackground : "inherit" }}>
{(typeof block.selection === "number")
? <div>
{roles.king === "object"
? <div className="h5 text-center clickable" onClick={() => setShowingExplanation({ role: "king", item: "object" })}>Object {roleIcon.king}</div>
: <div className="h5 text-center">Object</div>}
<div className="text-muted">Unspoken 3rd Pers. Masc. Plur.</div>
</div>
: <NPPicker
phraseIsComplete={phraseIsComplete}
heading={roles.king === "object"
? <div className="h5 text-center clickable" onClick={() => setShowingExplanation({ role: "king", item: "object" })}>Object {roleIcon.king}</div>
: <div className="h5 text-center">
Object
{` `}
<span className="clickable" onClick={() => setShowingExplanation({ role: "servant", item: "object" })}>{roleIcon.servant}</span>
{` `}
{(rendered && rendered.whatsAdjustable !== "king") &&
<span onClick={toggleServantShrink} className="mx-2 clickable">
{!servantIsShrunk ? "🪄" : "👶"}
</span>
}
</div>}
entryFeeder={props.entryFeeder}
role="object"
np={block.selection}
counterPart={subject}
onChange={handleObjectChange}
opts={props.opts}
isShrunk={(servantIsShrunk && roles.servant === "object")}
/>}
</div>
: null}
</div>;
})}
</>}
<div className="my-2">
<TensePicker
vps={vps}
onChange={quizLock(adjustVps)}
mode={mode}
/>
</div>
</div>} </div>}
{mode === "phrases" && <VPDisplay {mode === "phrases" && <VPDisplay
VPS={vps} VPS={vps}
@ -305,10 +176,6 @@ function VPExplorer(props: {
/>} />}
{mode === "charts" && <ChartDisplay VS={vps.verb} opts={props.opts} />} {mode === "charts" && <ChartDisplay VS={vps.verb} opts={props.opts} />}
{mode === "quiz" && <VPExplorerQuiz opts={props.opts} vps={vps} />} {mode === "quiz" && <VPExplorerQuiz opts={props.opts} vps={vps} />}
<VPExplorerExplanationModal
showing={showingExplanation}
setShowing={setShowingExplanation}
/>
{showClipped && <div className="alert alert-primary text-center" role="alert" style={{ {showClipped && <div className="alert alert-primary text-center" role="alert" style={{
position: "fixed", position: "fixed",
top: "30%", top: "30%",
@ -355,10 +222,3 @@ function getVPSFromUrl(): T.VPSelectionState | undefined {
return JSON.parse(decoded) as T.VPSelectionState; return JSON.parse(decoded) as T.VPSelectionState;
} }
function includesShrunkenServant(kids?: T.Kid[]): boolean {
if (!kids) return false;
return kids.some(k => (
k.type === "mini-pronoun" && k.source === "servant"
));
}

View File

@ -0,0 +1,181 @@
import NPPicker, { shrunkenBackground } from "../np-picker/NPPicker";
import TensePicker from "./TensePicker";
import * as T from "../../types";
import { useEffect, useRef, useState } from "react";
import { getKingAndServant, renderVP } from "../../lib/phrase-building/render-vp";
import { completeVPSelection, isPastTense } from "../../lib/phrase-building/vp-tools";
import VPExplorerExplanationModal, { roleIcon } from "./VPExplorerExplanationModal";
import { vpsReducer, VpsReducerAction } from "./vps-reducer";
import APPicker from "../ap-picker/APPicker";
import autoAnimate from "@formkit/auto-animate";
import { getObjectSelection, getSubjectSelection, includesShrunkenServant, isNoObject } from "../../lib/phrase-building/blocks-utils";
function VPPicker({ opts, vps, onChange, entryFeeder }: {
opts: T.TextOptions,
vps: T.VPSelectionState,
onChange: (eps: T.VPSelectionState) => void,
entryFeeder: T.EntryFeeder,
}) {
const parent = useRef<HTMLDivElement>(null);
useEffect(() => {
parent.current && autoAnimate(parent.current);
}, [parent]);
const [showingExplanation, setShowingExplanation] = useState<{ role: "servant" | "king", item: "subject" | "object" } | false>(false);
const [alert, setAlert] = useState<string | undefined>(undefined);
function flashMessage(msg: string) {
setAlert(msg);
setTimeout(() => {
setAlert(undefined);
}, 1500);
}
function adjustVps(action: VpsReducerAction) {
const newVps = vpsReducer(vps, action, flashMessage);
onChange(newVps);
}
function handleSubjectChange(subject: T.NPSelection | undefined, skipPronounConflictCheck?: boolean) {
adjustVps({
type: "set subject",
payload: { subject, skipPronounConflictCheck },
});
}
function handleObjectChange(object: T.NPSelection | undefined) {
adjustVps({
type: "set object",
payload: object,
});
}
const object = getObjectSelection(vps.blocks).selection;
const subject = getSubjectSelection(vps.blocks).selection;
const VPS = completeVPSelection(vps);
const phraseIsComplete = !!VPS;
const rendered = VPS ? renderVP(VPS) : undefined;
const servantIsShrunk = includesShrunkenServant(rendered?.kids);
const isPast = isPastTense(vps.verb.tenseCategory === "perfect" ? vps.verb.perfectTense : vps.verb.verbTense);
const roles = getKingAndServant(
isPast,
vps.verb.transitivity !== "intransitive",
);
return <div>
<div className="clickable h5" onClick={() => adjustVps({ type: "insert new AP" })}>+ AP</div>
<div ref={parent} className="d-flex flex-row justify-content-around flex-wrap" style={{ marginLeft: "-0.5rem", marginRight: "-0.5rem" }}>
{vps.blocks.map(({ block, key }, i, blocks) => {
if (isNoObject(block)) return null;
return <div className="my-2 card block-card p-1 mx-1" key={key} style={{
background: (servantIsShrunk && (
(roles.servant === "subject" && block?.type === "subjectSelection")
||
(roles.servant === "object" && block?.type === "objectSelection")
)) ? shrunkenBackground : "inherit",
}}>
<div className="d-flex flex-row justify-content-between mb-1" style={{ height: "1rem" }}>
{(i > 0 && !isNoObject(blocks[i - 1].block)) ? <div
className="small clickable ml-1"
onClick={() => adjustVps({ type: "shift block", payload: { index: i, direction: "back" }})}
>
<i className="fas fa-chevron-left" />
</div> : <div/>}
{(i < vps.blocks.length - 1 && !isNoObject(blocks[i + 1].block)) ? <div
className="small clickable mr-1"
onClick={() => adjustVps({ type: "shift block", payload: { index: i, direction: "forward" }})}
>
<i className="fas fa-chevron-right" />
</div> : <div/>}
</div>
{(!block || block.type === "AP")
? <APPicker
phraseIsComplete={phraseIsComplete}
heading="AP"
entryFeeder={entryFeeder}
AP={block}
opts={opts}
onChange={AP => adjustVps({ type: "set AP", payload: { index: i, AP } })}
onRemove={() => adjustVps({ type: "remove AP", payload: i })}
/>
: (block?.type === "subjectSelection")
? <NPPicker
phraseIsComplete={phraseIsComplete}
heading={roles.king === "subject"
? <div className="h5 text-center" onClick={() => setShowingExplanation({ role: "king", item: "subject" })}>Subject {roleIcon.king}</div>
: <div className="h5 text-center">
Subject
{` `}
<span className="clickable" onClick={() => setShowingExplanation({ role: "servant", item: "subject" })}>{roleIcon.servant}</span>
{` `}
{(rendered && rendered.whatsAdjustable !== "king") &&
<span onClick={() => adjustVps({ type: "toggle servant shrink" })} className="mx-2 clickable">
{!servantIsShrunk ? "🪄" : "👶"}
</span>
}
</div>}
entryFeeder={entryFeeder}
np={block.selection}
counterPart={vps.verb ? object : undefined}
role={(isPast && vps.verb.transitivity !== "intransitive")
? "ergative"
: "subject"
}
onChange={handleSubjectChange}
opts={opts}
isShrunk={(servantIsShrunk && roles.servant === "subject")}
/>
: (vps.verb && block?.type === "objectSelection" && block.selection !== "none")
? <div className="my-2" style={{ background: (servantIsShrunk && roles.servant === "object") ? shrunkenBackground : "inherit" }}>
{(typeof block.selection === "number")
? <div>
{roles.king === "object"
? <div className="h5 text-center clickable" onClick={() => setShowingExplanation({ role: "king", item: "object" })}>Object {roleIcon.king}</div>
: <div className="h5 text-center">Object</div>}
<div className="text-muted">Unspoken 3rd Pers. Masc. Plur.</div>
</div>
: <NPPicker
phraseIsComplete={phraseIsComplete}
heading={roles.king === "object"
? <div className="h5 text-center clickable" onClick={() => setShowingExplanation({ role: "king", item: "object" })}>Object {roleIcon.king}</div>
: <div className="h5 text-center">
Object
{` `}
<span className="clickable" onClick={() => setShowingExplanation({ role: "servant", item: "object" })}>{roleIcon.servant}</span>
{` `}
{(rendered && rendered.whatsAdjustable !== "king") &&
<span onClick={() => adjustVps({ type: "toggle servant shrink" })} className="mx-2 clickable">
{!servantIsShrunk ? "🪄" : "👶"}
</span>
}
</div>}
entryFeeder={entryFeeder}
role="object"
np={block.selection}
counterPart={subject}
onChange={handleObjectChange}
opts={opts}
isShrunk={(servantIsShrunk && roles.servant === "object")}
/>}
</div>
: null}
</div>;
})}
<div className="my-2">
<TensePicker
vps={vps}
onChange={adjustVps}
mode="phrases"
/>
</div>
</div>
<VPExplorerExplanationModal
showing={showingExplanation}
setShowing={setShowingExplanation}
/>
{alert && <div className="alert alert-warning text-center" role="alert" style={{
position: "fixed",
top: "30%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: 9999999999999,
}}>
{alert}
</div>}
</div>;
}
export default VPPicker;

View File

@ -44,11 +44,14 @@ export function getVerbFromBlocks(blocks: T.Block[][]): T.VerbRenderedBlock {
return v; return v;
} }
export function getVerbAndHeadFromBlocks(blocks: T.Block[][]): { verb: T.VerbRenderedBlock, perfectiveHead: T.PerfectiveHeadBlock } { export function getVerbAndHeadFromBlocks(blocks: T.Block[][]): { verb: T.VerbRenderedBlock, perfectiveHead: T.PerfectiveHeadBlock | undefined } {
const verb = getVerbFromBlocks(blocks); const verb = getVerbFromBlocks(blocks);
const perfectiveHead = blocks[0].find(f => f.type === "perfectiveHead"); const perfectiveHead = blocks[0].find(f => f.type === "perfectiveHead");
if (!perfectiveHead || perfectiveHead.type !== "perfectiveHead") { if (!perfectiveHead || perfectiveHead.type !== "perfectiveHead") {
throw new Error("perfectiveHead not found in blocks"); return {
verb,
perfectiveHead: undefined,
};
} }
return { return {
verb, verb,
@ -56,6 +59,13 @@ export function getVerbAndHeadFromBlocks(blocks: T.Block[][]): { verb: T.VerbRen
}; };
} }
export function includesShrunkenServant(kids?: T.Kid[]): boolean {
if (!kids) return false;
return kids.some(k => (
k.type === "mini-pronoun" && k.source === "servant"
));
}
export function getPredicateSelectionFromBlocks(blocks: T.Block[][]): T.Rendered<T.PredicateSelectionComplete> { export function getPredicateSelectionFromBlocks(blocks: T.Block[][]): T.Rendered<T.PredicateSelectionComplete> {
const b = blocks[0].find(f => f.type === "predicateSelection"); const b = blocks[0].find(f => f.type === "predicateSelection");
if (!b || b.type !== "predicateSelection") { if (!b || b.type !== "predicateSelection") {

View File

@ -30,6 +30,7 @@ import VerbInfo, { RootsAndStems } from "./components/verb-info/VerbInfo";
import VPExplorer from "./components/vp-explorer/VPExplorer"; import VPExplorer from "./components/vp-explorer/VPExplorer";
import useStickyState from "./lib/useStickyState"; import useStickyState from "./lib/useStickyState";
import Block, { NPBlock, APBlock } from "./components/blocks/Block"; import Block, { NPBlock, APBlock } from "./components/blocks/Block";
import { roleIcon } from "./components/vp-explorer/VPExplorerExplanationModal";
import { import {
makePsString, makePsString,
removeFVarients, removeFVarients,
@ -158,6 +159,8 @@ import genderColors from "./lib/gender-colors";
import * as Types from "./types"; import * as Types from "./types";
import * as typePredicates from "./lib/type-predicates"; import * as typePredicates from "./lib/type-predicates";
import APPicker from "./components/ap-picker/APPicker"; import APPicker from "./components/ap-picker/APPicker";
import VPDisplay from "./components/vp-explorer/VPDisplay";
import VPPicker from "./components/vp-explorer/VPPicker";
export { export {
// FUNCTIONS // FUNCTIONS
@ -228,6 +231,7 @@ export {
getPashtoFromRendered, getPashtoFromRendered,
renderAPSelection, renderAPSelection,
getEnglishVerb, getEnglishVerb,
roleIcon,
// protobuf helpers // protobuf helpers
readDictionary, readDictionary,
writeDictionary, writeDictionary,
@ -254,7 +258,9 @@ export {
APBlock, APBlock,
Block, Block,
EPDisplay, EPDisplay,
VPDisplay,
EPPicker, EPPicker,
VPPicker,
// OTHER // OTHER
typePredicates, typePredicates,
grammarUnits, grammarUnits,