Compare commits

..

No commits in common. "db2749bd0a3d1a441c9f6ee6c4687e29b08cff2c" and "c5a238ab05c06eb448878ed87235254760aa6737" have entirely different histories.

186 changed files with 60706 additions and 8312 deletions

1
.env Normal file
View File

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

View File

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

View File

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

43
.gitignore vendored
View File

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

2
.nvmrc
View File

@ -1 +1 @@
20
16

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import * as T from "./src/types";
import { inflectWord } from "./src/lib/src/pashto-inflector";
import * as tp from "./src/lib/src/type-predicates";
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
// no errors are thrown in the process
@ -14,8 +15,8 @@ type InflectionError = {
};
async function checkAll() {
console.log("Checking inflection functions on all dictionary words");
const res = await fetch(process.env.LINGDOCS_DICTIONARY_URL);
// @ts-ignore
const { entries }: T.Dictionary = await res.json();
const errors: InflectionError[] = [];

2
dev Executable file
View File

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

View File

@ -1,26 +0,0 @@
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,6 +6,7 @@
*
*/
const fs = require("fs");
const fetch = require("node-fetch-commonjs");
const path = require("path");
const verbCollectionPath = path.join(".", "vocab", "verbs");
const nounAdjCollectionPath = path.join(".", "vocab", "nouns-adjs");

1
global.d.ts vendored
View File

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

View File

@ -1,13 +0,0 @@
/** @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]
command = "yarn install-all && yarn test --runInBand --no-cache && yarn build-website"
publish = "dist/"
command = "yarn install-r && yarn test --silent && yarn build-website"
publish = "build/"

View File

@ -1,47 +1,82 @@
{
"name": "pashto-inflector-website",
"version": "7.6.3",
"type": "module",
"name": "pashto-inflector",
"version": "7.5.1",
"author": "lingdocs.com",
"description": "A Pashto inflection and verb conjugation engine, inculding React components for displaying Pashto text, inflections, and conjugations",
"homepage": "https://verbs.lingdocs.com",
"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": {
"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",
"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-all' not 'yarn install' ***!'",
"dev": "vite",
"lint": "eslint .",
"test": "jest",
"preview": "vite preview",
"install-all": "yarn install && node get-words.cjs && cd src/lib && yarn install && cd ../components && yarn install",
"build-website": "tsc -b && vite build",
"build-components": "rm -rf src/components/dist && tsc --project src/components/tsconfig.json && cd src/components && node post-build.cjs",
"build-lib": "rm -rf src/lib/dist && tsc --project src/lib/tsconfig.json",
"get-words": "node get-words.cjs",
"preinstall": "echo '*** Be sure to use yarn install-r not yarn install ***!'",
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"install-r": "yarn install && node get-words.js && cd src/lib && yarn install && cd ../components && yarn install",
"build-website": "node get-words.js && npm run 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",
"test-ci": "npm run test -- --watchAll=false",
"get-words": "node get-words.js",
"check-all-inflections": "tsx check-all-inflections.ts"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"bootstrap": "4.6.1",
"react-bootstrap": "1.5.1",
"@fortawesome/fontawesome-free": "^5.15.2"
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
],
"rules": {
"no-warning-comments": [
1,
{
"terms": [
"fixme",
"xxx"
],
"location": "anywhere"
}
]
}
},
"devDependencies": {
"@eslint/js": "^9.8.0",
"@types/jest": "^29.5.12",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"eslint": "^9.8.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"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"
}
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"dependencies": {}
}

View File

@ -2,11 +2,11 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<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" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/apple-touch-icon.png" />
<meta name="keywords" content="Pashto, Verbs, Conjugation, Grammar, Linguistics" />
<meta name="author" content="lingdocs.com" />
<link rel="canonical" href="https://pashto-inflector.lingdocs.com/" />
@ -25,11 +25,10 @@
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:creator" content="@lingdocs" />
<link rel="manifest" href="/manifest.json" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Pashto Inflector</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<body class="d-flex flex-column h-100" id="root">
<noscript>You need to enable JavaScript to run this app.</noscript>
</body>
</html>

10
shell.nix Normal file
View File

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

View File

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 4.0 KiB

2
src/components/.gitignore vendored Normal file
View File

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

View File

@ -0,0 +1,12 @@
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

@ -0,0 +1,16 @@
// 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

@ -0,0 +1,29 @@
{
"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,81 +1,80 @@
import Pashto from "./src/text-display/Pashto";
import Phonetics from "./src/text-display/Phonetics";
import InlinePs from "./src/text-display/InlinePs";
import Examples from "./src/text-display/Examples";
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";
/**
* 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 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 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 EntrySelect from "./src/EntrySelect";
import VerbInfo, { RootsAndStems } from "./src/verb-info/VerbInfo";
import VPExplorer from "./src/vp-explorer/VPExplorer";
import EPExplorer from "./src/ep-explorer/EPExplorer";
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 { 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 APPicker from "./src/ap-picker/APPicker";
import VPDisplay from "./src/vp-explorer/VPDisplay";
import VPPicker from "./src/vp-explorer/VPPicker";
import NPDisplay from "./src/vp-explorer/NPDisplay";
import HumanReadableInflectionPattern from "./src/tables/HumanReadableInflectionPattern";
import { psJSXMap } from "./src/text-display/jsx-map";
import HumanReadableInflectionPattern from "./src/HumanReadableInflectionPattern";
import { psJSXMap } from "./src/jsx-map";
import genderColors from "./src/gender-colors";
// this library also includes everything from the core inflect library
export * from "../lib/library";
export {
// text-display
InlinePs,
Pashto,
Phonetics,
CompiledPTextDisplay,
Examples,
useStickyState,
roleIcon,
vpsReducer,
makeVPSelectionState,
EPExplorer,
VPExplorer,
Examples,
VerbFormDisplay,
VerbTable,
VerbInfo,
RootsAndStems,
InflectionsTable,
Pashto,
Phonetics,
InlinePs,
ButtonSelect,
Hider,
EntrySelect,
NPPicker,
APPicker,
NPBlock,
APBlock,
Block,
EPDisplay,
VPDisplay,
NPDisplay,
EPPicker,
VPPicker,
CompiledPTextDisplay,
RenderedBlocksDisplay,
HumanReadableInflectionPattern,
psJSXMap,
genderColors,
}
// selects
ButtonSelect,
EntrySelect,
PersonSelection,
// tables
InflectionsTable,
VerbTable,
// block-pickers
APPicker,
NPPicker,
SandwichPicker,
// blocks
Block,
// misc
Hider,
useStickyState,
VerbFromDisplay,
VerbInfo,
VPExplorer,
EPExplorer,
playAudio,
roleIcon,
vpsReducer,
VpsReducerAction,
makeVPSelectionState,
RootsAndStems,
VPDisplay,
VPPicker,
NPDisplay,
HumanReadableInflectionPattern,
psJSXMap,
genderColors,
};
export type VpsReducerAction = VpsA;

35162
src/components/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "@lingdocs/ps-react",
"version": "7.6.3",
"version": "7.5.1",
"description": "Pashto inflector library module with React components",
"main": "dist/components/library.js",
"module": "dist/components/library.js",
@ -18,24 +18,35 @@
"LICENSE"
],
"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",
"license": "GPL-3.0",
"license": "MIT",
"dependencies": {
"@formkit/auto-animate": "^1.0.0-beta.3",
"classnames": "^2.5.1",
"classnames": "^2.2.6",
"fp-ts": "^2.16.0",
"lokijs": "^1.5.12",
"jsurl2": "^2.1.0",
"lz-string": "^1.4.4",
"micro-memoize": "^4.1.2",
"pbf": "^3.2.1",
"rambda": "^7.3.0",
"react-bootstrap": "^1.5.1",
"react-error-boundary": "^4.0.13",
"react-select": "^5.4.0"
},
"devDependencies": {
"@types/lokijs": "^1.5.14",
"fs-extra": "^11.2.0"
"@babel/core": "^7.21.0",
"@storybook/addon-actions": "^6.5.16",
"@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

@ -0,0 +1,48 @@
/**
* 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

@ -0,0 +1,37 @@
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

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

View File

@ -1,12 +1,31 @@
import * as T from "../../../types";
import * as T from "../../types";
import { StyleHTMLAttributes } from "react";
import Select from "react-select";
import Select, { StylesConfig } from "react-select";
import AsyncSelect from "react-select/async";
import {
makeSelectOption,
makeVerbSelectOption,
} from "../block-pickers/picker-tools";
import { customSelectStyles as customStyles } from "./select-styles";
} from "./np-picker/picker-tools";
export const customStyles: StylesConfig = {
menuPortal: (base: any) => ({
...base,
zIndex: 99999,
}),
menu: (base: any) => ({
...base,
zIndex: 999999,
}),
option: (provided: any, state: any) => ({
...provided,
padding: "10px 5px",
color: "#121418",
}),
input: (base: any) => ({
...base,
padding: 0,
}),
};
function EntrySelect<E extends T.DictionaryEntry | T.VerbEntry>(props: {
entryFeeder: T.EntryFeederSingleType<E>;
@ -62,7 +81,7 @@ function EntrySelect<E extends T.DictionaryEntry | T.VerbEntry>(props: {
isSearchable={true}
className="mb-2"
value={value}
// @ts-expect-error select issue
// @ts-ignore
onChange={onChange}
defaultOptions={[]}
loadOptions={options}
@ -101,7 +120,7 @@ function EntrySelect<E extends T.DictionaryEntry | T.VerbEntry>(props: {
styles={customStyles}
isSearchable={true}
value={value || null}
// @ts-expect-error select issue
// @ts-ignore - sadly gets messed up when using customStyles
onChange={onChange}
className="mb-2"
options={options}
@ -140,7 +159,7 @@ export function DeterminerSelect(props: {
isSearchable={true}
value={value}
isMulti
// @ts-expect-error select issue
// @ts-ignore - gets messed up when using customStyles
onChange={onChange}
className="mb-2"
options={options}
@ -197,7 +216,7 @@ export function SandwichSelect<E extends T.Sandwich>(props: {
styles={customStyles}
isSearchable={true}
value={value}
// @ts-expect-error select issue
// @ts-ignore - gets messed up when using customStyles
onChange={onChange}
className="mb-2"
options={options}

View File

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

View File

@ -9,88 +9,65 @@
import { createElement, useEffect, useRef } from "react";
import classNames from "classnames";
import * as T from "../../types";
// @ts-expect-error types needed
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"
>
<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 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">
<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 indentAfterLevel = 5;
function Hider(props: {
label: string | JSX.Element;
showing: boolean;
aspect?: T.Aspect;
handleChange: () => void;
children: React.ReactNode;
hLevel?: number;
ignore?: boolean;
label: string | JSX.Element,
showing: boolean,
aspect?: T.Aspect,
handleChange: () => void,
children: React.ReactNode,
hLevel?: number,
ignore?: boolean,
}) {
const parent = useRef<HTMLDivElement>(null);
useEffect(() => {
if (parent.current) {
autoAnimate(parent.current);
const parent = useRef<HTMLDivElement>(null);
useEffect(() => {
parent.current && autoAnimate(parent.current);
}, [parent]);
const hLev = Math.min((props.hLevel ? props.hLevel : defaultLevel), 6);
const extraMargin = (props.hLevel && (props.hLevel > indentAfterLevel))
? `ml-${(props.hLevel - indentAfterLevel) + 1}`
: "";
if (props.ignore) {
return <>
{props.children}
</>;
}
}, [parent]);
const hLev = Math.min(props.hLevel ? props.hLevel : defaultLevel, 6);
const extraMargin =
props.hLevel && props.hLevel > indentAfterLevel
? `ml-${props.hLevel - indentAfterLevel + 1}`
: "";
if (props.ignore) {
return <>{props.children}</>;
}
return (
<div className="mb-3" ref={parent}>
{createElement(
`h${hLev}`,
{
onClick: props.handleChange,
className: classNames("clickable", extraMargin),
},
<div className="d-flex flex-row align-items-center">
<div style={{ width: "1rem" }}>
{props.showing ? caretDown : caretRight}
</div>
{` `}
{props.aspect ? (
<i
className={`fas fa-${
props.aspect === "imperfective" ? "video" : "camera"
}`}
/>
) : (
""
)}
<div className="ml-2">{props.label}</div>
</div>
)}
{props.showing && props.children}
return <div className="mb-3" ref={parent}>
{createElement(
`h${hLev}`,
{
onClick: props.handleChange,
className: classNames(
"clickable",
extraMargin,
),
},
<div className="d-flex flex-row align-items-center">
<div style={{ width: "1rem" }}>
{props.showing ? caretDown : caretRight}
</div>
{` `}
{props.aspect
? <i className={`fas fa-${props.aspect === "imperfective" ? "video" : "camera"}`} />
: ""}
<div className="ml-2">
{props.label}
</div>
</div>,
)}
{props.showing && props.children}
</div>
);
}
export default Hider;

View File

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

View File

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

View File

@ -0,0 +1,37 @@
/**
* 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

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

View File

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

View File

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

View File

@ -0,0 +1,108 @@
/**
* 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

@ -0,0 +1,38 @@
/**
* 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

@ -0,0 +1,67 @@
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

@ -0,0 +1,26 @@
/**
* 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

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

View File

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

View File

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

View File

@ -0,0 +1,31 @@
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,33 +0,0 @@
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,33 +0,0 @@
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,38 +0,0 @@
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

@ -1,228 +0,0 @@
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

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

View File

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

View File

@ -1,113 +0,0 @@
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,151 +1,78 @@
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 ButtonSelect from "../selects/ButtonSelect";
import {
getPredicateSelectionFromBlocks,
getSubjectSelection,
getSubjectSelectionFromBlocks,
} from "../../../lib/src/phrase-building/blocks-utils";
import ButtonSelect from "../ButtonSelect";
import { getPredicateSelectionFromBlocks, getSubjectSelection, getSubjectSelectionFromBlocks } from "../../../lib/src/phrase-building/blocks-utils";
import { useState } from "react";
import CompiledPTextDisplay from "../text-display/CompiledPTextDisplay";
import EPBlocksDisplay from "../blocks/RenderedBlocksDisplay";
import ModeSelect, { Mode, ScriptSelect } from "../selects/DisplayModeSelect";
import CompiledPTextDisplay from "../CompiledPTextDisplay";
import EPBlocksDisplay from "../RenderedBlocksDisplay";
import ModeSelect, { Mode, ScriptSelect } from "../DisplayModeSelect";
import useStickyState from "../useStickyState";
function EPDisplay({
eps,
opts,
setOmitSubject,
justify,
onlyOne,
length,
mode: preferredMode,
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";
function EPDisplay({ eps, opts, setOmitSubject, justify, onlyOne, length, mode: preferredMode, 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 [script, setScript] = useStickyState<"p" | "f">(
preferredScript || "f",
"blockScriptChoice"
);
const EP = completeEPSelection(eps);
const subject = getSubjectSelection(eps.blocks);
const [mode, setMode] = useState<Mode>(preferredMode || "text");
const [script, setScript] = useStickyState<"p" | "f">(preferredScript || "f", "blockScriptChoice");
const EP = completeEPSelection(eps);
const subject = getSubjectSelection(eps.blocks);
if (!EP) {
return (
<div className="lead text-center my-4">
{!subject && !eps.predicate[eps.predicate.type]
? "Select Subject and Predicate"
: subject && !eps.predicate[eps.predicate.type]
? "Select Predicate"
: !subject && eps.predicate[eps.predicate.type]
? "Select Subject"
: ""}
</div>
);
}
const rendered = renderEP(EP);
const result = compileEP(rendered);
const renderedSubject = getSubjectSelectionFromBlocks(
rendered.blocks
).selection;
const renderedPredicate = getPredicateSelectionFromBlocks(
rendered.blocks
).selection;
return (
<div className="text-center pt-3">
<div className="mb-2 d-flex flex-row justify-content-between align-items-center">
<div className="d-flex flex-row">
<ModeSelect value={mode} onChange={setMode} />
{mode === "blocks" && (
<ScriptSelect value={script} onChange={setScript} />
)}
if (!EP) {
return <div className="lead text-center my-4">
{(!subject && !eps.predicate[eps.predicate.type])
? "Select Subject and Predicate"
: (subject && !eps.predicate[eps.predicate.type])
? "Select Predicate"
: (!subject && eps.predicate[eps.predicate.type])
? "Select Subject"
: ""}
</div>
{setOmitSubject !== false ? (
<ButtonSelect
small
value={(eps.omitSubject ? "true" : "false") as "true" | "false"}
options={[
{ value: "false", label: "Full" },
{ value: "true", label: "No Subj." },
]}
handleChange={setOmitSubject}
/>
) : (
<div />
)}
<div />
</div>
{mode === "text" ? (
<CompiledPTextDisplay
opts={opts}
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"
? result.e.join(" • ")
: onlyOne
? [result.e[0]]
: result.e.map((e, i) => <div key={i}>{e}</div>)}
}
const rendered = renderEP(EP);
const result = compileEP(rendered);
const renderedSubject = getSubjectSelectionFromBlocks(rendered.blocks).selection;
const renderedPredicate = getPredicateSelectionFromBlocks(rendered.blocks).selection;
return <div className="text-center pt-3">
<div className="mb-2 d-flex flex-row justify-content-between align-items-center">
<div className="d-flex flex-row">
<ModeSelect value={mode} onChange={setMode} />
{mode === "blocks" && <ScriptSelect value={script} onChange={setScript} />}
</div>
{setOmitSubject !== false ? <ButtonSelect
small
value={(eps.omitSubject ? "true" : "false") as "true" | "false"}
options={[
{ value: "false", label: "Full"},
{ value: "true", label: "No Subj."},
]}
handleChange={setOmitSubject}
/> : <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>
)}
{mode === "text"
? <CompiledPTextDisplay opts={opts} 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"
? result.e.join(" • ")
: onlyOne
? [result.e[0]]
: 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>
);
}
export default EPDisplay;

View File

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

View File

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

View File

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

View File

@ -0,0 +1,31 @@
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

@ -0,0 +1,26 @@
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

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

View File

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

View File

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

View File

@ -0,0 +1,34 @@
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

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

View File

@ -0,0 +1,198 @@
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,6 @@
import * as T from "../../../types";
import { sandwiches } from "../../../lib/src/sandwiches";
import { SandwichSelect } from "../selects/EntrySelect";
import { SandwichSelect } from "../EntrySelect";
import { useEffect, useState } from "react";
import NPPicker from "./NPPicker";

View File

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

View File

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

View File

@ -1,57 +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: 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,134 +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 "../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

@ -1,25 +0,0 @@
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

@ -0,0 +1,31 @@
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

@ -0,0 +1,26 @@
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,132 +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-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

@ -1,56 +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

@ -1,30 +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";
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

@ -1,40 +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,
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

@ -1,36 +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} ps={item} />
</div>
<div>
<Phonetics opts={textOptions} ps={item} />
</div>
{eng && <div className="text-muted">{eng}</div>}
</div>
);
}
export default SingleItemDisplay;

View File

@ -1,102 +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>
);
});
/**
* 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

@ -1,77 +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) {
console.error(e);
throw new Error("error mapping out PsJSX - unbalanced trees");
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,24 +1,28 @@
import { useEffect, useState } from "react";
import ButtonSelect from "../selects/ButtonSelect";
import ButtonSelect from "../ButtonSelect";
import {
combineIntoText,
flattenLengths,
} from "../../../lib/src/phrase-building/compile";
import { insertNegative } from "../../../lib/src/phrase-building/render-vp";
import * as T from "../../../types";
import TableCell from "../tables/TableCell";
import TableCell from "../TableCell";
import { choosePersInf, getLength } from "../../../lib/src/p-text-helpers";
import genderColors from "../gender-colors";
import { eqPsStringWVars } from "../../../lib/src/fp-ps";
import PersInfsPicker from "../selects/PersInfsPicker";
import PersInfsPicker from "../PersInfsPicker";
import "./form-display.css";
import {
makeBlock,
makeKid,
} from "../../../lib/src/phrase-building/blocks-utils";
import InlinePs from "../text-display/InlinePs";
import InlinePs from "../InlinePs";
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({
chart,
@ -326,7 +330,9 @@ function AgreementInfo({
</div>
{transitivity === "transitive" && past && objNP && (
<div>
<InlinePs opts={opts} ps={flattenLengths(objNP.selection.ps)[0]} />
<InlinePs opts={opts}>
{flattenLengths(objNP.selection.ps)[0]}
</InlinePs>
{` `}({printGenNum(personToGenNum(objNP.selection.person))})
</div>
)}

View File

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

View File

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

View File

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

View File

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

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