Compare commits

...

10 Commits

Author SHA1 Message Date
adueck db2749bd0a lowered library target for use with creat-react-app 2024-08-12 16:36:13 -04:00
adueck 4cae54eefe lowered library target for use with creat-react-app 2024-08-12 16:35:33 -04:00
adueck bb64b2c336 forgot .npmrc 2024-08-12 16:25:46 -04:00
adueck 25f10a8263 publish with vite 2024-08-12 16:23:01 -04:00
adueck d3c89cb832 fix for netlify tests not running 2024-08-12 16:20:35 -04:00
adueck 112368b31e try again with netlify 2024-08-12 16:06:33 -04:00
adueck 650b96ca2c trying something to get the tests to run on netlify 2024-08-12 16:04:04 -04:00
adueck 315ec0f030 fixup website build 2024-08-12 15:55:50 -04:00
adueck 42f3552b80 oops 2024-08-12 15:51:31 -04:00
adueck b4bee62036 switch to vite 2024-08-12 15:48:31 -04:00
186 changed files with 8301 additions and 60695 deletions

1
.env
View File

@ -1 +0,0 @@
SKIP_PREFLIGHT_CHECK=true

View File

@ -18,12 +18,13 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 16 node-version: 20
cache: "yarn" cache: "yarn"
- name: Install, build, test - name: Install, build, test
run: | run: |
yarn install-r yarn install-all
yarn build-library yarn build-lib
yarn build-components
yarn build-website yarn build-website
yarn test --silent yarn test --silent
yarn check-all-inflections yarn check-all-inflections

View File

@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 16 node-version: 20
cache: "yarn" cache: "yarn"
- name: Check if version has been updated - name: Check if version has been updated
id: check id: check
@ -27,10 +27,10 @@ jobs:
- name: Publish when version changed - name: Publish when version changed
if: steps.check.outputs.changed == 'true' if: steps.check.outputs.changed == 'true'
run: | run: |
yarn install-r yarn install-all
yarn build-library yarn build-lib
yarn build-components
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

43
.gitignore vendored
View File

@ -1,12 +1,15 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# compiled library files # fetched vocab
dist src/verbs.ts
src/nouns-adjs.ts
# dependencies
node_modules
/.pnp
.pnp.js
# testing # testing
/coverage /coverage
@ -14,20 +17,26 @@ node_modules
dict dict
diac.ts diac.ts
# production node_modules
build build
dist dist
dist-ssr
*.local
/.pnp
.pnp.js
# misc # Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
.DS_Store .DS_Store
.env.local .env.local
.env.development.local .env.development.local
.env.test.local .env.test.local
.env.production.local .env.production.local
*.suo
npm-debug.log* *.ntvs*
yarn-debug.log* *.njsproj
yarn-error.log* *.sln
*.sw?
src/verbs.ts
src/nouns-adjs.ts

2
.nvmrc
View File

@ -1 +1 @@
16 20

View File

@ -1,8 +1,5 @@
{ {
"typescript.preferences.autoImportFileExcludePatterns": [ "typescript.preferences.autoImportFileExcludePatterns": [
"../../library.ts" "library.ts"
],
"cSpell.words": [
"کارخانه"
], ],
} }

View File

@ -20,7 +20,7 @@ This library uses a 3-step process to generate gramattically correct Pashto phra
| | Pashto Inflector Function | Chomskian Grammar Level | | | Pashto Inflector Function | Chomskian Grammar Level |
|-|--------------------------| ----------------------- | |-|--------------------------| ----------------------- |
|1.| Assemble the phrase tree | Phrase Structure Rules | |1.| Assemble the phrase tree | Phrase Structure Rules |
|2.| Inflect the words in tree | Morphophonemic Rules | |2.| Inflect the words in tree | Morphophonemic Rules |
|3.| Arrange the inflected words in order | Transformational Rules | |3.| Arrange the inflected words in order | Transformational Rules |
### 1. Assemble the phrase tree ### 1. Assemble the phrase tree

View File

@ -2,7 +2,6 @@ import * as T from "./src/types";
import { inflectWord } from "./src/lib/src/pashto-inflector"; import { inflectWord } from "./src/lib/src/pashto-inflector";
import * as tp from "./src/lib/src/type-predicates"; import * as tp from "./src/lib/src/type-predicates";
import { conjugateVerb } from "./src/lib/src/verb-conjugation"; import { conjugateVerb } from "./src/lib/src/verb-conjugation";
import fetch from "node-fetch";
// Script to try inflecting all the words in the dictionary and make sure that // Script to try inflecting all the words in the dictionary and make sure that
// no errors are thrown in the process // no errors are thrown in the process
@ -15,8 +14,8 @@ type InflectionError = {
}; };
async function checkAll() { async function checkAll() {
console.log("Checking inflection functions on all dictionary words");
const res = await fetch(process.env.LINGDOCS_DICTIONARY_URL); const res = await fetch(process.env.LINGDOCS_DICTIONARY_URL);
// @ts-ignore
const { entries }: T.Dictionary = await res.json(); const { entries }: T.Dictionary = await res.json();
const errors: InflectionError[] = []; const errors: InflectionError[] = [];

2
dev
View File

@ -1,2 +0,0 @@
nix-shell --command return
node --version

26
eslint.config.js Normal file
View File

@ -0,0 +1,26 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config({
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
ignores: ['dist'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
})

View File

@ -6,7 +6,6 @@
* *
*/ */
const fs = require("fs"); const fs = require("fs");
const fetch = require("node-fetch-commonjs");
const path = require("path"); const path = require("path");
const verbCollectionPath = path.join(".", "vocab", "verbs"); const verbCollectionPath = path.join(".", "vocab", "verbs");
const nounAdjCollectionPath = path.join(".", "vocab", "nouns-adjs"); const nounAdjCollectionPath = path.join(".", "vocab", "nouns-adjs");

1
global.d.ts vendored Normal file
View File

@ -0,0 +1 @@
import "jest-extended";

View File

@ -2,11 +2,11 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta name="description" content="An open source TypeScript/React library for Pashto inflection, verb conjugation, phrase generation, text conversion, and more" /> <meta name="description" content="An open source TypeScript/React library for Pashto inflection, verb conjugation, phrase generation, text conversion, and more" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta name="keywords" content="Pashto, Verbs, Conjugation, Grammar, Linguistics" /> <meta name="keywords" content="Pashto, Verbs, Conjugation, Grammar, Linguistics" />
<meta name="author" content="lingdocs.com" /> <meta name="author" content="lingdocs.com" />
<link rel="canonical" href="https://pashto-inflector.lingdocs.com/" /> <link rel="canonical" href="https://pashto-inflector.lingdocs.com/" />
@ -25,10 +25,11 @@
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<meta name="twitter:creator" content="@lingdocs" /> <meta name="twitter:creator" content="@lingdocs" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="/manifest.json" />
<title>Pashto Inflector</title> <title>Pashto Inflector</title>
</head> </head>
<body class="d-flex flex-column h-100" id="root"> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

13
jest.config.js Normal file
View File

@ -0,0 +1,13 @@
/** @type {import('ts-jest').JestConfigWithTsJest} **/
export default {
testEnvironment: "node",
transform: {
"^.+.tsx?$": [
"ts-jest",
{
tsconfig: "tsconfig.app.json",
},
],
},
setupFilesAfterEnv: ["./testSetup.ts"],
};

View File

@ -1,3 +1,3 @@
[build] [build]
command = "yarn install-r && yarn test --silent && yarn build-website" command = "yarn install-all && yarn test --runInBand --no-cache && yarn build-website"
publish = "build/" publish = "dist/"

View File

@ -1,82 +1,47 @@
{ {
"name": "pashto-inflector", "name": "pashto-inflector-website",
"version": "7.5.1", "version": "7.6.3",
"author": "lingdocs.com", "type": "module",
"description": "A Pashto inflection and verb conjugation engine, inculding React components for displaying Pashto text, inflections, and conjugations",
"homepage": "https://verbs.lingdocs.com",
"license": "MIT",
"private": false,
"repository": {
"type": "git",
"url": "https://github.com/lingdocs/pashto-inflector.git"
},
"devDependencies": {
"@fortawesome/fontawesome-free": "^5.15.2",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.20",
"@types/node": "^15.12.1",
"@types/react": "^17.0.3",
"@types/react-dom": "^17.0.2",
"bootstrap": "^4.6.0",
"jest-extended": "^4.0.1",
"node-fetch": "^3.3.2",
"node-fetch-commonjs": "^3.2.4",
"pbf": "^3.2.1",
"react": "^17.0.1",
"react-bootstrap": "^1.5.1",
"react-dom": "^17.0.1",
"react-scripts": "4.0.3",
"tsx": "^3.14.0",
"typescript": "^5.1.6",
"web-vitals": "^1.0.1"
},
"scripts": { "scripts": {
"patch": "npm version patch --no-git-tag-version && cd src/lib && npm version patch --no-git-tag-version && cd ../components && npm version patch --no-git-tag-version", "patch": "npm version patch --no-git-tag-version && cd src/lib && npm version patch --no-git-tag-version && cd ../components && npm version patch --no-git-tag-version",
"minor": "npm version minor --no-git-tag-version && cd src/lib && npm version minor --no-git-tag-version && cd ../components && npm version minor --no-git-tag-version", "minor": "npm version minor --no-git-tag-version && cd src/lib && npm version minor --no-git-tag-version && cd ../components && npm version minor --no-git-tag-version",
"major": "npm version major --no-git-tag-version && cd src/lib && npm version major --no-git-tag-version && cd ../components && npm version major --no-git-tag-version", "major": "npm version major --no-git-tag-version && cd src/lib && npm version major --no-git-tag-version && cd ../components && npm version major --no-git-tag-version",
"preinstall": "echo '*** Be sure to use yarn install-r not yarn install ***!'", "preinstall": "echo '*** Be sure to use 'yarn install-all' not 'yarn install' ***!'",
"start": "react-scripts start", "dev": "vite",
"build": "react-scripts build", "lint": "eslint .",
"test": "react-scripts test", "test": "jest",
"eject": "react-scripts eject", "preview": "vite preview",
"install-r": "yarn install && node get-words.js && cd src/lib && yarn install && cd ../components && yarn install", "install-all": "yarn install && node get-words.cjs && cd src/lib && yarn install && cd ../components && yarn install",
"build-website": "node get-words.js && npm run build", "build-website": "tsc -b && vite build",
"build-library": "cd src/components && rimraf dist && tsc --project lib-tsconfig.json && node post-build.cjs && cd ../lib && rimraf dist && tsc --project lib-tsconfig.json && node_modules/rollup/dist/bin/rollup -c", "build-components": "rm -rf src/components/dist && tsc --project src/components/tsconfig.json && cd src/components && node post-build.cjs",
"test-ci": "npm run test -- --watchAll=false", "build-lib": "rm -rf src/lib/dist && tsc --project src/lib/tsconfig.json",
"get-words": "node get-words.js", "get-words": "node get-words.cjs",
"check-all-inflections": "tsx check-all-inflections.ts" "check-all-inflections": "tsx check-all-inflections.ts"
}, },
"eslintConfig": { "dependencies": {
"extends": [ "react": "^18.3.1",
"react-app", "react-dom": "^18.3.1",
"react-app/jest" "bootstrap": "4.6.1",
], "react-bootstrap": "1.5.1",
"rules": { "@fortawesome/fontawesome-free": "^5.15.2"
"no-warning-comments": [
1,
{
"terms": [
"fixme",
"xxx"
],
"location": "anywhere"
}
]
}
}, },
"browserslist": { "devDependencies": {
"production": [ "@eslint/js": "^9.8.0",
">0.2%", "@types/jest": "^29.5.12",
"not dead", "@types/react": "^18.3.3",
"not op_mini all" "@types/react-dom": "^18.3.0",
], "@vitejs/plugin-react": "^4.3.1",
"development": [ "eslint": "^9.8.0",
"last 1 chrome version", "eslint-plugin-react-hooks": "^5.1.0-rc.0",
"last 1 firefox version", "eslint-plugin-react-refresh": "^0.4.9",
"last 1 safari version" "globals": "^15.9.0",
] "jest": "^29.7.0",
}, "jest-environment-jsdom": "^29.7.0",
"dependencies": {} "jest-extended": "^4.0.2",
"ts-jest": "^29.2.4",
"tsx": "^4.17.0",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.0",
"vite": "^5.4.0"
}
} }

View File

@ -1,10 +0,0 @@
let
nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-23.05";
pkgs = import nixpkgs { config = {}; overlays = []; };
in
pkgs.mkShell {
packages = with pkgs; [
nodejs_20
];
}

View File

@ -8,7 +8,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import ButtonSelect from "./components/src/ButtonSelect"; import ButtonSelect from "./components/src/selects/ButtonSelect";
import { Modal } from "react-bootstrap"; import { Modal } from "react-bootstrap";
import * as T from "./types"; import * as T from "./types";
import defualtTextOptions from "./lib/src/default-text-options"; import defualtTextOptions from "./lib/src/default-text-options";
@ -36,6 +36,7 @@ function App() {
function handleHiderClick(label: string) { function handleHiderClick(label: string) {
setShowing((os) => (os === label ? "" : label)); setShowing((os) => (os === label ? "" : label));
} }
useEffect(() => { useEffect(() => {
document.documentElement.setAttribute("data-theme", theme); document.documentElement.setAttribute("data-theme", theme);
}, [theme]); }, [theme]);
@ -165,7 +166,9 @@ function App() {
show={showingTextOptions} show={showingTextOptions}
onHide={() => setShowingTextOptions(false)} onHide={() => setShowingTextOptions(false)}
> >
{/* @ts-expect-error strange react-bootstrap issue */}
<Modal.Header closeButton> <Modal.Header closeButton>
{/* @ts-expect-error strange react-bootstrap issue */}
<Modal.Title>Settings</Modal.Title> <Modal.Title>Settings</Modal.Title>
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>

1
src/assets/react.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -1,2 +0,0 @@
storybook-static
node_modules

View File

@ -1,12 +0,0 @@
module.exports = {
"stories": [
"../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)"
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions"
],
"framework": "@storybook/react"
}

View File

@ -1,16 +0,0 @@
// Bootstrap 4 and fontawesome 5 required for components css
import "bootstrap/dist/css/bootstrap.min.css";
import "@fortawesome/fontawesome-free/css/all.css";
// Plus some custom CSS
// TODO: this should be exported with the npm package!
import "../../App.css";
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
}

View File

@ -1,29 +0,0 @@
{
"compilerOptions": {
"target": "es6",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"downlevelIteration": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "ES6",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"declaration": true,
"jsx": "react-jsx",
"outDir": "dist"
},
"files": [
"library.ts",
"images.d.ts"
]
}

View File

@ -1,80 +1,81 @@
/** import Pashto from "./src/text-display/Pashto";
* Copyright (c) 2021 lingdocs.com import Phonetics from "./src/text-display/Phonetics";
* import InlinePs from "./src/text-display/InlinePs";
* This source code is licensed under the GPL3 license found in the import Examples from "./src/text-display/Examples";
* LICENSE file in the root directory of this source tree. import CompiledPTextDisplay from "./src/text-display/CompiledPTextDisplay";
* import ButtonSelect from "./src/selects/ButtonSelect";
*/ import EntrySelect from "./src/selects/EntrySelect";
import PersonSelection from "./src/selects/PersonSelection";
import InflectionsTable from "./src/InflectionsTable";
import Pashto from "./src/Pashto";
import Phonetics from "./src/Phonetics";
import InlinePs from "./src/InlinePs";
import ButtonSelect from "./src/ButtonSelect";
import VerbFormDisplay from "./src/VerbFormDisplay";
import VerbTable from "./src/VerbTable";
import EPDisplay from "./src/ep-explorer/EPDisplay";
import Examples from "./src/Examples";
import Hider from "./src/Hider"; import Hider from "./src/Hider";
import EntrySelect from "./src/EntrySelect"; import InflectionsTable from "./src/tables/InflectionsTable";
import VerbTable from "./src/tables/VerbTable";
import useStickyState from "./src/useStickyState";
import NPPicker from "./src/block-pickers/NPPicker";
import SandwichPicker from "./src/block-pickers/SandwichPicker";
import VerbFromDisplay from "./src/VerbFormDisplay";
import Block from "./src/blocks/Block";
import VerbInfo, { RootsAndStems } from "./src/verb-info/VerbInfo"; import VerbInfo, { RootsAndStems } from "./src/verb-info/VerbInfo";
import VPExplorer from "./src/vp-explorer/VPExplorer"; import VPExplorer from "./src/vp-explorer/VPExplorer";
import { makeVPSelectionState } from "../lib/src/phrase-building/verb-selection";
import { vpsReducer } from "../lib/src/phrase-building/vps-reducer";
import type { VpsReducerAction as VpsA } from "../lib/src/phrase-building/vps-reducer";
import useStickyState from "./src/useStickyState";
import Block, { NPBlock, APBlock } from "./src/blocks/Block";
import { roleIcon } from "./src/vp-explorer/VPExplorerExplanationModal";
import CompiledPTextDisplay from "./src/CompiledPTextDisplay";
import RenderedBlocksDisplay from "./src/RenderedBlocksDisplay";
import NPPicker from "./src/np-picker/NPPicker";
import EPPicker from "./src/ep-explorer/EPPicker";
import EPExplorer from "./src/ep-explorer/EPExplorer"; import EPExplorer from "./src/ep-explorer/EPExplorer";
import APPicker from "./src/ap-picker/APPicker"; import playAudio from "./src/play-audio";
import { roleIcon } from "./src/role-icons";
import { vpsReducer } from "../lib/src/phrase-building/vps-reducer";
import type { VpsReducerAction } from "../lib/src/phrase-building/vps-reducer";
import { makeVPSelectionState } from "../lib/src/phrase-building/verb-selection";
import APPicker from "./src/block-pickers/APPicker";
import VPDisplay from "./src/vp-explorer/VPDisplay"; import VPDisplay from "./src/vp-explorer/VPDisplay";
import VPPicker from "./src/vp-explorer/VPPicker"; import VPPicker from "./src/vp-explorer/VPPicker";
import NPDisplay from "./src/vp-explorer/NPDisplay"; import NPDisplay from "./src/vp-explorer/NPDisplay";
import HumanReadableInflectionPattern from "./src/HumanReadableInflectionPattern"; import HumanReadableInflectionPattern from "./src/tables/HumanReadableInflectionPattern";
import { psJSXMap } from "./src/jsx-map"; import { psJSXMap } from "./src/text-display/jsx-map";
import genderColors from "./src/gender-colors"; import genderColors from "./src/gender-colors";
// this library also includes everything from the core inflect library // this library also includes everything from the core inflect library
export * from "../lib/library"; export * from "../lib/library";
export { export {
useStickyState, // text-display
roleIcon, InlinePs,
vpsReducer, Pashto,
makeVPSelectionState, Phonetics,
EPExplorer, CompiledPTextDisplay,
VPExplorer, Examples,
Examples,
VerbFormDisplay,
VerbTable,
VerbInfo,
RootsAndStems,
InflectionsTable,
Pashto,
Phonetics,
InlinePs,
ButtonSelect,
Hider,
EntrySelect,
NPPicker,
APPicker,
NPBlock,
APBlock,
Block,
EPDisplay,
VPDisplay,
NPDisplay,
EPPicker,
VPPicker,
CompiledPTextDisplay,
RenderedBlocksDisplay,
HumanReadableInflectionPattern,
psJSXMap,
genderColors,
}
export type VpsReducerAction = VpsA; // selects
ButtonSelect,
EntrySelect,
PersonSelection,
// tables
InflectionsTable,
VerbTable,
// block-pickers
APPicker,
NPPicker,
SandwichPicker,
// blocks
Block,
// misc
Hider,
useStickyState,
VerbFromDisplay,
VerbInfo,
VPExplorer,
EPExplorer,
playAudio,
roleIcon,
vpsReducer,
VpsReducerAction,
makeVPSelectionState,
RootsAndStems,
VPDisplay,
VPPicker,
NPDisplay,
HumanReadableInflectionPattern,
psJSXMap,
genderColors,
};

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "@lingdocs/ps-react", "name": "@lingdocs/ps-react",
"version": "7.5.1", "version": "7.6.3",
"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",
@ -18,35 +18,24 @@
"LICENSE" "LICENSE"
], ],
"scripts": { "scripts": {
"build": "echo \"build from repo root\"", "build": "echo \"build from repo root\""
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
}, },
"author": "lingdocs.com", "author": "lingdocs.com",
"license": "MIT", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@formkit/auto-animate": "^1.0.0-beta.3", "@formkit/auto-animate": "^1.0.0-beta.3",
"classnames": "^2.2.6", "classnames": "^2.5.1",
"fp-ts": "^2.16.0", "fp-ts": "^2.16.0",
"jsurl2": "^2.1.0", "lokijs": "^1.5.12",
"lz-string": "^1.4.4", "lz-string": "^1.4.4",
"pbf": "^3.2.1", "micro-memoize": "^4.1.2",
"rambda": "^7.3.0", "rambda": "^7.3.0",
"react-bootstrap": "^1.5.1",
"react-error-boundary": "^4.0.13", "react-error-boundary": "^4.0.13",
"react-select": "^5.4.0" "react-select": "^5.4.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.21.0", "@types/lokijs": "^1.5.14",
"@storybook/addon-actions": "^6.5.16", "fs-extra": "^11.2.0"
"@storybook/addon-essentials": "^6.5.16",
"@storybook/addon-interactions": "^6.5.16",
"@storybook/addon-links": "^6.5.16",
"@storybook/builder-webpack4": "^6.5.16",
"@storybook/manager-webpack4": "^6.5.16",
"@storybook/react": "^6.5.16",
"@storybook/testing-library": "^0.0.13",
"@types/pbf": "^3.0.2",
"babel-loader": "^8.3.0",
"fs-extra": "^10.1.0"
} }
} }

View File

@ -1,48 +0,0 @@
/**
* Copyright (c) 2021 lingdocs.com
*
* This source code is licensed under the GPL3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import classNames from "classnames";
type PickerProps<T extends string> = {
options: { label: any, value: T, color?: string }[],
value: T,
handleChange: (payload: T) => void,
small?: boolean,
xSmall?: boolean,
faded?: boolean,
}
function ButtonSelect<L extends string>(props: PickerProps<L>) {
return <div className="btn-group">
{props.options.map((option) => (
<button
key={option.value}
type="button"
className={classNames(
"btn",
props.faded ? "btn-light" : "btn-outline-secondary",
{ active: props.value === option.value },
{ "btn-sm": props.small || props.xSmall },
)}
onClick={() => props.handleChange(option.value)}
style={{
...props.xSmall ?
{ fontSize: "small" }: {},
...(option.color && (props.value === option.value)) ?
{ backgroundColor: option.color } : {},
}}
>
<span className={classNames([{ "text-on-gender-color": option.color && (props.value === option.value) }])}>
{option.label}
</span>
</button>
))}
</div>
}
export default ButtonSelect;

View File

@ -1,37 +0,0 @@
import { getLength, getLong } from "../../lib/src/p-text-helpers";
import * as T from "../../types";
import Examples from "./Examples";
function CompiledPTextDisplay({ compiled, opts, justify, onlyOne, length }: {
compiled: {
ps: T.SingleOrLengthOpts<T.PsString[]>;
e?: string[] | undefined;
},
opts: T.TextOptions,
justify?: "left" | "right" | "center",
onlyOne?: boolean,
length?: "long" | "short",
}) {
function VariationLayer({ vs }: { vs: T.PsString[] }) {
return <div className="mb-2">
<Examples opts={opts} lineHeight={0}>{vs}</Examples>
</div>;
}
const ps = length
? getLength(compiled.ps, length)
: compiled.ps;
return <div className={justify === "left" ? "text-left" : justify === "right" ? "text-right" : "text-center"}>
{onlyOne
? <VariationLayer vs={[getLong(ps)[0]]} />
: "long" in ps ?
<div>
<VariationLayer vs={ps.long} />
<VariationLayer vs={ps.short} />
{ps.mini && <VariationLayer vs={ps.mini} />}
</div>
: <VariationLayer vs={ps} />
}
</div>;
}
export default CompiledPTextDisplay;

View File

@ -9,65 +9,88 @@
import { createElement, useEffect, useRef } from "react"; import { createElement, useEffect, useRef } from "react";
import classNames from "classnames"; import classNames from "classnames";
import * as T from "../../types"; import * as T from "../../types";
// @ts-expect-error types needed
import autoAnimate from "@formkit/auto-animate"; import autoAnimate from "@formkit/auto-animate";
const caretRight = <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-caret-right-fill" viewBox="0 0 16 16"> const caretRight = (
<path d="M12.14 8.753l-5.482 4.796c-.646.566-1.658.106-1.658-.753V3.204a1 1 0 0 1 1.659-.753l5.48 4.796a1 1 0 0 1 0 1.506z"/> <svg
</svg> xmlns="http://www.w3.org/2000/svg"
const caretDown = <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-caret-down-fill" viewBox="0 0 16 16"> width="16"
<path d="M7.247 11.14L2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z"/> height="16"
</svg> fill="currentColor"
className="bi bi-caret-right-fill"
viewBox="0 0 16 16"
>
<path d="M12.14 8.753l-5.482 4.796c-.646.566-1.658.106-1.658-.753V3.204a1 1 0 0 1 1.659-.753l5.48 4.796a1 1 0 0 1 0 1.506z" />
</svg>
);
const caretDown = (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
className="bi bi-caret-down-fill"
viewBox="0 0 16 16"
>
<path d="M7.247 11.14L2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z" />
</svg>
);
const defaultLevel = 4; const defaultLevel = 4;
const indentAfterLevel = 5; const indentAfterLevel = 5;
function Hider(props: { function Hider(props: {
label: string | JSX.Element, label: string | JSX.Element;
showing: boolean, showing: boolean;
aspect?: T.Aspect, aspect?: T.Aspect;
handleChange: () => void, handleChange: () => void;
children: React.ReactNode, children: React.ReactNode;
hLevel?: number, hLevel?: number;
ignore?: boolean, ignore?: boolean;
}) { }) {
const parent = useRef<HTMLDivElement>(null); const parent = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
parent.current && autoAnimate(parent.current); if (parent.current) {
}, [parent]); autoAnimate(parent.current);
const hLev = Math.min((props.hLevel ? props.hLevel : defaultLevel), 6);
const extraMargin = (props.hLevel && (props.hLevel > indentAfterLevel))
? `ml-${(props.hLevel - indentAfterLevel) + 1}`
: "";
if (props.ignore) {
return <>
{props.children}
</>;
} }
return <div className="mb-3" ref={parent}> }, [parent]);
{createElement( const hLev = Math.min(props.hLevel ? props.hLevel : defaultLevel, 6);
`h${hLev}`, const extraMargin =
{ props.hLevel && props.hLevel > indentAfterLevel
onClick: props.handleChange, ? `ml-${props.hLevel - indentAfterLevel + 1}`
className: classNames( : "";
"clickable", if (props.ignore) {
extraMargin, return <>{props.children}</>;
), }
}, return (
<div className="d-flex flex-row align-items-center"> <div className="mb-3" ref={parent}>
<div style={{ width: "1rem" }}> {createElement(
{props.showing ? caretDown : caretRight} `h${hLev}`,
</div> {
{` `} onClick: props.handleChange,
{props.aspect className: classNames("clickable", extraMargin),
? <i className={`fas fa-${props.aspect === "imperfective" ? "video" : "camera"}`} /> },
: ""} <div className="d-flex flex-row align-items-center">
<div className="ml-2"> <div style={{ width: "1rem" }}>
{props.label} {props.showing ? caretDown : caretRight}
</div> </div>
</div>, {` `}
)} {props.aspect ? (
{props.showing && props.children} <i
className={`fas fa-${
props.aspect === "imperfective" ? "video" : "camera"
}`}
/>
) : (
""
)}
<div className="ml-2">{props.label}</div>
</div>
)}
{props.showing && props.children}
</div> </div>
);
} }
export default Hider; export default Hider;

View File

@ -1,37 +0,0 @@
/**
* Copyright (c) 2021 lingdocs.com
*
* This source code is licensed under the GPL3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import Pashto from "./Pashto";
import Phonetics from "./Phonetics";
import * as T from "../../types";
function InlinePs ({
children,
ps,
opts,
}: ({
ps: T.PsString | (T.PsJSX & { e?: string }),
children?: T.PsString | (T.PsJSX & { e?: string }),
opts: T.TextOptions,
} | {
ps?: T.PsString | (T.PsJSX & { e?: string }),
children: T.PsString | (T.PsJSX & { e?: string }),
opts: T.TextOptions,
})) {
const text = children || ps as T.PsString | (T.PsJSX & { e?: string });
return (
<span>
<Pashto opts={opts}>{text}</Pashto>
{opts.phonetics !== "none" && " - "}
<Phonetics opts={opts}>{text}</Phonetics>
{text.e && <span className="text-muted"> ({text.e})</span>}
</span>
);
}
export default InlinePs;

View File

@ -1,108 +0,0 @@
/**
* Copyright (c) 2021 lingdocs.com
*
* This source code is licensed under the GPL3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { persons } from "../../lib/src/grammar-units";
import InlinePs from "./InlinePs";
import * as T from "../../types";
function PersonSelect(props: {
setting: "subject" | "object",
value: T.Person,
locked?: boolean,
handleChange: (person: T.Person) => void,
handleRandom: () => void,
}) {
return (
!props.locked ? <div className="input-group" style={{ maxWidth: "30rem" }}>
<select
className="custom-select"
value={props.value}
onChange={(e: any) => props.handleChange(
parseInt(e.target.value) as T.Person
)}
>
{persons.map((p) => (
<option value={p.person} key={"subject"+p.person}>
{p.label[props.setting]}
</option>
))}
</select>
<div className="input-group-append" onClick={props.handleRandom}>
<button className="btn btn-secondary">
<i className="fas fa-random" />
</button>
</div>
</div> : <input
className="form-control"
type="text"
placeholder={persons[props.value].label[props.setting]}
readOnly
/>
);
}
function PersonSelection(props: {
subject: T.Person,
object: T.Person,
info: T.NonComboVerbInfo,
handleRandom: (setting: "subject" | "object") => void,
handleChange: (payload: { setting: "subject" | "object", person: T.Person }) => void,
textOptions: T.TextOptions,
}) {
function getComp(comp: T.ObjComplement) {
const c = comp.plural
? comp.plural
: comp.entry;
return <InlinePs opts={props.textOptions}>{c}</InlinePs>;
}
return (
<div className="row align-items-baseline">
<div className="col">
<label className="form-label">
{/* TODO: Should I put the Subject/Agent label back in for non-transitive verbs?? */}
<strong>Subject</strong>
</label>
<PersonSelect
setting="subject"
value={props.subject}
handleChange={(person: T.Person) => props.handleChange({ setting: "subject", person })}
handleRandom={() => props.handleRandom("subject")}
/>
</div>
{(props.info.type === "dynamic compound" || props.info.type === "generative stative compound") ? <div className="col">
<label className="form-label"><strong>Object is the complement ({getComp(props.info.objComplement)})</strong></label>
<PersonSelect
setting="object"
value={props.info.objComplement.person}
locked
handleChange={(person: T.Person) => props.handleChange({ setting: "object", person })}
handleRandom={() => props.handleRandom("object")}
/>
</div> : props.info.transitivity === "transitive" ? <div className="col">
<label className="form-label"><strong>Object</strong></label>
<PersonSelect
setting="object"
value={props.object}
handleChange={(person: T.Person) => props.handleChange({ setting: "object", person })}
handleRandom={() => props.handleRandom("object")}
/>
</div> : props.info.transitivity === "grammatically transitive" ? <div className="col">
<label className="form-label"><strong>Object is unspoken</strong></label>
<PersonSelect
setting="object"
value={10}
locked
handleChange={(person: T.Person) => props.handleChange({ setting: "object", person })}
handleRandom={() => props.handleRandom("object")}
/>
</div> : null}
</div>
);
}
export default PersonSelection;

View File

@ -1,38 +0,0 @@
/**
* Copyright (c) 2021 lingdocs.com
*
* This source code is licensed under the GPL3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {
translatePhonetics,
} from "../../lib/src/translate-phonetics";
import { psJSXMap } from "./jsx-map";
import * as T from "../../types";
const Phonetics = ({ opts, children: text }: {
opts: T.TextOptions,
children: T.PsJSX | T.PsString | string,
}) => {
if (opts.phonetics === "none") {
return null;
}
const handleText = (f: string) => (
opts.phonetics === "lingdocs"
? f
: translatePhonetics(f, {
dialect: opts.dialect,
// @ts-ignore - weird TS not picking up the elimination of "none herre"
system: opts.phonetics,
})
);
return <span className="f-text">
{(typeof text !== "string" && typeof text.f !== "string")
? psJSXMap(text as T.PsJSX, "f", ({f}) => handleText(f))
: handleText(typeof text === "string" ? text : text.f as string)}
</span>
};
export default Phonetics;

View File

@ -1,67 +0,0 @@
import { useState } from "react";
import { filterForVisibleBlocksEP, filterForVisibleBlocksVP } from "../../lib/src/phrase-building/compile";
import * as T from "../../types";
import Block from "./blocks/Block";
import KidDisplay from "./blocks/KidDisplay";
function RenderedBlocksDisplay({ opts, rendered, justify, script }: {
script: "p" | "f",
opts: T.TextOptions,
rendered: T.EPRendered | T.VPRendered,
justify?: "left" | "right" | "center",
}) {
const [variation, setVariation] = useState<number>(0);
// not using autoAnimate here because we need a way to persist the keys in the blocks first
// const parent = useRef(null);
// useEffect(() => {
// parent.current && autoAnimate(parent.current)
// }, [parent]);
const blocksWVars: T.Block[][] = Array.isArray(rendered)
? rendered
: ("omitSubject" in rendered)
? filterForVisibleBlocksEP(rendered.blocks, rendered.omitSubject)
: filterForVisibleBlocksVP(rendered.blocks, rendered.form, rendered.king);
const king = "king" in rendered ? rendered.king : undefined;
const blocks = blocksWVars[variation];
function handleVariationChange() {
setVariation(ov => ((ov + 1) % blocksWVars.length));
}
return <div className={`d-flex flex-row justify-content-${justify ? justify : "center"}`}>
<div className={`d-flex flex-row${script === "p" ? "-reverse" : ""} justify-content-left align-items-end mt-3 pb-2`} style={{ overflowX: "auto" }}>
<div key={blocks[0].key} className="mr-2">
<Block opts={opts} block={blocks[0]} king={king} script={script} />
</div>
<KidsSection key="kidsSection" opts={opts} kids={rendered.kids} script={script} />
{blocks.slice(1).map((block) => (
<div key={block.key} className="mr-2">
<Block opts={opts} block={block} king={king} script={script} />
</div>
))}
<div style={{ height: "100%" }} className="d-flex flex-column justify-content-center">
{blocksWVars.length > 1 && <button onClick={handleVariationChange} className="btn btn-light btn-sm mx-2">V. {variation + 1}/{blocksWVars.length}</button>}
</div>
</div>
</div>
}
function KidsSection({ opts, kids, script }: {
opts: T.TextOptions,
kids: T.Kid[],
script: "p" | "f",
}) {
// not using autoAnimate here because we need a way to persist the keys in the blocks first
// const parent = useRef(null);
// useEffect(() => {
// parent.current && autoAnimate(parent.current)
// }, [parent]);
return kids.length > 0 ? <div className="text-center mx-1 mr-3" style={{ paddingBottom: "1rem"}}>
<div className={`d-flex flex-row${script === "p" ? "-reverse" : ""} mb-3 justify-content-center`}>
{kids.map(kid => (
<KidDisplay key={kid.key} opts={opts} kid={kid} script={script} />
))}
</div>
<div><strong>kids</strong></div>
</div> : null;
}
export default RenderedBlocksDisplay;

View File

@ -1,26 +0,0 @@
/**
* Copyright (c) 2021 lingdocs.com
*
* This source code is licensed under the GPL3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import Pashto from "./Pashto";
import Phonetics from "./Phonetics";
import * as T from "../../types";
function SingleItemDisplay({ item, textOptions, english }: {
item: T.PsString,
textOptions: T.TextOptions,
english?: T.EnglishBlock | string,
}) {
const eng = Array.isArray(english) ? english[0][0] : english;
return <div className="text-center mt-3 mb-2">
<div><Pashto opts={textOptions}>{item}</Pashto></div>
<div><Phonetics opts={textOptions}>{item}</Phonetics></div>
{eng && <div className="text-muted">{eng}</div>}
</div>;
}
export default SingleItemDisplay;

View File

@ -7,11 +7,11 @@
*/ */
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import PersonInfsPicker from "./PersInfsPicker"; import PersonInfsPicker from "./selects/PersInfsPicker";
import InflectionsTable from "./InflectionsTable"; import InflectionsTable from "./tables/InflectionsTable";
import SingleItemDisplay from "./SingleItemDisplay"; import SingleItemDisplay from "./text-display/SingleItemDisplay";
import ButtonSelect from "./ButtonSelect"; import ButtonSelect from "./selects/ButtonSelect";
import VerbTable from "./VerbTable"; import VerbTable from "./tables/VerbTable";
import { import {
getEnglishPersonInfo, getEnglishPersonInfo,
isSentenceForm, isSentenceForm,

View File

@ -1,111 +0,0 @@
/**
* Copyright (c) 2021 lingdocs.com
*
* This source code is licensed under the GPL3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import TableCell from "./TableCell";
import {
psStringEquals,
isAllOne,
addEnglish,
} from "../../lib/src/p-text-helpers";
import { isSentenceForm } from "../../lib/src/misc-helpers";
import * as T from "../../types";
import genderColors from "./gender-colors";
const genderAbbrev = (gender: "masc" | "fem" | undefined): " m." | " f." | "" => (
gender === "masc"
? " m."
: gender === "fem"
? " f."
: ""
);
const minifyTableGender = (block: T.VerbBlock | T.ImperativeBlock): Array<T.PersonLine | {
masc: T.PersonLine,
fem: T.PersonLine,
}> => {
// @ts-ignore
return block.reduce((table, person, i, src) => {
const isFem = i % 2 !== 0;
if (isFem) {
return table;
}
const femPersAhead = src[i+1];
const femPersIsTheSame = (
psStringEquals(person[0][0], femPersAhead[0][0]) &&
psStringEquals(person[1][0], femPersAhead[1][0])
);
if (femPersAhead && !femPersIsTheSame) {
return [...table, {
masc: person,
fem: femPersAhead,
}];
}
return [...table, person];
}, []);
};
function VerbTable({ block, textOptions, english }: {
block: T.VerbBlock | T.ImperativeBlock | T.ArrayOneOrMore<T.PsString>,
english?: T.EnglishBlock | string,
textOptions: T.TextOptions,
}) {
const blockWEng = english ? addEnglish(english, block) : block;
if (isSentenceForm(blockWEng) || isAllOne(blockWEng as T.VerbBlock | T.ImperativeBlock)) {
const item = isSentenceForm(blockWEng)
? block as unknown as T.ArrayOneOrMore<T.PsString>
: (() => {
const b = block as T.ImperativeBlock | T.VerbBlock
return b[0][0];
})();
return <table className="table text-center">
<tbody>
<tr>
<TableCell item={item} textOptions={textOptions} center noBorder />
</tr>
</tbody>
</table>
}
const bl = blockWEng as T.VerbBlock | T.ImperativeBlock;
const b = minifyTableGender(bl);
return <table className="table mt-2" style={{ tableLayout: "fixed" }}>
<thead>
<tr>
<th scope="col" style={{ width: "3rem" }}>Pers.</th>
<th scope="col">Singular</th>
<th scope="col">Plural</th>
</tr>
</thead>
<tbody>
{b.reduce((rows: React.ReactNode[], person, i, arr) => {
function drawRow({ line, gender }: { line: T.PersonLine, gender?: "masc" | "fem" }) {
const pers = arr.length > 1 ? ["1st", "2nd", "3rd"] : ["2nd"];
const rowLabel = `${pers[i]}${genderAbbrev(gender)}`;
const color = !gender
? "inherit"
: genderColors[gender];
return (
<tr key={`${i}${gender}`}>
<th scope="row" style={{ color }}>{rowLabel}</th>
<TableCell item={line[0]} textOptions={textOptions} />
<TableCell item={line[1]} textOptions={textOptions} />
</tr>
);
}
return "masc" in person
? [
...rows,
drawRow({ line: person.masc, gender: "masc" }),
drawRow({ line: person.fem, gender: "fem" }),
]
: [...rows, drawRow({ line: person })];
}, [])}
</tbody>
</table>
}
export default VerbTable;

View File

@ -1,31 +0,0 @@
import { makeAdverbSelection } from "../../../lib/src/phrase-building/make-selections";
import * as T from "../../../types";
import EntrySelect from "../EntrySelect";
function AdverbPicker(props: {
entryFeeder: T.EntryFeederSingleType<T.AdverbEntry>,
adjective: T.AdverbSelection | undefined,
onChange: (p: T.AdverbSelection | undefined) => void,
opts: T.TextOptions,
}) {
function onEntrySelect(entry: T.AdverbEntry | undefined) {
if (!entry) {
return props.onChange(undefined);
}
props.onChange(makeAdverbSelection(entry));
}
return <div style={{ maxWidth: "225px", minWidth: "125px" }}>
<div className="h6">Adverb</div>
<div>
<EntrySelect
value={props.adjective?.entry}
entryFeeder={props.entryFeeder}
onChange={onEntrySelect}
name="Adverb"
opts={props.opts}
/>
</div>
</div>;
}
export default AdverbPicker;

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import * as T from "../../../types"; import * as T from "../../../types";
import SandwichPicker from "../np-picker/SandwichPicker"; import SandwichPicker from "../block-pickers/SandwichPicker";
import AdverbPicker from "./AdverbPicker"; import AdverbPicker from "./AdverbPicker";
type APType = "adverb" | "sandwich"; type APType = "adverb" | "sandwich";
const types: APType[] = ["adverb", "sandwich"]; const types: APType[] = ["adverb", "sandwich"];

View File

@ -1,7 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { makeAdjectiveSelection } from "../../../lib/src/phrase-building/make-selections"; import { makeAdjectiveSelection } from "../../../lib/src/phrase-building/make-selections";
import * as T from "../../../types"; import * as T from "../../../types";
import EntrySelect from "../EntrySelect"; import EntrySelect from "../selects/EntrySelect";
import SandwichPicker from "./SandwichPicker"; import SandwichPicker from "./SandwichPicker";
function AdjectivePicker(props: { function AdjectivePicker(props: {

View File

@ -0,0 +1,33 @@
import { makeAdverbSelection } from "../../../lib/src/phrase-building/make-selections";
import * as T from "../../../types";
import EntrySelect from "../selects/EntrySelect";
function AdverbPicker(props: {
entryFeeder: T.EntryFeederSingleType<T.AdverbEntry>;
adjective: T.AdverbSelection | undefined;
onChange: (p: T.AdverbSelection | undefined) => void;
opts: T.TextOptions;
}) {
function onEntrySelect(entry: T.AdverbEntry | undefined) {
if (!entry) {
return props.onChange(undefined);
}
props.onChange(makeAdverbSelection(entry));
}
return (
<div style={{ maxWidth: "225px", minWidth: "125px" }}>
<div className="h6">Adverb</div>
<div>
<EntrySelect
value={props.adjective?.entry}
entryFeeder={props.entryFeeder}
onChange={onEntrySelect}
name="Adverb"
opts={props.opts}
/>
</div>
</div>
);
}
export default AdverbPicker;

View File

@ -1,8 +1,8 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import * as T from "../../types"; import * as T from "../../../types";
import AdjectivePicker from "./np-picker/AdjectivePicker"; import AdjectivePicker from "./AdjectivePicker";
import LocativeAdverbPicker from "./ep-explorer/eq-comp-picker/LocativeAdverbPicker"; import LocativeAdverbPicker from "./LocativeAdverbPicker";
import SandwichPicker from "./np-picker/SandwichPicker"; import SandwichPicker from "./SandwichPicker";
const compTypes: T.ComplementType[] = [ const compTypes: T.ComplementType[] = [
"adjective", "adjective",
"loc. adv.", "loc. adv.",

View File

@ -1,5 +1,5 @@
import * as T from "../../../types"; import * as T from "../../../types";
import { DeterminerSelect } from "../EntrySelect"; import { DeterminerSelect } from "../selects/EntrySelect";
export default function DeterminersPicker({ export default function DeterminersPicker({
determiners, determiners,

View File

@ -0,0 +1,33 @@
import { makeLocativeAdverbSelection } from "../../../lib/src/phrase-building/make-selections";
import * as T from "../../../types";
import EntrySelect from "../selects/EntrySelect";
function LocativeAdverbPicker(props: {
entryFeeder: T.EntryFeederSingleType<T.LocativeAdverbEntry>;
adjective: T.LocativeAdverbSelection | undefined;
onChange: (p: T.LocativeAdverbSelection | undefined) => void;
opts: T.TextOptions;
}) {
function onEntrySelect(entry: T.LocativeAdverbEntry | undefined) {
if (!entry) {
return props.onChange(undefined);
}
props.onChange(makeLocativeAdverbSelection(entry));
}
return (
<div style={{ maxWidth: "225px", minWidth: "125px" }}>
<div className="h6">Locative Adverb</div>
<div>
<EntrySelect
value={props.adjective?.entry}
entryFeeder={props.entryFeeder}
onChange={onEntrySelect}
name="Locative Adverb"
opts={props.opts}
/>
</div>
</div>
);
}
export default LocativeAdverbPicker;

View File

@ -1,10 +1,10 @@
import { makeNounSelection } from "../../../lib/src/phrase-building/make-selections"; import { makeNounSelection } from "../../../lib/src/phrase-building/make-selections";
import * as T from "../../../types"; import * as T from "../../../types";
import ButtonSelect from "../ButtonSelect"; import ButtonSelect from "../selects/ButtonSelect";
import InlinePs from "../InlinePs"; import InlinePs from "../text-display/InlinePs";
// import { isFemNounEntry, isPattern1Entry, isPattern2Entry, isPattern3Entry, isPattern4Entry, isPattern5Entry, isPattern6FemEntry } from "../../lib/type-predicates"; // import { isFemNounEntry, isPattern1Entry, isPattern2Entry, isPattern3Entry, isPattern4Entry, isPattern5Entry, isPattern6FemEntry } from "../../lib/type-predicates";
import EntrySelect from "../EntrySelect"; import EntrySelect from "../selects/EntrySelect";
import AdjectiveManager from "./AdjectiveManager"; import AdjectiveManager from "./AdjectiveManager";
import { useState } from "react"; import { useState } from "react";
import DeterminersPicker from "./DeterminersPicker"; import DeterminersPicker from "./DeterminersPicker";
@ -136,7 +136,6 @@ function NPNounPicker(props: {
/> />
</div> </div>
)} )}
{/* {showFilter && <div className="mb-2 text-center"> {/* {showFilter && <div className="mb-2 text-center">
<div className="d-flex flex-row justify-content-between"> <div className="d-flex flex-row justify-content-between">
<div className="text-small mb-1">Filter by inflection pattern</div> <div className="text-small mb-1">Filter by inflection pattern</div>
@ -192,9 +191,10 @@ function NPNounPicker(props: {
Compound: Compound:
</div> </div>
<div className="mb-3 text-center"> <div className="mb-3 text-center">
<InlinePs opts={props.opts}> <InlinePs
{{ p: props.noun.entry.p, f: props.noun.entry.f }} opts={props.opts}
</InlinePs> ps={{ p: props.noun.entry.p, f: props.noun.entry.f }}
/>
<div className="text-muted">{props.noun.entry.e}</div> <div className="text-muted">{props.noun.entry.e}</div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,38 @@
import EntrySelect from "../selects/EntrySelect";
import * as T from "../../../types";
import { makeParticipleSelection } from "../../../lib/src/phrase-building/make-selections";
function NPParticiplePicker(props: {
entryFeeder: T.EntryFeederSingleType<T.VerbEntry>;
participle: T.ParticipleSelection | undefined;
onChange: (p: T.ParticipleSelection | undefined) => void;
opts: T.TextOptions;
}) {
function onEntrySelect(entry: T.VerbEntry | undefined) {
if (!entry) {
props.onChange(undefined);
return;
}
props.onChange(makeParticipleSelection(entry));
}
return (
<div style={{ maxWidth: "225px" }}>
<div className="h6">Participle</div>
<EntrySelect
value={props.participle?.verb}
entryFeeder={props.entryFeeder}
onChange={onEntrySelect}
name="Pariticple"
opts={props.opts}
/>
{props.participle && (
<div className="my-2 d-flex flex-row justify-content-around align-items-center">
<div>Masc.</div>
<div>Plur.</div>
</div>
)}
</div>
);
}
export default NPParticiplePicker;

View File

@ -246,6 +246,7 @@ function NPPicker(props: {
pronoun={props.np.selection} pronoun={props.np.selection}
onChange={(p) => onChange({ type: "NP", selection: p })} onChange={(p) => onChange({ type: "NP", selection: p })}
is2ndPersonPicker={props.is2ndPersonPicker} is2ndPersonPicker={props.is2ndPersonPicker}
// @ts-expect-error this is fine
opts={props.opts} opts={props.opts}
/> />
) : npType === "noun" ? ( ) : npType === "noun" ? (

View File

@ -0,0 +1,228 @@
import * as T from "../../../types";
import ButtonSelect from "../selects/ButtonSelect";
import useStickyState from "../useStickyState";
import classNames from "classnames";
import { isSecondPerson } from "../../../lib/src/misc-helpers";
const gColors = {
masc: "LightSkyBlue",
fem: "pink",
};
// TODO: better logic on this
const labels = (role: "subject" | "object" | "ergative" | "possesor") => ({
// persons: [
// ["1st", "1st pl."],
// ["2nd", "2nd pl."],
// ["3rd", "3rd pl."],
// ],
e:
role === "object"
? [
["me", "us"],
["you", "you (pl.)"],
[{ masc: "him/it", fem: "her/it" }, "them"],
]
: role === "possesor"
? [
["my", "our"],
["your", "your (pl.)"],
[{ masc: "his/its", fem: "her/its" }, "their"],
]
: [
["I", "We"],
["You", "You (pl.)"],
[{ masc: "He/It", fem: "She/It" }, "They"],
],
p:
role === "subject"
? {
far: [
["زه", "مونږ"],
["ته", "تاسو"],
["هغه", "هغوی"],
],
near: [
["زه", "مونږ"],
["ته", "تاسو"],
[{ masc: "دی", fem: "دا" }, "دوی"],
],
}
: role === "object"
? {
far: [
["زه", "مونږ"],
["ته", "تاسو"],
[{ masc: "هغهٔ", fem: "هغې" }, "هغوی"],
],
near: [
["زه", "مونږ"],
["ته", "تاسو"],
[{ masc: "دهٔ", fem: "دې" }, "دوی"],
],
}
: role === "possesor"
? {
far: [
["زما", "زمونږ"],
["ستا", "ستاسو"],
[{ masc: "د هغهٔ", fem: "د هغې" }, "د هغوی"],
],
near: [
["زما", "زمونږ"],
["ستا", "ستاسو"],
[{ masc: "د دهٔ", fem: "د دې" }, "د دوی"],
],
}
: {
far: [
["ما", "مونږ"],
["تا", "تاسو"],
[{ masc: "هغهٔ", fem: "هغې" }, "هغوی"],
],
near: [
["ما", "مونږ"],
["تا", "تاسو"],
[{ masc: "دهٔ", fem: "دې" }, "دوی"],
],
},
});
type PickerState = { row: number; col: number; gender: T.Gender };
function personToPickerState(person: T.Person): PickerState {
const col = person > 5 ? 1 : 0;
const row = Math.floor((person > 5 ? person - 6 : person) / 2);
const gender: T.Gender = person % 2 ? "fem" : "masc";
return { col, row, gender };
}
function pickerStateToPerson(s: PickerState): T.Person {
return s.row * 2 + (s.gender === "masc" ? 0 : 1) + 6 * s.col;
}
function NPPronounPicker({
onChange,
pronoun,
role,
is2ndPersonPicker,
}: {
pronoun: T.PronounSelection;
onChange: (p: T.PronounSelection) => void;
role: "object" | "subject" | "ergative" | "possesor";
// opts: T.TextOptions,
is2ndPersonPicker?: boolean;
}) {
if (is2ndPersonPicker && !isSecondPerson(pronoun.person)) {
throw new Error("can't use 2ndPerson NPProunounPicker without a pronoun");
}
const [display, setDisplay] = useStickyState<"p" | "e">(
"e",
"prounoun-picker-display2"
);
const p = personToPickerState(pronoun.person);
function handleClick(row: number, col: number) {
const person = pickerStateToPerson({ ...p, row, col });
onChange({
...pronoun,
person,
});
}
function handleGenderChange(gender: T.Gender) {
const person = pickerStateToPerson({ ...p, gender });
onChange({
...pronoun,
person,
});
}
function handlePronounTypeChange(distance: "far" | "near") {
onChange({
...pronoun,
distance,
});
}
function handleDisplayChange() {
const newPerson = display === "p" ? "e" : "p";
setDisplay(newPerson);
}
const prs = labels(role)[display];
const pSpecA = "near" in prs ? prs[pronoun.distance] : prs;
const pSpec = is2ndPersonPicker ? [pSpecA[1]] : pSpecA;
return (
<div style={{ maxWidth: "145px", padding: 0, margin: "0 auto" }}>
<div className="d-flex flex-row justify-content-between mb-2">
<ButtonSelect
xSmall
options={[
{ label: "Far", value: "far" },
{ label: "Near", value: "near" },
]}
value={pronoun.distance}
handleChange={(g) => handlePronounTypeChange(g as "far" | "near")}
/>
<button
className="btn btn-sm btn-outline-secondary"
onClick={handleDisplayChange}
>
{display === "p" ? "PS" : "EN"}
</button>
</div>
<table
className="table table-bordered table-sm"
style={{ textAlign: "center", minWidth: "100px", tableLayout: "fixed" }}
>
<tbody>
{pSpec.map((rw, i) => (
<tr key={`pronounPickerRow${i}`}>
{rw.map((r, j) => {
const active = is2ndPersonPicker
? p.col === j
: p.row === i && p.col === j;
const content = typeof r === "string" ? r : r[p.gender];
return (
<td
key={`pronounPickerCell${i}${j}`}
onClick={() => {
handleClick(is2ndPersonPicker ? 1 : i, j);
}}
className={classNames({
"table-active": active,
"text-on-gender-color": active,
})}
style={{
backgroundColor: active ? gColors[p.gender] : "inherit",
padding: "0.15rem 0",
}}
>
<div className="my-1">{content}</div>
</td>
);
})}
</tr>
))}
</tbody>
</table>
<div className="text-center">
<ButtonSelect
small
options={[
{
label: <div style={{ margin: "0.15rem" }}>Masc.</div>,
value: "masc",
color: gColors.masc,
},
{
label: <div style={{ margin: "0.15rem" }}>Fem.</div>,
value: "fem",
color: gColors.fem,
},
]}
value={p.gender}
handleChange={(g) => handleGenderChange(g as T.Gender)}
/>
</div>
</div>
);
}
export default NPPronounPicker;

View File

@ -1,6 +1,6 @@
import * as T from "../../../types"; import * as T from "../../../types";
import { sandwiches } from "../../../lib/src/sandwiches"; import { sandwiches } from "../../../lib/src/sandwiches";
import { SandwichSelect } from "../EntrySelect"; import { SandwichSelect } from "../selects/EntrySelect";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import NPPicker from "./NPPicker"; import NPPicker from "./NPPicker";

View File

@ -8,7 +8,7 @@ import {
} from "../../../lib/src/misc-helpers"; } from "../../../lib/src/misc-helpers";
import { useState } from "react"; import { useState } from "react";
import { getLength } from "../../../lib/src/p-text-helpers"; import { getLength } from "../../../lib/src/p-text-helpers";
import { roleIcon } from "../vp-explorer/VPExplorerExplanationModal"; import { roleIcon } from "../role-icons";
import { negativeParticle } from "../../../lib/src/grammar-units"; import { negativeParticle } from "../../../lib/src/grammar-units";
import { flattenLengths } from "../../library"; import { flattenLengths } from "../../library";
@ -139,7 +139,7 @@ function Border({
} }
function VBBlock({ function VBBlock({
opts, // opts,
block, block,
script, script,
}: { }: {
@ -286,7 +286,7 @@ function WeldedBlock({
// } // }
function PerfHeadBlock({ function PerfHeadBlock({
opts, // opts,
ps, ps,
script, script,
}: { }: {
@ -319,7 +319,7 @@ function PerfHeadBlock({
// } // }
function NegBlock({ function NegBlock({
opts, // opts,
imperative, imperative,
script, script,
}: { }: {
@ -339,7 +339,7 @@ function NegBlock({
} }
function EquativeBlock({ function EquativeBlock({
opts, // opts,
eq, eq,
script, script,
}: { }: {
@ -425,7 +425,7 @@ function ObjectBlock({
} }
function NCompBlock({ function NCompBlock({
opts, // opts,
comp, comp,
script, script,
}: { }: {
@ -465,7 +465,7 @@ function ComplementBlock({
inside?: boolean; inside?: boolean;
}) { }) {
function AdjectiveBlock({ function AdjectiveBlock({
opts, // opts,
adj, adj,
}: { }: {
opts: T.TextOptions; opts: T.TextOptions;
@ -486,7 +486,7 @@ function ComplementBlock({
} }
function LocAdvBlock({ function LocAdvBlock({
opts, // opts,
adv, adv,
}: { }: {
opts: T.TextOptions; opts: T.TextOptions;
@ -601,7 +601,7 @@ function Sandwich({
} }
function Determiners({ function Determiners({
opts, // opts,
script, script,
children, children,
}: { }: {
@ -628,7 +628,7 @@ function Determiners({
} }
function CompNounBlock({ function CompNounBlock({
opts, // opts,
noun, noun,
script, script,
}: { }: {
@ -703,7 +703,7 @@ export function NPBlock({
{flattenLengths(np.selection.ps)[0][script]} {flattenLengths(np.selection.ps)[0][script]}
</div>, </div>,
].filter((x) => { ].filter((x) => {
// @ts-ignore // @ts-expect-error yes I know
return x !== " "; return x !== " ";
}); });
const el = script === "p" ? elements.reverse() : elements; const el = script === "p" ? elements.reverse() : elements;
@ -793,7 +793,7 @@ function Possesors({
} }
function Adjectives({ function Adjectives({
opts, // opts,
children, children,
script, script,
}: { }: {

View File

@ -1,21 +1,27 @@
import { baParticle } from "../../../lib/src/grammar-units"; import { baParticle } from "../../../lib/src/grammar-units";
import * as T from "../../../types"; import * as T from "../../../types";
import Pashto from "../Pashto"; import Pashto from "../text-display/Pashto";
import Phonetics from "../Phonetics"; import Phonetics from "../text-display/Phonetics";
function KidDisplay({ opts, kid, script }: { function KidDisplay({
opts: T.TextOptions, opts,
kid: T.Kid, kid,
script: "p" | "f", script,
}: {
opts: T.TextOptions;
kid: T.Kid;
script: "p" | "f";
}) { }) {
const ps = kid.kid.type === "ba" const ps = kid.kid.type === "ba" ? baParticle : kid.kid.ps;
? baParticle return (
: kid.kid.ps; <div className="mx-1" key={kid.key}>
return <div className="mx-1" key={kid.key}> {script === "p" ? (
{script === "p" <Pashto opts={opts} ps={ps} />
? <Pashto opts={opts}>{ps}</Pashto> ) : (
: <Phonetics opts={opts}>{ps}</Phonetics>} <Phonetics opts={opts} ps={ps} />
)}
</div> </div>
);
} }
export default KidDisplay; export default KidDisplay;

View File

@ -0,0 +1,113 @@
import { useState } from "react";
import {
filterForVisibleBlocksEP,
filterForVisibleBlocksVP,
} from "../../../lib/src/phrase-building/compile";
import * as T from "../../../types";
import Block from "./Block";
import KidDisplay from "./KidDisplay";
function RenderedBlocksDisplay({
opts,
rendered,
justify,
script,
}: {
script: "p" | "f";
opts: T.TextOptions;
rendered: T.EPRendered | T.VPRendered;
justify?: "left" | "right" | "center";
}) {
const [variation, setVariation] = useState<number>(0);
// not using autoAnimate here because we need a way to persist the keys in the blocks first
// const parent = useRef(null);
// useEffect(() => {
// parent.current && autoAnimate(parent.current)
// }, [parent]);
const blocksWVars: T.Block[][] = Array.isArray(rendered)
? rendered
: "omitSubject" in rendered
? filterForVisibleBlocksEP(rendered.blocks, rendered.omitSubject)
: filterForVisibleBlocksVP(rendered.blocks, rendered.form, rendered.king);
const king = "king" in rendered ? rendered.king : undefined;
const blocks = blocksWVars[variation];
function handleVariationChange() {
setVariation((ov) => (ov + 1) % blocksWVars.length);
}
return (
<div
className={`d-flex flex-row justify-content-${
justify ? justify : "center"
}`}
>
<div
className={`d-flex flex-row${
script === "p" ? "-reverse" : ""
} justify-content-left align-items-end mt-3 pb-2`}
style={{ overflowX: "auto" }}
>
<div key={blocks[0].key} className="mr-2">
<Block opts={opts} block={blocks[0]} king={king} script={script} />
</div>
<KidsSection
key="kidsSection"
opts={opts}
kids={rendered.kids}
script={script}
/>
{blocks.slice(1).map((block) => (
<div key={block.key} className="mr-2">
<Block opts={opts} block={block} king={king} script={script} />
</div>
))}
<div
style={{ height: "100%" }}
className="d-flex flex-column justify-content-center"
>
{blocksWVars.length > 1 && (
<button
onClick={handleVariationChange}
className="btn btn-light btn-sm mx-2"
>
V. {variation + 1}/{blocksWVars.length}
</button>
)}
</div>
</div>
</div>
);
}
function KidsSection({
opts,
kids,
script,
}: {
opts: T.TextOptions;
kids: T.Kid[];
script: "p" | "f";
}) {
// not using autoAnimate here because we need a way to persist the keys in the blocks first
// const parent = useRef(null);
// useEffect(() => {
// parent.current && autoAnimate(parent.current)
// }, [parent]);
return kids.length > 0 ? (
<div className="text-center mx-1 mr-3" style={{ paddingBottom: "1rem" }}>
<div
className={`d-flex flex-row${
script === "p" ? "-reverse" : ""
} mb-3 justify-content-center`}
>
{kids.map((kid) => (
<KidDisplay key={kid.key} opts={opts} kid={kid} script={script} />
))}
</div>
<div>
<strong>kids</strong>
</div>
</div>
) : null;
}
export default RenderedBlocksDisplay;

View File

@ -1,78 +1,151 @@
import * as T from "../../../types"; import * as T from "../../../types";
import { completeEPSelection, renderEP } from "../../../lib/src/phrase-building/render-ep"; import {
completeEPSelection,
renderEP,
} from "../../../lib/src/phrase-building/render-ep";
import { compileEP } from "../../../lib/src/phrase-building/compile"; import { compileEP } from "../../../lib/src/phrase-building/compile";
import ButtonSelect from "../ButtonSelect"; import ButtonSelect from "../selects/ButtonSelect";
import { getPredicateSelectionFromBlocks, getSubjectSelection, getSubjectSelectionFromBlocks } from "../../../lib/src/phrase-building/blocks-utils"; import {
getPredicateSelectionFromBlocks,
getSubjectSelection,
getSubjectSelectionFromBlocks,
} from "../../../lib/src/phrase-building/blocks-utils";
import { useState } from "react"; import { useState } from "react";
import CompiledPTextDisplay from "../CompiledPTextDisplay"; import CompiledPTextDisplay from "../text-display/CompiledPTextDisplay";
import EPBlocksDisplay from "../RenderedBlocksDisplay"; import EPBlocksDisplay from "../blocks/RenderedBlocksDisplay";
import ModeSelect, { Mode, ScriptSelect } from "../DisplayModeSelect"; import ModeSelect, { Mode, ScriptSelect } from "../selects/DisplayModeSelect";
import useStickyState from "../useStickyState"; import useStickyState from "../useStickyState";
function EPDisplay({ eps, opts, setOmitSubject, justify, onlyOne, length, mode: preferredMode, script: preferredScript }: { function EPDisplay({
eps: T.EPSelectionState, eps,
opts: T.TextOptions, opts,
setOmitSubject: ((value: "true" | "false") => void) | false setOmitSubject,
justify?: "left" | "right" | "center", justify,
onlyOne?: boolean | "concat", onlyOne,
length?: "long" | "short", length,
mode?: Mode, mode: preferredMode,
script?: "p" | "f", script: preferredScript,
}: {
eps: T.EPSelectionState;
opts: T.TextOptions;
setOmitSubject: ((value: "true" | "false") => void) | false;
justify?: "left" | "right" | "center";
onlyOne?: boolean | "concat";
length?: "long" | "short";
mode?: Mode;
script?: "p" | "f";
}) { }) {
const [mode, setMode] = useState<Mode>(preferredMode || "text"); const [mode, setMode] = useState<Mode>(preferredMode || "text");
const [script, setScript] = useStickyState<"p" | "f">(preferredScript || "f", "blockScriptChoice"); const [script, setScript] = useStickyState<"p" | "f">(
const EP = completeEPSelection(eps); preferredScript || "f",
const subject = getSubjectSelection(eps.blocks); "blockScriptChoice"
);
const EP = completeEPSelection(eps);
const subject = getSubjectSelection(eps.blocks);
if (!EP) { if (!EP) {
return <div className="lead text-center my-4"> return (
{(!subject && !eps.predicate[eps.predicate.type]) <div className="lead text-center my-4">
? "Select Subject and Predicate" {!subject && !eps.predicate[eps.predicate.type]
: (subject && !eps.predicate[eps.predicate.type]) ? "Select Subject and Predicate"
? "Select Predicate" : subject && !eps.predicate[eps.predicate.type]
: (!subject && eps.predicate[eps.predicate.type]) ? "Select Predicate"
? "Select Subject" : !subject && eps.predicate[eps.predicate.type]
: ""} ? "Select Subject"
: ""}
</div>
);
}
const rendered = renderEP(EP);
const result = compileEP(rendered);
const renderedSubject = getSubjectSelectionFromBlocks(
rendered.blocks
).selection;
const renderedPredicate = getPredicateSelectionFromBlocks(
rendered.blocks
).selection;
return (
<div className="text-center pt-3">
<div className="mb-2 d-flex flex-row justify-content-between align-items-center">
<div className="d-flex flex-row">
<ModeSelect value={mode} onChange={setMode} />
{mode === "blocks" && (
<ScriptSelect value={script} onChange={setScript} />
)}
</div> </div>
} {setOmitSubject !== false ? (
const rendered = renderEP(EP); <ButtonSelect
const result = compileEP(rendered); small
const renderedSubject = getSubjectSelectionFromBlocks(rendered.blocks).selection; value={(eps.omitSubject ? "true" : "false") as "true" | "false"}
const renderedPredicate = getPredicateSelectionFromBlocks(rendered.blocks).selection; options={[
return <div className="text-center pt-3"> { value: "false", label: "Full" },
<div className="mb-2 d-flex flex-row justify-content-between align-items-center"> { value: "true", label: "No Subj." },
<div className="d-flex flex-row"> ]}
<ModeSelect value={mode} onChange={setMode} /> handleChange={setOmitSubject}
{mode === "blocks" && <ScriptSelect value={script} onChange={setScript} />} />
</div> ) : (
{setOmitSubject !== false ? <ButtonSelect <div />
small )}
value={(eps.omitSubject ? "true" : "false") as "true" | "false"} <div />
options={[ </div>
{ value: "false", label: "Full"}, {mode === "text" ? (
{ value: "true", label: "No Subj."}, <CompiledPTextDisplay
]} opts={opts}
handleChange={setOmitSubject} compiled={result}
/> : <div />} justify={justify}
<div /> onlyOne={!!onlyOne}
length={length || "short"}
/>
) : (
<EPBlocksDisplay
opts={opts}
rendered={rendered}
justify={justify}
script={script}
/>
)}
{result.e && (
<div
className={`text-muted mt-2 text-${
justify === "left"
? "left"
: justify === "right"
? "right"
: "center"
}`}
>
{onlyOne === "concat"
? result.e.join(" • ")
: onlyOne
? [result.e[0]]
: result.e.map((e, i) => <div key={i}>{e}</div>)}
</div> </div>
{mode === "text" )}
? <CompiledPTextDisplay opts={opts} compiled={result} justify={justify} onlyOne={!!onlyOne} length={length || "short"} /> {EP.predicate.selection.selection.type === "participle" && (
: <EPBlocksDisplay opts={opts} rendered={rendered} justify={justify} script={script} />} <div
{result.e && <div className={`text-muted mt-2 text-${justify === "left" ? "left" : justify === "right" ? "right" : "center"}`}> style={{ maxWidth: "6 00px", margin: "0 auto" }}
{onlyOne === "concat" className="alert alert-warning mt-3 pt-4"
? result.e.join(" • ") >
: onlyOne <p>
? [result.e[0]] NOTE: This means that the subject{" "}
: result.e.map((e, i) => <div key={i}>{e}</div>)} {renderedSubject.selection.e
</div>} ? `(${renderedSubject.selection.e})`
{EP.predicate.selection.selection.type === "participle" && <div style={{ maxWidth: "6 00px", margin: "0 auto" }} className="alert alert-warning mt-3 pt-4"> : ""}{" "}
<p> NOTE: This means that the subject {renderedSubject.selection.e ? `(${renderedSubject.selection.e})` : ""} is <strong>the action/idea</strong> of is <strong>the action/idea</strong> of
{` `} {` `}"
"{renderedPredicate.selection.e ? renderedPredicate.selection.e : "the particple"}".</p> {renderedPredicate.selection.e
<p>It <strong>does not</strong> mean that the subject is doing the action, which is what the transaltion sounds like in English.</p> ? renderedPredicate.selection.e
</div>} : "the particple"}
".
</p>
<p>
It <strong>does not</strong> mean that the subject is doing the
action, which is what the transaltion sounds like in English.
</p>
</div>
)}
</div> </div>
);
} }
export default EPDisplay; export default EPDisplay;

View File

@ -1,140 +1,154 @@
import * as T from "../../../types"; import * as T from "../../../types";
import useStickyState, { useStickyReducer } from "../useStickyState"; import useStickyState, { useStickyReducer } from "../useStickyState";
import EPDisplay from "./EPDisplay"; import EPDisplay from "./EPDisplay";
import ButtonSelect from "../ButtonSelect"; import ButtonSelect from "../selects/ButtonSelect";
import EqChartsDisplay from "./EqChartsDisplay"; import EqChartsDisplay from "./EqChartsDisplay";
import epsReducer from "../../../lib/src/phrase-building/eps-reducer"; import epsReducer from "../../../lib/src/phrase-building/eps-reducer";
import { import { useEffect, useRef, useState } from "react";
useEffect,
useRef,
useState,
} from "react";
import { completeEPSelection } from "../../../lib/src/phrase-building/render-ep"; import { completeEPSelection } from "../../../lib/src/phrase-building/render-ep";
import { makeEPSBlocks } from "../../../lib/src/phrase-building/blocks-utils"; import { makeEPSBlocks } from "../../../lib/src/phrase-building/blocks-utils";
// @ts-expect-error types not working for this
import autoAnimate from "@formkit/auto-animate"; import autoAnimate from "@formkit/auto-animate";
// @ts-ignore
import LZString from "lz-string"; import LZString from "lz-string";
import EPPicker from "./EPPicker"; import EPPicker from "./EPPicker";
const epPhraseURLParam = "ep"; const epPhraseURLParam = "ep";
const blankEps: T.EPSelectionState = { const blankEps: T.EPSelectionState = {
blocks: makeEPSBlocks(), blocks: makeEPSBlocks(),
predicate: { predicate: {
type: "Complement", type: "Complement",
NP: undefined, NP: undefined,
Complement: undefined, Complement: undefined,
}, },
equative: { equative: {
tense: "present", tense: "present",
negative: false, negative: false,
}, },
omitSubject: false, omitSubject: false,
}; };
// TODO: put the clear button beside the title in the predicate picker? // TODO: put the clear button beside the title in the predicate picker?
function EPExplorer(props: { function EPExplorer(props: {
opts: T.TextOptions, opts: T.TextOptions;
entryFeeder: T.EntryFeeder, entryFeeder: T.EntryFeeder;
}) { }) {
const [mode, setMode] = useStickyState<"charts" | "phrases">("charts", "EPExplorerMode"); const [mode, setMode] = useStickyState<"charts" | "phrases">(
const [eps, adjustEps] = useStickyReducer( "charts",
epsReducer, "EPExplorerMode"
blankEps, );
"EPState8", const [eps, adjustEps] = useStickyReducer(
flashMessage, epsReducer,
); blankEps,
// const [ "EPState8",
// // alertMsg, flashMessage
// // setAlertMsg, );
// ] = useState<string | undefined>(undefined); // const [
const [showClipped, setShowClipped] = useState<string>(""); // // alertMsg,
const parent = useRef<HTMLDivElement>(null); // // setAlertMsg,
useEffect(() => { // ] = useState<string | undefined>(undefined);
parent.current && autoAnimate(parent.current); const [showClipped, setShowClipped] = useState<string>("");
}, [parent]); const parent = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
const EPSFromUrl = getEPSFromUrl(); if (parent.current) {
if (EPSFromUrl) { autoAnimate(parent.current);
setMode("phrases");
adjustEps({
type: "load EPS",
payload: EPSFromUrl,
});
}
// eslint-disable-next-line
}, []);
function handleEpsChange(e: T.EPSelectionState) {
adjustEps({ type: "load EPS", payload: e });
} }
function flashMessage(msg: string) { }, [parent]);
alert(msg); useEffect(() => {
// for some crazy reason using this alert functionality breaks the flow! const EPSFromUrl = getEPSFromUrl();
// setAlertMsg(msg); if (EPSFromUrl) {
// setTimeout(() => { setMode("phrases");
// setAlertMsg(undefined); adjustEps({
// }, 1500); type: "load EPS",
payload: EPSFromUrl,
});
} }
function flashClippedMessage(m: string) { // eslint-disable-next-line
setShowClipped(m); }, []);
setTimeout(() => { function handleEpsChange(e: T.EPSelectionState) {
setShowClipped(""); adjustEps({ type: "load EPS", payload: e });
}, 1250); }
} function flashMessage(msg: string) {
function handleCopyCode() { alert(msg);
const code = getCode(eps); // for some crazy reason using this alert functionality breaks the flow!
navigator.clipboard.writeText(code); // setAlertMsg(msg);
flashClippedMessage("Copied phrase code to clipboard"); // setTimeout(() => {
} // setAlertMsg(undefined);
function handleCopyShareLink() { // }, 1500);
const shareUrl = getShareUrl(eps); }
navigator.clipboard.writeText(shareUrl); function flashClippedMessage(m: string) {
flashClippedMessage("Copied phrase URL to clipboard"); setShowClipped(m);
} setTimeout(() => {
const phraseIsComplete = !!completeEPSelection(eps); setShowClipped("");
return <div> }, 1250);
<div className="mt-2 mb-3 d-flex flex-row justify-content-between align-items-center"> }
<div /> function handleCopyCode() {
<ButtonSelect const code = getCode(eps);
value={mode} navigator.clipboard.writeText(code);
options={[ flashClippedMessage("Copied phrase code to clipboard");
{ label: "Charts", value: "charts" }, }
{ label: "Phrases", value: "phrases" }, function handleCopyShareLink() {
]} const shareUrl = getShareUrl(eps);
handleChange={setMode} navigator.clipboard.writeText(shareUrl);
/> flashClippedMessage("Copied phrase URL to clipboard");
{!("eps" in props) && <div className="d-flex flex-row"> }
<div const phraseIsComplete = !!completeEPSelection(eps);
className="clickable mr-4" return (
onClick={mode === "phrases" ? handleCopyCode : undefined} <div>
style={{ width: "1rem" }} <div className="mt-2 mb-3 d-flex flex-row justify-content-between align-items-center">
> <div />
{(mode === "phrases" && phraseIsComplete) ? <i className="fas fa-code" /> : ""} <ButtonSelect
</div> value={mode}
<div options={[
className="clickable" { label: "Charts", value: "charts" },
onClick={mode === "phrases" ? handleCopyShareLink : undefined} { label: "Phrases", value: "phrases" },
style={{ width: "1rem" }} ]}
> handleChange={setMode}
{mode === "phrases" ? <i className="fas fa-share-alt" /> : ""} />
</div> {!("eps" in props) && (
</div>} <div className="d-flex flex-row">
</div> <div
{mode === "phrases" && className="clickable mr-4"
<EPPicker onClick={mode === "phrases" ? handleCopyCode : undefined}
opts={props.opts} style={{ width: "1rem" }}
entryFeeder={props.entryFeeder} >
eps={eps} {mode === "phrases" && phraseIsComplete ? (
onChange={handleEpsChange} <i className="fas fa-code" />
/> ) : (
} ""
{mode === "charts" && <EqChartsDisplay opts={props.opts} />} )}
{mode === "phrases" && <EPDisplay </div>
opts={props.opts} <div
eps={eps} className="clickable"
setOmitSubject={"eps" in props ? false : payload => adjustEps({ type: "set omitSubject", payload })} onClick={mode === "phrases" ? handleCopyShareLink : undefined}
/>} style={{ width: "1rem" }}
{/* {alertMsg && <div className="alert alert-warning text-center" role="alert" style={{ >
{mode === "phrases" ? <i className="fas fa-share-alt" /> : ""}
</div>
</div>
)}
</div>
{mode === "phrases" && (
<EPPicker
opts={props.opts}
entryFeeder={props.entryFeeder}
eps={eps}
onChange={handleEpsChange}
/>
)}
{mode === "charts" && <EqChartsDisplay opts={props.opts} />}
{mode === "phrases" && (
<EPDisplay
opts={props.opts}
eps={eps}
setOmitSubject={
"eps" in props
? false
: (payload) => adjustEps({ type: "set omitSubject", payload })
}
/>
)}
{/* {alertMsg && <div className="alert alert-warning text-center" role="alert" style={{
position: "fixed", position: "fixed",
top: "30%", top: "30%",
left: "50%", left: "50%",
@ -143,39 +157,46 @@ function EPExplorer(props: {
}}> }}>
{alertMsg} {alertMsg}
</div>} */} </div>} */}
{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%",
left: "50%", left: "50%",
transform: "translate(-50%, -50%)", transform: "translate(-50%, -50%)",
zIndex: 9999999999999, zIndex: 9999999999999,
}}> }}
{showClipped} >
</div>} {showClipped}
</div>; </div>
)}
</div>
);
} }
export default EPExplorer; export default EPExplorer;
function getShareUrl(eps: T.EPSelectionState): string { function getShareUrl(eps: T.EPSelectionState): string {
const code = getCode(eps); const code = getCode(eps);
const encoded = LZString.compressToEncodedURIComponent(code); const encoded = LZString.compressToEncodedURIComponent(code);
const url = new URL(window.location.href); const url = new URL(window.location.href);
// need to delete or else you could just get a second param written after // need to delete or else you could just get a second param written after
// which gets ignored // which gets ignored
url.searchParams.delete(epPhraseURLParam); url.searchParams.delete(epPhraseURLParam);
url.searchParams.append(epPhraseURLParam, encoded); url.searchParams.append(epPhraseURLParam, encoded);
return url.toString(); return url.toString();
} }
function getCode(eps: T.EPSelectionState): string { function getCode(eps: T.EPSelectionState): string {
return JSON.stringify(eps); return JSON.stringify(eps);
} }
function getEPSFromUrl(): T.EPSelectionState | undefined { function getEPSFromUrl(): T.EPSelectionState | undefined {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const fromParams = params.get(epPhraseURLParam); const fromParams = params.get(epPhraseURLParam);
if (!fromParams) return; if (!fromParams) return;
const decoded = LZString.decompressFromEncodedURIComponent(fromParams); const decoded = LZString.decompressFromEncodedURIComponent(fromParams);
return JSON.parse(decoded) as T.EPSelectionState; return JSON.parse(decoded) as T.EPSelectionState;
} }

View File

@ -1,14 +1,15 @@
import * as T from "../../../types"; import * as T from "../../../types";
import NPPicker from "../np-picker/NPPicker"; import NPPicker from "../block-pickers/NPPicker";
import EquativePicker from "./EquativePicker"; import EquativePicker from "./EquativePicker";
import ButtonSelect from "../ButtonSelect"; import ButtonSelect from "../selects/ButtonSelect";
import ComplementPicker from "../ComplementPicker"; import ComplementPicker from "../block-pickers/ComplementPicker";
import epsReducer, { import epsReducer, {
EpsReducerAction, EpsReducerAction,
} from "../../../lib/src/phrase-building/eps-reducer"; } from "../../../lib/src/phrase-building/eps-reducer";
import { useEffect, useRef } from "react"; import { useEffect, useRef } 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 "../block-pickers/APPicker";
// @ts-expect-error types not working for this
import autoAnimate from "@formkit/auto-animate"; import autoAnimate from "@formkit/auto-animate";
function EPPicker({ function EPPicker({
@ -24,7 +25,9 @@ function EPPicker({
}) { }) {
const parent = useRef<HTMLDivElement>(null); const parent = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
parent.current && autoAnimate(parent.current); if (parent.current) {
autoAnimate(parent.current);
}
}, [parent]); }, [parent]);
function adjustEps(action: EpsReducerAction) { function adjustEps(action: EpsReducerAction) {
onChange(epsReducer(eps, action)); onChange(epsReducer(eps, action));

View File

@ -1,82 +1,107 @@
import * as T from "../../../types" import * as T from "../../../types";
import Select from "react-select"; import Select from "react-select";
import ButtonSelect from "../ButtonSelect"; import ButtonSelect from "../selects/ButtonSelect";
import { epTenseOptions as options } from "./epTenseOptions"; import { epTenseOptions as options } from "./epTenseOptions";
import { customStyles } from "../EntrySelect"; import { customSelectStyles as customStyles } from "../selects/select-styles";
function EquativePicker({ equative, onChange, hideNegative }: { function EquativePicker({
equative: { tense: T.EquativeTense, negative: boolean }, equative,
onChange: (e: { tense: T.EquativeTense, negative: boolean }) => void, onChange,
hideNegative?: boolean, hideNegative,
}: {
equative: { tense: T.EquativeTense; negative: boolean };
onChange: (e: { tense: T.EquativeTense; negative: boolean }) => void;
hideNegative?: boolean;
}) { }) {
function onTenseSelect(o: { value: T.EquativeTense } | null) { function onTenseSelect(o: { value: T.EquativeTense } | null) {
const value = o?.value ? o.value : undefined; const value = o?.value ? o.value : undefined;
if (!value) { if (!value) {
return; return;
}
onChange({
...equative,
tense: value,
});
}
function moveTense(dir: "forward" | "back") {
return () => {
const currIndex = options.findIndex((tn) => tn.value === equative.tense);
if (currIndex === -1) {
console.error("error moving tense", dir);
return;
}
const newIndex =
dir === "forward"
? (currIndex + 1) % options.length
: currIndex === 0
? options.length - 1
: currIndex - 1;
const newTense = options[newIndex];
onChange({
...equative,
tense: newTense.value,
});
};
}
function onPosNegSelect(value: "true" | "false") {
onChange({
...equative,
negative: value === "true",
});
}
return (
<div>
<div style={{ maxWidth: "300px", minWidth: "250px", margin: "0 auto" }}>
<div className="h5">Tense:</div>
<Select
isSearchable={false}
// for some reason can't use tOptions with find here;
value={options.find((o) => o.value === equative.tense)}
// @ts-expect-error issues with react-select
onChange={onTenseSelect}
className="mb-2"
options={options}
styles={customStyles}
/>
{
<div
className="d-flex flex-row justify-content-between align-items-center mt-3 mb-1"
style={{ width: "100%" }}
>
<div
className="btn btn-light clickable"
onClick={moveTense("back")}
>
<i className="fas fa-chevron-left" />
</div>
{!hideNegative && (
<ButtonSelect
small
value={equative.negative.toString() as "true" | "false"}
options={[
{
label: "Pos.",
value: "false",
},
{
label: "Neg.",
value: "true",
},
]}
handleChange={onPosNegSelect}
/>
)}
<div
onClick={moveTense("forward")}
className="btn btn-light clickable"
>
<i className="fas fa-chevron-right" />
</div>
</div>
} }
onChange({ </div>
...equative, </div>
tense: value, );
});
}
function moveTense(dir: "forward" | "back") {
return () => {
const currIndex = options.findIndex(tn => tn.value === equative.tense)
if (currIndex === -1) {
console.error("error moving tense", dir);
return;
}
const newIndex = dir === "forward"
? ((currIndex + 1) % options.length)
: (currIndex === 0 ? (options.length - 1) : (currIndex - 1))
const newTense = options[newIndex];
onChange({
...equative,
tense: newTense.value,
});
};
}
function onPosNegSelect(value: "true" | "false") {
onChange({
...equative,
negative: value === "true",
});
}
return <div>
<div style={{ maxWidth: "300px", minWidth: "250px", margin: "0 auto" }}>
<div className="h5">Tense:</div>
<Select
isSearchable={false}
// for some reason can't use tOptions with find here;
value={options.find(o => o.value === equative.tense)}
// @ts-ignore
onChange={onTenseSelect}
className="mb-2"
options={options}
styles={customStyles}
/>
{<div className="d-flex flex-row justify-content-between align-items-center mt-3 mb-1" style={{ width: "100%" }}>
<div className="btn btn-light clickable" onClick={moveTense("back")}>
<i className="fas fa-chevron-left" />
</div>
{!hideNegative && <ButtonSelect
small
value={equative.negative.toString() as "true" | "false"}
options={[{
label: "Pos.",
value: "false",
}, {
label: "Neg.",
value: "true",
}]}
handleChange={onPosNegSelect}
/>}
<div onClick={moveTense("forward")} className="btn btn-light clickable">
<i className="fas fa-chevron-right" />
</div>
</div>}
</div>
</div>;
} }
export default EquativePicker; export default EquativePicker;

View File

@ -1,31 +0,0 @@
import { makeLocativeAdverbSelection } from "../../../../lib/src/phrase-building/make-selections";
import * as T from "../../../../types";
import EntrySelect from "../../EntrySelect";
function LocativeAdverbPicker(props: {
entryFeeder: T.EntryFeederSingleType<T.LocativeAdverbEntry>,
adjective: T.LocativeAdverbSelection | undefined,
onChange: (p: T.LocativeAdverbSelection | undefined) => void,
opts: T.TextOptions,
}) {
function onEntrySelect(entry: T.LocativeAdverbEntry | undefined) {
if (!entry) {
return props.onChange(undefined);
}
props.onChange(makeLocativeAdverbSelection(entry));
}
return <div style={{ maxWidth: "225px", minWidth: "125px" }}>
<div className="h6">Locative Adverb</div>
<div>
<EntrySelect
value={props.adjective?.entry}
entryFeeder={props.entryFeeder}
onChange={onEntrySelect}
name="Locative Adverb"
opts={props.opts}
/>
</div>
</div>;
}
export default LocativeAdverbPicker;

View File

@ -1,26 +0,0 @@
import { psJSXMap } from "./jsx-map";
test("psJSXMap should work with p as a target", () => {
const input = { p: <>زه کور ته <strong>ځم</strong></>, f: <>zu kor ta <strong>dzum</strong></> };
const output = psJSXMap(input, "p", (ps) => ps.p.replace(/ځ/g, "ز"));
expect(output).toEqual(<>زه کور ته <strong>زم</strong></>);
});
test("psJSXMap should work with f as a target", () => {
const input = { p: <>زه کور ته <strong>ځم</strong></>, f: <>zu kor ta <strong>dzum</strong></> };
const output = psJSXMap(input, "f", (ps) => ps.f.replace(/dz/g, "z"));
expect(output).toEqual(<>zu kor ta <strong>zum</strong></>);
});
test("psJSXMap should work with single child", () => {
const input = { p: <div><span>کور</span></div>, f: <div><span>kor</span></div> };
const output = psJSXMap(input, "f", (ps) => ps.f.replace(/k/g, "q"));
expect(output).toEqual(<div><span>qor</span></div>);
});
test("psJSXMap will error if given an uneven/unbalanced pair of JSX Elements", () => {
expect(() => {
const input = { p: <>زه کور ته <strong>ځم</strong></>, f: <>zu kor ta dzum</> };
psJSXMap(input, "p", (ps) => ps.p.replace(/ځ/g, "ز"));
}).toThrow("error mapping out PsJSX - unbalanced trees");
});

View File

@ -1,62 +0,0 @@
/**
* Copyright (c) 2021 lingdocs.com
*
* This source code is licensed under the GPL3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import * as T from "../../types";
/**
* Allows PsString transforming methods to be applied to a Pashto/Phonetics set of JSX elements
* outputs a single part of the pair (p or f) with the transform function applied to all the text
* within
* eg: { p: <>زه <strong>ځم</strong></>, f : <>zu <strong>dzum</strong></> }
*
* @param ps
* @param target
* @param dealWithString
* @returns
*/
export function psJSXMap(ps: T.PsJSX, target: "p" | "f", dealWithString: (ps: T.PsString) => string | JSX.Element): JSX.Element {
const base = ps[target];
const sec = ps[target === "p" ? "f" : "p"];
try {
return {
...base,
props: {
// TODO: could make this a lot cleaner/less repetitive by following a better recursive formula
// see https://adueck.github.io/blog/recursively-modify-text-jsx-react/
...base.props,
children: typeof base.props.children === "string"
? dealWithString({
p: (target === "p" ? base : sec).props.children,
f: (target === "p" ? sec : base).props.children,
})
: base.props.children.map
? base.props.children.map((x: string | JSX.Element, i: number) => (
(typeof x === "string")
? dealWithString({
p: (target === "p" ? x : sec.props.children[i]),
f: (target === "p" ? sec.props.children[i] : x),
})
: psJSXMap({
p: (target === "p" ? x : sec.props.children[i]),
f: (target === "p" ? sec.props.children[i] : x),
}, target, dealWithString)
)) : (typeof base.props.children === "string")
? dealWithString({
p: (target === "p" ? base.props.children : sec.props.children),
f: (target === "p" ? sec.props.children : base.props.children),
})
: psJSXMap({
p: (target === "p" ? base.props.children : sec.props.children),
f: (target === "p" ? sec.props.children : base.props.children),
}, target, dealWithString)
},
};
} catch (e) {
throw new Error("error mapping out PsJSX - unbalanced trees");
}
}

View File

@ -1,34 +0,0 @@
import EntrySelect from "../EntrySelect";
import * as T from "../../../types";
import { makeParticipleSelection } from "../../../lib/src/phrase-building/make-selections";
function NPParticiplePicker(props: {
entryFeeder: T.EntryFeederSingleType<T.VerbEntry>,
participle: T.ParticipleSelection | undefined,
onChange: (p: T.ParticipleSelection | undefined) => void,
opts: T.TextOptions,
}) {
function onEntrySelect(entry: T.VerbEntry | undefined) {
if (!entry) {
props.onChange(undefined);
return;
}
props.onChange(makeParticipleSelection(entry));
}
return <div style={{ maxWidth: "225px" }}>
<div className="h6">Participle</div>
<EntrySelect
value={props.participle?.verb}
entryFeeder={props.entryFeeder}
onChange={onEntrySelect}
name="Pariticple"
opts={props.opts}
/>
{props.participle && <div className="my-2 d-flex flex-row justify-content-around align-items-center">
<div>Masc.</div>
<div>Plur.</div>
</div>}
</div>;
}
export default NPParticiplePicker;

View File

@ -1,198 +0,0 @@
import * as T from "../../../types";
import ButtonSelect from "../ButtonSelect";
import useStickyState from "../useStickyState";
import classNames from "classnames";
import {
isSecondPerson,
} from "../../../lib/src/misc-helpers";
const gColors = {
masc: "LightSkyBlue",
fem: "pink",
};
// TODO: better logic on this
const labels = (role: "subject" | "object" | "ergative" | "possesor") => ({
// persons: [
// ["1st", "1st pl."],
// ["2nd", "2nd pl."],
// ["3rd", "3rd pl."],
// ],
e: role === "object" ? [
["me", "us"],
["you", "you (pl.)"],
[{ masc: "him/it", fem: "her/it"}, "them"],
] : role === "possesor" ? [
["my", "our"],
["your", "your (pl.)"],
[{ masc: "his/its", fem: "her/its"}, "their"],
] : [
["I", "We"],
["You", "You (pl.)"],
[{ masc: "He/It", fem: "She/It"}, "They"],
],
p: role === "subject" ? {
far: [
["زه", "مونږ"],
["ته", "تاسو"],
["هغه", "هغوی"],
],
near: [
["زه", "مونږ"],
["ته", "تاسو"],
[{ masc: "دی", fem: "دا" }, "دوی"],
],
} : role === "object" ? {
far: [
["زه", "مونږ"],
["ته", "تاسو"],
[{ masc: "هغهٔ", fem: "هغې" }, "هغوی"],
],
near: [
["زه", "مونږ"],
["ته", "تاسو"],
[{ masc: "دهٔ", fem: "دې" }, "دوی"],
],
} : role === "possesor" ? {
far: [
["زما", "زمونږ"],
["ستا", "ستاسو"],
[{ masc: "د هغهٔ", fem: "د هغې" }, "د هغوی"],
],
near: [
["زما", "زمونږ"],
["ستا", "ستاسو"],
[{ masc: "د دهٔ", fem: "د دې" }, "د دوی"],
],
} : {
far: [
["ما", "مونږ"],
["تا", "تاسو"],
[{ masc: "هغهٔ", fem: "هغې" }, "هغوی"],
],
near: [
["ما", "مونږ"],
["تا", "تاسو"],
[{ masc: "دهٔ", fem: "دې" }, "دوی"],
],
},
});
type PickerState = { row: number, col: number, gender: T.Gender };
function personToPickerState(person: T.Person): PickerState {
const col = person > 5 ? 1 : 0;
const row = Math.floor((person > 5 ? (person - 6) : person) / 2);
const gender: T.Gender = (person % 2) ? "fem" : "masc";
return { col, row, gender };
}
function pickerStateToPerson(s: PickerState): T.Person {
return (s.row * 2)
+ (s.gender === "masc" ? 0 : 1)
+ (6 * s.col);
}
function NPPronounPicker({ onChange, pronoun, role, opts, is2ndPersonPicker }: {
pronoun: T.PronounSelection,
onChange: (p: T.PronounSelection) => void,
role: "object" | "subject" | "ergative" | "possesor",
opts: T.TextOptions,
is2ndPersonPicker?: boolean,
}) {
if (is2ndPersonPicker && !isSecondPerson(pronoun.person)) {
throw new Error("can't use 2ndPerson NPProunounPicker without a pronoun");
}
const [display, setDisplay] = useStickyState<"p" | "e">("e", "prounoun-picker-display2");
const p = personToPickerState(pronoun.person);
function handleClick(row: number, col: number) {
const person = pickerStateToPerson({ ...p, row, col });
onChange({
...pronoun,
person,
});
}
function handleGenderChange(gender: T.Gender) {
const person = pickerStateToPerson({ ...p, gender });
onChange({
...pronoun,
person,
});
}
function handlePronounTypeChange(distance: "far" | "near") {
onChange({
...pronoun,
distance,
});
}
function handleDisplayChange() {
const newPerson = display === "p"
? "e"
: "p";
setDisplay(newPerson);
}
const prs = labels(role)[display];
const pSpecA = "near" in prs ? prs[pronoun.distance] : prs;
const pSpec = is2ndPersonPicker
? [pSpecA[1]]
: pSpecA;
return <div style={{ maxWidth: "145px", padding: 0, margin: "0 auto" }}>
<div className="d-flex flex-row justify-content-between mb-2">
<ButtonSelect
xSmall
options={[
{ label: "Far", value: "far" },
{ label: "Near", value: "near" },
]}
value={pronoun.distance}
handleChange={(g) => handlePronounTypeChange(g as "far" | "near")}
/>
<button className="btn btn-sm btn-outline-secondary" onClick={handleDisplayChange}>
{display === "p" ? "PS" : "EN"}
</button>
</div>
<table className="table table-bordered table-sm" style={{ textAlign: "center", minWidth: "100px", tableLayout: "fixed" }}>
<tbody>
{pSpec.map((rw, i) => (
<tr key={`pronounPickerRow${i}`}>
{rw.map((r, j) => {
const active = is2ndPersonPicker
? (p.col === j)
: (p.row === i && p.col === j);
const content = typeof r === "string" ? r : r[p.gender];
return <td
key={`pronounPickerCell${i}${j}`}
onClick={() => {
handleClick(is2ndPersonPicker ? 1 : i, j);
}}
className={classNames({ "table-active": active, "text-on-gender-color": active })}
style={{
backgroundColor: active ? gColors[p.gender] : "inherit",
padding: "0.15rem 0",
}}
>
<div className="my-1">
{content}
</div>
</td>;
})}
</tr>
))}
</tbody>
</table>
<div className="text-center">
<ButtonSelect
small
options={[
{ label: <div style={{ margin: "0.15rem"}}>Masc.</div>, value: "masc", color: gColors.masc },
{ label: <div style={{ margin: "0.15rem"}}>Fem.</div>, value: "fem", color: gColors.fem },
]}
value={p.gender}
handleChange={(g) => handleGenderChange(g as T.Gender)}
/>
</div>
</div>;
};
export default NPPronounPicker;

View File

@ -1,11 +1,13 @@
export default function playAudio(a: string) { export default function playAudio(a: string) {
if (!a) return; if (!a) return;
let audio = new Audio(`https://verbs.lingdocs.com/sounds/${a}.mp3`); const audio = new Audio(
audio.addEventListener("ended", () => { `https://pashto-inflector.lingdocs.com/sounds/${a}.mp3`
audio.remove(); );
audio.srcObject = null; audio.addEventListener("ended", () => {
}); audio.remove();
audio.play().catch((e) => { audio.srcObject = null;
console.error(e); });
}); audio.play().catch((e) => {
console.error(e);
});
} }

View File

@ -0,0 +1,4 @@
export const roleIcon = {
king: <i className="mx-1 fas fa-crown" />,
servant: <i className="mx-1 fas fa-male" />,
};

View File

@ -0,0 +1,57 @@
/**
* Copyright (c) 2021 lingdocs.com
*
* This source code is licensed under the GPL3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import classNames from "classnames";
type PickerProps<T extends string> = {
options: { label: React.ReactNode; value: T; color?: string }[];
value: T;
handleChange: (payload: T) => void;
small?: boolean;
xSmall?: boolean;
faded?: boolean;
};
function ButtonSelect<L extends string>(props: PickerProps<L>) {
return (
<div className="btn-group">
{props.options.map((option) => (
<button
key={option.value}
type="button"
className={classNames(
"btn",
props.faded ? "btn-light" : "btn-outline-secondary",
{ active: props.value === option.value },
{ "btn-sm": props.small || props.xSmall }
)}
onClick={() => props.handleChange(option.value)}
style={{
...(props.xSmall ? { fontSize: "small" } : {}),
...(option.color && props.value === option.value
? { backgroundColor: option.color }
: {}),
}}
>
<span
className={classNames([
{
"text-on-gender-color":
option.color && props.value === option.value,
},
])}
>
{option.label}
</span>
</button>
))}
</div>
);
}
export default ButtonSelect;

View File

@ -1,31 +1,12 @@
import * as T from "../../types"; import * as T from "../../../types";
import { StyleHTMLAttributes } from "react"; import { StyleHTMLAttributes } from "react";
import Select, { StylesConfig } from "react-select"; import Select 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 "../block-pickers/picker-tools";
import { customSelectStyles as customStyles } from "./select-styles";
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,
}),
};
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>;
@ -81,7 +62,7 @@ function EntrySelect<E extends T.DictionaryEntry | T.VerbEntry>(props: {
isSearchable={true} isSearchable={true}
className="mb-2" className="mb-2"
value={value} value={value}
// @ts-ignore // @ts-expect-error select issue
onChange={onChange} onChange={onChange}
defaultOptions={[]} defaultOptions={[]}
loadOptions={options} loadOptions={options}
@ -120,7 +101,7 @@ function EntrySelect<E extends T.DictionaryEntry | T.VerbEntry>(props: {
styles={customStyles} styles={customStyles}
isSearchable={true} isSearchable={true}
value={value || null} value={value || null}
// @ts-ignore - sadly gets messed up when using customStyles // @ts-expect-error select issue
onChange={onChange} onChange={onChange}
className="mb-2" className="mb-2"
options={options} options={options}
@ -159,7 +140,7 @@ export function DeterminerSelect(props: {
isSearchable={true} isSearchable={true}
value={value} value={value}
isMulti isMulti
// @ts-ignore - gets messed up when using customStyles // @ts-expect-error select issue
onChange={onChange} onChange={onChange}
className="mb-2" className="mb-2"
options={options} options={options}
@ -216,7 +197,7 @@ export function SandwichSelect<E extends T.Sandwich>(props: {
styles={customStyles} styles={customStyles}
isSearchable={true} isSearchable={true}
value={value} value={value}
// @ts-ignore - gets messed up when using customStyles // @ts-expect-error select issue
onChange={onChange} onChange={onChange}
className="mb-2" className="mb-2"
options={options} options={options}

View File

@ -6,7 +6,7 @@
* *
*/ */
import * as T from "../../types"; import * as T from "../../../types";
const persInfs: { const persInfs: {
label: string; label: string;
@ -35,7 +35,7 @@ function PersInfsPicker(props: {
persInf: T.PersonInflectionsField; persInf: T.PersonInflectionsField;
handleChange: (persInf: T.PersonInflectionsField) => void; handleChange: (persInf: T.PersonInflectionsField) => void;
}) { }) {
function hChange(e: any) { function hChange(e: React.ChangeEvent<HTMLSelectElement>) {
const newValue = e.target.value as T.PersonInflectionsField; const newValue = e.target.value as T.PersonInflectionsField;
props.handleChange(newValue); props.handleChange(newValue);
} }

View File

@ -0,0 +1,134 @@
/**
* Copyright (c) 2021 lingdocs.com
*
* This source code is licensed under the GPL3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { persons } from "../../../lib/src/grammar-units";
import InlinePs from "../text-display/InlinePs";
import * as T from "../../../types";
function PersonSelect(props: {
setting: "subject" | "object";
value: T.Person;
locked?: boolean;
handleChange: (person: T.Person) => void;
handleRandom: () => void;
}) {
return !props.locked ? (
<div className="input-group" style={{ maxWidth: "30rem" }}>
<select
className="custom-select"
value={props.value}
onChange={(e) =>
props.handleChange(parseInt(e.target.value) as T.Person)
}
>
{persons.map((p) => (
<option value={p.person} key={"subject" + p.person}>
{p.label[props.setting]}
</option>
))}
</select>
<div className="input-group-append" onClick={props.handleRandom}>
<button className="btn btn-secondary">
<i className="fas fa-random" />
</button>
</div>
</div>
) : (
<input
className="form-control"
type="text"
placeholder={persons[props.value].label[props.setting]}
readOnly
/>
);
}
function PersonSelection(props: {
subject: T.Person;
object: T.Person;
info: T.NonComboVerbInfo;
handleRandom: (setting: "subject" | "object") => void;
handleChange: (payload: {
setting: "subject" | "object";
person: T.Person;
}) => void;
textOptions: T.TextOptions;
}) {
function getComp(comp: T.ObjComplement) {
const c = comp.plural ? comp.plural : comp.entry;
return <InlinePs opts={props.textOptions} ps={c} />;
}
return (
<div className="row align-items-baseline">
<div className="col">
<label className="form-label">
{/* TODO: Should I put the Subject/Agent label back in for non-transitive verbs?? */}
<strong>Subject</strong>
</label>
<PersonSelect
setting="subject"
value={props.subject}
handleChange={(person: T.Person) =>
props.handleChange({ setting: "subject", person })
}
handleRandom={() => props.handleRandom("subject")}
/>
</div>
{props.info.type === "dynamic compound" ||
props.info.type === "generative stative compound" ? (
<div className="col">
<label className="form-label">
<strong>
Object is the complement ({getComp(props.info.objComplement)})
</strong>
</label>
<PersonSelect
setting="object"
value={props.info.objComplement.person}
locked
handleChange={(person: T.Person) =>
props.handleChange({ setting: "object", person })
}
handleRandom={() => props.handleRandom("object")}
/>
</div>
) : props.info.transitivity === "transitive" ? (
<div className="col">
<label className="form-label">
<strong>Object</strong>
</label>
<PersonSelect
setting="object"
value={props.object}
handleChange={(person: T.Person) =>
props.handleChange({ setting: "object", person })
}
handleRandom={() => props.handleRandom("object")}
/>
</div>
) : props.info.transitivity === "grammatically transitive" ? (
<div className="col">
<label className="form-label">
<strong>Object is unspoken</strong>
</label>
<PersonSelect
setting="object"
value={10}
locked
handleChange={(person: T.Person) =>
props.handleChange({ setting: "object", person })
}
handleRandom={() => props.handleRandom("object")}
/>
</div>
) : null}
</div>
);
}
export default PersonSelection;

View File

@ -0,0 +1,25 @@
import { StylesConfig } from "react-select";
export const customSelectStyles: StylesConfig = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
menuPortal: (base: any) => ({
...base,
zIndex: 99999,
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
menu: (base: any) => ({
...base,
zIndex: 999999,
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
option: (provided: any) => ({
...provided,
padding: "10px 5px",
color: "#121418",
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
input: (base: any) => ({
...base,
padding: 0,
}),
};

View File

@ -1,31 +0,0 @@
import React, { useState } from "react";
import opts from "../../../lib/src/default-text-options";
import ButtonSelect from "../ButtonSelect";
import { Story } from "@storybook/react";
type ButtonSelectProps = Parameters<typeof ButtonSelect>[0];
export default {
title: "ButtonSelect",
component: ButtonSelect,
};
const Template: Story<ButtonSelectProps> = (args) => {
const [value, setValue] = useState<string>("a");
return <div>
<ButtonSelect
{...args}
value={value}
handleChange={e => setValue(e)}
/>
</div>;
}
export const Basic = Template.bind({});
Basic.args = {
options: [
{ value: "a", label: "Option A" },
{ value: "b", label: "Option B" },
{ value: "c", label: "Option C" },
],
};

View File

@ -1,26 +0,0 @@
import React from "react";
import opts from "../../../lib/src/default-text-options";
import Examples from "../Examples";
import { Story } from "@storybook/react";
type ExamplesProps = Parameters<typeof Examples>[0];
export default {
title: "Examples",
component: Examples,
};
const Template: Story<ExamplesProps> = (args) => <div>
<p>Before example</p>
<Examples {...args} />
<p>After Example</p>
</div>;
export const Basic = Template.bind({});
Basic.args = {
opts: opts,
ex: [
{ p: "زه کور ته ځم", f: "zu kor ta dzum", e: "I'm going home" },
{ p: "مونږ کور ته ځو", f: "moonG kor ta dzoo", e: "We're going home" },
],
};

View File

@ -1,5 +1,5 @@
import * as T from "../../types"; import * as T from "../../../types";
import InlinePs from "./InlinePs"; import InlinePs from "./../text-display/InlinePs";
export default function HumanReadableInflectionPattern( export default function HumanReadableInflectionPattern(
p: T.InflectionPattern, p: T.InflectionPattern,
@ -10,12 +10,12 @@ export default function HumanReadableInflectionPattern(
) : p === 2 ? ( ) : p === 2 ? (
<span> <span>
#2 Unstressed{" "} #2 Unstressed{" "}
<InlinePs opts={textOptions}>{{ p: "ی", f: "ay", e: "" }}</InlinePs> <InlinePs opts={textOptions} ps={{ p: "ی", f: "ay", e: "" }} />
</span> </span>
) : p === 3 ? ( ) : p === 3 ? (
<span> <span>
#3 Stressed{" "} #3 Stressed{" "}
<InlinePs opts={textOptions}>{{ p: "ی", f: "áy", e: "" }}</InlinePs> <InlinePs opts={textOptions} ps={{ p: "ی", f: "áy", e: "" }} />
</span> </span>
) : p === 4 ? ( ) : p === 4 ? (
<span>#4 "Pashtoon"</span> <span>#4 "Pashtoon"</span>
@ -24,7 +24,7 @@ export default function HumanReadableInflectionPattern(
) : p === 6 ? ( ) : p === 6 ? (
<span> <span>
#6 Fem. inan.{" "} #6 Fem. inan.{" "}
<InlinePs opts={textOptions}>{{ p: "ي", f: "ee", e: "" }}</InlinePs> <InlinePs opts={textOptions} ps={{ p: "ي", f: "ee", e: "" }} />
</span> </span>
) : null; ) : null;
} }

View File

@ -13,8 +13,8 @@
// import Pashto from "./Pashto"; // import Pashto from "./Pashto";
// import { Modal } from "react-bootstrap"; // import { Modal } from "react-bootstrap";
import TableCell from "./TableCell"; import TableCell from "./TableCell";
import * as T from "../../types"; import * as T from "../../../types";
import { isPluralInflections } from "../../lib/src/p-text-helpers"; import { isPluralInflections } from "../../../lib/src/p-text-helpers";
// const explanation = (inf: T.Inflections | T.PluralInflections, textOptions: T.TextOptions) => { // const explanation = (inf: T.Inflections | T.PluralInflections, textOptions: T.TextOptions) => {
// const isPluralInfs = isPluralInflections(inf); // const isPluralInfs = isPluralInflections(inf);
@ -114,6 +114,8 @@ const InflectionTable = ({
<tr key={title}> <tr key={title}>
<th scope="row">{title}</th> <th scope="row">{title}</th>
{"masc" in inf && ( {"masc" in inf && (
// eslint-disable-next-line
// @ts-ignore
<TableCell <TableCell
item={inf.masc[i]} item={inf.masc[i]}
textOptions={textOptions} textOptions={textOptions}
@ -122,6 +124,8 @@ const InflectionTable = ({
/> />
)} )}
{"fem" in inf && (!("masc" in inf) || i < 2) && ( {"fem" in inf && (!("masc" in inf) || i < 2) && (
// eslint-disable-next-line
// @ts-ignore
<TableCell item={inf.fem[i]} textOptions={textOptions} center /> <TableCell item={inf.fem[i]} textOptions={textOptions} center />
)} )}
</tr> </tr>

View File

@ -1,7 +1,7 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import * as T from "../../types"; import * as T from "../../../types";
import Pashto from "./Pashto"; import Pashto from "./../text-display/Pashto";
import Phonetics from "./Phonetics"; import Phonetics from "./../text-display/Phonetics";
const arrowDown = ( const arrowDown = (
<svg <svg
@ -19,7 +19,7 @@ const arrowDown = (
</svg> </svg>
); );
function TableCell({ const TableCell = ({
item, item,
textOptions, textOptions,
center, center,
@ -31,7 +31,7 @@ function TableCell({
center?: boolean; center?: boolean;
noBorder?: boolean; noBorder?: boolean;
colSpan?: 1 | 2; colSpan?: 1 | 2;
}) { }) => {
const [version, setVersion] = useState(0); const [version, setVersion] = useState(0);
useEffect(() => setVersion(0), [item]); useEffect(() => setVersion(0), [item]);
function advanceVersion() { function advanceVersion() {
@ -56,10 +56,10 @@ function TableCell({
> >
<div> <div>
<div> <div>
<Pashto opts={textOptions}>{w}</Pashto> <Pashto opts={textOptions} ps={w} />
</div> </div>
<div> <div>
<Phonetics opts={textOptions}>{w}</Phonetics> <Phonetics opts={textOptions} ps={w} />
</div> </div>
{w.e && {w.e &&
(Array.isArray(w.e) ? ( (Array.isArray(w.e) ? (
@ -83,6 +83,6 @@ function TableCell({
</div> </div>
</td> </td>
); );
} };
export default TableCell; export default TableCell;

View File

@ -0,0 +1,132 @@
/**
* Copyright (c) 2021 lingdocs.com
*
* This source code is licensed under the GPL3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import TableCell from "./TableCell";
import {
psStringEquals,
isAllOne,
addEnglish,
} from "../../../lib/src/p-text-helpers";
import { isSentenceForm } from "../../../lib/src/misc-helpers";
import * as T from "../../../types";
import genderColors from "../gender-colors";
const genderAbbrev = (gender: "masc" | "fem" | undefined): " m." | " f." | "" =>
gender === "masc" ? " m." : gender === "fem" ? " f." : "";
const minifyTableGender = (
block: T.VerbBlock | T.ImperativeBlock
): Array<
| T.PersonLine
| {
masc: T.PersonLine;
fem: T.PersonLine;
}
> => {
// @ts-expect-error because
return block.reduce((table, person, i, src) => {
const isFem = i % 2 !== 0;
if (isFem) {
return table;
}
const femPersAhead = src[i + 1];
const femPersIsTheSame =
psStringEquals(person[0][0], femPersAhead[0][0]) &&
psStringEquals(person[1][0], femPersAhead[1][0]);
if (femPersAhead && !femPersIsTheSame) {
return [
...table,
{
masc: person,
fem: femPersAhead,
},
];
}
return [...table, person];
}, []);
};
function VerbTable({
block,
textOptions,
english,
}: {
block: T.VerbBlock | T.ImperativeBlock | T.ArrayOneOrMore<T.PsString>;
english?: T.EnglishBlock | string;
textOptions: T.TextOptions;
}) {
const blockWEng = english ? addEnglish(english, block) : block;
if (
isSentenceForm(blockWEng) ||
isAllOne(blockWEng as T.VerbBlock | T.ImperativeBlock)
) {
const item = isSentenceForm(blockWEng)
? (block as unknown as T.ArrayOneOrMore<T.PsString>)
: (() => {
const b = block as T.ImperativeBlock | T.VerbBlock;
return b[0][0];
})();
return (
<table className="table text-center">
<tbody>
<tr>
<TableCell item={item} textOptions={textOptions} center noBorder />
</tr>
</tbody>
</table>
);
}
const bl = blockWEng as T.VerbBlock | T.ImperativeBlock;
const b = minifyTableGender(bl);
return (
<table className="table mt-2" style={{ tableLayout: "fixed" }}>
<thead>
<tr>
<th scope="col" style={{ width: "3rem" }}>
Pers.
</th>
<th scope="col">Singular</th>
<th scope="col">Plural</th>
</tr>
</thead>
<tbody>
{b.reduce((rows: React.ReactNode[], person, i, arr) => {
function drawRow({
line,
gender,
}: {
line: T.PersonLine;
gender?: "masc" | "fem";
}) {
const pers = arr.length > 1 ? ["1st", "2nd", "3rd"] : ["2nd"];
const rowLabel = `${pers[i]}${genderAbbrev(gender)}`;
const color = !gender ? "inherit" : genderColors[gender];
return (
<tr key={`${i}${gender}`}>
<th scope="row" style={{ color }}>
{rowLabel}
</th>
<TableCell item={line[0]} textOptions={textOptions} />
<TableCell item={line[1]} textOptions={textOptions} />
</tr>
);
}
return "masc" in person
? [
...rows,
drawRow({ line: person.masc, gender: "masc" }),
drawRow({ line: person.fem, gender: "fem" }),
]
: [...rows, drawRow({ line: person })];
}, [])}
</tbody>
</table>
);
}
export default VerbTable;

View File

@ -0,0 +1,56 @@
import { getLength, getLong } from "../../../lib/src/p-text-helpers";
import * as T from "../../../types";
import Examples from "./Examples";
function CompiledPTextDisplay({
compiled,
opts,
justify,
onlyOne,
length,
}: {
compiled: {
ps: T.SingleOrLengthOpts<T.PsString[]>;
e?: string[] | undefined;
};
opts: T.TextOptions;
justify?: "left" | "right" | "center";
onlyOne?: boolean;
length?: "long" | "short";
}) {
function VariationLayer({ vs }: { vs: T.PsString[] }) {
return (
<div className="mb-2">
<Examples opts={opts} lineHeight={0}>
{vs}
</Examples>
</div>
);
}
const ps = length ? getLength(compiled.ps, length) : compiled.ps;
return (
<div
className={
justify === "left"
? "text-left"
: justify === "right"
? "text-right"
: "text-center"
}
>
{onlyOne ? (
<VariationLayer vs={[getLong(ps)[0]]} />
) : "long" in ps ? (
<div>
<VariationLayer vs={ps.long} />
<VariationLayer vs={ps.short} />
{ps.mini && <VariationLayer vs={ps.mini} />}
</div>
) : (
<VariationLayer vs={ps} />
)}
</div>
);
}
export default CompiledPTextDisplay;

View File

@ -8,9 +8,10 @@
import Pashto from "./Pashto"; import Pashto from "./Pashto";
import Phonetics from "./Phonetics"; import Phonetics from "./Phonetics";
import * as T from "../../types"; import * as T from "../../../types";
import { ReactNode } from "react";
type PsStringWSub = T.PsString & { sub?: any }; type PsStringWSub = T.PsString & { sub?: ReactNode };
function EnglishContent({ function EnglishContent({
children, children,
@ -54,10 +55,10 @@ function Examples(
} }
> >
<div> <div>
<Pashto opts={props.opts}>{text}</Pashto> <Pashto opts={props.opts} ps={text} />
</div> </div>
<div> <div>
<Phonetics opts={props.opts}>{text}</Phonetics> <Phonetics opts={props.opts} ps={text} />
</div> </div>
{text.e && <EnglishContent>{text.e}</EnglishContent>} {text.e && <EnglishContent>{text.e}</EnglishContent>}
{text.sub && <div className="small text-muted">{text.sub}</div>} {text.sub && <div className="small text-muted">{text.sub}</div>}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2021 lingdocs.com
*
* This source code is licensed under the GPL3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import Pashto from "./Pashto";
import Phonetics from "./Phonetics";
import * as T from "../../../types";
const InlinePs = ({
ps,
opts,
}: {
ps: T.PsString | (T.PsJSX & { e?: string });
opts: T.TextOptions;
}) => {
return (
<span>
<Pashto opts={opts} ps={ps} />
{opts.phonetics !== "none" && " - "}
<Phonetics opts={opts} ps={ps} />
{ps.e && <span className="text-muted"> ({ps.e})</span>}
</span>
);
};
export default InlinePs;

View File

@ -6,17 +6,17 @@
* *
*/ */
import { convertSpelling } from "../../lib/src/convert-spelling"; import { convertSpelling } from "../../../lib/src/convert-spelling";
import { phoneticsToDiacritics } from "../../lib/src/phonetics-to-diacritics"; import { phoneticsToDiacritics } from "../../../lib/src/phonetics-to-diacritics";
import { psJSXMap } from "./jsx-map"; import { psJSXMap } from "./jsx-map";
import * as T from "../../types"; import * as T from "../../../types";
const Pashto = ({ const Pashto = ({
opts, opts,
children: text, ps,
}: { }: {
opts: T.TextOptions; opts: T.TextOptions;
children: T.PsString | T.PsJSX; ps: T.PsString | T.PsJSX;
}) => { }) => {
function convertText(ps: T.PsString, opts: T.TextOptions): string { function convertText(ps: T.PsString, opts: T.TextOptions): string {
const p = opts.diacritics const p = opts.diacritics
@ -30,11 +30,11 @@ const Pashto = ({
: { fontSize: opts.pTextSize === "larger" ? "large" : "larger" }; : { fontSize: opts.pTextSize === "larger" ? "large" : "larger" };
return ( return (
<span className="p-text" dir="rtl" style={style} lang="ps"> <span className="p-text" dir="rtl" style={style} lang="ps">
{typeof text.p !== "string" && typeof text.f !== "string" {typeof ps.p !== "string" && typeof ps.f !== "string"
? psJSXMap(text as T.PsJSX, "p", (ps: T.PsString) => ? psJSXMap(ps as T.PsJSX, "p", (ps: T.PsString) =>
convertText(ps, opts) convertText(ps, opts)
) )
: convertText(text as T.PsString, opts)} : convertText(ps as T.PsString, opts)}
</span> </span>
); );
}; };

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) 2021 lingdocs.com
*
* This source code is licensed under the GPL3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { translatePhonetics } from "../../../lib/src/translate-phonetics";
import { psJSXMap } from "./jsx-map";
import * as T from "../../../types";
const Phonetics = ({
opts,
ps,
}: {
opts: T.TextOptions;
ps: T.PsJSX | T.PsString | string;
}) => {
if (opts.phonetics === "none") {
return null;
}
const handleText = (f: string) =>
opts.phonetics === "lingdocs"
? f
: translatePhonetics(f, {
dialect: opts.dialect,
// @ts-expect-error - weird TS not picking up the elimination of "none herre"
system: opts.phonetics,
});
return (
<span className="f-text">
{typeof ps !== "string" && typeof ps.f !== "string"
? psJSXMap(ps as T.PsJSX, "f", ({ f }) => handleText(f))
: handleText(typeof ps === "string" ? ps : (ps.f as string))}
</span>
);
};
export default Phonetics;

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) 2021 lingdocs.com
*
* This source code is licensed under the GPL3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import Pashto from "./Pashto";
import Phonetics from "./Phonetics";
import * as T from "../../../types";
function SingleItemDisplay({
item,
textOptions,
english,
}: {
item: T.PsString;
textOptions: T.TextOptions;
english?: T.EnglishBlock | string;
}) {
const eng = Array.isArray(english) ? english[0][0] : english;
return (
<div className="text-center mt-3 mb-2">
<div>
<Pashto opts={textOptions} ps={item} />
</div>
<div>
<Phonetics opts={textOptions} ps={item} />
</div>
{eng && <div className="text-muted">{eng}</div>}
</div>
);
}
export default SingleItemDisplay;

View File

@ -0,0 +1,102 @@
import { psJSXMap } from "./jsx-map";
test("psJSXMap should work with p as a target", () => {
const input = {
p: (
<>
زه کور ته <strong>ځم</strong>
</>
),
f: (
<>
zu kor ta <strong>dzum</strong>
</>
),
};
const output = psJSXMap(input, "p", (ps) => ps.p.replace(/ځ/g, "ز"));
expect(output).toEqual(
<>
زه کور ته <strong>زم</strong>
</>
);
});
test("psJSXMap should work with f as a target", () => {
const input = {
p: (
<>
زه کور ته <strong>ځم</strong>
</>
),
f: (
<>
zu kor ta <strong>dzum</strong>
</>
),
};
const output = psJSXMap(input, "f", (ps) => ps.f.replace(/dz/g, "z"));
expect(output).toEqual(
<>
zu kor ta <strong>zum</strong>
</>
);
});
test("psJSXMap should work with single child", () => {
const input = {
p: (
<div>
<span>کور</span>
</div>
),
f: (
<div>
<span>kor</span>
</div>
),
};
const output = psJSXMap(input, "f", (ps) => ps.f.replace(/k/g, "q"));
expect(output).toEqual(
<div>
<span>qor</span>
</div>
);
});
/**
* Helps prevent error logs blowing up as a result of expecting an error to be thrown,
* when using a library (such as enzyme)
*
* @param func Function that you would normally pass to `expect(func).toThrow()`
*/
export const expectToThrow = (
func: () => unknown,
error?: JestToErrorArg
): void => {
// Even though the error is caught, it still gets printed to the console
// so we mock that out to avoid the wall of red text.
const spy = jest.spyOn(console, "error");
spy.mockImplementation(() => {});
expect(func).toThrow(error);
spy.mockRestore();
};
type JestToErrorArg = Parameters<
jest.Matchers<unknown, () => unknown>["toThrow"]
>[0];
test("psJSXMap will error if given an uneven/unbalanced pair of JSX Elements", () => {
expectToThrow(() => {
const input = {
p: (
<>
زه کور ته <strong>ځم</strong>
</>
),
f: <>zu kor ta dzum</>,
};
psJSXMap(input, "p", (ps) => ps.p.replace(/ځ/g, "ز"));
});
});

View File

@ -0,0 +1,77 @@
/**
* Copyright (c) 2021 lingdocs.com
*
* This source code is licensed under the GPL3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import * as T from "../../../types";
/**
* Allows PsString transforming methods to be applied to a Pashto/Phonetics set of JSX elements
* outputs a single part of the pair (p or f) with the transform function applied to all the text
* within
* eg: { p: <>زه <strong>ځم</strong></>, f : <>zu <strong>dzum</strong></> }
*
* @param ps
* @param target
* @param dealWithString
* @returns
*/
export function psJSXMap(
ps: T.PsJSX,
target: "p" | "f",
dealWithString: (ps: T.PsString) => string | JSX.Element
): JSX.Element {
const base = ps[target];
const sec = ps[target === "p" ? "f" : "p"];
try {
return {
...base,
props: {
// TODO: could make this a lot cleaner/less repetitive by following a better recursive formula
// see https://adueck.github.io/blog/recursively-modify-text-jsx-react/
...base.props,
children:
typeof base.props.children === "string"
? dealWithString({
p: (target === "p" ? base : sec).props.children,
f: (target === "p" ? sec : base).props.children,
})
: base.props.children.map
? base.props.children.map((x: string | JSX.Element, i: number) =>
typeof x === "string"
? dealWithString({
p: target === "p" ? x : sec.props.children[i],
f: target === "p" ? sec.props.children[i] : x,
})
: psJSXMap(
{
p: target === "p" ? x : sec.props.children[i],
f: target === "p" ? sec.props.children[i] : x,
},
target,
dealWithString
)
)
: typeof base.props.children === "string"
? dealWithString({
p: target === "p" ? base.props.children : sec.props.children,
f: target === "p" ? sec.props.children : base.props.children,
})
: psJSXMap(
{
p: target === "p" ? base.props.children : sec.props.children,
f: target === "p" ? sec.props.children : base.props.children,
},
target,
dealWithString
),
},
};
} catch (e) {
console.error(e);
throw new Error("error mapping out PsJSX - unbalanced trees");
}
}

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
type SaveableData = string | number | object | boolean | undefined | null type SaveableData = string | number | object | boolean | undefined | null;
/** /**
* replacement from the React useState hook that will persist the state in local storage * replacement from the React useState hook that will persist the state in local storage
@ -10,14 +10,13 @@ type SaveableData = string | number | object | boolean | undefined | null
* @param key a key for saving the state in localStorage * @param key a key for saving the state in localStorage
* @returns * @returns
*/ */
export default function useStickyState<T extends SaveableData>(defaultValue: T | ((old: T | undefined) => T), key: string): [ export default function useStickyState<T extends SaveableData>(
value: T, defaultValue: T | ((old: T | undefined) => T),
setValue: React.Dispatch<React.SetStateAction<T>>, key: string
] { ): [value: T, setValue: React.Dispatch<React.SetStateAction<T>>] {
const [value, setValue] = useState<T>(() => { const [value, setValue] = useState<T>(() => {
const v = typeof window === "undefined" const v =
? null typeof window === "undefined" ? null : window.localStorage.getItem(key);
: window.localStorage.getItem(key);
// nothing saved // nothing saved
if (v === null) { if (v === null) {
if (typeof defaultValue === "function") { if (typeof defaultValue === "function") {
@ -34,14 +33,16 @@ export default function useStickyState<T extends SaveableData>(defaultValue: T |
return old; return old;
} catch (e) { } catch (e) {
console.error("error parsting saved state from stickState"); console.error("error parsting saved state from stickState");
return (typeof defaultValue === "function") console.error(e);
return typeof defaultValue === "function"
? defaultValue(undefined) ? defaultValue(undefined)
: defaultValue; : defaultValue;
} }
}); });
useEffect(() => { useEffect(() => {
if (typeof window !== undefined) { // eslint-disable-next-line
if (!typeof window !== undefined) {
window.localStorage.setItem(key, JSON.stringify(value)); window.localStorage.setItem(key, JSON.stringify(value));
} }
}, [key, value]); }, [key, value]);
@ -54,15 +55,11 @@ export function useStickyReducer<T extends SaveableData, A>(
defaultValue: T | ((old: T | undefined) => T), defaultValue: T | ((old: T | undefined) => T),
key: string, key: string,
sendAlert?: (msg: string) => void, sendAlert?: (msg: string) => void,
onChange?: (state: T) => void, onChange?: (state: T) => void
): [ ): [T, (action: A) => void, ((msg: string) => void) | undefined] {
T,
(action: A) => void,
((msg: string) => void) | undefined,
] {
const [state, unsafeSetState] = useStickyState<T>(defaultValue, key); const [state, unsafeSetState] = useStickyState<T>(defaultValue, key);
function adjustState(action: A) { function adjustState(action: A) {
unsafeSetState(oldState => { unsafeSetState((oldState) => {
const newState = reducer(oldState, action, sendAlert); const newState = reducer(oldState, action, sendAlert);
if (onChange) { if (onChange) {
onChange(newState); onChange(newState);

View File

@ -13,7 +13,7 @@ import {
noPersInfs, noPersInfs,
} from "../../../lib/src/misc-helpers"; } from "../../../lib/src/misc-helpers";
import VerbInfoItemDisplay from "./VerbInfoItemDisplay"; import VerbInfoItemDisplay from "./VerbInfoItemDisplay";
import PersonInfsPicker from "../PersInfsPicker"; import PersonInfsPicker from "../selects/PersInfsPicker";
import VerbTypeInfo from "./VerbTypeInfo"; import VerbTypeInfo from "./VerbTypeInfo";
import Hider from "../Hider"; import Hider from "../Hider";
import * as T from "../../../types"; import * as T from "../../../types";

View File

@ -7,75 +7,91 @@
*/ */
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import ButtonSelect from "../ButtonSelect"; import ButtonSelect from "../selects/ButtonSelect";
import Pashto from "../Pashto"; import Pashto from "../text-display/Pashto";
import Phonetics from "../Phonetics"; import Phonetics from "../text-display/Phonetics";
import * as T from "../../../types"; import * as T from "../../../types";
import { concatPsString } from "../../../lib/src/p-text-helpers"; import { concatPsString } from "../../../lib/src/p-text-helpers";
type InputItem = T.SingleOrLengthOpts<T.PsString | [T.PsString, T.PsString]>; type InputItem = T.SingleOrLengthOpts<T.PsString | [T.PsString, T.PsString]>;
function VerbInfoItemDisplay({ item, textOptions, tails }: { function VerbInfoItemDisplay({
item: InputItem, item,
textOptions: T.TextOptions, textOptions,
tails?: boolean, tails,
}: {
item: InputItem;
textOptions: T.TextOptions;
tails?: boolean;
}) { }) {
const [length, setLength] = useState<T.Length>("long"); const [length, setLength] = useState<T.Length>("long");
const getL = (x: InputItem): T.PsString | [T.PsString, T.PsString] => ( const getL = (x: InputItem): T.PsString | [T.PsString, T.PsString] =>
"long" in x "long" in x ? x[length] || x.short : x;
? x[length] || x.short useEffect(() => {
: x setLength((l) => (l === "mini" && !("mini" in item) ? "short" : l));
); }, [item]);
useEffect(() => { // const lengthsAvailable = "long" in item
setLength(l => (l === "mini" && !("mini" in item)) ? "short" : l); // ? [...["long", "short"], ..."mini" in item ? ["mini"] : []]
}, [item]); // : [];
// const lengthsAvailable = "long" in item const text = getL(item);
// ? [...["long", "short"], ..."mini" in item ? ["mini"] : []]
// : [];
const text = getL(item);
function addTails(text: T.PsString): T.PsString { function addTails(text: T.PsString): T.PsString {
return concatPsString(text, { p: "ـ", f: ""}); return concatPsString(text, { p: "ـ", f: "" });
} }
return <> return (
{Array.isArray(text) ? <>
<div> {Array.isArray(text) ? (
<div className="text-center" dir="rtl"> <div>
<Pashto opts={textOptions}>{text[0]}</Pashto> <div className="text-center" dir="rtl">
<span className="mx-1"> __ </span> <Pashto opts={textOptions} ps={text[0]} />
<Pashto opts={textOptions}>{tails ? addTails(text[1]) : text[1]}</Pashto> <span className="mx-1"> __ </span>
</div> <Pashto
<div className="text-center"> opts={textOptions}
<Phonetics opts={textOptions}>{text[0]}</Phonetics> ps={tails ? addTails(text[1]) : text[1]}
<span className="mx-1"> __ </span>
<Phonetics opts={textOptions}>{tails ? addTails(text[1]) : text[1]}</Phonetics>
</div>
</div>
:
<div>
<div className="text-center">
<Pashto opts={textOptions}>{tails ? addTails(text) : text}</Pashto>
</div>
<div className="text-center">
<Phonetics opts={textOptions}>{tails ? addTails(text) : text}</Phonetics>
</div>
</div>
}
{"long" in item && <div className="mt-2 text-center">
<ButtonSelect
xSmall
options={[
{ label: "Long", value: "long" },
{ label: "Short", value: "short" },
..."mini" in item ? [{
label: "Mini", value: "mini",
}] : [],
]}
value={length}
handleChange={(p) => setLength(p as T.Length)}
/> />
</div>} </div>
</>; <div className="text-center">
<Phonetics opts={textOptions} ps={text[0]} />
<span className="mx-1"> __ </span>
<Phonetics
opts={textOptions}
ps={tails ? addTails(text[1]) : text[1]}
/>
</div>
</div>
) : (
<div>
<div className="text-center">
<Pashto opts={textOptions} ps={tails ? addTails(text) : text} />
</div>
<div className="text-center">
<Phonetics opts={textOptions} ps={tails ? addTails(text) : text} />
</div>
</div>
)}
{"long" in item && (
<div className="mt-2 text-center">
<ButtonSelect
xSmall
options={[
{ label: "Long", value: "long" },
{ label: "Short", value: "short" },
...("mini" in item
? [
{
label: "Mini",
value: "mini",
},
]
: []),
]}
value={length}
handleChange={(p) => setLength(p as T.Length)}
/>
</div>
)}
</>
);
} }
export default VerbInfoItemDisplay; export default VerbInfoItemDisplay;

View File

@ -7,12 +7,10 @@
*/ */
import { useState } from "react"; import { useState } from "react";
import InlinePs from "../InlinePs"; import InlinePs from "../text-display/InlinePs";
import Pashto from "../Pashto"; import Pashto from "../text-display/Pashto";
import Phonetics from "../Phonetics"; import Phonetics from "../text-display/Phonetics";
import { import { makePsString } from "../../../lib/src/accent-and-ps-utils";
makePsString,
} from "../../../lib/src/accent-and-ps-utils";
import { Modal } from "react-bootstrap"; import { Modal } from "react-bootstrap";
import stativeCompTrans from "./stative-compound-transitive.svg"; import stativeCompTrans from "./stative-compound-transitive.svg";
import stativeCompIntrans from "./stative-compound-intransitive.svg"; import stativeCompIntrans from "./stative-compound-intransitive.svg";
@ -27,271 +25,390 @@ import gramTransitiveDiagramPresent from "./grammatically-transitive-present.svg
import gramTransitiveDiagramPast from "./grammatically-transitive-past.svg"; import gramTransitiveDiagramPast from "./grammatically-transitive-past.svg";
import * as T from "../../../types"; import * as T from "../../../types";
function CompoundFormula({ a, b }: { function CompoundFormula({ a, b }: { a: React.ReactNode; b: React.ReactNode }) {
a: React.ReactNode, return (
b: React.ReactNode, <div className="row align-items-center mb-3">
}) { <div className="col-5 text-center">{a}</div>
return ( <div className="col-2 text-center" style={{ fontSize: "2.5rem" }}>
<div className="row align-items-center mb-3"> <strong>+</strong>
<div className="col-5 text-center"> </div>
{a} <div className="col-5 text-center">{b}</div>
</div> </div>
<div className="col-2 text-center" style={{ fontSize: "2.5rem" }}> );
<strong>+</strong>
</div>
<div className="col-5 text-center">
{b}
</div>
</div>
);
} }
function ExplanationImage({ src, alt }: { function ExplanationImage({ src, alt }: { src: string; alt?: string }) {
src: any, return (
alt?: string, <img
}) { src={src}
return ( alt={alt}
<img className="mx-auto d-block mb-2"
src={src} style={{ maxWidth: "100%" }}
alt={alt} />
className="mx-auto d-block mb-2" );
style={{ maxWidth: "100%" }}
/>
);
} }
const typeInfo = ( const typeInfo = (type: string, opts: T.TextOptions) => {
type: string, return type === "simple" ? (
opts: T.TextOptions, <>
) => { <p>
return type === "simple" A <strong>simple verb</strong> is a verb that is just one piece. It
? <> can't be broken down into different words with different meanings, like
<p>A <strong>simple verb</strong> is a verb that is just one piece. It can't be broken down into different words with different meanings, like compound verbs can.</p> compound verbs can.
</p>
</> </>
: type === "stative compound" ) : type === "stative compound" ? (
? <> <>
<p>A <strong>stative compound</strong> talks about <strong>something changing from one state to another</strong>.</p> <p>
<CompoundFormula A <strong>stative compound</strong> talks about{" "}
a={<> <strong>something changing from one state to another</strong>.
<div style={{ fontSize: "larger" }}> </p>
<strong>Complement</strong> <CompoundFormula
</div> a={
<div>Adjective, Noun, or Adverb</div> <>
</>} <div style={{ fontSize: "larger" }}>
b={<> <strong>Complement</strong>
<div style={{ fontSize: "larger" }}> </div>
<strong>Aux. Verb</strong> <div>Adjective, Noun, or Adverb</div>
</div> </>
<div> }
<div className="small text-muted">With transitive:</div> b={
<div><InlinePs opts={opts}>{{ p: "کول", f: "kawul" }}</InlinePs> (to make)</div> <>
<div className="small text-muted">With intransitive:</div> <div style={{ fontSize: "larger" }}>
<div><InlinePs opts={opts}>{{ p: "کېدل", f: "kedul" }}</InlinePs> (to become)</div> <strong>Aux. Verb</strong>
</div> </div>
</>} <div>
/> <div className="small text-muted">With transitive:</div>
<h4>Transitive stative compounds</h4> <div>
<ExplanationImage src={stativeCompTrans} /> <InlinePs opts={opts} ps={{ p: "کول", f: "kawul" }} /> (to make)
<h4>Intransitive stative compounds</h4> </div>
<ExplanationImage src={stativeCompIntrans} /> <div className="small text-muted">With intransitive:</div>
<p>It's important to remember that the aux. verbs used for these compounds <InlinePs opts={opts}>{{ p: "کول", f: "kawul" }}</InlinePs> (to make) and <InlinePs opts={opts}>{{ p: "کېدل", f: "kedul" }}</InlinePs> (to become) will never, ever take a <InlinePs opts={opts}>{{ p: "و", f: "oo" }}</InlinePs> prefix. 🚫</p> <div>
<h5>A couple of other notes:</h5> <InlinePs opts={opts} ps={{ p: "کېدل", f: "kedul" }} /> (to
<p>In the imperfective aspect, the complement and aux. verb are often fused together. For example: <InlinePs opts={opts}>{{ p: "پوخ", f: "pokh" }}</InlinePs> + <InlinePs opts={opts}>{{ p: "کول", f: "kawul" }}</InlinePs> = <InlinePs opts={opts}>{{ p: "بخول", f: "pakhawul" }}</InlinePs>. But they always stay broken apart in the perfective aspect.</p> become)
<p>When complements are nouns or adverbs, they act (and carry a meaning) almost as if they were adjectives.</p> </div>
</div>
</>
}
/>
<h4>Transitive stative compounds</h4>
<ExplanationImage src={stativeCompTrans} />
<h4>Intransitive stative compounds</h4>
<ExplanationImage src={stativeCompIntrans} />
<p>
It's important to remember that the aux. verbs used for these compounds{" "}
<InlinePs opts={opts} ps={{ p: "کول", f: "kawul" }} /> (to make) and{" "}
<InlinePs opts={opts} ps={{ p: "کېدل", f: "kedul" }} /> (to become) will
never, ever take a <InlinePs opts={opts} ps={{ p: "و", f: "oo" }} />{" "}
prefix. 🚫
</p>
<h5>A couple of other notes:</h5>
<p>
In the imperfective aspect, the complement and aux. verb are often fused
together. For example:{" "}
<InlinePs opts={opts} ps={{ p: "پوخ", f: "pokh" }} /> +{" "}
<InlinePs opts={opts} ps={{ p: "کول", f: "kawul" }} /> ={" "}
<InlinePs opts={opts} ps={{ p: "بخول", f: "pakhawul" }} /> . But they
always stay broken apart in the perfective aspect.
</p>
<p>
When complements are nouns or adverbs, they act (and carry a meaning)
almost as if they were adjectives.
</p>
</> </>
: type === "dynamic compound" ) : type === "dynamic compound" ? (
? <> <>
<p>A <strong>dynamic compound</strong> talks about <strong>some action being done</strong>.</p> <p>
<CompoundFormula A <strong>dynamic compound</strong> talks about{" "}
a={<> <strong>some action being done</strong>.
<div style={{ fontSize: "larger" }}> </p>
<strong>Complement</strong> <CompoundFormula
</div> a={
<div>Noun (action or activity)</div> <>
</>} <div style={{ fontSize: "larger" }}>
b={<> <strong>Complement</strong>
<div style={{ fontSize: "larger" }}> </div>
<strong>Aux. Verb</strong> <div>Noun (action or activity)</div>
</div> </>
</>} }
/> b={
<h4>Transitive Dynamic Compounds</h4> <>
<p>These talk about someone doing an action or activity.</p> <div style={{ fontSize: "larger" }}>
<ExplanationImage src={dynamicCompTrans} /> <strong>Aux. Verb</strong>
<p>There is a subject/agent doing the action, and <em>the action or activity is the object of the sentence</em>. It's important to remember that with these compounds, the object of the sentence is included inside the compound, and you can't have any other object in the sentence.</p> </div>
<h4>Intransitive Dynamic Compounds</h4> </>
<p>These talk about an action or activity happening.</p> }
<ExplanationImage src={dynamicCompIntrans} /> />
<p>Here the complement, the activity included in the compound, is the subject of the sentence.</p> <h4>Transitive Dynamic Compounds</h4>
<h6>Other notes:</h6> <p>These talk about someone doing an action or activity.</p>
<p>Dynamic compounds made with <InlinePs opts={opts}>{{ p: "کول", f: "kawul" }}</InlinePs> (to do) will also have an intransitive version made with <InlinePs opts={opts}>{{ p: "کېدل", f: "kedul" }}</InlinePs> (to happen).</p> <ExplanationImage src={dynamicCompTrans} />
<p>Dynamic compounds made with <InlinePs opts={opts}>{{ p: "کول", f: "kawul" }}</InlinePs> (to do) or <InlinePs opts={opts}>{{ p: "کېدل", f: "kedul" }}</InlinePs> (to happen) will <em>always</em> take a <InlinePs opts={opts}>{{ p: "و", f: "oo" }}</InlinePs> prefix in the perfective aspect.</p> <p>
There is a subject/agent doing the action, and{" "}
<em>the action or activity is the object of the sentence</em>. It's
important to remember that with these compounds, the object of the
sentence is included inside the compound, and you can't have any other
object in the sentence.
</p>
<h4>Intransitive Dynamic Compounds</h4>
<p>These talk about an action or activity happening.</p>
<ExplanationImage src={dynamicCompIntrans} />
<p>
Here the complement, the activity included in the compound, is the
subject of the sentence.
</p>
<h6>Other notes:</h6>
<p>
Dynamic compounds made with{" "}
<InlinePs opts={opts} ps={{ p: "کول", f: "kawul" }} /> (to do) will also
have an intransitive version made with{" "}
<InlinePs opts={opts} ps={{ p: "کېدل", f: "kedul" }} /> (to happen).
</p>
<p>
Dynamic compounds made with{" "}
<InlinePs opts={opts} ps={{ p: "کول", f: "kawul" }} /> (to do) or{" "}
<InlinePs opts={opts} ps={{ p: "کېدل", f: "kedul" }} /> (to happen) will{" "}
<em>always</em> take a <InlinePs opts={opts} ps={{ p: "و", f: "oo" }} />{" "}
prefix in the perfective aspect.
</p>
</> </>
: type === "generative stative compound" ) : type === "generative stative compound" ? (
? <> <>
<p><strong>Generative stative compounds</strong> are strange compounds that seem to behave like a cross between a stative compound and a dynamic compound.</p> <p>
<ul> <strong>Generative stative compounds</strong> are strange compounds that
<li>The object is included in the compound... like with dynamic compounds</li> seem to behave like a cross between a stative compound and a dynamic
<li>But they also use <InlinePs opts={opts}>{{ p: "کول", f: "kawul" }}</InlinePs> (to make), the verb that never gets the <InlinePs opts={opts}>{{ p: "و", f: "oo" }}</InlinePs> prefix... like with stative compounds</li> compound.
</ul> </p>
<p>This may seem quite confusing at first.</p> <ul>
<p>We can think of these verbs as <strong>stative compounds</strong> but <strong>with an implied complement</strong> meaning something like "existing." So they talk about some thing being created or brought out into existence.</p> <li>
<h4>Transitive Generative Stative Compounds</h4> The object is included in the compound... like with dynamic compounds
<ExplanationImage src={generativeStatCompTrans} /> </li>
<h4>Intransitive Generative Stative Compounds</h4> <li>
<ExplanationImage src={generativeStatCompIntrans} /> But they also use{" "}
<InlinePs opts={opts} ps={{ p: "کول", f: "kawul" }} /> (to make), the
verb that never gets the{" "}
<InlinePs opts={opts} ps={{ p: "و", f: "oo" }} /> prefix... like with
stative compounds
</li>
</ul>
<p>This may seem quite confusing at first.</p>
<p>
We can think of these verbs as <strong>stative compounds</strong> but{" "}
<strong>with an implied complement</strong> meaning something like
"existing." So they talk about some thing being created or brought out
into existence.
</p>
<h4>Transitive Generative Stative Compounds</h4>
<ExplanationImage src={generativeStatCompTrans} />
<h4>Intransitive Generative Stative Compounds</h4>
<ExplanationImage src={generativeStatCompIntrans} />
</> </>
: null; ) : null;
}; };
const transitivityInfo = ( const transitivityInfo = (type: string) => {
type: string, return type === "transitive" ? (
textOptions: T.TextOptions, <>
) => { <p>
return type === "transitive" <strong>Transitive</strong> verbs are{" "}
? <> <strong>verbs that take an object</strong>.
<p><strong>Transitive</strong> verbs are <strong>verbs that take an object</strong>.</p> </p>
<p>Transitive verbs are especially difficult because <strong>they work totally differently in the past tense</strong>. (They are ergative in the past tense only.) This takes a lot of hard work for the learner to get used to!</p> <p>
<h4>In all non-past forms</h4> Transitive verbs are especially difficult because{" "}
<ul> <strong>they work totally differently in the past tense</strong>. (They
<li>The subject is not inflected</li> are ergative in the past tense only.) This takes a lot of hard work for
<li>The object is not inflected, or it can alse be an enclitic (mini) pronoun (exception: the object is inflected when it's a 1st or 2nd person pronoun)</li> the learner to get used to!
<li>The verb agrees with the <strong>subject</strong></li> </p>
</ul> <h4>In all non-past forms</h4>
<ExplanationImage src={transitiveDiagramPresent} /> <ul>
<h4>In the past tense</h4> <li>The subject is not inflected</li>
<ul> <li>
<li>The subject is inflected, or it can be an enclitic (mini) pronoun</li> The object is not inflected, or it can alse be an enclitic (mini)
<li>The object is not inflected</li> pronoun (exception: the object is inflected when it's a 1st or 2nd
<li>The verb agrees with the <strong>object</strong></li> person pronoun)
</ul> </li>
<ExplanationImage src={transitiveDiagramPast} /> <li>
The verb agrees with the <strong>subject</strong>
</li>
</ul>
<ExplanationImage src={transitiveDiagramPresent} />
<h4>In the past tense</h4>
<ul>
<li>
The subject is inflected, or it can be an enclitic (mini) pronoun
</li>
<li>The object is not inflected</li>
<li>
The verb agrees with the <strong>object</strong>
</li>
</ul>
<ExplanationImage src={transitiveDiagramPast} />
</> </>
: type === "intransitive" ) : type === "intransitive" ? (
? <> <>
<p><strong>Intransitive</strong> verbs are <strong>verbs that don't take an object</strong>. They only take a subject, which is a person or thing that experiences the action of the verb.</p> <p>
<p>- The subject is always a <em>uninflected/plain</em> noun or pronoun.</p> <strong>Intransitive</strong> verbs are{" "}
<p>- The verb always agrees with the subject.</p> <strong>verbs that don't take an object</strong>. They only take a
<ExplanationImage src={intransitiveDiagram} /> subject, which is a person or thing that experiences the action of the
verb.
</p>
<p>
- The subject is always a <em>uninflected/plain</em> noun or pronoun.
</p>
<p>- The verb always agrees with the subject.</p>
<ExplanationImage src={intransitiveDiagram} />
</> </>
: type === "grammatically transitive" ) : type === "grammatically transitive" ? (
? <> <>
<p><strong>Gramatically transitive</strong> verbs are <strong>verbs that don't appear to have an object, but actually work as if they do</strong>!</p> <p>
<p>These work just like transitive verbs, except that the object is an implied (unspoken) 3rd person masculine plural entity.</p> <strong>Gramatically transitive</strong> verbs are{" "}
<h4>In all non-past forms</h4> <strong>
<ExplanationImage src={gramTransitiveDiagramPresent} /> verbs that don't appear to have an object, but actually work as if
<h4>In the past tense</h4> they do
<ExplanationImage src={gramTransitiveDiagramPast} /> </strong>
!
</p>
<p>
These work just like transitive verbs, except that the object is an
implied (unspoken) 3rd person masculine plural entity.
</p>
<h4>In all non-past forms</h4>
<ExplanationImage src={gramTransitiveDiagramPresent} />
<h4>In the past tense</h4>
<ExplanationImage src={gramTransitiveDiagramPast} />
</> </>
: null; ) : null;
} };
function CompoundBreakdown({ info, textOptions }: { function CompoundBreakdown({
info: T.NonComboVerbInfo, info,
textOptions: T.TextOptions, textOptions,
}: {
info: T.NonComboVerbInfo;
textOptions: T.TextOptions;
}) { }) {
const isComplement = ("complement" in info || "objComplement" in info); const isComplement = "complement" in info || "objComplement" in info;
if (!isComplement) { if (!isComplement) {
return null; return null;
}
const complement = ((): T.PsString => {
if ("objComplement" in info) {
return info.objComplement.plural
? info.objComplement.plural
: makePsString(info.objComplement.entry.p, info.objComplement.entry.f);
} }
const complement = ((): T.PsString => { if ("complement" in info) {
if ("objComplement" in info) { return info.complement.masc[0][0];
return info.objComplement.plural } else return makePsString("aa", "aa");
? info.objComplement.plural })();
: makePsString(info.objComplement.entry.p, info.objComplement.entry.f) const aux = ((): { ps: T.PsString; e: string } => {
if (
info.type === "stative compound" ||
info.type === "generative stative compound"
) {
return info.transitivity === "transitive"
? { ps: { p: "کول", f: "kawul" }, e: "to make" }
: { ps: { p: "کېدل", f: "kedul" }, e: "to become" };
}
if (!("auxVerb" in info)) return { ps: { p: "", f: "" }, e: "" };
const kawulDyn =
info.type === "dynamic compound" && info.auxVerb.p === "کول";
return {
ps: makePsString(info.auxVerb.p, info.auxVerb.f),
e: kawulDyn ? "to do" : "",
};
})();
return (
<div className="d-block mx-auto my-3" style={{ maxWidth: "400px" }}>
<CompoundFormula
a={
<div className="compound-breakdown">
<div>
<Pashto opts={textOptions} ps={complement} />
</div>
<div>
<Phonetics opts={textOptions} ps={complement} />
</div>
</div>
} }
if ("complement" in info) { b={
return info.complement.masc[0][0]; <div className="compound-breakdown">
<div>
<Pashto opts={textOptions} ps={aux.ps} />
</div>
<div>
<Phonetics opts={textOptions} ps={aux.ps} />
</div>
{aux.e && <div>{aux.e}</div>}
</div>
} }
else return makePsString("aa", "aa"); />
})(); </div>
const aux = ((): { ps: T.PsString, e: string } => { );
if (info.type === "stative compound" || info.type === "generative stative compound") {
return info.transitivity === "transitive"
? { ps: { p: "کول", f: "kawul" }, e: "to make"}
: { ps: { p: "کېدل", f: "kedul" }, e: "to become"};
}
if (!("auxVerb" in info)) return { ps: {p: "", f: ""}, e: ""};
const kawulDyn = info.type === "dynamic compound" && info.auxVerb.p === "کول";
return {
ps: makePsString(info.auxVerb.p, info.auxVerb.f),
e: kawulDyn ? "to do" : "",
}
})();
return (
<div className="d-block mx-auto my-3" style={{ maxWidth: "400px" }}>
<CompoundFormula
a={<div className="compound-breakdown">
<div>
<Pashto opts={textOptions}>{complement}</Pashto>
</div>
<div>
<Phonetics opts={textOptions}>{complement}</Phonetics>
</div>
</div>}
b={<div className="compound-breakdown">
<div>
<Pashto opts={textOptions}>{aux.ps}</Pashto>
</div>
<div>
<Phonetics opts={textOptions}>{aux.ps}</Phonetics>
</div>
{aux.e && <div>{aux.e}</div>}
</div>}
/>
</div>
);
} }
function VerbTypeInfo({ info, textOptions }: { function VerbTypeInfo({
info: T.NonComboVerbInfo, info,
textOptions: T.TextOptions, textOptions,
}: {
info: T.NonComboVerbInfo;
textOptions: T.TextOptions;
}) { }) {
const [showingTypeModal, setShowingTypeModal] = useState<boolean>(false); const [showingTypeModal, setShowingTypeModal] = useState<boolean>(false);
const [showingTransModal, setShowingTransModal] = useState<boolean>(false); const [showingTransModal, setShowingTransModal] = useState<boolean>(false);
return ( return (
<div> <div>
<div className="text-center my-2"> <div className="text-center my-2">
This is a This is a
<button <button
className="btn btn-sm btn-light mx-2 my-1" className="btn btn-sm btn-light mx-2 my-1"
onClick={() => setShowingTypeModal(true)} onClick={() => setShowingTypeModal(true)}
> >
<strong>{info.type}</strong> <strong>{info.type}</strong>
</button> </button>
verb and it's verb and it's
<button <button
className="btn btn-sm btn-light mx-2 my-1" className="btn btn-sm btn-light mx-2 my-1"
onClick={() => setShowingTransModal(true)} onClick={() => setShowingTransModal(true)}
> >
<strong>{info.transitivity}</strong> <strong>{info.transitivity}</strong>
</button> </button>
</div> </div>
<CompoundBreakdown info={info} textOptions={textOptions} /> <CompoundBreakdown info={info} textOptions={textOptions} />
<Modal show={showingTypeModal} onHide={() => setShowingTypeModal(false)}> <Modal show={showingTypeModal} onHide={() => setShowingTypeModal(false)}>
<Modal.Header closeButton> <Modal.Header closeButton>
<Modal.Title>About {info.type} verbs</Modal.Title> <Modal.Title>About {info.type} verbs</Modal.Title>
</Modal.Header> </Modal.Header>
<Modal.Body>{typeInfo(info.type, textOptions)}</Modal.Body> <Modal.Body>{typeInfo(info.type, textOptions)}</Modal.Body>
<Modal.Footer> <Modal.Footer>
<button type="button" className="btn btn-primary clb" onClick={() => setShowingTypeModal(false)}> <button
Close type="button"
</button> className="btn btn-primary clb"
</Modal.Footer> onClick={() => setShowingTypeModal(false)}
</Modal> >
<Modal show={showingTransModal} onHide={() => setShowingTransModal(false)}> Close
<Modal.Header closeButton> </button>
<Modal.Title>What does "{info.transitivity}" mean?</Modal.Title> </Modal.Footer>
</Modal.Header> </Modal>
<Modal.Body>{transitivityInfo(info.transitivity, textOptions)}</Modal.Body> <Modal
<Modal.Footer> show={showingTransModal}
<button type="button" className="btn btn-primary clb" onClick={() => setShowingTransModal(false)}> onHide={() => setShowingTransModal(false)}
Close >
</button> <Modal.Header closeButton>
</Modal.Footer> <Modal.Title>What does "{info.transitivity}" mean?</Modal.Title>
</Modal> </Modal.Header>
</div> <Modal.Body>{transitivityInfo(info.transitivity)}</Modal.Body>
); <Modal.Footer>
<button
type="button"
className="btn btn-primary clb"
onClick={() => setShowingTransModal(false)}
>
Close
</button>
</Modal.Footer>
</Modal>
</div>
);
} }
export default VerbTypeInfo; export default VerbTypeInfo;

View File

@ -1,105 +1,113 @@
import ButtonSelect from "../ButtonSelect"; import ButtonSelect from "../selects/ButtonSelect";
import * as T from "../../../types"; import * as T from "../../../types";
import { roleIcon } from "./VPExplorerExplanationModal"; import { roleIcon } from "../role-icons";
import classNames from "classnames"; import classNames from "classnames";
const options = [ const options = [
{ {
label: "Full", label: "Full",
value: "full", value: "full",
}, },
{ {
label: <div className="m1-2">No {roleIcon.king}</div>, label: <div className="m1-2">No {roleIcon.king}</div>,
value: "noKing", value: "noKing",
}, },
{ {
label: <div>Mini {roleIcon.servant}</div>, label: <div>Mini {roleIcon.servant}</div>,
value: "shrinkServant", value: "shrinkServant",
}, },
{ {
label: <div>Both</div>, label: <div>Both</div>,
value: "shortest", value: "shortest",
}, },
]; ];
function formToValue(f: T.FormVersion) { function formToValue(f: T.FormVersion) {
if (f.removeKing === false && f.shrinkServant === false) { if (f.removeKing === false && f.shrinkServant === false) {
return "full"; return "full";
} }
if (f.removeKing === true && f.shrinkServant === false) { if (f.removeKing === true && f.shrinkServant === false) {
return "noKing"; return "noKing";
} }
if (f.removeKing === false && f.shrinkServant === true) { if (f.removeKing === false && f.shrinkServant === true) {
return "shrinkServant"; return "shrinkServant";
} }
if (f.removeKing === true && f.shrinkServant === true) { if (f.removeKing === true && f.shrinkServant === true) {
return "shortest"; return "shortest";
} }
throw new Error("unrecognized abbreviation form"); throw new Error("unrecognized abbreviation form");
} }
function limitOptions(adjustable: "both" | "king" | "servant") { function limitOptions(adjustable: "both" | "king" | "servant") {
if (adjustable === "both") { if (adjustable === "both") {
return options; return options;
} }
if (adjustable === "king") { if (adjustable === "king") {
return options.filter(o => !["shrinkServant", "shortest"].includes(o.value)); return options.filter(
} (o) => !["shrinkServant", "shortest"].includes(o.value)
if (adjustable === "servant") { );
return options.filter(o => !["noKing", "shortest"].includes(o.value)); }
} if (adjustable === "servant") {
return options.filter((o) => !["noKing", "shortest"].includes(o.value));
}
} }
function limitValue(value: string, adjustable: "both" | "king" | "servant") { function limitValue(value: string, adjustable: "both" | "king" | "servant") {
if (adjustable === "both") return value; if (adjustable === "both") return value;
if (adjustable === "king") { if (adjustable === "king") {
return (value === "shortest") return value === "shortest"
? "noKing" ? "noKing"
: (value === "shrinkServant") : value === "shrinkServant"
? "full" ? "full"
: value; : value;
} }
if (adjustable === "servant") { if (adjustable === "servant") {
return (value === "shortest") return value === "shortest"
? "shrinkServant" ? "shrinkServant"
: (value === "noKing") : value === "noKing"
? "full" ? "full"
: value; : value;
} }
throw new Error("unrecognized adjustable value"); throw new Error("unrecognized adjustable value");
} }
function AbbreviationFormSelector({ form, onChange, adjustable, inline }: { function AbbreviationFormSelector({
form: T.FormVersion, form,
onChange: (f: T.FormVersion) => void, onChange,
adjustable: "both" | "king" | "servant", adjustable,
inline?: boolean, inline,
}: {
form: T.FormVersion;
onChange: (f: T.FormVersion) => void;
adjustable: "both" | "king" | "servant";
inline?: boolean;
}) { }) {
function handleChange(f: "full" | "noKing" | "shrinkServant" | "shortest") { function handleChange(f: "full" | "noKing" | "shrinkServant" | "shortest") {
if (f === "full") { if (f === "full") {
onChange({ removeKing: false, shrinkServant: false }); onChange({ removeKing: false, shrinkServant: false });
} else if (f === "noKing") { } else if (f === "noKing") {
onChange({ removeKing: true, shrinkServant: false }); onChange({ removeKing: true, shrinkServant: false });
} else if (f === "shrinkServant") { } else if (f === "shrinkServant") {
onChange({ removeKing: false, shrinkServant: true }); onChange({ removeKing: false, shrinkServant: true });
} else if (f === "shortest") { } else if (f === "shortest") {
onChange({ removeKing: true, shrinkServant: true }); onChange({ removeKing: true, shrinkServant: true });
}
} }
// TODO: limit display of shrinking options based on the verb type }
return <div className={classNames("mx-3", { "mb-3": !inline })}> // TODO: limit display of shrinking options based on the verb type
{/* <div className="text-center text-small mb-2">Abbreviation Options</div> */} return (
<ButtonSelect <div className={classNames("mx-3", { "mb-3": !inline })}>
faded={inline} {/* <div className="text-center text-small mb-2">Abbreviation Options</div> */}
small <ButtonSelect
// @ts-ignore faded={inline}
value={limitValue(formToValue(form), adjustable)} small
// @ts-ignore // @ts-expect-error slight mismatch but its ok
options={limitOptions(adjustable)} value={limitValue(formToValue(form), adjustable)}
// @ts-ignore // @ts-expect-error slight mismatch but its ok
handleChange={handleChange} options={limitOptions(adjustable)}
/> handleChange={handleChange}
/>
</div> </div>
);
} }
export default AbbreviationFormSelector; export default AbbreviationFormSelector;

View File

@ -9,7 +9,7 @@ import Hider from "../Hider";
import * as T from "../../../types"; import * as T from "../../../types";
import useStickyState from "../useStickyState"; import useStickyState from "../useStickyState";
import { isImperativeTense } from "../../../lib/src/type-predicates"; import { isImperativeTense } from "../../../lib/src/type-predicates";
import ButtonSelect from "../ButtonSelect"; import ButtonSelect from "../selects/ButtonSelect";
import { VpsReducerAction } from "../../../lib/src/phrase-building/vps-reducer"; import { VpsReducerAction } from "../../../lib/src/phrase-building/vps-reducer";
import { useState } from "react"; import { useState } from "react";
import { statVerb } from "../../../lib/src/new-verb-engine/roots-and-stems"; import { statVerb } from "../../../lib/src/new-verb-engine/roots-and-stems";

View File

@ -1,114 +1,120 @@
import * as T from "../../../types"; import * as T from "../../../types";
import Pashto from "../Pashto"; import Pashto from "../text-display/Pashto";
import Phonetics from "../Phonetics"; import Phonetics from "../text-display/Phonetics";
import { import { makePsString } from "../../../lib/src/accent-and-ps-utils";
makePsString,
} from "../../../lib/src/accent-and-ps-utils";
import { ReactNode } from "react"; import { ReactNode } from "react";
import classNames from "classnames"; import classNames from "classnames";
import { import { kawulStat, kedulStat } from "../../../lib/src/irregular-conjugations";
kawulStat,
kedulStat,
} from "../../../lib/src/irregular-conjugations";
function CompoundFormula({ a, b }: { function CompoundFormula({ a, b }: { a: ReactNode; b: ReactNode }) {
a: ReactNode, return (
b: ReactNode, <div className="row align-items-center mb-3">
}) { <div className="col-5 text-center">{a}</div>
return ( <div className="col-2 text-center" style={{ fontSize: "2.5rem" }}>
<div className="row align-items-center mb-3"> <strong>+</strong>
<div className="col-5 text-center"> </div>
{a} <div className="col-5 text-center">{b}</div>
</div> </div>
<div className="col-2 text-center" style={{ fontSize: "2.5rem" }}> );
<strong>+</strong>
</div>
<div className="col-5 text-center">
{b}
</div>
</div>
);
} }
function CompoundDisplay({ info, opts, handleLinkClick }: { function CompoundDisplay({
info: T.NonComboVerbInfo, info,
opts: T.TextOptions, opts,
handleLinkClick: ((ts: number) => void) | "none", handleLinkClick,
}: {
info: T.NonComboVerbInfo;
opts: T.TextOptions;
handleLinkClick: ((ts: number) => void) | "none";
}) { }) {
const isComplement = ("complement" in info || "objComplement" in info); const isComplement = "complement" in info || "objComplement" in info;
if (!isComplement) { if (!isComplement) {
return null; return null;
}
const complement = ((): T.PsString => {
if ("objComplement" in info) {
return info.objComplement.plural
? info.objComplement.plural
: info.objComplement.entry;
} }
const complement = ((): T.PsString => { if ("complement" in info) {
if ("objComplement" in info) { return info.complement.masc[0][0];
return info.objComplement.plural } else return makePsString("aa", "aa");
? info.objComplement.plural })();
: info.objComplement.entry; const aux = ((): { ps: T.PsString; e: string } => {
if (
info.type === "stative compound" ||
info.type === "generative stative compound"
) {
return info.transitivity === "transitive"
? { ps: { p: "کول", f: "kawul" }, e: "to make" }
: { ps: { p: "کېدل", f: "kedul" }, e: "to become" };
}
if (!("auxVerb" in info)) return { ps: { p: "", f: "" }, e: "" };
const kawulDyn =
info.type === "dynamic compound" && info.auxVerb.p === "کول";
return {
ps: makePsString(info.auxVerb.p, info.auxVerb.f),
e: kawulDyn ? "to do" : "",
};
})();
return (
<div className="d-block mx-auto my-3" style={{ maxWidth: "400px" }}>
<div className="text-center">{info.type}</div>
<CompoundFormula
a={
<div
className={classNames([
{ clickable: typeof handleLinkClick === "function" },
])}
onClick={
handleLinkClick
? // @ts-expect-error - thinks there might not be a complement, but there will be
() => handleLinkClick(info.entry.complement?.ts)
: undefined
}
>
<div>
<Pashto opts={opts} ps={complement} />
</div>
<div>
<Phonetics opts={opts} ps={complement} />
</div>
<div className="small text-muted">{info.entry.complement?.c}</div>
{info.type === "dynamic compound" && <div>(Object)</div>}
</div>
} }
if ("complement" in info) { b={
return info.complement.masc[0][0]; <div
className={classNames([{ clickable: handleLinkClick }])}
onClick={
handleLinkClick !== "none"
? () =>
handleLinkClick(
"auxVerb" in info
? // dyanmic compound auxVerb ts
info.auxVerb.ts
: // stative compound auxVerb ts
(info.transitivity === "intransitive"
? kedulStat
: kawulStat
).info.entry.entry.ts
)
: undefined
}
>
<div>
<Pashto opts={opts} ps={aux.ps} />
</div>
<div>
<Phonetics opts={opts} ps={aux.ps} />
</div>
{aux.e && <div>{aux.e}</div>}
</div>
} }
else return makePsString("aa", "aa"); />
})(); </div>
const aux = ((): { ps: T.PsString, e: string } => { );
if (info.type === "stative compound" || info.type === "generative stative compound") {
return info.transitivity === "transitive"
? { ps: { p: "کول", f: "kawul" }, e: "to make"}
: { ps: { p: "کېدل", f: "kedul" }, e: "to become"};
}
if (!("auxVerb" in info)) return { ps: {p: "", f: ""}, e: ""};
const kawulDyn = info.type === "dynamic compound" && info.auxVerb.p === "کول";
return {
ps: makePsString(info.auxVerb.p, info.auxVerb.f),
e: kawulDyn ? "to do" : "",
}
})();
return (
<div className="d-block mx-auto my-3" style={{ maxWidth: "400px" }}>
<div className="text-center">{info.type}</div>
<CompoundFormula
a={<div
className={classNames([{ clickable: typeof handleLinkClick === "function" }])}
onClick={(handleLinkClick)
// @ts-ignore - thinks there might not be a complement, but there will be
? () => handleLinkClick(info.entry.complement?.ts)
: undefined}
>
<div>
<Pashto opts={opts}>{complement}</Pashto>
</div>
<div>
<Phonetics opts={opts}>{complement}</Phonetics>
</div>
<div className="small text-muted">{info.entry.complement?.c}</div>
{info.type === "dynamic compound" && <div>
(Object)
</div>}
</div>}
b={<div
className={classNames([{ clickable: handleLinkClick }])}
onClick={(handleLinkClick !== "none")
? () => handleLinkClick(
"auxVerb" in info
// dyanmic compound auxVerb ts
? info.auxVerb.ts
// stative compound auxVerb ts
: ((info.transitivity === "intransitive"
? kedulStat
: kawulStat).info.entry.entry.ts)
)
: undefined}>
<div>
<Pashto opts={opts}>{aux.ps}</Pashto>
</div>
<div>
<Phonetics opts={opts}>{aux.ps}</Phonetics>
</div>
{aux.e && <div>{aux.e}</div>}
</div>}
/>
</div>
);
} }
export default CompoundDisplay; export default CompoundDisplay;

View File

@ -13,7 +13,7 @@ export function EditIcon() {
function EditableVP({ function EditableVP({
children, children,
opts, opts,
formChoice, // formChoice,
noEdit, noEdit,
length, length,
mode, mode,
@ -23,7 +23,7 @@ function EditableVP({
}: { }: {
children: T.VPSelectionState; children: T.VPSelectionState;
opts: T.TextOptions; opts: T.TextOptions;
formChoice?: boolean; // formChoice?: boolean;
noEdit?: boolean; noEdit?: boolean;
length?: "long" | "short"; length?: "long" | "short";
mode?: "text" | "blocks"; mode?: "text" | "blocks";

View File

@ -10,7 +10,8 @@ const Keyframes = (props: IProps) => {
typeof cssObject === "string" typeof cssObject === "string"
? cssObject ? cssObject
: Object.keys(cssObject).reduce((accumulator, key) => { : Object.keys(cssObject).reduce((accumulator, key) => {
const cssKey = key.replace(/[A-Z]/g, v => `-${v.toLowerCase()}`); const cssKey = key.replace(/[A-Z]/g, (v) => `-${v.toLowerCase()}`);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const cssValue = (cssObject as any)[key].toString().replace("'", ""); const cssValue = (cssObject as any)[key].toString().replace("'", "");
return `${accumulator}${cssKey}:${cssValue};`; return `${accumulator}${cssKey}:${cssValue};`;
}, ""); }, "");
@ -19,7 +20,7 @@ const Keyframes = (props: IProps) => {
<style> <style>
{`@keyframes ${props.name} { {`@keyframes ${props.name} {
${Object.keys(props) ${Object.keys(props)
.map(key => { .map((key) => {
return ["from", "to"].includes(key) return ["from", "to"].includes(key)
? `${key} { ${toCss(props[key])} }` ? `${key} { ${toCss(props[key])} }`
: /^_[0-9]+$/.test(key) : /^_[0-9]+$/.test(key)

View File

@ -1,7 +1,7 @@
import * as T from "../../../types"; import * as T from "../../../types";
import ModeSelect, { Mode, ScriptSelect } from "../DisplayModeSelect"; import ModeSelect, { Mode, ScriptSelect } from "../selects/DisplayModeSelect";
import { useState } from "react"; import { useState } from "react";
import CompiledPTextDisplay from "../CompiledPTextDisplay"; import CompiledPTextDisplay from "../text-display/CompiledPTextDisplay";
import useStickyState from "../useStickyState"; import useStickyState from "../useStickyState";
import { import {
getEnglishFromRendered, getEnglishFromRendered,

View File

@ -1,28 +1,24 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import ButtonSelect from "../ButtonSelect"; import ButtonSelect from "../selects/ButtonSelect";
import { import {
combineIntoText, combineIntoText,
flattenLengths, flattenLengths,
} from "../../../lib/src/phrase-building/compile"; } from "../../../lib/src/phrase-building/compile";
import { insertNegative } from "../../../lib/src/phrase-building/render-vp"; import { insertNegative } from "../../../lib/src/phrase-building/render-vp";
import * as T from "../../../types"; import * as T from "../../../types";
import TableCell from "../TableCell"; import TableCell from "../tables/TableCell";
import { choosePersInf, getLength } from "../../../lib/src/p-text-helpers"; import { choosePersInf, getLength } from "../../../lib/src/p-text-helpers";
import genderColors from "../gender-colors"; import genderColors from "../gender-colors";
import { eqPsStringWVars } from "../../../lib/src/fp-ps"; import { eqPsStringWVars } from "../../../lib/src/fp-ps";
import PersInfsPicker from "../PersInfsPicker"; import PersInfsPicker from "../selects/PersInfsPicker";
import "./form-display.css"; import "./form-display.css";
import { import {
makeBlock, makeBlock,
makeKid, makeKid,
} from "../../../lib/src/phrase-building/blocks-utils"; } from "../../../lib/src/phrase-building/blocks-utils";
import InlinePs from "../InlinePs"; import InlinePs from "../text-display/InlinePs";
import { personToGenNum } from "../../../lib/src/misc-helpers"; import { personToGenNum } from "../../../lib/src/misc-helpers";
import { roleIcon } from "./../role-icons";
export const roleIcon = {
king: <i className="mx-1 fas fa-crown" />,
servant: <i className="mx-1 fas fa-male" />,
};
function VerbChartDisplay({ function VerbChartDisplay({
chart, chart,
@ -330,9 +326,7 @@ function AgreementInfo({
</div> </div>
{transitivity === "transitive" && past && objNP && ( {transitivity === "transitive" && past && objNP && (
<div> <div>
<InlinePs opts={opts}> <InlinePs opts={opts} ps={flattenLengths(objNP.selection.ps)[0]} />
{flattenLengths(objNP.selection.ps)[0]}
</InlinePs>
{` `}({printGenNum(personToGenNum(objNP.selection.person))}) {` `}({printGenNum(personToGenNum(objNP.selection.person))})
</div> </div>
)} )}

View File

@ -1,186 +1,248 @@
import Select from "react-select"; import Select from "react-select";
import * as T from "../../../types"; import * as T from "../../../types";
import ButtonSelect from "../ButtonSelect"; import ButtonSelect from "../selects/ButtonSelect";
import { isImperativeTense, isAbilityTense, isPerfectTense, isVerbTense } from "../../../lib/src/type-predicates";
import useStickyState from "../useStickyState"; import useStickyState from "../useStickyState";
import { customStyles } from "../EntrySelect"; import { customSelectStyles as customStyles } from "../selects/select-styles";
import { VpsReducerAction } from "../../../lib/src/phrase-building/vps-reducer";
import { import {
VpsReducerAction, imperativeTenseOptions,
} from "../../../lib/src/phrase-building/vps-reducer"; perfectTenseOptions,
import { imperativeTenseOptions, perfectTenseOptions, verbTenseOptions } from "./verbTenseOptions"; verbTenseOptions,
} from "./verbTenseOptions";
import { getTenseCategory } from "./vp-explorer-util";
function composeFormula(formula: string, prefix: "passive" | "ability"): string { function composeFormula(
return formula.replace(/^perfective/, `${prefix} perfective`) formula: string,
.replace(/^imperfective/, `${prefix} imperfective`) prefix: "passive" | "ability"
.replace("continuous", `${prefix} continuous`) ): string {
.replace("simple", `${prefix} simple`) return formula
.replace(/present$/, `${prefix} present`) .replace(/^perfective/, `${prefix} perfective`)
.replace(/subjunctive$/, `${prefix} subjunctive`) .replace(/^imperfective/, `${prefix} imperfective`)
.replace("past participle", `${prefix} past participle`); .replace("continuous", `${prefix} continuous`)
.replace("simple", `${prefix} simple`)
.replace(/present$/, `${prefix} present`)
.replace(/subjunctive$/, `${prefix} subjunctive`)
.replace("past participle", `${prefix} past participle`);
} }
export function getRandomTense(o?: T.PerfectTense | T.VerbTense | T.AbilityTense | T.ImperativeTense): T.PerfectTense | T.VerbTense | T.AbilityTense | T.ImperativeTense { function TensePicker(
let tns: T.PerfectTense | T.VerbTense | T.AbilityTense | T.ImperativeTense; props: (
const oldTenseCategory = !o | {
? undefined vps: T.VPSelectionState;
: getTenseCategory(o); }
const tenseOptions = oldTenseCategory === "perfect" | {
? perfectTenseOptions vpsComplete: T.VPSelectionComplete;
: oldTenseCategory === "modal" }
? verbTenseOptions.map(x => ({ ...x, value: `${x.value}Modal` as T.AbilityTense })) ) & {
: oldTenseCategory === "imperative" onChange: (p: VpsReducerAction) => void;
? imperativeTenseOptions mode: "charts" | "phrases" | "quiz";
: verbTenseOptions; }
do { ) {
tns = tenseOptions[ const [showFormula, setShowFormula] = useStickyState<boolean>(
Math.floor(Math.random()*tenseOptions.length) false,
].value; "showFormula"
} while (o === tns); );
return tns; function onTenseSelect(
} o: { value: T.VerbTense | T.PerfectTense | T.ImperativeTense } | null
) {
function TensePicker(props: ({ if ("vpsComplete" in props) return;
vps: T.VPSelectionState, const tense = o?.value ? o.value : undefined;
} | { props.onChange({
vpsComplete: T.VPSelectionComplete, type: "set tense",
}) & { payload: tense,
onChange: (p: VpsReducerAction) => void, });
mode: "charts" | "phrases" | "quiz", }
}) { function moveTense(dir: "forward" | "back") {
const [showFormula, setShowFormula] = useStickyState<boolean>(false, "showFormula"); if ("vpsComplete" in props) return;
function onTenseSelect(o: { value: T.VerbTense | T.PerfectTense | T.ImperativeTense } | null) { if (!props.vps.verb) return;
if ("vpsComplete" in props) return; return () => {
const tense = o?.value ? o.value : undefined; // TODO: ABSTRACT THIS - SAFER
props.onChange({ const tenses =
type: "set tense", props.vps.verb.tenseCategory === "perfect"
payload: tense, ? perfectTenseOptions
}); : props.vps.verb.tenseCategory === "imperative"
} ? imperativeTenseOptions
function moveTense(dir: "forward" | "back") { : verbTenseOptions;
if ("vpsComplete" in props) return; const currIndex = tenses.findIndex(
if (!props.vps.verb) return; (tn) =>
return () => { tn.value ===
// TODO: ABSTRACT THIS - SAFER props.vps.verb[
const tenses = props.vps.verb.tenseCategory === "perfect" // TODO: ABSTRACT THIS? - SAFER
? perfectTenseOptions props.vps.verb.tenseCategory === "perfect"
: props.vps.verb.tenseCategory === "imperative" ? "perfectTense"
? imperativeTenseOptions : props.vps.verb.tenseCategory === "imperative"
: verbTenseOptions; ? "imperativeTense"
const currIndex = tenses.findIndex(tn => tn.value === props.vps.verb[ : "verbTense"
// TODO: ABSTRACT THIS? - SAFER ]
props.vps.verb.tenseCategory === "perfect" );
? "perfectTense" if (currIndex === -1) {
: props.vps.verb.tenseCategory === "imperative" console.error("error moving tense", dir);
? "imperativeTense" return;
: "verbTense" }
]); const newIndex =
if (currIndex === -1) { dir === "forward"
console.error("error moving tense", dir); ? (currIndex + 1) % tenses.length
return; : currIndex === 0
} ? tenses.length - 1
const newIndex = dir === "forward" : currIndex - 1;
? ((currIndex + 1) % tenses.length) const newTense = tenses[newIndex];
: (currIndex === 0 ? (tenses.length - 1) : (currIndex - 1)) onTenseSelect(newTense);
const newTense = tenses[newIndex]; };
onTenseSelect(newTense); }
}; function onPosNegSelect(payload: "true" | "false") {
} if ("vpsComplete" in props) return;
function onPosNegSelect(payload: "true" | "false") { props.onChange({
if ("vpsComplete" in props) return; type: "set negativity",
props.onChange({ payload,
type: "set negativity", });
payload, }
}); function onTenseCategorySelect(
} payload: "basic" | "modal" | "perfect" | "imperative"
function onTenseCategorySelect(payload: "basic" | "modal" | "perfect" | "imperative") { ) {
if ("vpsComplete" in props) return; if ("vpsComplete" in props) return;
props.onChange({ props.onChange({
type: "set tense category", type: "set tense category",
payload, payload,
}); });
} }
const tOptions = ("vps" in props && (props.vps.verb?.tenseCategory === "perfect")) const tOptions =
? perfectTenseOptions "vps" in props && props.vps.verb?.tenseCategory === "perfect"
: ("vps" in props && (props.vps.verb?.tenseCategory === "imperative")) ? perfectTenseOptions
? imperativeTenseOptions : "vps" in props && props.vps.verb?.tenseCategory === "imperative"
: verbTenseOptions; ? imperativeTenseOptions
const showImperativeOption = ("vps" in props && props.vps.verb.voice === "active") : verbTenseOptions;
|| ("vpsComplete" in props && props.vpsComplete.verb.voice !== "active"); const showImperativeOption =
const inAllTensesMode = props.mode === "charts"; ("vps" in props && props.vps.verb.voice === "active") ||
const canHaveFormula = "vps" in props ("vpsComplete" in props && props.vpsComplete.verb.voice !== "active");
&& props.mode !== "quiz" const inAllTensesMode = props.mode === "charts";
&& !inAllTensesMode; const canHaveFormula =
return <div> "vps" in props && props.mode !== "quiz" && !inAllTensesMode;
<div style={{ maxWidth: "300px", minWidth: "250px", margin: "0 auto" }}> return (
<div className="d-flex flex-row justify-content-between align-items-center"> <div>
<div className="h5"> <div style={{ maxWidth: "300px", minWidth: "250px", margin: "0 auto" }}>
{props.mode === "charts" <div className="d-flex flex-row justify-content-between align-items-center">
? "Tense Category:" <div className="h5">
: "Verb Tense:"} {props.mode === "charts" ? "Tense Category:" : "Verb Tense:"}
</div> </div>
{canHaveFormula && <div className="clickable mb-2 small" onClick={() => setShowFormula(x => !x)}> {canHaveFormula && (
🧪 {!showFormula ? "Show" : "Hide"} Formula <div
</div>} className="clickable mb-2 small"
onClick={() => setShowFormula((x) => !x)}
>
🧪 {!showFormula ? "Show" : "Hide"} Formula
</div> </div>
{("vpsComplete" in props || props.vps.verb) && <div className="mb-2"> )}
<ButtonSelect </div>
small {("vpsComplete" in props || props.vps.verb) && (
value={"vpsComplete" in props <div className="mb-2">
? getTenseCategory(props.vpsComplete.verb.tense) <ButtonSelect
: props.vps.verb.tenseCategory} small
// @ts-ignore value={
options={showImperativeOption ? [{ "vpsComplete" in props
? getTenseCategory(props.vpsComplete.verb.tense)
: props.vps.verb.tenseCategory
}
options={
showImperativeOption
? [
{
label: "Basic", label: "Basic",
value: "basic", value: "basic",
}, { },
{
label: "Perfect", label: "Perfect",
value: "perfect", value: "perfect",
}, { },
{
label: "Ability", label: "Ability",
value: "modal", value: "modal",
}, { },
{
label: "Imperative", label: "Imperative",
value: "imperative", value: "imperative",
}] : [{ },
]
: [
{
label: "Basic", label: "Basic",
value: "basic", value: "basic",
}, { },
{
label: "Perfect", label: "Perfect",
value: "perfect", value: "perfect",
}, { },
{
label: "Ability", label: "Ability",
value: "modal", value: "modal",
}]} },
handleChange={props.mode !== "quiz" ? onTenseCategorySelect : () => null} ]
/> }
</div>} handleChange={
{"vpsComplete" in props props.mode !== "quiz" ? onTenseCategorySelect : () => null
? <div style={{ fontSize: "larger" }} className="mb-3"> }
{[...verbTenseOptions, ...perfectTenseOptions, ...imperativeTenseOptions].find(o => o.value === props.vpsComplete.verb.tense)?.label} />
</div> </div>
: <> )}
{!inAllTensesMode && <Select {"vpsComplete" in props ? (
isSearchable={false} <div style={{ fontSize: "larger" }} className="mb-3">
// for some reason can't use tOptions with find here; {
value={props.vps.verb && ([...verbTenseOptions, ...perfectTenseOptions, ...imperativeTenseOptions].find(o => o.value === props.vps.verb[ [
...verbTenseOptions,
...perfectTenseOptions,
...imperativeTenseOptions,
].find((o) => o.value === props.vpsComplete.verb.tense)?.label
}
</div>
) : (
<>
{!inAllTensesMode && (
<Select
isSearchable={false}
// for some reason can't use tOptions with find here;
value={
props.vps.verb &&
[
...verbTenseOptions,
...perfectTenseOptions,
...imperativeTenseOptions,
].find(
(o) =>
o.value ===
props.vps.verb[
props.vps.verb.tenseCategory === "perfect" props.vps.verb.tenseCategory === "perfect"
? "perfectTense" ? "perfectTense"
: props.vps.verb.tenseCategory === "imperative" : props.vps.verb.tenseCategory === "imperative"
? "imperativeTense" ? "imperativeTense"
: "verbTense" : "verbTense"
]))} ]
// @ts-ignore - gets messed up when using customStyles )
onChange={onTenseSelect} }
className="mb-2" // @ts-expect-error slight mismatch but it's ok
options={tOptions} onChange={onTenseSelect}
styles={customStyles} className="mb-2"
/>} options={tOptions}
</>} styles={customStyles}
{"vps" in props && props.vps.verb && (props.mode !== "quiz") && <div className="d-flex flex-row justify-content-between align-items-center mt-2 mb-1" style={{ width: "100%" }}> />
{!inAllTensesMode ? <div className="btn btn-light clickable" onClick={moveTense("back")}> )}
<i className="fas fa-chevron-left" /> </>
</div> : <div />} )}
{/* {props.mode === "charts" && <ButtonSelect {"vps" in props && props.vps.verb && props.mode !== "quiz" && (
<div
className="d-flex flex-row justify-content-between align-items-center mt-2 mb-1"
style={{ width: "100%" }}
>
{!inAllTensesMode ? (
<div
className="btn btn-light clickable"
onClick={moveTense("back")}
>
<i className="fas fa-chevron-left" />
</div>
) : (
<div />
)}
{/* {props.mode === "charts" && <ButtonSelect
small small
value={props.chartMode} value={props.chartMode}
options={[{ options={[{
@ -192,64 +254,81 @@ function TensePicker(props: ({
}]} }]}
handleChange={props.onChartModeChange} handleChange={props.onChartModeChange}
/>} */} />} */}
{props.mode === "phrases" && <ButtonSelect {props.mode === "phrases" && (
small <ButtonSelect
value={props.vps.verb.negative.toString() as "true" | "false"} small
options={[{ value={props.vps.verb.negative.toString() as "true" | "false"}
label: "Pos.", options={[
value: "false", {
}, { label: "Pos.",
label: "Neg.", value: "false",
value: "true", },
}]} {
handleChange={onPosNegSelect} label: "Neg.",
/>} value: "true",
{!inAllTensesMode ? <div onClick={moveTense("forward")} className="btn btn-light clickable"> },
<i className="fas fa-chevron-right" /> ]}
</div> : <div />} handleChange={onPosNegSelect}
</div>} />
{(canHaveFormula && showFormula) && (() => { )}
// TODO: Be able to show modal formulas too {!inAllTensesMode ? (
const curr = (props.vps.verb.tenseCategory === "imperative" && props.vps.verb.negative) <div
? imperativeTenseOptions.find(x => x.value === "imperfectiveImperative") onClick={moveTense("forward")}
: [...verbTenseOptions, ...perfectTenseOptions, ...imperativeTenseOptions].find(o => o.value === props.vps.verb[ className="btn btn-light clickable"
>
<i className="fas fa-chevron-right" />
</div>
) : (
<div />
)}
</div>
)}
{canHaveFormula &&
showFormula &&
(() => {
// TODO: Be able to show modal formulas too
const curr =
props.vps.verb.tenseCategory === "imperative" &&
props.vps.verb.negative
? imperativeTenseOptions.find(
(x) => x.value === "imperfectiveImperative"
)
: [
...verbTenseOptions,
...perfectTenseOptions,
...imperativeTenseOptions,
].find(
(o) =>
o.value ===
props.vps.verb[
props.vps.verb.tenseCategory === "perfect" props.vps.verb.tenseCategory === "perfect"
? "perfectTense" ? "perfectTense"
: props.vps.verb.tenseCategory === "imperative" : props.vps.verb.tenseCategory === "imperative"
? "imperativeTense" ? "imperativeTense"
: "verbTense" : "verbTense"
]); ]
const formula = !curr );
? "" const formula = !curr
: (props.vps.verb.tenseCategory === "modal") ? ""
? composeFormula(curr.formula, "ability") : props.vps.verb.tenseCategory === "modal"
: (props.vps.verb.voice === "passive") ? composeFormula(curr.formula, "ability")
? composeFormula(curr.formula, "passive") : props.vps.verb.voice === "passive"
: curr.formula; ? composeFormula(curr.formula, "passive")
if (curr && "formula" in curr) { : curr.formula;
return <div className="mb-2" style={{ width: "250px", overflowY: "auto" }}> if (curr && "formula" in curr) {
<samp>{formula}</samp> return (
</div> <div
} className="mb-2"
})()} style={{ width: "250px", overflowY: "auto" }}
</div> >
</div>; <samp>{formula}</samp>
</div>
);
}
})()}
</div>
</div>
);
} }
export default TensePicker; export default TensePicker;
function getTenseCategory(tense: T.VerbTense | T.PerfectTense | T.AbilityTense | T.ImperativeTense): "basic" | "perfect" | "modal" | "imperative" {
if (isPerfectTense(tense)) {
return "perfect";
}
if (isVerbTense(tense)) {
return "basic";
}
if (isAbilityTense(tense)) {
return "modal";
}
if (isImperativeTense(tense)) {
return "imperative";
}
throw new Error("can't catagorize tense");
}

View File

@ -11,10 +11,10 @@ import ModeSelect, {
LengthSelect, LengthSelect,
Mode, Mode,
ScriptSelect, ScriptSelect,
} from "../DisplayModeSelect"; } from "../selects/DisplayModeSelect";
import { useState } from "react"; import { useState } from "react";
import CompiledPTextDisplay from "../CompiledPTextDisplay"; import CompiledPTextDisplay from "../text-display/CompiledPTextDisplay";
import RenderedBlocksDisplay from "../RenderedBlocksDisplay"; import RenderedBlocksDisplay from "../blocks/RenderedBlocksDisplay";
import useStickyState from "../useStickyState"; import useStickyState from "../useStickyState";
function VPDisplay({ function VPDisplay({

View File

@ -1,14 +1,13 @@
import VerbPicker from "./VerbPicker"; import VerbPicker from "./VerbPicker";
import TensePicker from "./TensePicker"; import TensePicker from "./TensePicker";
import VPDisplay from "./VPDisplay"; import VPDisplay from "./VPDisplay";
import ButtonSelect from "../ButtonSelect"; import ButtonSelect from "../selects/ButtonSelect";
import * as T from "../../../types"; import * as T from "../../../types";
import useStickyState, { useStickyReducer } from "../useStickyState"; import useStickyState, { useStickyReducer } from "../useStickyState";
import { makeVPSelectionState } from "../../../lib/src/phrase-building/verb-selection"; import { makeVPSelectionState } from "../../../lib/src/phrase-building/verb-selection";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { completeVPSelection } from "../../../lib/src/phrase-building/vp-tools"; import { completeVPSelection } from "../../../lib/src/phrase-building/vp-tools";
import VPExplorerQuiz from "./VPExplorerQuiz"; import VPExplorerQuiz from "./VPExplorerQuiz";
// @ts-ignore
import LZString from "lz-string"; import LZString from "lz-string";
import { import {
VpsReducerAction, VpsReducerAction,

View File

@ -1,56 +1,95 @@
import { Modal } from "react-bootstrap"; import { Modal } from "react-bootstrap";
import structureDiagram from "./vp-structure.svg"; import structureDiagram from "./vp-structure.svg";
import { roleIcon } from "../role-icons";
export const roleIcon = { function VPExplorerExplanationModal({
king: <i className="mx-1 fas fa-crown" />, showing,
servant: <i className="mx-1 fas fa-male" />, setShowing,
}; }: {
showing: { role: "servant" | "king"; item: "subject" | "object" } | false;
function VPExplorerExplanationModal({ showing, setShowing }: { setShowing: (
showing: { role: "servant" | "king", item: "subject" | "object" } | false, s: { role: "servant" | "king"; item: "subject" | "object" } | false
setShowing: (s: { role: "servant" | "king", item: "subject" | "object" } | false) => void, ) => void;
}) { }) {
if (!showing) return null; if (!showing) return null;
return <> return (
<Modal show={!!showing} onHide={() => setShowing(false)}> <>
<Modal.Header closeButton> <Modal show={!!showing} onHide={() => setShowing(false)}>
<Modal.Title>About the {showing.role === "king" <Modal.Header closeButton>
? <span>King {roleIcon.king}</span> <Modal.Title>
: <span>Servant {roleIcon.servant}</span>} About the{" "}
</Modal.Title> {showing.role === "king" ? (
</Modal.Header> <span>King {roleIcon.king}</span>
<Modal.Body> ) : (
<p>In this tense/form, the {showing.item} is the <strong>{showing.role}</strong> {roleIcon[showing.role]} of the phrase.</p> <span>Servant {roleIcon.servant}</span>
<p>That means that:</p> )}
{showing.role === "king" </Modal.Title>
? <ul className="mt-2"> </Modal.Header>
<li> <Modal.Body>
<div>It <strong>controls the verb conjugation</strong>. The verb agrees with the gender and number of the king.</div> <p>
</li> In this tense/form, the {showing.item} is the{" "}
<li> <strong>{showing.role}</strong> {roleIcon[showing.role]} of the
<div>It can be removed / left out from the phrase.</div> phrase.
<div className="text-muted">(You can <strong>kill the king</strong>)</div> </p>
</li> <p>That means that:</p>
</ul> {showing.role === "king" ? (
: <ul> <ul className="mt-2">
<li> <li>
<div>It can shrink it into a <a target="_blank" rel="noreferrer" href="https://grammar.lingdocs.com/pronouns/pronouns-mini/">mini-pronoun</a>.</div> <div>
<div className="text-muted">(You can <strong>shrink the servant</strong>)</div> It <strong>controls the verb conjugation</strong>. The verb
</li> agrees with the gender and number of the king.
</ul> </div>
} </li>
<h4>Mnemonic for shortening phrases:</h4> <li>
<p className="text-muted lead">"🚫 Kill the king 👶 Shrink the servant"</p> <div>It can be removed / left out from the phrase.</div>
<h4 className="mb-3">Verb Phrase Structure</h4> <div className="text-muted">
<img className="img-fluid" alt="Pashto verb phrase structure diagram" src={structureDiagram} /> (You can <strong>kill the king</strong>)
</Modal.Body> </div>
<Modal.Footer> </li>
<button type="button" className="btn btn-primary clb" onClick={() => setShowing(false)}> </ul>
Close ) : (
</button> <ul>
</Modal.Footer> <li>
</Modal> <div>
</>; It can shrink it into a{" "}
<a
target="_blank"
rel="noreferrer"
href="https://grammar.lingdocs.com/pronouns/pronouns-mini/"
>
mini-pronoun
</a>
.
</div>
<div className="text-muted">
(You can <strong>shrink the servant</strong>)
</div>
</li>
</ul>
)}
<h4>Mnemonic for shortening phrases:</h4>
<p className="text-muted lead">
"🚫 Kill the king 👶 Shrink the servant"
</p>
<h4 className="mb-3">Verb Phrase Structure</h4>
<img
className="img-fluid"
alt="Pashto verb phrase structure diagram"
src={structureDiagram}
/>
</Modal.Body>
<Modal.Footer>
<button
type="button"
className="btn btn-primary clb"
onClick={() => setShowing(false)}
>
Close
</button>
</Modal.Footer>
</Modal>
</>
);
} }
export default VPExplorerExplanationModal; export default VPExplorerExplanationModal;

View File

@ -5,14 +5,14 @@ import { baParticle } from "../../../lib/src/grammar-units";
import { randomSubjObj } from "../../../lib/src/np-tools"; import { randomSubjObj } from "../../../lib/src/np-tools";
import { standardizePashto } from "../../../lib/src/standardize-pashto"; import { standardizePashto } from "../../../lib/src/standardize-pashto";
import shuffleArray from "../../../lib/src/shuffle-array"; import shuffleArray from "../../../lib/src/shuffle-array";
import InlinePs from "../InlinePs"; import InlinePs from "../text-display/InlinePs";
import { psStringEquals } from "../../../lib/src/p-text-helpers"; import { psStringEquals } from "../../../lib/src/p-text-helpers";
import { renderVP } from "../../../lib/src/phrase-building/render-vp"; import { renderVP } from "../../../lib/src/phrase-building/render-vp";
import { import {
compileVP, compileVP,
flattenLengths, flattenLengths,
} from "../../../lib/src/phrase-building/compile"; } from "../../../lib/src/phrase-building/compile";
import { getRandomTense } from "./TensePicker"; import { getRandomTense } from "./vp-explorer-util";
import { import {
getTenseFromVerbSelection, getTenseFromVerbSelection,
removeBa, removeBa,
@ -20,7 +20,7 @@ import {
} from "../../../lib/src/phrase-building/vp-tools"; } from "../../../lib/src/phrase-building/vp-tools";
import playAudio from "../play-audio"; import playAudio from "../play-audio";
import TensePicker from "./TensePicker"; import TensePicker from "./TensePicker";
import Keyframes from "../Keyframes"; import Keyframes from "./Keyframes";
import { isImperativeTense } from "../../../lib/src/type-predicates"; import { isImperativeTense } from "../../../lib/src/type-predicates";
import { import {
adjustObjectSelection, adjustObjectSelection,
@ -114,7 +114,7 @@ function VPExplorerQuiz(props: {
const correct = const correct =
"p" in a "p" in a
? isInAnswer(a, quizState.answer) ? isInAnswer(a, quizState.answer)
: // @ts-ignore // TODO: CLEANUP : // @ts-expect-error // TODO: CLEANUP
blanksAnswerCorrect(a, quizState.answer); blanksAnswerCorrect(a, quizState.answer);
setAnswerBlank(""); setAnswerBlank("");
setWithBa(false); setWithBa(false);
@ -190,9 +190,7 @@ function VPExplorerQuiz(props: {
<div className="text-center"> <div className="text-center">
<div <div
style={ style={
showCheck showCheck ? answerFeedback : { ...answerFeedback, opacity: 0 }
? answerFeedback
: ({ ...answerFeedback, opacity: 0 } as any)
} }
> >
{currentCorrectEmoji} {currentCorrectEmoji}
@ -224,7 +222,7 @@ function VPExplorerQuiz(props: {
className="btn btn-answer btn-outline-secondary" className="btn btn-answer btn-outline-secondary"
onClick={() => checkAnswer(o)} onClick={() => checkAnswer(o)}
> >
<InlinePs opts={props.opts}>{o}</InlinePs> <InlinePs opts={props.opts} ps={o} />
</button> </button>
</div> </div>
))} ))}
@ -272,7 +270,7 @@ function VPExplorerQuiz(props: {
className="form-check-label text-muted" className="form-check-label text-muted"
htmlFor="addBa" htmlFor="addBa"
> >
add <InlinePs opts={props.opts}>{baParticle}</InlinePs> in add <InlinePs opts={props.opts} ps={baParticle} /> in
kids' section kids' section
</label> </label>
</div> </div>
@ -288,13 +286,14 @@ function VPExplorerQuiz(props: {
{quizState.stage === "multiple choice" ? ( {quizState.stage === "multiple choice" ? (
<div> <div>
<div className="my-4 lead">The correct answer was:</div> <div className="my-4 lead">The correct answer was:</div>
<InlinePs opts={props.opts}> <InlinePs
{ opts={props.opts}
ps={
quizState.options.find((x) => quizState.options.find((x) =>
isInAnswer(x, quizState.answer) isInAnswer(x, quizState.answer)
) as T.PsString ) as T.PsString
} }
</InlinePs> />
</div> </div>
) : ( ) : (
<div> <div>
@ -303,7 +302,7 @@ function VPExplorerQuiz(props: {
</div> </div>
{quizState.answer.ps.map((p, i) => ( {quizState.answer.ps.map((p, i) => (
<div key={i}> <div key={i}>
<InlinePs opts={props.opts}>{p}</InlinePs> <InlinePs opts={props.opts} ps={p} />
</div> </div>
))} ))}
<div className="mt-2"> <div className="mt-2">
@ -312,8 +311,8 @@ function VPExplorerQuiz(props: {
? "With" ? "With"
: "without"} : "without"}
</strong> </strong>
{` `}a <InlinePs opts={props.opts}>{baParticle}</InlinePs>{" "} {` `}a <InlinePs opts={props.opts} ps={baParticle} /> in the
in the phrase phrase
</div> </div>
</div> </div>
)} )}
@ -399,9 +398,10 @@ function QuizNPDisplay({
<div className="text-centered" style={{ fontSize: "large" }}> <div className="text-centered" style={{ fontSize: "large" }}>
{stage === "blanks" && ( {stage === "blanks" && (
<div> <div>
<InlinePs opts={opts}> <InlinePs
{flattenLengths(children.selection.ps)[0]} opts={opts}
</InlinePs> ps={flattenLengths(children.selection.ps)[0]}
/>
</div> </div>
)} )}
<div>{children.selection.e}</div> <div>{children.selection.e}</div>
@ -441,7 +441,6 @@ function tickQuizState(
v = getRandomVPSelection("tenses")( v = getRandomVPSelection("tenses")(
SOSwitch ? switchSubjObj(newVps) : newVps SOSwitch ? switchSubjObj(newVps) : newVps
); );
// eslint-disable-next-line
} while (wrongVpsS.find((x) => x.verb.tense === v.verb.tense)); } while (wrongVpsS.find((x) => x.verb.tense === v.verb.tense));
wrongVpsS.push(v); wrongVpsS.push(v);
}); });
@ -455,7 +454,7 @@ function tickQuizState(
: "stage" in startingWith : "stage" in startingWith
? startingWith.stage ? startingWith.stage
: "multiple choice"; : "multiple choice";
const blanksAnswer = getBlanksAnswer(newVps); const blanksAnswer = getBlanksAnswer(/* newVps */);
if (stage === "blanks") { if (stage === "blanks") {
return { return {
stage, stage,
@ -480,7 +479,7 @@ function tickQuizState(
return out; return out;
} }
function getBlanksAnswer(vps: T.VPSelectionComplete): { function getBlanksAnswer(/* vps: T.VPSelectionComplete */): {
ps: T.PsString[]; ps: T.PsString[];
withBa: boolean; withBa: boolean;
} { } {

Some files were not shown because too many files have changed in this diff Show More