diff --git a/README.md b/README.md index 61d5307..ef07afb 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,12 @@ Also includes the [Pashto Verb Explorer](https://verbs.lingdocs.com) website. This library is **published as two libraries**: -- @lingdocs/inflect +- @lingdocs/inflect + - `/src/components` - The core inflection engine with grammatical information and tools for processing LingDocs dictionary entries and Pashto text. - Can be used with Node 16, as CommonJS - @lingdocs/ps-react + - `/src/lib` - @lingdocs/inflect plus react components for displaying Pashto text, phrase engine UI etc. - Only available as an ES6 Module diff --git a/public/index.html b/public/index.html index 94b7605..4ae1410 100644 --- a/public/index.html +++ b/public/index.html @@ -5,28 +5,28 @@ - + - + - - - - - + + + + + - - - + + + - Pashto Verb Explorer + Pashto Inflector diff --git a/src/App.tsx b/src/App.tsx index 07e031c..697d4fe 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,261 +6,97 @@ * */ -import { useEffect } from "react"; -import verbs from "./verbs"; -import nounsAdjs from "./nouns-adjs"; -import Pashto from "./components/src/Pashto"; -import Phonetics from "./components/src/Phonetics"; -import { getVerbInfo } from "./lib/src/verb-info"; +import { useEffect, useState } from "react"; + import ButtonSelect from "./components/src/ButtonSelect"; -import { - clamp -} from "./lib/src/p-text-helpers"; -import { - randomNumber, -} from "./lib/src/misc-helpers"; import { Modal } from "react-bootstrap"; import * as T from "./types"; -import { isAdjectiveEntry, isAdverbEntry, isLocativeAdverbEntry, isNounEntry } from "./lib/src/type-predicates"; import defualtTextOptions from "./lib/src/default-text-options"; -import PhraseBuilder from "./components/src/vp-explorer/VPExplorer"; import useStickyState from "./components/src/useStickyState"; import EPExplorer from "./components/src/ep-explorer/EPExplorer"; - -type VerbType = "simple" | "stative compound" | "dynamic compound"; -const verbTypes: VerbType[] = [ - "simple", - "stative compound", - "dynamic compound", -]; - -const nouns = nounsAdjs.filter(isNounEntry); -const adjectives = nounsAdjs.filter(isAdjectiveEntry); -const locativeAdverbs = nounsAdjs.filter(isLocativeAdverbEntry); -const adverbs = nounsAdjs.filter(isAdverbEntry); -const entryFeeder: T.EntryFeeder = { - locativeAdverbs, - nouns, - adjectives, - verbs, - adverbs, -}; - -const transitivities: T.Transitivity[] = [ - "transitive", - "intransitive", - "grammatically transitive", -]; - -const allVerbs = verbs.map((v: { entry: T.DictionaryEntry, complement?: T.DictionaryEntry }) => ({ - verb: v, - info: getVerbInfo(v.entry, v.complement), -})); - - +import VPBuilderDemo from "./demo-components/VPBuilderDemo"; +import { entryFeeder } from "./demo-components/entryFeeder"; +import { Hider } from "./components/library"; +import InflectionDemo from "./demo-components/InflectionDemo"; +import SpellingDemo from "./demo-components/SpellingDemo"; function App() { - const [verbTs, setVerbTs] = useStickyState(0, "verbTs1"); - const [verbTypeShowing, setVerbTypeShowing] = useStickyState("simple", "vTypeShowing"); - const [transitivityShowing, setTransitivityShowing] = useStickyState("intransitive", "transitivityShowing1"); const [showingTextOptions, setShowingTextOptions] = useStickyState(false, "showTextOpts1"); const [textOptions, setTextOptions] = useStickyState(defualtTextOptions, "textOpts1"); const [theme, setTheme] = useStickyState<"light" | "dark">("light", "theme1"); - // const onlyGrammTrans = (arr: Transitivity[]) => ( - // arr.length === 1 && arr[0] === "grammatically transitive" - // ); - // const ensureSimpleVerbTypeSelected = () => { - // if (!verbTypesShowing.includes["simple"]) { - // setVerbTypesShowing([...verbTypesShowing, "simple"]); - // } - // } - + const [showing, setShowing] = useState(""); + function handleHiderClick(label: string) { + setShowing(os => os === label + ? "" + : label); + } useEffect(() => { document.documentElement.setAttribute("data-theme", theme); - }, [theme]) - - const handleVerbIndexChange = (e: any) => { - setVerbTs(parseInt(e.target.value)); - } - const handleTypeSelection = (e: any) => { - const type = e.target.value as VerbType; - if (type === "dynamic compound") { - setTransitivityShowing("transitive"); - } - if (type === "stative compound" && transitivityShowing === "grammatically transitive") { - setTransitivityShowing("transitive"); - } - setVerbTypeShowing(type); - } - const handleTransitivitySelection = (e: any) => { - const transitivity = e.target.value as T.Transitivity; - if (transitivity === "grammatically transitive") { - setVerbTypeShowing("simple"); - } - if (transitivity === "intransitive" && verbTypeShowing === "dynamic compound") { - setTransitivityShowing("transitive"); - return; - } - setTransitivityShowing(e.target.value as T.Transitivity); - } - const verbsAvailable = allVerbs.filter((verb) => ( - ( - (verb.info.type === "transitive or grammatically transitive simple" && verbTypeShowing === "simple") && (transitivityShowing === "transitive" || transitivityShowing === "grammatically transitive") - ) || - (( - verbTypeShowing === verb.info.type || - (verbTypeShowing === "stative compound" && verb.info.type === "dynamic or stative compound") || - (verbTypeShowing === "dynamic compound" && verb.info.type === "dynamic or stative compound") || - (verbTypeShowing === "dynamic compound" && verb.info.type === "dynamic or generative stative compound") || - (verbTypeShowing === "stative compound" && verb.info.type === "dynamic or generative stative compound") - ) - && ( - transitivityShowing === verb.info.transitivity - )) - )).sort((a, b) => a.verb.entry.p.localeCompare(b.verb.entry.p, "ps")); - - const v = (() => { - const vFound = verbsAvailable.find(v => v.verb.entry.ts === verbTs); - if (vFound) return vFound; - if (verbsAvailable.length === 0) return undefined; - const vTopOfList = verbsAvailable[0]; - setVerbTs(vTopOfList.verb.entry.ts); - return vTopOfList; - })(); - - const pickRandomVerb = () => { - let newIndex: number; - do { - newIndex = randomNumber(0, verbsAvailable.length); - } while(verbsAvailable[newIndex].verb.entry.ts === verbTs); - setVerbTs(verbsAvailable[newIndex].verb.entry.ts); - }; - const makeVerbLabel = (entry: T.DictionaryEntry): string => ( - `${entry.p} - ${clamp(entry.e, 20)}` - ); + }, [theme]); return <>
setShowingTextOptions(true)} - > - -
-
setTheme(theme === "light" ? "dark" : "light")} >
+
setShowingTextOptions(true)} + > + +
-
-

Pashto Verb Explorer

-

by LingDocs

-

- Each form is made from one simple formula which works for all verbs. 👨‍🔬 +

+

Pashto Inflector

+

+ An open source TypeScript/React library for Pashto inflection, verb conjugation, phrase generation, text conversion, and more

-

Choose a verb 👇, look at its roots and stems 🌳, see how all the forms are made. 🤓

+

Used in the LingDocs Pashto Dictionary and LingDocs Pashto Grammar

+

by Adam Dueck - Source Code on GitHub

+
-
-
-
-
- {v ? -
-
Select a verb:
-
- -
- -
-
-
-
- - {v.verb.entry} - {` `}-{` `} - {v.verb.entry} - - {` `} - {v.verb.entry.c} -
-
{v.verb.entry.e}
-
-
- :
- No such verbs available... -
- } -
-
-
Verb type:
-
- {verbTypes.map((type) => ( -
- null} - /> - -
- ))} -
-
Transitivity:
-
- {transitivities.map((transitivity) => ( -
- null} - value={transitivity} - /> - -
- ))} -
-
-
-
-
- {v?.verb.entry &&
- Demos: + handleHiderClick("verbs")} + > + + + handleHiderClick("equatives")} + > + -
} -

Equative Phrase Builder

- + + handleHiderClick("inflection")} + > + + + handleHiderClick("spelling")} + > + +
setShowingTextOptions(false)}> @@ -308,7 +144,7 @@ function App() { { label: "LingDocs", value: "lingdocs" }, { label: "IPA", value: "ipa" }, { label: "ALAC", value: "alalc" }, - { label: "None", value: "none" }, + // { label: "None", value: "none" }, ]} value={textOptions.phonetics} handleChange={(p) => setTextOptions({ ...textOptions, phonetics: p })} @@ -320,11 +156,11 @@ function App() { - */} } diff --git a/src/components/src/EntrySelect.tsx b/src/components/src/EntrySelect.tsx index ea132a4..d624b25 100644 --- a/src/components/src/EntrySelect.tsx +++ b/src/components/src/EntrySelect.tsx @@ -101,7 +101,7 @@ function EntrySelect(props: { + + + ))} + + + +
+
Select word:
+ +
+ {"previous"} + {"next"} +
+
+ + + + {inf ?
+ {inf.inflections && word && (() => { + const pattern = getInflectionPattern(word); + return ; + })()} + {"plural" in inf && inf.plural !== undefined &&
+
Plural
+ +
} + {"arabicPlural" in inf && inf.arabicPlural !== undefined &&
+
Arabic Plural
+ +
} +
:
+
} + ; +} + +function inflectionSubUrl(pattern: T.InflectionPattern): string { + return pattern === 0 + ? "" + : pattern === 1 + ? "#1-basic" + : pattern === 2 + ? "#2-words-ending-in-an-unstressed-ی---ey" + : pattern === 3 + ? "#3-words-ending-in-a-stressed-ی---éy" + : pattern === 4 + ? "#4-words-with-the-pashtoon-pattern" + : pattern === 5 + ? "#5-shorter-words-that-squish" + // : pattern === 6 + : "#6-inanimate-feminine-nouns-ending-in-ي---ee" +} + +export default InflectionDemo; \ No newline at end of file diff --git a/src/demo-components/SpellingDemo.tsx b/src/demo-components/SpellingDemo.tsx new file mode 100644 index 0000000..9b79980 --- /dev/null +++ b/src/demo-components/SpellingDemo.tsx @@ -0,0 +1,131 @@ +import Examples from "../components/src/Examples"; +import ButtonSelect from "../components/src/ButtonSelect"; +import * as T from "../types"; + +const spellingOptions: { + value: T.TextOptions["spelling"], + label: string, +}[] = [ + { + value: "Afghan", + label: "Afghan", + }, + { + value: "Pakistani ي", + label: "Pakistani ي", + }, + { + value: "Pakistani ی", + label: "Pakistani ی", + }, +]; + +const phoneticsOptions: { + value: T.TextOptions["phonetics"], + label: string, +}[] = [ + { + value: "lingdocs", + label: "LingDocs", + }, + { + value: "ipa", + label: "IPA", + }, + { + value: "alalc", + label: "ALALC", + }, +]; + +function SpellingDemo({ opts, onChange }: { + opts: T.TextOptions, + onChange: (opts: T.TextOptions) => void, +}) { + return
+
    +
  • Converts text between Afghan and Pakistani spelling conventions
  • +
  • Generates diacritics for Pashto script when given phonetic script along with Pashto script
  • +
+
+
+
+
+
Pashto Spelling Convention:
+
+ {spellingOptions.map(({ value, label }) => ( +
+ { + onChange({ + ...opts, + spelling: value, + }); + }} + /> + +
+ ))} +
+
+
+
Latin Phonetic System:
+
+ {phoneticsOptions.map(({ value, label }) => ( +
+ { + onChange({ + ...opts, + phonetics: value, + }); + }} + /> + +
+ ))} +
+
+
+
Diacritics
+ onChange({ ...opts, diacritics: p === "true" })} + /> +
+
+
+
+ + {[ + { + p: "زما زوی مکتب ته ځي", + f: "zmaa zooy maktab ta dzee", + }, + { + p: "دا ښه سړی دی", + f: "daa xu saRey dey", + }, + ]} +
; +} + +export default SpellingDemo; \ No newline at end of file diff --git a/src/demo-components/VPBuilderDemo.tsx b/src/demo-components/VPBuilderDemo.tsx new file mode 100644 index 0000000..eb76211 --- /dev/null +++ b/src/demo-components/VPBuilderDemo.tsx @@ -0,0 +1,203 @@ +import PhraseBuilder from "../components/src/vp-explorer/VPExplorer"; +import * as T from "../types"; +import Pashto from "../components/src/Pashto"; +import Phonetics from "../components/src/Phonetics"; +import { getVerbInfo } from "../lib/src/verb-info"; +import verbs from "../verbs"; +import { useStickyState } from "../components/library"; +import { + clamp +} from "../lib/src/p-text-helpers"; +import { + randomNumber, +} from "../lib/src/misc-helpers"; +import { entryFeeder } from "./entryFeeder"; + + +const transitivities: T.Transitivity[] = [ + "transitive", + "intransitive", + "grammatically transitive", +]; + +const allVerbs = verbs.map((v: { entry: T.DictionaryEntry, complement?: T.DictionaryEntry }) => ({ + verb: v, + info: getVerbInfo(v.entry, v.complement), +})); + +type VerbType = "simple" | "stative compound" | "dynamic compound"; +const verbTypes: VerbType[] = [ + "simple", + "stative compound", + "dynamic compound", +]; + +function VPBuilderDemo({ opts }: { + opts: T.TextOptions, +}) { + const [verbTs, setVerbTs] = useStickyState(0, "verbTs1"); + const [verbTypeShowing, setVerbTypeShowing] = useStickyState("simple", "vTypeShowing"); + const [transitivityShowing, setTransitivityShowing] = useStickyState("intransitive", "transitivityShowing1"); + // const onlyGrammTrans = (arr: Transitivity[]) => ( + // arr.length === 1 && arr[0] === "grammatically transitive" + // ); + // const ensureSimpleVerbTypeSelected = () => { + // if (!verbTypesShowing.includes["simple"]) { + // setVerbTypesShowing([...verbTypesShowing, "simple"]); + // } + // } + const handleVerbIndexChange = (e: any) => { + setVerbTs(parseInt(e.target.value)); + } + const handleTypeSelection = (e: any) => { + const type = e.target.value as VerbType; + if (type === "dynamic compound") { + setTransitivityShowing("transitive"); + } + if (type === "stative compound" && transitivityShowing === "grammatically transitive") { + setTransitivityShowing("transitive"); + } + setVerbTypeShowing(type); + } + const handleTransitivitySelection = (e: any) => { + const transitivity = e.target.value as T.Transitivity; + if (transitivity === "grammatically transitive") { + setVerbTypeShowing("simple"); + } + if (transitivity === "intransitive" && verbTypeShowing === "dynamic compound") { + setTransitivityShowing("transitive"); + return; + } + setTransitivityShowing(e.target.value as T.Transitivity); + } + const verbsAvailable = allVerbs.filter((verb) => ( + ( + (verb.info.type === "transitive or grammatically transitive simple" && verbTypeShowing === "simple") && (transitivityShowing === "transitive" || transitivityShowing === "grammatically transitive") + ) || + (( + verbTypeShowing === verb.info.type || + (verbTypeShowing === "stative compound" && verb.info.type === "dynamic or stative compound") || + (verbTypeShowing === "dynamic compound" && verb.info.type === "dynamic or stative compound") || + (verbTypeShowing === "dynamic compound" && verb.info.type === "dynamic or generative stative compound") || + (verbTypeShowing === "stative compound" && verb.info.type === "dynamic or generative stative compound") + ) + && ( + transitivityShowing === verb.info.transitivity + )) + )).sort((a, b) => a.verb.entry.p.localeCompare(b.verb.entry.p, "ps")); + + const v = (() => { + const vFound = verbsAvailable.find(v => v.verb.entry.ts === verbTs); + if (vFound) return vFound; + if (verbsAvailable.length === 0) return undefined; + const vTopOfList = verbsAvailable[0]; + setVerbTs(vTopOfList.verb.entry.ts); + return vTopOfList; + })(); + + const pickRandomVerb = () => { + let newIndex: number; + do { + newIndex = randomNumber(0, verbsAvailable.length); + } while(verbsAvailable[newIndex].verb.entry.ts === verbTs); + setVerbTs(verbsAvailable[newIndex].verb.entry.ts); + }; + const makeVerbLabel = (entry: T.DictionaryEntry): string => ( + `${entry.p} - ${clamp(entry.e, 20)}` + ); + return <> +
+
+
+
+ {v ? +
+
Select a verb:
+
+ +
+ +
+
+
+
+ + {v.verb.entry} + {` `}-{` `} + {v.verb.entry} + + {` `} + {v.verb.entry.c} +
+
{v.verb.entry.e}
+
+
+ :
+ No such verbs available... +
+ } +
+
+
Verb type:
+
+ {verbTypes.map((type) => ( +
+ null} + /> + +
+ ))} +
+
Transitivity:
+
+ {transitivities.map((transitivity) => ( +
+ null} + value={transitivity} + /> + +
+ ))} +
+
+
+
+
+ {v?.verb.entry &&
+ +
} + ; +} + +export default VPBuilderDemo; \ No newline at end of file diff --git a/src/demo-components/chevron_left-24px.svg b/src/demo-components/chevron_left-24px.svg new file mode 100644 index 0000000..2dc390c --- /dev/null +++ b/src/demo-components/chevron_left-24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/demo-components/chevron_right-24px.svg b/src/demo-components/chevron_right-24px.svg new file mode 100644 index 0000000..aef49e2 --- /dev/null +++ b/src/demo-components/chevron_right-24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/demo-components/entryFeeder.ts b/src/demo-components/entryFeeder.ts new file mode 100644 index 0000000..2c61227 --- /dev/null +++ b/src/demo-components/entryFeeder.ts @@ -0,0 +1,21 @@ +import verbs from "../verbs"; +import nounsAdjs from "../nouns-adjs"; +import { + isAdjectiveEntry, + isAdverbEntry, + isLocativeAdverbEntry, + isNounEntry, +} from "../lib/src/type-predicates"; +import * as T from "../types"; + +const nouns = nounsAdjs.filter(isNounEntry); +const adjectives = nounsAdjs.filter(isAdjectiveEntry); +const locativeAdverbs = nounsAdjs.filter(isLocativeAdverbEntry); +const adverbs = nounsAdjs.filter(isAdverbEntry); +export const entryFeeder: T.EntryFeeder = { + locativeAdverbs, + nouns, + adjectives, + verbs, + adverbs, +}; \ No newline at end of file diff --git a/src/lib/src/phrase-building/new-render-verb.txt b/src/lib/src/phrase-building/new-render-verb.txt new file mode 100644 index 0000000..83d72e7 --- /dev/null +++ b/src/lib/src/phrase-building/new-render-verb.txt @@ -0,0 +1,31 @@ +import * as T from "../../../types"; + +function renderVerb(vs: T.VerbSelectionComplete): { + verbBlocks: VerbBlocks, + hasBa: boolean, +} { + const base = chooseRootOrStem(vs.tense, vs.verb); + const b = concatPsString(base, grammarUnits.presentEndings[0][0][0]); + return { + verbBlocks: [{ + type: "verb", + block: { + ...vs, + hasBa: false, + ps: [], + person: 0, + complementWelded: undefined, + } + }], + hasBa: false, + } +} + +function chooseRootOrStem(tense: T.VerbFormName, entry: T.VerbEntry): T.FullForm { + const info = getVerbInfo(entry.entry, entry.complement) + if ("stative" in info || "transitive" in info) { + throw new Error("multiple verb types not supported yet"); + } + return info.stem.imperfective + +} diff --git a/src/lib/src/phrase-building/render-vp.ts b/src/lib/src/phrase-building/render-vp.ts index 2009b8a..b379b60 100644 --- a/src/lib/src/phrase-building/render-vp.ts +++ b/src/lib/src/phrase-building/render-vp.ts @@ -35,6 +35,8 @@ import { renderAPSelection } from "./render-ap"; import { findPossesivesToShrink, orderKids, getMiniPronounPs } from "./render-common"; import { renderComplementSelection } from "./render-complement"; import { makeNounSelection } from "./make-selections"; +// import { getVerbInfo } from "../verb-info"; +// import { grammarUnits } from "../../library"; export function renderVP(VP: T.VPSelectionComplete): T.VPRendered { const subject = getSubjectSelection(VP.blocks).selection; @@ -353,6 +355,7 @@ type VerbBlocks = | [T.PerfectParticipleBlock, T.PerfectEquativeBlock] // perfect verb | [T.ModalVerbBlock, T.ModalVerbKedulPart] // modal verb + function renderVerbSelection(vs: T.VerbSelectionComplete, person: T.Person, complementPerson: T.Person | undefined, externalComplement: T.ComplementSelection | T.UnselectedComplementSelection | undefined): { verbBlocks: VerbBlocks hasBa: boolean, diff --git a/src/lib/src/type-predicates.ts b/src/lib/src/type-predicates.ts index 7ce6e8b..9e9b3a3 100644 --- a/src/lib/src/type-predicates.ts +++ b/src/lib/src/type-predicates.ts @@ -68,6 +68,34 @@ export function isAdjOrUnisexNounEntry(e: T.Entry): e is (T.AdjectiveEntry | T.U ); } +export function isPattern(p: T.InflectionPattern | "all"): (entry: T.NounEntry | T.AdjectiveEntry) => boolean { + if (p === 0) { + return (e: T.NounEntry | T.AdjectiveEntry) => ( + !isPattern1Entry(e) && !isPattern2Entry(e) && !isPattern3Entry(e) + && !isPattern4Entry(e) && !isPattern5Entry(e) && !isPattern6FemEntry(e) + ) + } + if (p === 1) { + return isPattern1Entry; + } + if (p === 2) { + return isPattern2Entry; + } + if (p === 3) { + return isPattern3Entry; + } + if (p === 4) { + return isPattern4Entry; + } + if (p === 5) { + return isPattern5Entry; + } + if (p === 6) { + return isPattern6FemEntry; + } + return () => true; +} + /** * shows if a noun/adjective has the basic (consonant / ه) inflection pattern * @@ -153,7 +181,7 @@ export function isPattern5Entry(e: T ); } -export function isPattern6FemEntry(e: T.FemNounEntry): e is T.Pattern6FemEntry { +export function isPattern6FemEntry(e: T.NounEntry | T.AdjectiveEntry): e is T.Pattern6FemEntry { if (!isFemNounEntry(e)) return false; if (e.c.includes("anim.")) return false; return e.p.slice(-1) === "ي";