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

@ -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,
makeVPSelectionState,
EPExplorer,
VPExplorer,
Examples,
VerbFormDisplay,
VerbTable,
VerbInfo,
RootsAndStems,
InflectionsTable,
Pashto, Pashto,
Phonetics, Phonetics,
InlinePs,
ButtonSelect,
Hider,
EntrySelect,
NPPicker,
APPicker,
NPBlock,
APBlock,
Block,
EPDisplay,
VPDisplay,
NPDisplay,
EPPicker,
VPPicker,
CompiledPTextDisplay, CompiledPTextDisplay,
RenderedBlocksDisplay, Examples,
// 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, HumanReadableInflectionPattern,
psJSXMap, psJSXMap,
genderColors, genderColors,
} };
export type VpsReducerAction = VpsA;

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) {
autoAnimate(parent.current);
}
}, [parent]); }, [parent]);
const hLev = Math.min((props.hLevel ? props.hLevel : defaultLevel), 6); const hLev = Math.min(props.hLevel ? props.hLevel : defaultLevel, 6);
const extraMargin = (props.hLevel && (props.hLevel > indentAfterLevel)) const extraMargin =
? `ml-${(props.hLevel - indentAfterLevel) + 1}` props.hLevel && props.hLevel > indentAfterLevel
? `ml-${props.hLevel - indentAfterLevel + 1}`
: ""; : "";
if (props.ignore) { if (props.ignore) {
return <> return <>{props.children}</>;
{props.children}
</>;
} }
return <div className="mb-3" ref={parent}> return (
<div className="mb-3" ref={parent}>
{createElement( {createElement(
`h${hLev}`, `h${hLev}`,
{ {
onClick: props.handleChange, onClick: props.handleChange,
className: classNames( className: classNames("clickable", extraMargin),
"clickable",
extraMargin,
),
}, },
<div className="d-flex flex-row align-items-center"> <div className="d-flex flex-row align-items-center">
<div style={{ width: "1rem" }}> <div style={{ width: "1rem" }}>
{props.showing ? caretDown : caretRight} {props.showing ? caretDown : caretRight}
</div> </div>
{` `} {` `}
{props.aspect {props.aspect ? (
? <i className={`fas fa-${props.aspect === "imperfective" ? "video" : "camera"}`} /> <i
: ""} className={`fas fa-${
<div className="ml-2"> props.aspect === "imperfective" ? "video" : "camera"
{props.label} }`}
/>
) : (
""
)}
<div className="ml-2">{props.label}</div>
</div> </div>
</div>,
)} )}
{props.showing && props.children} {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">(
preferredScript || "f",
"blockScriptChoice"
);
const EP = completeEPSelection(eps); const EP = completeEPSelection(eps);
const subject = getSubjectSelection(eps.blocks); 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">
{!subject && !eps.predicate[eps.predicate.type]
? "Select Subject and Predicate" ? "Select Subject and Predicate"
: (subject && !eps.predicate[eps.predicate.type]) : subject && !eps.predicate[eps.predicate.type]
? "Select Predicate" ? "Select Predicate"
: (!subject && eps.predicate[eps.predicate.type]) : !subject && eps.predicate[eps.predicate.type]
? "Select Subject" ? "Select Subject"
: ""} : ""}
</div> </div>
);
} }
const rendered = renderEP(EP); const rendered = renderEP(EP);
const result = compileEP(rendered); const result = compileEP(rendered);
const renderedSubject = getSubjectSelectionFromBlocks(rendered.blocks).selection; const renderedSubject = getSubjectSelectionFromBlocks(
const renderedPredicate = getPredicateSelectionFromBlocks(rendered.blocks).selection; rendered.blocks
return <div className="text-center pt-3"> ).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="mb-2 d-flex flex-row justify-content-between align-items-center">
<div className="d-flex flex-row"> <div className="d-flex flex-row">
<ModeSelect value={mode} onChange={setMode} /> <ModeSelect value={mode} onChange={setMode} />
{mode === "blocks" && <ScriptSelect value={script} onChange={setScript} />} {mode === "blocks" && (
<ScriptSelect value={script} onChange={setScript} />
)}
</div> </div>
{setOmitSubject !== false ? <ButtonSelect {setOmitSubject !== false ? (
<ButtonSelect
small small
value={(eps.omitSubject ? "true" : "false") as "true" | "false"} value={(eps.omitSubject ? "true" : "false") as "true" | "false"}
options={[ options={[
{ value: "false", label: "Full"}, { value: "false", label: "Full" },
{ value: "true", label: "No Subj."}, { value: "true", label: "No Subj." },
]} ]}
handleChange={setOmitSubject} handleChange={setOmitSubject}
/> : <div />} />
) : (
<div />
)}
<div /> <div />
</div> </div>
{mode === "text" {mode === "text" ? (
? <CompiledPTextDisplay opts={opts} compiled={result} justify={justify} onlyOne={!!onlyOne} length={length || "short"} /> <CompiledPTextDisplay
: <EPBlocksDisplay opts={opts} rendered={rendered} justify={justify} script={script} />} opts={opts}
{result.e && <div className={`text-muted mt-2 text-${justify === "left" ? "left" : justify === "right" ? "right" : "center"}`}> compiled={result}
justify={justify}
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" {onlyOne === "concat"
? result.e.join(" • ") ? result.e.join(" • ")
: onlyOne : onlyOne
? [result.e[0]] ? [result.e[0]]
: result.e.map((e, i) => <div key={i}>{e}</div>)} : result.e.map((e, i) => <div key={i}>{e}</div>)}
</div>}
{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
{` `}
"{renderedPredicate.selection.e ? renderedPredicate.selection.e : "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>
)}
{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
{` `}"
{renderedPredicate.selection.e
? renderedPredicate.selection.e
: "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>
);
} }
export default EPDisplay; export default EPDisplay;

View File

@ -1,18 +1,14 @@
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";
@ -34,15 +30,18 @@ const blankEps: T.EPSelectionState = {
// 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">(
"charts",
"EPExplorerMode"
);
const [eps, adjustEps] = useStickyReducer( const [eps, adjustEps] = useStickyReducer(
epsReducer, epsReducer,
blankEps, blankEps,
"EPState8", "EPState8",
flashMessage, flashMessage
); );
// const [ // const [
// // alertMsg, // // alertMsg,
@ -51,7 +50,9 @@ function EPExplorer(props: {
const [showClipped, setShowClipped] = useState<string>(""); const [showClipped, setShowClipped] = useState<string>("");
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]);
useEffect(() => { useEffect(() => {
const EPSFromUrl = getEPSFromUrl(); const EPSFromUrl = getEPSFromUrl();
@ -92,7 +93,8 @@ function EPExplorer(props: {
flashClippedMessage("Copied phrase URL to clipboard"); flashClippedMessage("Copied phrase URL to clipboard");
} }
const phraseIsComplete = !!completeEPSelection(eps); const phraseIsComplete = !!completeEPSelection(eps);
return <div> return (
<div>
<div className="mt-2 mb-3 d-flex flex-row justify-content-between align-items-center"> <div className="mt-2 mb-3 d-flex flex-row justify-content-between align-items-center">
<div /> <div />
<ButtonSelect <ButtonSelect
@ -103,13 +105,18 @@ function EPExplorer(props: {
]} ]}
handleChange={setMode} handleChange={setMode}
/> />
{!("eps" in props) && <div className="d-flex flex-row"> {!("eps" in props) && (
<div className="d-flex flex-row">
<div <div
className="clickable mr-4" className="clickable mr-4"
onClick={mode === "phrases" ? handleCopyCode : undefined} onClick={mode === "phrases" ? handleCopyCode : undefined}
style={{ width: "1rem" }} style={{ width: "1rem" }}
> >
{(mode === "phrases" && phraseIsComplete) ? <i className="fas fa-code" /> : ""} {mode === "phrases" && phraseIsComplete ? (
<i className="fas fa-code" />
) : (
""
)}
</div> </div>
<div <div
className="clickable" className="clickable"
@ -118,22 +125,29 @@ function EPExplorer(props: {
> >
{mode === "phrases" ? <i className="fas fa-share-alt" /> : ""} {mode === "phrases" ? <i className="fas fa-share-alt" /> : ""}
</div> </div>
</div>}
</div> </div>
{mode === "phrases" && )}
</div>
{mode === "phrases" && (
<EPPicker <EPPicker
opts={props.opts} opts={props.opts}
entryFeeder={props.entryFeeder} entryFeeder={props.entryFeeder}
eps={eps} eps={eps}
onChange={handleEpsChange} onChange={handleEpsChange}
/> />
} )}
{mode === "charts" && <EqChartsDisplay opts={props.opts} />} {mode === "charts" && <EqChartsDisplay opts={props.opts} />}
{mode === "phrases" && <EPDisplay {mode === "phrases" && (
<EPDisplay
opts={props.opts} opts={props.opts}
eps={eps} eps={eps}
setOmitSubject={"eps" in props ? false : payload => adjustEps({ type: "set omitSubject", payload })} setOmitSubject={
/>} "eps" in props
? false
: (payload) => adjustEps({ type: "set omitSubject", payload })
}
/>
)}
{/* {alertMsg && <div className="alert alert-warning text-center" role="alert" style={{ {/* {alertMsg && <div className="alert alert-warning text-center" role="alert" style={{
position: "fixed", position: "fixed",
top: "30%", top: "30%",
@ -143,16 +157,23 @@ 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} {showClipped}
</div>} </div>
</div>; )}
</div>
);
} }
export default EPExplorer; export default EPExplorer;

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,13 +1,17 @@
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;
@ -21,14 +25,17 @@ function EquativePicker({ equative, onChange, hideNegative }: {
} }
function moveTense(dir: "forward" | "back") { function moveTense(dir: "forward" | "back") {
return () => { return () => {
const currIndex = options.findIndex(tn => tn.value === equative.tense) const currIndex = options.findIndex((tn) => tn.value === equative.tense);
if (currIndex === -1) { if (currIndex === -1) {
console.error("error moving tense", dir); console.error("error moving tense", dir);
return; return;
} }
const newIndex = dir === "forward" const newIndex =
? ((currIndex + 1) % options.length) dir === "forward"
: (currIndex === 0 ? (options.length - 1) : (currIndex - 1)) ? (currIndex + 1) % options.length
: currIndex === 0
? options.length - 1
: currIndex - 1;
const newTense = options[newIndex]; const newTense = options[newIndex];
onChange({ onChange({
...equative, ...equative,
@ -42,41 +49,59 @@ function EquativePicker({ equative, onChange, hideNegative }: {
negative: value === "true", negative: value === "true",
}); });
} }
return <div> return (
<div>
<div style={{ maxWidth: "300px", minWidth: "250px", margin: "0 auto" }}> <div style={{ maxWidth: "300px", minWidth: "250px", margin: "0 auto" }}>
<div className="h5">Tense:</div> <div className="h5">Tense:</div>
<Select <Select
isSearchable={false} isSearchable={false}
// for some reason can't use tOptions with find here; // for some reason can't use tOptions with find here;
value={options.find(o => o.value === equative.tense)} value={options.find((o) => o.value === equative.tense)}
// @ts-ignore // @ts-expect-error issues with react-select
onChange={onTenseSelect} onChange={onTenseSelect}
className="mb-2" className="mb-2"
options={options} options={options}
styles={customStyles} 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")}> <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" /> <i className="fas fa-chevron-left" />
</div> </div>
{!hideNegative && <ButtonSelect {!hideNegative && (
<ButtonSelect
small small
value={equative.negative.toString() as "true" | "false"} value={equative.negative.toString() as "true" | "false"}
options={[{ options={[
{
label: "Pos.", label: "Pos.",
value: "false", value: "false",
}, { },
{
label: "Neg.", label: "Neg.",
value: "true", value: "true",
}]} },
]}
handleChange={onPosNegSelect} handleChange={onPosNegSelect}
/>} />
<div onClick={moveTense("forward")} className="btn btn-light clickable"> )}
<div
onClick={moveTense("forward")}
className="btn btn-light clickable"
>
<i className="fas fa-chevron-right" /> <i className="fas fa-chevron-right" />
</div> </div>
</div>}
</div> </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,6 +1,8 @@
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(
`https://pashto-inflector.lingdocs.com/sounds/${a}.mp3`
);
audio.addEventListener("ended", () => { audio.addEventListener("ended", () => {
audio.remove(); audio.remove();
audio.srcObject = null; audio.srcObject = null;

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,27 +7,28 @@
*/ */
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
: x
);
useEffect(() => { useEffect(() => {
setLength(l => (l === "mini" && !("mini" in item)) ? "short" : l); setLength((l) => (l === "mini" && !("mini" in item) ? "short" : l));
}, [item]); }, [item]);
// const lengthsAvailable = "long" in item // const lengthsAvailable = "long" in item
// ? [...["long", "short"], ..."mini" in item ? ["mini"] : []] // ? [...["long", "short"], ..."mini" in item ? ["mini"] : []]
@ -35,47 +36,62 @@ function VerbInfoItemDisplay({ item, textOptions, tails }: {
const text = getL(item); 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) ? <>
{Array.isArray(text) ? (
<div> <div>
<div className="text-center" dir="rtl"> <div className="text-center" dir="rtl">
<Pashto opts={textOptions}>{text[0]}</Pashto> <Pashto opts={textOptions} ps={text[0]} />
<span className="mx-1"> __ </span> <span className="mx-1"> __ </span>
<Pashto opts={textOptions}>{tails ? addTails(text[1]) : text[1]}</Pashto> <Pashto
opts={textOptions}
ps={tails ? addTails(text[1]) : text[1]}
/>
</div> </div>
<div className="text-center"> <div className="text-center">
<Phonetics opts={textOptions}>{text[0]}</Phonetics> <Phonetics opts={textOptions} ps={text[0]} />
<span className="mx-1"> __ </span> <span className="mx-1"> __ </span>
<Phonetics opts={textOptions}>{tails ? addTails(text[1]) : text[1]}</Phonetics> <Phonetics
opts={textOptions}
ps={tails ? addTails(text[1]) : text[1]}
/>
</div> </div>
</div> </div>
: ) : (
<div> <div>
<div className="text-center"> <div className="text-center">
<Pashto opts={textOptions}>{tails ? addTails(text) : text}</Pashto> <Pashto opts={textOptions} ps={tails ? addTails(text) : text} />
</div> </div>
<div className="text-center"> <div className="text-center">
<Phonetics opts={textOptions}>{tails ? addTails(text) : text}</Phonetics> <Phonetics opts={textOptions} ps={tails ? addTails(text) : text} />
</div> </div>
</div> </div>
} )}
{"long" in item && <div className="mt-2 text-center"> {"long" in item && (
<div className="mt-2 text-center">
<ButtonSelect <ButtonSelect
xSmall xSmall
options={[ options={[
{ label: "Long", value: "long" }, { label: "Long", value: "long" },
{ label: "Short", value: "short" }, { label: "Short", value: "short" },
..."mini" in item ? [{ ...("mini" in item
label: "Mini", value: "mini", ? [
}] : [], {
label: "Mini",
value: "mini",
},
]
: []),
]} ]}
value={length} value={length}
handleChange={(p) => setLength(p as T.Length)} handleChange={(p) => setLength(p as T.Length)}
/> />
</div>} </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,29 +25,19 @@ 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,
b: React.ReactNode,
}) {
return ( return (
<div className="row align-items-center mb-3"> <div className="row align-items-center mb-3">
<div className="col-5 text-center"> <div className="col-5 text-center">{a}</div>
{a}
</div>
<div className="col-2 text-center" style={{ fontSize: "2.5rem" }}> <div className="col-2 text-center" style={{ fontSize: "2.5rem" }}>
<strong>+</strong> <strong>+</strong>
</div> </div>
<div className="col-5 text-center"> <div className="col-5 text-center">{b}</div>
{b}
</div>
</div> </div>
); );
} }
function ExplanationImage({ src, alt }: { function ExplanationImage({ src, alt }: { src: string; alt?: string }) {
src: any,
alt?: string,
}) {
return ( return (
<img <img
src={src} src={src}
@ -60,137 +48,245 @@ function ExplanationImage({ src, alt }: {
); );
} }
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>
A <strong>stative compound</strong> talks about{" "}
<strong>something changing from one state to another</strong>.
</p>
<CompoundFormula <CompoundFormula
a={<> a={
<>
<div style={{ fontSize: "larger" }}> <div style={{ fontSize: "larger" }}>
<strong>Complement</strong> <strong>Complement</strong>
</div> </div>
<div>Adjective, Noun, or Adverb</div> <div>Adjective, Noun, or Adverb</div>
</>} </>
b={<> }
b={
<>
<div style={{ fontSize: "larger" }}> <div style={{ fontSize: "larger" }}>
<strong>Aux. Verb</strong> <strong>Aux. Verb</strong>
</div> </div>
<div> <div>
<div className="small text-muted">With transitive:</div> <div className="small text-muted">With transitive:</div>
<div><InlinePs opts={opts}>{{ p: "کول", f: "kawul" }}</InlinePs> (to make)</div> <div>
<div className="small text-muted">With intransitive:</div> <InlinePs opts={opts} ps={{ p: "کول", f: "kawul" }} /> (to make)
<div><InlinePs opts={opts}>{{ p: "کېدل", f: "kedul" }}</InlinePs> (to become)</div>
</div> </div>
</>} <div className="small text-muted">With intransitive:</div>
<div>
<InlinePs opts={opts} ps={{ p: "کېدل", f: "kedul" }} /> (to
become)
</div>
</div>
</>
}
/> />
<h4>Transitive stative compounds</h4> <h4>Transitive stative compounds</h4>
<ExplanationImage src={stativeCompTrans} /> <ExplanationImage src={stativeCompTrans} />
<h4>Intransitive stative compounds</h4> <h4>Intransitive stative compounds</h4>
<ExplanationImage src={stativeCompIntrans} /> <ExplanationImage src={stativeCompIntrans} />
<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> <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> <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}>{{ 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> <p>
<p>When complements are nouns or adverbs, they act (and carry a meaning) almost as if they were adjectives.</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>
A <strong>dynamic compound</strong> talks about{" "}
<strong>some action being done</strong>.
</p>
<CompoundFormula <CompoundFormula
a={<> a={
<>
<div style={{ fontSize: "larger" }}> <div style={{ fontSize: "larger" }}>
<strong>Complement</strong> <strong>Complement</strong>
</div> </div>
<div>Noun (action or activity)</div> <div>Noun (action or activity)</div>
</>} </>
b={<> }
b={
<>
<div style={{ fontSize: "larger" }}> <div style={{ fontSize: "larger" }}>
<strong>Aux. Verb</strong> <strong>Aux. Verb</strong>
</div> </div>
</>} </>
}
/> />
<h4>Transitive Dynamic Compounds</h4> <h4>Transitive Dynamic Compounds</h4>
<p>These talk about someone doing an action or activity.</p> <p>These talk about someone doing an action or activity.</p>
<ExplanationImage src={dynamicCompTrans} /> <ExplanationImage src={dynamicCompTrans} />
<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> <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> <h4>Intransitive Dynamic Compounds</h4>
<p>These talk about an action or activity happening.</p> <p>These talk about an action or activity happening.</p>
<ExplanationImage src={dynamicCompIntrans} /> <ExplanationImage src={dynamicCompIntrans} />
<p>Here the complement, the activity included in the compound, is the subject of the sentence.</p> <p>
Here the complement, the activity included in the compound, is the
subject of the sentence.
</p>
<h6>Other notes:</h6> <h6>Other notes:</h6>
<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> <p>
<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> 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>
<strong>Generative stative compounds</strong> are strange compounds that
seem to behave like a cross between a stative compound and a dynamic
compound.
</p>
<ul> <ul>
<li>The object is included in the compound... like with dynamic compounds</li> <li>
<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> The object is included in the compound... like with dynamic compounds
</li>
<li>
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> </ul>
<p>This may seem quite confusing at first.</p> <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> <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> <h4>Transitive Generative Stative Compounds</h4>
<ExplanationImage src={generativeStatCompTrans} /> <ExplanationImage src={generativeStatCompTrans} />
<h4>Intransitive Generative Stative Compounds</h4> <h4>Intransitive Generative Stative Compounds</h4>
<ExplanationImage src={generativeStatCompIntrans} /> <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>
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>
<h4>In all non-past forms</h4> <h4>In all non-past forms</h4>
<ul> <ul>
<li>The subject is not inflected</li> <li>The subject is not inflected</li>
<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> <li>
<li>The verb agrees with the <strong>subject</strong></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>
<li>
The verb agrees with the <strong>subject</strong>
</li>
</ul> </ul>
<ExplanationImage src={transitiveDiagramPresent} /> <ExplanationImage src={transitiveDiagramPresent} />
<h4>In the past tense</h4> <h4>In the past tense</h4>
<ul> <ul>
<li>The subject is inflected, or it can be an enclitic (mini) pronoun</li> <li>
The subject is inflected, or it can be an enclitic (mini) pronoun
</li>
<li>The object is not inflected</li> <li>The object is not inflected</li>
<li>The verb agrees with the <strong>object</strong></li> <li>
The verb agrees with the <strong>object</strong>
</li>
</ul> </ul>
<ExplanationImage src={transitiveDiagramPast} /> <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{" "}
<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>
- The subject is always a <em>uninflected/plain</em> noun or pronoun.
</p>
<p>- The verb always agrees with the subject.</p> <p>- The verb always agrees with the subject.</p>
<ExplanationImage src={intransitiveDiagram} /> <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{" "}
<strong>
verbs that don't appear to have an object, but actually work as if
they do
</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> <h4>In all non-past forms</h4>
<ExplanationImage src={gramTransitiveDiagramPresent} /> <ExplanationImage src={gramTransitiveDiagramPresent} />
<h4>In the past tense</h4> <h4>In the past tense</h4>
<ExplanationImage src={gramTransitiveDiagramPast} /> <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;
} }
@ -198,54 +294,64 @@ function CompoundBreakdown({ info, textOptions }: {
if ("objComplement" in info) { if ("objComplement" in info) {
return info.objComplement.plural return info.objComplement.plural
? info.objComplement.plural ? info.objComplement.plural
: makePsString(info.objComplement.entry.p, info.objComplement.entry.f) : makePsString(info.objComplement.entry.p, info.objComplement.entry.f);
} }
if ("complement" in info) { if ("complement" in info) {
return info.complement.masc[0][0]; return info.complement.masc[0][0];
} } else return makePsString("aa", "aa");
else return makePsString("aa", "aa");
})(); })();
const aux = ((): { ps: T.PsString, e: string } => { const aux = ((): { ps: T.PsString; e: string } => {
if (info.type === "stative compound" || info.type === "generative stative compound") { if (
info.type === "stative compound" ||
info.type === "generative stative compound"
) {
return info.transitivity === "transitive" return info.transitivity === "transitive"
? { ps: { p: "کول", f: "kawul" }, e: "to make"} ? { ps: { p: "کول", f: "kawul" }, e: "to make" }
: { ps: { p: "کېدل", f: "kedul" }, e: "to become"}; : { ps: { p: "کېدل", f: "kedul" }, e: "to become" };
} }
if (!("auxVerb" in info)) return { ps: {p: "", f: ""}, e: ""}; if (!("auxVerb" in info)) return { ps: { p: "", f: "" }, e: "" };
const kawulDyn = info.type === "dynamic compound" && info.auxVerb.p === "کول"; const kawulDyn =
info.type === "dynamic compound" && info.auxVerb.p === "کول";
return { return {
ps: makePsString(info.auxVerb.p, info.auxVerb.f), ps: makePsString(info.auxVerb.p, info.auxVerb.f),
e: kawulDyn ? "to do" : "", e: kawulDyn ? "to do" : "",
} };
})(); })();
return ( return (
<div className="d-block mx-auto my-3" style={{ maxWidth: "400px" }}> <div className="d-block mx-auto my-3" style={{ maxWidth: "400px" }}>
<CompoundFormula <CompoundFormula
a={<div className="compound-breakdown"> a={
<div className="compound-breakdown">
<div> <div>
<Pashto opts={textOptions}>{complement}</Pashto> <Pashto opts={textOptions} ps={complement} />
</div> </div>
<div> <div>
<Phonetics opts={textOptions}>{complement}</Phonetics> <Phonetics opts={textOptions} ps={complement} />
</div> </div>
</div>} </div>
b={<div className="compound-breakdown"> }
b={
<div className="compound-breakdown">
<div> <div>
<Pashto opts={textOptions}>{aux.ps}</Pashto> <Pashto opts={textOptions} ps={aux.ps} />
</div> </div>
<div> <div>
<Phonetics opts={textOptions}>{aux.ps}</Phonetics> <Phonetics opts={textOptions} ps={aux.ps} />
</div> </div>
{aux.e && <div>{aux.e}</div>} {aux.e && <div>{aux.e}</div>}
</div>} </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);
@ -274,18 +380,29 @@ function VerbTypeInfo({ info, textOptions }: {
</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
type="button"
className="btn btn-primary clb"
onClick={() => setShowingTypeModal(false)}
>
Close Close
</button> </button>
</Modal.Footer> </Modal.Footer>
</Modal> </Modal>
<Modal show={showingTransModal} onHide={() => setShowingTransModal(false)}> <Modal
show={showingTransModal}
onHide={() => setShowingTransModal(false)}
>
<Modal.Header closeButton> <Modal.Header closeButton>
<Modal.Title>What does "{info.transitivity}" mean?</Modal.Title> <Modal.Title>What does "{info.transitivity}" mean?</Modal.Title>
</Modal.Header> </Modal.Header>
<Modal.Body>{transitivityInfo(info.transitivity, textOptions)}</Modal.Body> <Modal.Body>{transitivityInfo(info.transitivity)}</Modal.Body>
<Modal.Footer> <Modal.Footer>
<button type="button" className="btn btn-primary clb" onClick={() => setShowingTransModal(false)}> <button
type="button"
className="btn btn-primary clb"
onClick={() => setShowingTransModal(false)}
>
Close Close
</button> </button>
</Modal.Footer> </Modal.Footer>

View File

@ -1,6 +1,6 @@
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 = [
@ -43,37 +43,44 @@ function limitOptions(adjustable: "both" | "king" | "servant") {
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") { if (adjustable === "servant") {
return options.filter(o => !["noKing", "shortest"].includes(o.value)); 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") {
@ -87,19 +94,20 @@ function AbbreviationFormSelector({ form, onChange, adjustable, inline }: {
} }
} }
// TODO: limit display of shrinking options based on the verb type // TODO: limit display of shrinking options based on the verb type
return <div className={classNames("mx-3", { "mb-3": !inline })}> return (
<div className={classNames("mx-3", { "mb-3": !inline })}>
{/* <div className="text-center text-small mb-2">Abbreviation Options</div> */} {/* <div className="text-center text-small mb-2">Abbreviation Options</div> */}
<ButtonSelect <ButtonSelect
faded={inline} faded={inline}
small small
// @ts-ignore // @ts-expect-error slight mismatch but its ok
value={limitValue(formToValue(form), adjustable)} value={limitValue(formToValue(form), adjustable)}
// @ts-ignore // @ts-expect-error slight mismatch but its ok
options={limitOptions(adjustable)} options={limitOptions(adjustable)}
// @ts-ignore
handleChange={handleChange} 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,41 +1,33 @@
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,
b: ReactNode,
}) {
return ( return (
<div className="row align-items-center mb-3"> <div className="row align-items-center mb-3">
<div className="col-5 text-center"> <div className="col-5 text-center">{a}</div>
{a}
</div>
<div className="col-2 text-center" style={{ fontSize: "2.5rem" }}> <div className="col-2 text-center" style={{ fontSize: "2.5rem" }}>
<strong>+</strong> <strong>+</strong>
</div> </div>
<div className="col-5 text-center"> <div className="col-5 text-center">{b}</div>
{b}
</div>
</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;
} }
@ -47,65 +39,79 @@ function CompoundDisplay({ info, opts, handleLinkClick }: {
} }
if ("complement" in info) { if ("complement" in info) {
return info.complement.masc[0][0]; return info.complement.masc[0][0];
} } else return makePsString("aa", "aa");
else return makePsString("aa", "aa");
})(); })();
const aux = ((): { ps: T.PsString, e: string } => { const aux = ((): { ps: T.PsString; e: string } => {
if (info.type === "stative compound" || info.type === "generative stative compound") { if (
info.type === "stative compound" ||
info.type === "generative stative compound"
) {
return info.transitivity === "transitive" return info.transitivity === "transitive"
? { ps: { p: "کول", f: "kawul" }, e: "to make"} ? { ps: { p: "کول", f: "kawul" }, e: "to make" }
: { ps: { p: "کېدل", f: "kedul" }, e: "to become"}; : { ps: { p: "کېدل", f: "kedul" }, e: "to become" };
} }
if (!("auxVerb" in info)) return { ps: {p: "", f: ""}, e: ""}; if (!("auxVerb" in info)) return { ps: { p: "", f: "" }, e: "" };
const kawulDyn = info.type === "dynamic compound" && info.auxVerb.p === "کول"; const kawulDyn =
info.type === "dynamic compound" && info.auxVerb.p === "کول";
return { return {
ps: makePsString(info.auxVerb.p, info.auxVerb.f), ps: makePsString(info.auxVerb.p, info.auxVerb.f),
e: kawulDyn ? "to do" : "", e: kawulDyn ? "to do" : "",
} };
})(); })();
return ( return (
<div className="d-block mx-auto my-3" style={{ maxWidth: "400px" }}> <div className="d-block mx-auto my-3" style={{ maxWidth: "400px" }}>
<div className="text-center">{info.type}</div> <div className="text-center">{info.type}</div>
<CompoundFormula <CompoundFormula
a={<div a={
className={classNames([{ clickable: typeof handleLinkClick === "function" }])} <div
onClick={(handleLinkClick) className={classNames([
// @ts-ignore - thinks there might not be a complement, but there will be { clickable: typeof handleLinkClick === "function" },
? () => handleLinkClick(info.entry.complement?.ts) ])}
: undefined} onClick={
handleLinkClick
? // @ts-expect-error - thinks there might not be a complement, but there will be
() => handleLinkClick(info.entry.complement?.ts)
: undefined
}
> >
<div> <div>
<Pashto opts={opts}>{complement}</Pashto> <Pashto opts={opts} ps={complement} />
</div> </div>
<div> <div>
<Phonetics opts={opts}>{complement}</Phonetics> <Phonetics opts={opts} ps={complement} />
</div> </div>
<div className="small text-muted">{info.entry.complement?.c}</div> <div className="small text-muted">{info.entry.complement?.c}</div>
{info.type === "dynamic compound" && <div> {info.type === "dynamic compound" && <div>(Object)</div>}
(Object) </div>
</div>} }
</div>} b={
b={<div <div
className={classNames([{ clickable: handleLinkClick }])} className={classNames([{ clickable: handleLinkClick }])}
onClick={(handleLinkClick !== "none") onClick={
? () => handleLinkClick( handleLinkClick !== "none"
? () =>
handleLinkClick(
"auxVerb" in info "auxVerb" in info
// dyanmic compound auxVerb ts ? // dyanmic compound auxVerb ts
? info.auxVerb.ts info.auxVerb.ts
// stative compound auxVerb ts : // stative compound auxVerb ts
: ((info.transitivity === "intransitive" (info.transitivity === "intransitive"
? kedulStat ? kedulStat
: kawulStat).info.entry.entry.ts) : kawulStat
).info.entry.entry.ts
) )
: undefined}> : undefined
}
>
<div> <div>
<Pashto opts={opts}>{aux.ps}</Pashto> <Pashto opts={opts} ps={aux.ps} />
</div> </div>
<div> <div>
<Phonetics opts={opts}>{aux.ps}</Phonetics> <Phonetics opts={opts} ps={aux.ps} />
</div> </div>
{aux.e && <div>{aux.e}</div>} {aux.e && <div>{aux.e}</div>}
</div>} </div>
}
/> />
</div> </div>
); );

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,16 +1,23 @@
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 { customStyles } from "../EntrySelect";
import {
VpsReducerAction,
} from "../../../lib/src/phrase-building/vps-reducer";
import { imperativeTenseOptions, perfectTenseOptions, verbTenseOptions } from "./verbTenseOptions";
function composeFormula(formula: string, prefix: "passive" | "ability"): string { import useStickyState from "../useStickyState";
return formula.replace(/^perfective/, `${prefix} perfective`) import { customSelectStyles as customStyles } from "../selects/select-styles";
import { VpsReducerAction } from "../../../lib/src/phrase-building/vps-reducer";
import {
imperativeTenseOptions,
perfectTenseOptions,
verbTenseOptions,
} from "./verbTenseOptions";
import { getTenseCategory } from "./vp-explorer-util";
function composeFormula(
formula: string,
prefix: "passive" | "ability"
): string {
return formula
.replace(/^perfective/, `${prefix} perfective`)
.replace(/^imperfective/, `${prefix} imperfective`) .replace(/^imperfective/, `${prefix} imperfective`)
.replace("continuous", `${prefix} continuous`) .replace("continuous", `${prefix} continuous`)
.replace("simple", `${prefix} simple`) .replace("simple", `${prefix} simple`)
@ -19,36 +26,26 @@ function composeFormula(formula: string, prefix: "passive" | "ability"): string
.replace("past participle", `${prefix} past participle`); .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: ({
vps: T.VPSelectionState,
} | {
vpsComplete: T.VPSelectionComplete,
}) & {
onChange: (p: VpsReducerAction) => void,
mode: "charts" | "phrases" | "quiz",
}) {
const [showFormula, setShowFormula] = useStickyState<boolean>(false, "showFormula");
function onTenseSelect(o: { value: T.VerbTense | T.PerfectTense | T.ImperativeTense } | null) {
if ("vpsComplete" in props) return; if ("vpsComplete" in props) return;
const tense = o?.value ? o.value : undefined; const tense = o?.value ? o.value : undefined;
props.onChange({ props.onChange({
@ -61,26 +58,34 @@ function TensePicker(props: ({
if (!props.vps.verb) return; if (!props.vps.verb) return;
return () => { return () => {
// TODO: ABSTRACT THIS - SAFER // TODO: ABSTRACT THIS - SAFER
const tenses = props.vps.verb.tenseCategory === "perfect" const tenses =
props.vps.verb.tenseCategory === "perfect"
? perfectTenseOptions ? perfectTenseOptions
: props.vps.verb.tenseCategory === "imperative" : props.vps.verb.tenseCategory === "imperative"
? imperativeTenseOptions ? imperativeTenseOptions
: verbTenseOptions; : verbTenseOptions;
const currIndex = tenses.findIndex(tn => tn.value === props.vps.verb[ const currIndex = tenses.findIndex(
(tn) =>
tn.value ===
props.vps.verb[
// TODO: ABSTRACT THIS? - SAFER // TODO: ABSTRACT THIS? - SAFER
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"
]); ]
);
if (currIndex === -1) { if (currIndex === -1) {
console.error("error moving tense", dir); console.error("error moving tense", dir);
return; return;
} }
const newIndex = dir === "forward" const newIndex =
? ((currIndex + 1) % tenses.length) dir === "forward"
: (currIndex === 0 ? (tenses.length - 1) : (currIndex - 1)) ? (currIndex + 1) % tenses.length
: currIndex === 0
? tenses.length - 1
: currIndex - 1;
const newTense = tenses[newIndex]; const newTense = tenses[newIndex];
onTenseSelect(newTense); onTenseSelect(newTense);
}; };
@ -92,94 +97,151 @@ function TensePicker(props: ({
payload, 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 =
"vps" in props && props.vps.verb?.tenseCategory === "perfect"
? perfectTenseOptions ? perfectTenseOptions
: ("vps" in props && (props.vps.verb?.tenseCategory === "imperative")) : "vps" in props && props.vps.verb?.tenseCategory === "imperative"
? imperativeTenseOptions ? imperativeTenseOptions
: verbTenseOptions; : verbTenseOptions;
const showImperativeOption = ("vps" in props && props.vps.verb.voice === "active") const showImperativeOption =
|| ("vpsComplete" in props && props.vpsComplete.verb.voice !== "active"); ("vps" in props && props.vps.verb.voice === "active") ||
("vpsComplete" in props && props.vpsComplete.verb.voice !== "active");
const inAllTensesMode = props.mode === "charts"; const inAllTensesMode = props.mode === "charts";
const canHaveFormula = "vps" in props const canHaveFormula =
&& props.mode !== "quiz" "vps" in props && props.mode !== "quiz" && !inAllTensesMode;
&& !inAllTensesMode; return (
return <div> <div>
<div style={{ maxWidth: "300px", minWidth: "250px", margin: "0 auto" }}> <div style={{ maxWidth: "300px", minWidth: "250px", margin: "0 auto" }}>
<div className="d-flex flex-row justify-content-between align-items-center"> <div className="d-flex flex-row justify-content-between align-items-center">
<div className="h5"> <div className="h5">
{props.mode === "charts" {props.mode === "charts" ? "Tense Category:" : "Verb Tense:"}
? "Tense Category:"
: "Verb Tense:"}
</div> </div>
{canHaveFormula && <div className="clickable mb-2 small" onClick={() => setShowFormula(x => !x)}> {canHaveFormula && (
<div
className="clickable mb-2 small"
onClick={() => setShowFormula((x) => !x)}
>
🧪 {!showFormula ? "Show" : "Hide"} Formula 🧪 {!showFormula ? "Show" : "Hide"} Formula
</div>}
</div> </div>
{("vpsComplete" in props || props.vps.verb) && <div className="mb-2"> )}
</div>
{("vpsComplete" in props || props.vps.verb) && (
<div className="mb-2">
<ButtonSelect <ButtonSelect
small small
value={"vpsComplete" in props value={
"vpsComplete" in props
? getTenseCategory(props.vpsComplete.verb.tense) ? getTenseCategory(props.vpsComplete.verb.tense)
: props.vps.verb.tenseCategory} : props.vps.verb.tenseCategory
// @ts-ignore }
options={showImperativeOption ? [{ 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} ]
}
handleChange={
props.mode !== "quiz" ? onTenseCategorySelect : () => null
}
/> />
</div>}
{"vpsComplete" in props
? <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 ? (
<div style={{ fontSize: "larger" }} className="mb-3">
{
[
...verbTenseOptions,
...perfectTenseOptions,
...imperativeTenseOptions,
].find((o) => o.value === props.vpsComplete.verb.tense)?.label
}
</div>
) : (
<>
{!inAllTensesMode && (
<Select
isSearchable={false} isSearchable={false}
// for some reason can't use tOptions with find here; // for some reason can't use tOptions with find here;
value={props.vps.verb && ([...verbTenseOptions, ...perfectTenseOptions, ...imperativeTenseOptions].find(o => o.value === props.vps.verb[ 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 )
}
// @ts-expect-error slight mismatch but it's ok
onChange={onTenseSelect} onChange={onTenseSelect}
className="mb-2" className="mb-2"
options={tOptions} options={tOptions}
styles={customStyles} 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")}> )}
{"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" /> <i className="fas fa-chevron-left" />
</div> : <div />} </div>
) : (
<div />
)}
{/* {props.mode === "charts" && <ButtonSelect {/* {props.mode === "charts" && <ButtonSelect
small small
value={props.chartMode} value={props.chartMode}
@ -192,64 +254,81 @@ function TensePicker(props: ({
}]} }]}
handleChange={props.onChartModeChange} handleChange={props.onChartModeChange}
/>} */} />} */}
{props.mode === "phrases" && <ButtonSelect {props.mode === "phrases" && (
<ButtonSelect
small small
value={props.vps.verb.negative.toString() as "true" | "false"} value={props.vps.verb.negative.toString() as "true" | "false"}
options={[{ options={[
{
label: "Pos.", label: "Pos.",
value: "false", value: "false",
}, { },
{
label: "Neg.", label: "Neg.",
value: "true", value: "true",
}]} },
]}
handleChange={onPosNegSelect} handleChange={onPosNegSelect}
/>} />
{!inAllTensesMode ? <div onClick={moveTense("forward")} className="btn btn-light clickable"> )}
{!inAllTensesMode ? (
<div
onClick={moveTense("forward")}
className="btn btn-light clickable"
>
<i className="fas fa-chevron-right" /> <i className="fas fa-chevron-right" />
</div> : <div />} </div>
</div>} ) : (
{(canHaveFormula && showFormula) && (() => { <div />
)}
</div>
)}
{canHaveFormula &&
showFormula &&
(() => {
// TODO: Be able to show modal formulas too // TODO: Be able to show modal formulas too
const curr = (props.vps.verb.tenseCategory === "imperative" && props.vps.verb.negative) const curr =
? imperativeTenseOptions.find(x => x.value === "imperfectiveImperative") props.vps.verb.tenseCategory === "imperative" &&
: [...verbTenseOptions, ...perfectTenseOptions, ...imperativeTenseOptions].find(o => o.value === props.vps.verb[ 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") : props.vps.verb.tenseCategory === "modal"
? composeFormula(curr.formula, "ability") ? composeFormula(curr.formula, "ability")
: (props.vps.verb.voice === "passive") : props.vps.verb.voice === "passive"
? composeFormula(curr.formula, "passive") ? composeFormula(curr.formula, "passive")
: curr.formula; : curr.formula;
if (curr && "formula" in curr) { if (curr && "formula" in curr) {
return <div className="mb-2" style={{ width: "250px", overflowY: "auto" }}> return (
<div
className="mb-2"
style={{ width: "250px", overflowY: "auto" }}
>
<samp>{formula}</samp> <samp>{formula}</samp>
</div> </div>
);
} }
})()} })()}
</div> </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 show={!!showing} onHide={() => setShowing(false)}>
<Modal.Header closeButton> <Modal.Header closeButton>
<Modal.Title>About the {showing.role === "king" <Modal.Title>
? <span>King {roleIcon.king}</span> About the{" "}
: <span>Servant {roleIcon.servant}</span>} {showing.role === "king" ? (
<span>King {roleIcon.king}</span>
) : (
<span>Servant {roleIcon.servant}</span>
)}
</Modal.Title> </Modal.Title>
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>
<p>In this tense/form, the {showing.item} is the <strong>{showing.role}</strong> {roleIcon[showing.role]} of the phrase.</p> <p>
In this tense/form, the {showing.item} is the{" "}
<strong>{showing.role}</strong> {roleIcon[showing.role]} of the
phrase.
</p>
<p>That means that:</p> <p>That means that:</p>
{showing.role === "king" {showing.role === "king" ? (
? <ul className="mt-2"> <ul className="mt-2">
<li> <li>
<div>It <strong>controls the verb conjugation</strong>. The verb agrees with the gender and number of the king.</div> <div>
It <strong>controls the verb conjugation</strong>. The verb
agrees with the gender and number of the king.
</div>
</li> </li>
<li> <li>
<div>It can be removed / left out from the phrase.</div> <div>It can be removed / left out from the phrase.</div>
<div className="text-muted">(You can <strong>kill the king</strong>)</div> <div className="text-muted">
(You can <strong>kill the king</strong>)
</div>
</li> </li>
</ul> </ul>
: <ul> ) : (
<ul>
<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 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> </li>
</ul> </ul>
} )}
<h4>Mnemonic for shortening phrases:</h4> <h4>Mnemonic for shortening phrases:</h4>
<p className="text-muted lead">"🚫 Kill the king 👶 Shrink the servant"</p> <p className="text-muted lead">
"🚫 Kill the king 👶 Shrink the servant"
</p>
<h4 className="mb-3">Verb Phrase Structure</h4> <h4 className="mb-3">Verb Phrase Structure</h4>
<img className="img-fluid" alt="Pashto verb phrase structure diagram" src={structureDiagram} /> <img
className="img-fluid"
alt="Pashto verb phrase structure diagram"
src={structureDiagram}
/>
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<button type="button" className="btn btn-primary clb" onClick={() => setShowing(false)}> <button
type="button"
className="btn btn-primary clb"
onClick={() => setShowing(false)}
>
Close Close
</button> </button>
</Modal.Footer> </Modal.Footer>
</Modal> </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;
} { } {

View File

@ -1,4 +1,4 @@
import NPPicker, { shrunkenBackground } from "../../src/np-picker/NPPicker"; import NPPicker, { shrunkenBackground } from "../block-pickers/NPPicker";
import TensePicker from "./TensePicker"; import TensePicker from "./TensePicker";
import * as T from "../../../types"; import * as T from "../../../types";
import { import {
@ -14,10 +14,9 @@ import {
completeVPSelection, completeVPSelection,
isPastTense, isPastTense,
} from "../../../lib/src/phrase-building/vp-tools"; } from "../../../lib/src/phrase-building/vp-tools";
import VPExplorerExplanationModal, { import { roleIcon } from "../role-icons";
roleIcon, import VPExplorerExplanationModal from "./VPExplorerExplanationModal";
} from "./VPExplorerExplanationModal"; import APPicker from "../block-pickers/APPicker";
import APPicker from "../../src/ap-picker/APPicker";
// import autoAnimate from "@formkit/auto-animate"; // import autoAnimate from "@formkit/auto-animate";
import { import {
getObjectSelection, getObjectSelection,
@ -25,7 +24,7 @@ import {
includesShrunkenServant, includesShrunkenServant,
isNoObject, isNoObject,
} from "../../../lib/src/phrase-building/blocks-utils"; } from "../../../lib/src/phrase-building/blocks-utils";
import ComplementPicker from "../ComplementPicker"; import ComplementPicker from "../block-pickers/ComplementPicker";
import { import {
vpsReducer, vpsReducer,
VpsReducerAction, VpsReducerAction,
@ -46,10 +45,9 @@ function VPPicker({
// useEffect(() => { // useEffect(() => {
// parent.current && autoAnimate(parent.current); // parent.current && autoAnimate(parent.current);
// }, [parent]); // }, [parent]);
const [showingExplanation, setShowingExplanation] = const [showingExplanation, setShowingExplanation] = useState<
useState<{ role: "servant" | "king"; item: "subject" | "object" } | false>( { role: "servant" | "king"; item: "subject" | "object" } | false
false >(false);
);
function adjustVps(action: VpsReducerAction) { function adjustVps(action: VpsReducerAction) {
onChange(vpsReducer(vps, action)); onChange(vpsReducer(vps, action));
} }

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