commit 2759f10d096206db03fbd06023c82444cf63ad0f Author: IRBorisov <8611739+IRBorisov@users.noreply.github.com> Date: Fri Jun 7 20:17:03 2024 +0300 Initial commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..3754bfc1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,69 @@ +# Docs +README.md +LICENSE +TODO.txt + +# Git +.git +.gitignore + + +# Windows specific +*.ps1 + +# Environment variables +.env.* +*/.env.* + + +# Local build/utility folders +**/venv +**/build + + +# Byte-compiled / optimized / DLL files +**/__pycache__/ +*.py[cod] +*$py.class + + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +.mypy_cache/ +cover/ + + +# Django +rsconcept/frontend/static +rsconcept/frontend/media +*.log +db.sqlite3 +db.sqlite3-journal + +# React +.DS_* +*.log +logs +**/*.backup.* +**/*.back.* + +node_modules +bower_components + +*.sublime* + + +# Specific items +docker-compose-dev.yml +docker-compose-prod.yml \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..dd84ea78 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..bbcbbe7d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml new file mode 100644 index 00000000..4ac7da68 --- /dev/null +++ b/.github/workflows/backend.yml @@ -0,0 +1,43 @@ +name: Backend CI + +defaults: + run: + working-directory: rsconcept/backend + +on: + push: + branches: [ "main" ] + paths: + - rsconcept/backend/** + - .github/workflows/backend.yml + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-22.04 + strategy: + max-parallel: 4 + matrix: + python-version: [3.12] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + - name: Lint + run: | + pylint project apps + mypy project apps + - name: Run Tests + if: '!cancelled()' + run: | + python manage.py check + python manage.py test diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml new file mode 100644 index 00000000..fe467311 --- /dev/null +++ b/.github/workflows/frontend.yml @@ -0,0 +1,43 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Frontend CI + +defaults: + run: + working-directory: rsconcept/frontend + +on: + push: + branches: [ "main" ] + paths: + - rsconcept/frontend/** + - .github/workflows/frontend.yml + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-22.04 + + strategy: + matrix: + node-version: [18.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache-dependency-path: rsconcept/frontend/package-lock.json + cache: 'npm' + - name: Build + run: | + npm ci + npm run build --if-present + - name: Test + run: | + npm test diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..fa0da0f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,67 @@ +# SECURITY SENSITIVE FILES +secrets/ +nginx/cert/*.pem + +# External distributions +rsconcept/backend/import/*.whl +rsconcept/backend/static +rsconcept/backend/media +rsconcept/frontend/dist + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + + +# Distribution / packaging +.Python +build/ +eggs/ +.eggs/ + + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ +.mypy_cache/ + + +# Django +*.log +db.sqlite3 +db.sqlite3-journal + + +# React +.DS_* +*.log +logs +**/*.backup.* +**/*.back.* + +node_modules +bower_components + +*.sublime* + +# NextJS +**/.next/ +**/out/ + + +# Environments +venv/ +/GitExtensions.settings +rsconcept/frontend/public/privacy.pdf diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..9ba8fe0c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,87 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Run", + "type": "PowerShell", + "request": "launch", + "script": "${workspaceFolder}/scripts/dev/RunServer.ps1", + "args": [] + }, + { + "name": "Lint", + "type": "PowerShell", + "request": "launch", + "script": "${workspaceFolder}/scripts/dev/RunLint.ps1", + "args": [] + }, + { + "name": "Test", + "type": "PowerShell", + "request": "launch", + "script": "${workspaceFolder}/scripts/dev/RunTests.ps1", + "args": [] + }, + { + "name": "BE-DebugTestFile", + "type": "debugpy", + "request": "launch", + "cwd": "${workspaceFolder}/rsconcept/backend", + "program": "${workspaceFolder}/rsconcept/backend/manage.py", + "args": ["test", "-k", "${fileBasenameNoExtension}"], + "django": true + }, + { + "name": "FE-DebugTestAll", + "type": "node", + "request": "launch", + "runtimeExecutable": "${workspaceFolder}/rsconcept/frontend/node_modules/.bin/jest", + "args": [ + "${fileBasenameNoExtension}", + "--runInBand", + "--watch", + "--coverage=false", + "--no-cache" + ], + "cwd": "${workspaceFolder}/rsconcept/frontend", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "sourceMaps": true, + "windows": { + "program": "${workspaceFolder}/rsconcept/frontend/node_modules/jest/bin/jest" + } + }, + { + "name": "FE-Debug", + "type": "chrome", + "request": "launch", + "url": "http://localhost:3000/library", + "webRoot": "${workspaceFolder}/rsconcept/frontend" + }, + { + "name": "BE-Debug", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/rsconcept/backend/manage.py", + "args": ["runserver"], + "django": true + }, + { + "name": "BE-Coverage", + "type": "PowerShell", + "request": "launch", + "script": "${workspaceFolder}/scripts/dev/RunCoverage.ps1", + "args": [] + }, + { + "name": "Restart", + "type": "PowerShell", + "request": "launch", + "script": "${workspaceFolder}/scripts/dev/RunServer.ps1", + "args": ["-freshStart"] + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..5774325c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,191 @@ +{ + "search.exclude": { + ".mypy_cache/": true, + ".pytest_cache/": true + }, + "typescript.tsdk": "rsconcept/frontend/node_modules/typescript/lib", + "eslint.workingDirectories": ["rsconcept/frontend"], + "isort.args": [ + "--line-length", + "100", + "--multi-line", + "3", + "--project", + "apps" + ], + "autopep8.args": [ + "--max-line-length", + "120", + "--aggressive", + "--ignore", + "E303" + ], + "[python]": { + "editor.defaultFormatter": "ms-python.autopep8", + "editor.formatOnSave": true, + "editor.tabSize": 4, + "editor.insertSpaces": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + } + }, + "python.testing.unittestArgs": ["-v", "-s", "./tests", "-p", "test*.py"], + "python.testing.pytestEnabled": false, + "python.testing.unittestEnabled": true, + "python.analysis.typeCheckingMode": "off", + "python.analysis.ignore": ["**/tests/**", "**/node_modules/**", "**/venv/**"], + "python.analysis.packageIndexDepths": [ + { + "name": "django", + "depth": 5 + } + ], + "colorize.include": [".tsx", ".jsx", ".ts", ".js"], + "colorize.languages": [ + "typescript", + "javascript", + "css", + "typescriptreact", + "javascriptreact" + ], + "cSpell.words": [ + "ablt", + "acconcept", + "accs", + "actv", + "ADJF", + "ADJS", + "ADVB", + "Analyse", + "Backquote", + "BIGPR", + "cctext", + "Certbot", + "CIHT", + "clsx", + "codemirror", + "Constituenta", + "corsheaders", + "csrftoken", + "cstlist", + "csttype", + "datv", + "Debool", + "Decart", + "djangorestframework", + "Downvote", + "EMPTYSET", + "exteor", + "femn", + "filterset", + "forceatlas", + "futr", + "Geologica", + "Grammeme", + "Grammemes", + "GRND", + "impr", + "inan", + "incapsulation", + "indc", + "INFN", + "Infr", + "INTJ", + "Keymap", + "lezer", + "Litr", + "loct", + "moprho", + "multiword", + "mypy", + "nocheck", + "nomn", + "nooverlap", + "NPRO", + "NUMR", + "Opencorpora", + "overscroll", + "passwordreset", + "perfectivity", + "PNCT", + "ponomarev", + "PRCL", + "PRTF", + "PRTS", + "pssv", + "pyconcept", + "Pylance", + "pylint", + "pymorphy", + "Quantor", + "razdel", + "reagraph", + "redef", + "REDOC", + "Reindex", + "rsconcept", + "rsedit", + "rseditor", + "rsform", + "rsforms", + "rsgraph", + "rslang", + "rstemplates", + "setexpr", + "SIDELIST", + "signup", + "Slng", + "SMALLPR", + "Stylesheet", + "symminus", + "tagset", + "tailwindcss", + "tanstack", + "toastify", + "tooltipic", + "tsdoc", + "unknwn", + "Upvote", + "Viewset", + "viewsets", + "wordform", + "Wordforms", + "Биективная", + "биективной", + "Булеан", + "Бурбаки", + "Версионирование", + "Десинглетон", + "доксинг", + "интерпретируемости", + "интерпретируемость", + "компаратив", + "конституент", + "Конституента", + "конституентами", + "конституенте", + "конституенту", + "конституенты", + "Кучкаров", + "Кучкарова", + "неинтерпретируемый", + "неитерируемого", + "пересинтез", + "Родоструктурная", + "родоструктурного", + "Родоструктурное", + "родоструктурной", + "родоструктурном", + "Синглетон", + "твор", + "Терминологизация", + "троллинг", + "Цермелло", + "ЦИВТ", + "Экстеор", + "Экстеора", + "Экстеоре" + ], + "cSpell.language": "en,ru", + "cSpell.ignorePaths": ["node_modules/**", "*.json"] +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..5aba2165 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +portal@acconcept.ru. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..7bd01923 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 CIHT CONCEPT, IRBorisov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..ea99477d --- /dev/null +++ b/README.md @@ -0,0 +1,167 @@ +
+ + + +
+ +
+
+ +[![Backend CI](https://github.com/IRBorisov/ConceptPortal/actions/workflows/backend.yml/badge.svg?branch=main)](https://github.com/IRBorisov/ConceptPortal/actions/workflows/backend.yml) +[![Frontend CI](https://github.com/IRBorisov/ConceptPortal/actions/workflows/frontend.yml/badge.svg?branch=main)](https://github.com/IRBorisov/ConceptPortal/actions/workflows/frontend.yml) + +React + Django based web portal for editing RSForm schemas. +This readme file is used mostly to document project dependencies + +## ❤️ Contributing notes + +- feel free to open issues, discussion topics, contact maintainer directly +- use Test config in VSCode to run tests before pushing commits / requests +- use github actions to setup linter checks and test builds + +## ✨ Frontend [Vite + React + Typescript] + +
+ npm install +
+  - axios
+  - clsx
+  - react-icons
+  - react-router-dom 
+  - react-toastify
+  - react-loader-spinner
+  - react-tabs
+  - react-intl
+  - react-select
+  - react-error-boundary
+  - react-pdf
+  - react-tooltip
+  - js-file-download
+  - use-debounce
+  - framer-motion
+  - reagraph
+  - @tanstack/react-table
+  - @uiw/react-codemirror
+  - @uiw/codemirror-themes
+  - @lezer/lr
+  
+
+
+ npm install -D +
+  - tailwindcss
+  - postcss
+  - autoprefixer
+  - eslint-plugin-simple-import-sort
+  - eslint-plugin-tsdoc
+  - jest
+  - ts-jest
+  - @types/jest
+  - @lezer/generator
+  
+
+
+ VS Code plugins +
+  - ESLint
+  - Colorize
+  - Code Spell Checker (eng + rus)
+  - Backticks
+  - Svg Preview
+  - TODO Highlight v2
+  
+
+
+ Google fonts +
+  - Fira Code
+  - Rubik
+  - Alegreya Sans SC
+  - Noto Sans Math
+  
+
+ +## 🗃️ Backend [Django + PostgreSQL/SQLite] + +- [ConceptCore](https://github.com/IRBorisov/ConceptCore) +
+ requirements +
+  - django
+  - djangorestframework
+  - django-cors-headers
+  - django-filter
+  - drf-spectacular
+  - tzdata
+  - gunicorn
+  - coreapi
+  - psycopg2-binary
+  - cctext
+  - pyconcept
+  
+
+
+ requirements-dev +
+  - coverage
+  - pylint
+  - mypy
+  - djangorestframework-stubs[compatible-mypy]
+  
+
+
+ VS Code plugins +
+  - Pylance
+  - Pylint
+  - Django
+  - autopep8
+  
+
+ +## ⚙️ DevOps + +- Docker compose +- PowerShell +- Certbot +- Docker VSCode extension + +# Developer Notes + +## 🖥️ Local build (Windows 10+) + +This is the build for local Development + +- Install Python 3.12, NodeJS, VSCode, Docker Desktop +- copy import wheels from ConceptCore to rsconcept/backend/import +- run rsconcept/backend/LocalEnvSetup.ps1 +- use VSCode configs in root folder to start development +- use 'npm run prepare' to regenerate frontend parsers (if you change grammar files) + +## 🔭 Local docker build + +This build does not use HTTPS and nginx for networking + +- backend and frontend debugging is supported +- hmr (hot updates) for frontend +- run via 'docker compose -f "docker-compose-dev.yml" up --build -d' +- populate initial data: 'scripts/dev/PopulateDevData.ps1' + +## 📦 Local production build + +This build is same as production except not using production secrets and working on localhost + +- provide TLS certificate (can be self-signed) 'nginx/cert/local-cert.pem' and 'nginx/cert/local-key.pem' +- run via 'docker compose -f "docker-compose-prod-local.yml" up --build -d' + +## 🔥 Production build + +This build is deployed on server. + +- provide secrets: 'secrets/db_password.txt', 'django_key.txt', 'email_host.txt', 'email_password.txt', 'email_user.txt' +- check if you need to change SSL/TLS and PORT in 'rsconcept\backend\.env.prod' +- setup domain names for application and API in configs: 'frontend\env\.env.production', 'rsconcept\backend\.env.dev', 'nginx\production.conf' +- provide privacy policy document in PDF: 'frontend/public/privacy.pdf' +- use certbot to obtain certificates via 'docker compose -f "docker-compose-prod.yml" run --rm certbot certonly --webroot --webroot-path /var/www/certbot/ -d portal.acconcept.ru api.portal.acconcept.ru' +- run via 'docker compose -f "docker-compose-prod.yml" up --build -d' +- update via 'bash scripts/prod/UpdateProd.sh' diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..811f4cc2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,11 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 1.0.0 | :white_check_mark: | + +## Reporting a Vulnerability + +Please report any security vulnerabilities directly to maintainers, do not use public discussions for cricitical security issues. diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 00000000..5da26944 --- /dev/null +++ b/TODO.txt @@ -0,0 +1,64 @@ +!! This is not complete list of TODOs !! +For more specific TODOs see comments in code + +[Functionality - PROGRESS] +- Landing page +- Home page (user specific) + +- Operational synthesis schema as LibraryItem ? + +- Draggable rows in constituents table +- Clickable IDs in RSEditor tooltips + +- Library organization, search and exploration. Consider new user experience +- Private projects and permissions. Consider cooperative editing +- Rework access setup: project-based, user-based, enable sharing. Prevent enumerating access to private schemas by default + + +[Functionality - PENDING] +- User notifications on edit - consider spam prevention and change aggregation +- Static analyzer for RSForm +- Content based search in Library +- User profile: Settings + settings persistency + +- Export PDF (Items list, Graph) +- ARIA (accessibility considerations) - for now machine reading not supported +- Internationalization - at least english version. Consider react.intl +- Focus on codemirror editor when label is clicked (need React 19 ref for clean code solution) + + +[Tech] +- add debounce to some search fields +- duplicate syntax parsing and type info calculations to client. Consider moving backend to Nodejs or embedding c++ lib + +- DataTable: fixed percentage columns, especially for SubstituteTable. Rework column sizing mechanics + + +[Deployment] +- logs collection +- status dashboard for servers + + +[Security] +- password-reset leaks info of email being used +- improve nginx config. Consider DDOS and other types of attacks on infrastructure + + +[Research] +Research and consider integration +- django-allauth - consider supporting popular auth providers +- drf-messages + +- backend error message unification +https://drf-standardized-errors.readthedocs.io/en/latest/error_response.html + +- radix-ui +- shadcn-ui + +- Zod +- use-debounce + +- react-query +- react-hook-form + +- node-based UI \ No newline at end of file diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml new file mode 100644 index 00000000..51804c7d --- /dev/null +++ b/docker-compose-dev.yml @@ -0,0 +1,51 @@ +name: dev-concept-portal + +volumes: + postgres_volume: + name: "dev-portal-data" + django_static_volume: + name: "dev-portal-static" + django_media_volume: + name: "dev-portal-media" + +networks: + default: + name: dev-concept-api-net + +services: + frontend: + container_name: dev-portal-frontend + restart: always + depends_on: + - backend + build: + context: ./rsconcept/frontend + dockerfile: Dockerfile.dev + args: + BUILD_TYPE: development + ports: + - 3002:3002 + command: npm run dev -- --host + + backend: + container_name: dev-portal-backend + restart: always + depends_on: + - postgresql-db + build: + context: ./rsconcept/backend + env_file: ./rsconcept/backend/.env.dev + ports: + - 8002:8002 + volumes: + - django_static_volume:/home/app/web/static + - django_media_volume:/home/app/web/media + command: gunicorn -w 3 project.wsgi --bind 0.0.0.0:8002 + + postgresql-db: + container_name: dev-portal-db + restart: always + image: postgres:alpine + env_file: ./postgresql/.env.dev + volumes: + - postgres_volume:/var/lib/postgresql/data diff --git a/docker-compose-prod-local.yml b/docker-compose-prod-local.yml new file mode 100644 index 00000000..3b941b55 --- /dev/null +++ b/docker-compose-prod-local.yml @@ -0,0 +1,69 @@ +name: local-concept-portal + +volumes: + postgres_volume: + name: "local-portal-data" + django_static_volume: + name: "local-portal-static" + django_media_volume: + name: "local-portal-media" + +networks: + default: + name: local-concept-api-net + +services: + frontend: + container_name: local-portal-frontend + restart: no + depends_on: + - backend + build: + context: ./rsconcept/frontend + args: + BUILD_TYPE: production.local + expose: + - 3001 + command: serve -s /home/node -l 3001 + + backend: + container_name: local-portal-backend + restart: no + depends_on: + - postgresql-db + build: + context: ./rsconcept/backend + env_file: ./rsconcept/backend/.env.prod.local + expose: + - 8001 + volumes: + - django_static_volume:/home/app/web/static + - django_media_volume:/home/app/web/media + command: gunicorn -w 3 project.wsgi --bind 0.0.0.0:8001 + + postgresql-db: + container_name: local-portal-db + restart: no + image: postgres:16-alpine + env_file: ./postgresql/.env.prod.local + volumes: + - postgres_volume:/var/lib/postgresql/data + + nginx: + container_name: local-portal-router + restart: no + build: + context: ./nginx + dockerfile: Dockerfile.local + args: + BUILD_TYPE: production.local + ports: + - 8001:8001 + - 3001:3001 + depends_on: + - backend + - frontend + command: '/bin/sh -c ''while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g "daemon off;"''' + volumes: + - django_static_volume:/var/www/static + - django_media_volume:/var/www/media diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml new file mode 100644 index 00000000..6cab7a6b --- /dev/null +++ b/docker-compose-prod.yml @@ -0,0 +1,110 @@ +name: concept-portal + +volumes: + postgres_volume: + name: "portal-data" + django_static_volume: + name: "portal-static" + django_media_volume: + name: "portal-media" + cerbot_www_volume: + name: "portal-certbot-serve" + cerbot_conf_volume: + name: "portal-certbot-config" + +networks: + default: + name: concept-api-net + +secrets: + django_key: + file: ./secrets/django_key.txt + db_password: + file: ./secrets/db_password.txt + email_host: + file: ./secrets/email_host.txt + email_user: + file: ./secrets/email_user.txt + email_password: + file: ./secrets/email_password.txt + +services: + frontend: + container_name: portal-frontend + restart: always + depends_on: + - backend + build: + context: ./rsconcept/frontend + args: + BUILD_TYPE: production + expose: + - 3000 + command: serve -s /home/node -l 3000 + + backend: + container_name: portal-backend + restart: always + depends_on: + - postgresql-db + secrets: + - db_password + - django_key + - email_host + - email_user + - email_password + build: + context: ./rsconcept/backend + env_file: ./rsconcept/backend/.env.prod + environment: + SECRET_KEY: /run/secrets/django_key + DB_PASSWORD: /run/secrets/db_password + EMAIL_HOST: /run/secrets/email_host + EMAIL_HOST_USER: /run/secrets/email_user + EMAIL_HOST_PASSWORD: /run/secrets/email_password + expose: + - 8000 + volumes: + - django_static_volume:/home/app/web/static + - django_media_volume:/home/app/web/media + command: gunicorn -w 3 project.wsgi --bind 0.0.0.0:8000 + + postgresql-db: + container_name: portal-db + restart: always + image: postgres:16-alpine + secrets: + - db_password + env_file: ./postgresql/.env.prod + environment: + POSTGRES_PASSWORD_FILE: /run/secrets/db_password + volumes: + - postgres_volume:/var/lib/postgresql/data + + certbot: + container_name: portal-certbot + restart: no + image: certbot/certbot:latest + volumes: + - cerbot_www_volume:/var/www/certbot/:rw + - cerbot_conf_volume:/etc/letsencrypt/:rw + + nginx: + container_name: portal-router + restart: always + build: + context: ./nginx + args: + BUILD_TYPE: production + ports: + - 80:80 + - 443:443 + depends_on: + - backend + - frontend + command: '/bin/sh -c ''while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g "daemon off;"''' + volumes: + - django_static_volume:/var/www/static + - django_media_volume:/var/www/media + - cerbot_www_volume:/var/www/certbot/:ro + - cerbot_conf_volume:/etc/nginx/ssl/:ro diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 00000000..6d721ae8 --- /dev/null +++ b/nginx/Dockerfile @@ -0,0 +1,5 @@ +FROM nginx:stable-alpine3.17-slim +ARG BUILD_TYPE=production + +# Сopу nginx configuration to the proxy-server +COPY ./$BUILD_TYPE.conf /etc/nginx/conf.d/default.conf \ No newline at end of file diff --git a/nginx/Dockerfile.local b/nginx/Dockerfile.local new file mode 100644 index 00000000..10a392be --- /dev/null +++ b/nginx/Dockerfile.local @@ -0,0 +1,6 @@ +FROM nginx:stable-alpine3.17-slim +ARG BUILD_TYPE=production + +# Сopу nginx configuration to the proxy-server +COPY ./$BUILD_TYPE.conf /etc/nginx/conf.d/default.conf +COPY ./cert/*.pem /etc/ssl/private/ \ No newline at end of file diff --git a/nginx/cert/README.txt b/nginx/cert/README.txt new file mode 100644 index 00000000..d3de4bb9 --- /dev/null +++ b/nginx/cert/README.txt @@ -0,0 +1,2 @@ +THIS DIRECTORY IS USED ONLY FOR LOCAL CERTIFICATES +USE CERTBOT IN PRODUCTION CONTAINER TO SUPPLY PRODUCTION CERTIFICATES \ No newline at end of file diff --git a/nginx/production.conf b/nginx/production.conf new file mode 100644 index 00000000..2c74fa62 --- /dev/null +++ b/nginx/production.conf @@ -0,0 +1,78 @@ +upstream innerdjango { + server backend:8000; +} + +upstream innerreact { + server frontend:3000; +} + + +server { + listen 80; + listen [::]:80; + + server_name api.portal.acconcept.ru www.api.portal.acconcept.ru; + server_tokens off; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://api.portal.acconcept.ru$request_uri; + } +} + +server { + listen 80; + listen [::]:80; + + server_name portal.acconcept.ru www.portal.acconcept.ru; + server_tokens off; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://portal.acconcept.ru$request_uri; + } +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + + ssl_certificate /etc/nginx/ssl/live/api.portal.acconcept.ru/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/live/api.portal.acconcept.ru/privkey.pem; + server_name api.portal.acconcept.ru www.api.portal.acconcept.ru; + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_pass http://innerdjango; + proxy_redirect default; + } + location /static/ { + alias /var/www/static/; + } + location /media/ { + alias /var/www/media/; + } +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + + ssl_certificate /etc/nginx/ssl/live/portal.acconcept.ru/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/live/portal.acconcept.ru/privkey.pem; + server_name portal.acconcept.ru www.portal.acconcept.ru; + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_pass http://innerreact; + proxy_redirect default; + } +} \ No newline at end of file diff --git a/nginx/production.local.conf b/nginx/production.local.conf new file mode 100644 index 00000000..68c61b84 --- /dev/null +++ b/nginx/production.local.conf @@ -0,0 +1,41 @@ +upstream innerdjango { + server backend:8001; +} + +upstream innerreact { + server frontend:3001; +} + +server { + listen 8001 ssl; + ssl_certificate /etc/ssl/private/local-cert.pem; + ssl_certificate_key /etc/ssl/private/local-key.pem; + server_name localhost; + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_pass http://innerdjango; + proxy_redirect default; + } + location /static/ { + alias /var/www/static/; + } + location /media/ { + alias /var/www/media/; + } +} + +server { + listen 3001 ssl; + ssl_certificate /etc/ssl/private/local-cert.pem; + ssl_certificate_key /etc/ssl/private/local-key.pem; + server_name localhost; + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_pass http://innerreact; + proxy_redirect default; + } +} \ No newline at end of file diff --git a/nginx/starter.conf b/nginx/starter.conf new file mode 100644 index 00000000..fb4b491d --- /dev/null +++ b/nginx/starter.conf @@ -0,0 +1,39 @@ +upstream innerdjango { + server backend:8000; +} + +upstream innerreact { + server frontend:3000; +} + +server { + listen 80; + listen [::]:80; + + server_name api.portal.acconcept.ru www.api.portal.acconcept.ru; + server_tokens off; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://api.portal.acconcept.ru$request_uri; + } +} + +server { + listen 80; + listen [::]:80; + + server_name portal.acconcept.ru www.portal.acconcept.ru; + server_tokens off; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://portal.acconcept.ru$request_uri; + } +} diff --git a/postgresql/.env.dev b/postgresql/.env.dev new file mode 100644 index 00000000..f7e1745f --- /dev/null +++ b/postgresql/.env.dev @@ -0,0 +1,5 @@ +# WARNING! This config does not use 'real' production values for secrets +# DO NOT use PRODUCTION LOCAL build for deployment! +POSTGRES_USER=portal-admin +POSTGRES_DB=portal-db +POSTGRES_PASSWORD=78ACF6C4F3 \ No newline at end of file diff --git a/postgresql/.env.prod b/postgresql/.env.prod new file mode 100644 index 00000000..69f1a2b7 --- /dev/null +++ b/postgresql/.env.prod @@ -0,0 +1,2 @@ +POSTGRES_USER=portal-admin +POSTGRES_DB=portal-db \ No newline at end of file diff --git a/postgresql/.env.prod.local b/postgresql/.env.prod.local new file mode 100644 index 00000000..f7e1745f --- /dev/null +++ b/postgresql/.env.prod.local @@ -0,0 +1,5 @@ +# WARNING! This config does not use 'real' production values for secrets +# DO NOT use PRODUCTION LOCAL build for deployment! +POSTGRES_USER=portal-admin +POSTGRES_DB=portal-db +POSTGRES_PASSWORD=78ACF6C4F3 \ No newline at end of file diff --git a/rsconcept/backend/.dockerignore b/rsconcept/backend/.dockerignore new file mode 100644 index 00000000..3af53493 --- /dev/null +++ b/rsconcept/backend/.dockerignore @@ -0,0 +1,36 @@ +# Windows specific +*.ps1 + +# Dev specific +requirements-dev.txt +.gitignore + + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + + +# Django +static/ +media/ +*.log +db.sqlite3 +db.sqlite3-journal + + +# Byte-compiled / optimized / DLL files +**/__pycache__/ +*.py[cod] +*$py.class \ No newline at end of file diff --git a/rsconcept/backend/.env.dev b/rsconcept/backend/.env.dev new file mode 100644 index 00000000..6b6eb27a --- /dev/null +++ b/rsconcept/backend/.env.dev @@ -0,0 +1,37 @@ +# Application settings +# WARNING! This config does not use 'real' production values for secrets +# DO NOT use PRODUCTION LOCAL build for deployment! +SECRET_KEY=s-55j!5jlan=x%8-6m1qnst^7s6nwby4dx@vei)5w8t)3_=mv1 +ALLOWED_HOSTS=localhost +CSRF_TRUSTED_ORIGINS=http://localhost:3002;http://localhost:8002 +CORS_ALLOWED_ORIGINS=http://localhost:3002 + + +# File locations +STATIC_ROOT=/home/app/web/static +MEDIA_ROOT=/home/app/web/media + + +# Email +EMAIL_HOST=localhost +EMAIL_PORT=1025 +EMAIL_HOST_USER=False +EMAIL_HOST_PASSWORD=False +EMAIL_SSL=False +EMAIL_TLS=False + + +# Database settings +DB_ENGINE=django.db.backends.postgresql_psycopg2 +DB_NAME=portal-db +DB_USER=portal-admin +DB_HOST=postgresql-db +DB_PORT=5432 +DB_PASSWORD=78ACF6C4F3 + + +# Debug settings +DEBUG=1 +PYTHONDEVMODE=1 +PYTHONTRACEMALLOC=1 +DJANGO_LOG_LEVEL=DEBUG \ No newline at end of file diff --git a/rsconcept/backend/.env.prod b/rsconcept/backend/.env.prod new file mode 100644 index 00000000..25e3709f --- /dev/null +++ b/rsconcept/backend/.env.prod @@ -0,0 +1,37 @@ +# Application settings + +# SECRET_KEY= +ALLOWED_HOSTS=portal.acconcept.ru;api.portal.acconcept.ru +CSRF_TRUSTED_ORIGINS=https://portal.acconcept.ru;https://api.portal.acconcept.ru +CORS_ALLOWED_ORIGINS=https://portal.acconcept.ru +CSRF_COOKIE_DOMAIN=.portal.acconcept.ru + + +# File locations +STATIC_ROOT=/home/app/web/static +MEDIA_ROOT=/home/app/web/media + + +# Email +# EMAIL_HOST= +# EMAIL_HOST_USER= +# EMAIL_HOST_PASSWORD= +EMAIL_PORT=465 +EMAIL_SSL=True +EMAIL_TLS=False + + +# Database settings +DB_ENGINE=django.db.backends.postgresql_psycopg2 +DB_NAME=portal-db +DB_USER=portal-admin +DB_HOST=postgresql-db +DB_PORT=5432 +# DB_PASSWORD= + + +# Debug settings +DEBUG=0 +PYTHONDEVMODE=0 +PYTHONTRACEMALLOC=0 +DJANGO_LOG_LEVEL=DEBUG \ No newline at end of file diff --git a/rsconcept/backend/.env.prod.local b/rsconcept/backend/.env.prod.local new file mode 100644 index 00000000..bf516315 --- /dev/null +++ b/rsconcept/backend/.env.prod.local @@ -0,0 +1,37 @@ +# Application settings +# WARNING! This config does not use 'real' production values for secrets +# DO NOT use PRODUCTION LOCAL build for deployment! +SECRET_KEY=s-55j!5jlan=x%8-6m1qnst^7s6nwby4dx@vei)5w8t)3_=mv1 +ALLOWED_HOSTS=localhost +CSRF_TRUSTED_ORIGINS=https://localhost:3001;https://localhost:8001 +CORS_ALLOWED_ORIGINS=https://localhost:3001 + + +# File locations +STATIC_ROOT=/home/app/web/static +MEDIA_ROOT=/home/app/web/media + + +# Email +EMAIL_HOST=localhost +EMAIL_PORT=1025 +EMAIL_HOST_USER=False +EMAIL_HOST_PASSWORD=False +EMAIL_SSL=False +EMAIL_TLS=False + + +# Database settings +DB_ENGINE=django.db.backends.postgresql_psycopg2 +DB_NAME=portal-db +DB_USER=portal-admin +DB_HOST=postgresql-db +DB_PORT=5432 +DB_PASSWORD=78ACF6C4F3 + + +# Debug settings +DEBUG=0 +PYTHONDEVMODE=0 +PYTHONTRACEMALLOC=0 +DJANGO_LOG_LEVEL=DEBUG \ No newline at end of file diff --git a/rsconcept/backend/.pylintrc b/rsconcept/backend/.pylintrc new file mode 100644 index 00000000..831045e9 --- /dev/null +++ b/rsconcept/backend/.pylintrc @@ -0,0 +1,635 @@ +[MAIN] + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Clear in-memory caches upon conclusion of linting. Useful if running pylint +# in a server-like mode. +clear-cache-post-run=no + +# Load and enable all available extensions. Use --list-extensions to see a list +# all available extensions. +#enable-all-extensions= + +# In error mode, messages with a category besides ERROR or FATAL are +# suppressed, and no reports are done by default. Error mode is compatible with +# disabling specific errors. +#errors-only= + +# Always return a 0 (non-error) status code, even if lint errors are found. +# This is primarily useful in continuous integration scripts. +#exit-zero= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list=pyconcept + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold under which the program will exit with error. +fail-under=10 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +#from-stdin= + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\\' represents the directory delimiter on Windows systems, +# it can't be used as an escape character. +ignore-paths=t_.*,.*migrations.* + +# Files or directories matching the regular expression patterns are skipped. +# The regex matches against base names, not paths. The default value ignores +# Emacs file locks +ignore-patterns=t_.*?py + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.9 + +# Discover python modules and packages in the file system subtree. +recursive=no + +# Add paths to the list of the source roots. Supports globbing patterns. The +# source root is an absolute path or a path relative to the current working +# directory used to determine a package namespace for modules located under the +# source root. +source-roots= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# In verbose mode, extra non-checker-related info will be displayed. +#verbose= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Regular expression matching correct type alias names. If left empty, type +# alias names will be checked with the set naming style. +#typealias-rgx= + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +#variable-rgx= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + asyncSetUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=builtins.BaseException,builtins.Exception + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow explicit reexports by alias from a package __init__. +allow-reexport-from-package=no + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=too-many-public-methods, + invalid-name, + no-else-break, + no-else-continue, + no-else-return, + no-member, + too-many-ancestors, + too-many-return-statements, + too-many-locals, + too-many-instance-attributes, + too-few-public-methods, + unused-argument, + missing-function-docstring, + attribute-defined-outside-init, + ungrouped-imports, + abstract-method + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[METHOD_ARGS] + +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request +max-line-length=120 + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +notes-rgx= + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +#output-format= + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. No available dictionaries : You need to install +# both the python package and the system dependency for enchant to work.. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io diff --git a/rsconcept/backend/Dockerfile b/rsconcept/backend/Dockerfile new file mode 100644 index 00000000..bb2db3f4 --- /dev/null +++ b/rsconcept/backend/Dockerfile @@ -0,0 +1,84 @@ +# ========================================== +# ============ Multi-stage build =========== +# ========================================== +FROM ubuntu:jammy as python-base + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update -qq && \ + apt-get full-upgrade -y && \ + apt-get install -y --no-install-recommends \ + curl \ + gpg-agent \ + software-properties-common && \ + add-apt-repository -y ppa:deadsnakes/ppa && \ + add-apt-repository -y ppa:ubuntu-toolchain-r/test && \ + apt-get install -y --no-install-recommends \ + python3.12 \ + libstdc++6 && \ + curl -sS https://bootstrap.pypa.io/get-pip.py | python3.12 && \ + python3.12 -m pip install --upgrade pip && \ + python3.12 -m pip install wheel && \ + apt-get autoclean -y && \ + apt-get autoremove -y && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# ========= Builder ============== +FROM python-base as builder + +# Set env variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +COPY ./requirements.txt ./ +RUN python3.12 -m pip wheel \ + --no-cache-dir --no-deps \ + --wheel-dir=/wheels -r requirements.txt + + +# ======== Application ============ +FROM python-base + +# Install security updates and system packages +RUN apt-get update -qq && \ + apt-get upgrade -y && \ + apt-get install -y \ + netcat && \ + rm -rf /var/lib/apt/lists/* + +# Setup the app user +ENV USER_HOME=/home/app +ENV APP_HOME=/home/app/web + +RUN mkdir -p $USER_HOME && \ + mkdir -p $APP_HOME && \ + mkdir -p $APP_HOME/static && \ + mkdir -p $APP_HOME/media && \ + mkdir -p $APP_HOME/backup && \ + adduser --system --group app + +# Install python dependencies +WORKDIR $APP_HOME +COPY --from=builder /wheels /wheels +RUN pip install --no-cache /wheels/* && \ + rm -rf /wheels + +# Copy application sources and setup permissions +COPY apps/ ./apps +COPY project/ ./project +COPY fixtures/ ./fixtures +COPY manage.py entrypoint.sh ./ +RUN sed -i 's/\r$//g' $APP_HOME/entrypoint.sh && \ + chmod +x $APP_HOME/entrypoint.sh && \ + chown -R app:app $APP_HOME && \ + chmod -R a+rwx $APP_HOME/static && \ + chmod -R a+rwx $APP_HOME/media && \ + chmod -R a+rwx $APP_HOME/backup + +RUN + +USER app +WORKDIR $APP_HOME + +ENTRYPOINT ["sh", "entrypoint.sh"] \ No newline at end of file diff --git a/rsconcept/backend/apps/__init__.py b/rsconcept/backend/apps/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rsconcept/backend/apps/rsform/__init__.py b/rsconcept/backend/apps/rsform/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rsconcept/backend/apps/rsform/admin.py b/rsconcept/backend/apps/rsform/admin.py new file mode 100644 index 00000000..e99c50b1 --- /dev/null +++ b/rsconcept/backend/apps/rsform/admin.py @@ -0,0 +1,69 @@ +''' Admin view: RSForms for conceptual schemas. ''' +from django.contrib import admin + +from . import models + + +class ConstituentaAdmin(admin.ModelAdmin): + ''' Admin model: Constituenta. ''' + ordering = ['schema', 'order'] + list_display = ['schema', 'alias', 'term_resolved', 'definition_resolved'] + search_fields = ['term_resolved', 'definition_resolved'] + + +class LibraryItemAdmin(admin.ModelAdmin): + ''' Admin model: LibraryItem. ''' + date_hierarchy = 'time_update' + list_display = [ + 'alias', 'title', 'owner', + 'visible', 'read_only', 'access_policy', 'location', + 'time_update' + ] + list_filter = ['visible', 'read_only', 'access_policy', 'location', 'time_update'] + search_fields = ['alias', 'title', 'location'] + + +class LibraryTemplateAdmin(admin.ModelAdmin): + ''' Admin model: LibraryTemplate. ''' + list_display = ['id', 'alias'] + list_select_related = ['lib_source'] + + def alias(self, template: models.LibraryTemplate): + if template.lib_source: + return template.lib_source.alias + else: + return 'N/A' + + +class SubscriptionAdmin(admin.ModelAdmin): + ''' Admin model: Subscriptions. ''' + list_display = ['id', 'item', 'user'] + search_fields = [ + 'item__title', 'item__alias', + 'user__username', 'user__first_name', 'user__last_name' + ] + + +class EditorAdmin(admin.ModelAdmin): + ''' Admin model: Editors. ''' + list_display = ['id', 'item', 'editor'] + search_fields = [ + 'item__title', 'item__alias', + 'editor__username', 'editor__first_name', 'editor__last_name' + ] + + +class VersionAdmin(admin.ModelAdmin): + ''' Admin model: Versions. ''' + list_display = ['id', 'item', 'version', 'description', 'time_create'] + search_fields = [ + 'item__title', 'item__alias' + ] + + +admin.site.register(models.Constituenta, ConstituentaAdmin) +admin.site.register(models.LibraryItem, LibraryItemAdmin) +admin.site.register(models.LibraryTemplate, LibraryTemplateAdmin) +admin.site.register(models.Subscription, SubscriptionAdmin) +admin.site.register(models.Version, VersionAdmin) +admin.site.register(models.Editor, EditorAdmin) diff --git a/rsconcept/backend/apps/rsform/apps.py b/rsconcept/backend/apps/rsform/apps.py new file mode 100644 index 00000000..39f7228f --- /dev/null +++ b/rsconcept/backend/apps/rsform/apps.py @@ -0,0 +1,8 @@ +''' Application: RSForms for conceptual schemas. ''' +from django.apps import AppConfig + + +class RsformConfig(AppConfig): + ''' Application config. ''' + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.rsform' diff --git a/rsconcept/backend/apps/rsform/graph.py b/rsconcept/backend/apps/rsform/graph.py new file mode 100644 index 00000000..4177840d --- /dev/null +++ b/rsconcept/backend/apps/rsform/graph.py @@ -0,0 +1,142 @@ +''' Utility: Graph implementation. ''' +import copy +from typing import Generic, Iterable, Optional, TypeVar + +ItemType = TypeVar("ItemType") + + +class Graph(Generic[ItemType]): + ''' Directed graph. ''' + + def __init__(self, graph: Optional[dict[ItemType, list[ItemType]]] = None): + if graph is None: + self.outputs: dict[ItemType, list[ItemType]] = {} + self.inputs: dict[ItemType, list[ItemType]] = {} + else: + self.outputs = graph + self.inputs: dict[ItemType, list[ItemType]] = {id: [] for id in graph.keys()} # type: ignore[no-redef] + for parent in graph.keys(): + for child in graph[parent]: + self.inputs[child].append(parent) + + def contains(self, node_id: ItemType) -> bool: + ''' Check if node is in graph. ''' + return node_id in self.outputs + + def has_edge(self, src: ItemType, dest: ItemType) -> bool: + ''' Check if edge is in graph. ''' + return self.contains(src) and dest in self.outputs[src] + + def add_node(self, node_id: ItemType): + ''' Add node to graph. ''' + if not self.contains(node_id): + self.outputs[node_id] = [] + self.inputs[node_id] = [] + + def add_edge(self, src: ItemType, dest: ItemType): + ''' Add edge to graph. ''' + self.add_node(src) + self.add_node(dest) + if dest not in self.outputs[src]: + self.outputs[src].append(dest) + if src not in self.inputs[dest]: + self.inputs[dest].append(src) + + def expand_inputs(self, origin: Iterable[ItemType]) -> list[ItemType]: + ''' Expand origin nodes forward through graph edges. ''' + result: list[ItemType] = [] + marked: set[ItemType] = set(origin) + for node_id in origin: + if self.contains(node_id): + for child_id in self.inputs[node_id]: + if child_id not in marked and child_id not in result: + result.append(child_id) + position: int = 0 + while position < len(result): + node_id = result[position] + position += 1 + if node_id not in marked: + marked.add(node_id) + for child_id in self.inputs[node_id]: + if child_id not in marked and child_id not in result: + result.append(child_id) + return result + + def expand_outputs(self, origin: Iterable[ItemType]) -> list[ItemType]: + ''' Expand origin nodes forward through graph edges. ''' + result: list[ItemType] = [] + marked: set[ItemType] = set(origin) + for node_id in origin: + if self.contains(node_id): + for child_id in self.outputs[node_id]: + if child_id not in marked and child_id not in result: + result.append(child_id) + position: int = 0 + while position < len(result): + node_id = result[position] + position += 1 + if node_id not in marked: + marked.add(node_id) + for child_id in self.outputs[node_id]: + if child_id not in marked and child_id not in result: + result.append(child_id) + return result + + def transitive_closure(self) -> dict[ItemType, list[ItemType]]: + ''' Generate transitive closure - list of reachable nodes for each node. ''' + result = copy.deepcopy(self.outputs) + order = self.topological_order() + order.reverse() + for node_id in order: + if len(self.inputs[node_id]) == 0: + continue + for parent in self.inputs[node_id]: + result[parent] = result[parent] + [id for id in result[node_id] if not id in result[parent]] + return result + + def topological_order(self) -> list[ItemType]: + ''' Return nodes in SOME topological order. ''' + result: list[ItemType] = [] + marked: set[ItemType] = set() + for node_id in self.outputs.keys(): + if node_id in marked: + continue + to_visit: list[ItemType] = [node_id] + while len(to_visit) > 0: + node = to_visit[-1] + if node in marked: + if node not in result: + result.append(node) + to_visit.remove(node) + else: + marked.add(node) + if len(self.outputs[node]) <= 0: + continue + for child_id in self.outputs[node]: + if child_id not in marked: + to_visit.append(child_id) + result.reverse() + return result + + def sort_stable(self, target: list[ItemType]) -> list[ItemType]: + ''' Returns target stable sorted in topological order based on minimal modifications. ''' + if len(target) <= 1: + return target + reachable = self.transitive_closure() + test_set: set[ItemType] = set() + result: list[ItemType] = [] + for node_id in reversed(target): + need_move = node_id in test_set + test_set = test_set.union(reachable[node_id]) + if not need_move: + result.append(node_id) + continue + for (index, parent) in enumerate(result): + if node_id in reachable[parent]: + if parent in reachable[node_id]: + result.append(node_id) + else: + result.insert(index, node_id) + break + result.reverse() + return result diff --git a/rsconcept/backend/apps/rsform/messages.py b/rsconcept/backend/apps/rsform/messages.py new file mode 100644 index 00000000..9469bbdb --- /dev/null +++ b/rsconcept/backend/apps/rsform/messages.py @@ -0,0 +1,66 @@ +''' Utility: Text messages. ''' +# pylint: skip-file + + +def constituentaNotOwned(title: str): + return f'Конституента не принадлежит схеме: {title}' + + +def substitutionNotInList(): + return 'Отождествляемая конституента отсутствует в списке' + + +def schemaNotOwned(): + return 'Нет доступа к схеме' + + +def renameTrivial(name: str): + return f'Имя должно отличаться от текущего: {name}' + + +def substituteTrivial(name: str): + return f'Отождествление конституенты с собой не корректно: {name}' + + +def substituteDouble(name: str): + return f'Повторное отождествление: {name}' + + +def aliasTaken(name: str): + return f'Имя уже используется: {name}' + + +def invalidLocation(): + return f'Некорректная строка расположения' + + +def invalidEnum(value: str): + return f'Неподдерживаемое значение параметра: {value}' + + +def pyconceptFailure(): + return 'Invalid data response from pyconcept' + + +def typificationInvalidStr(): + return 'Invalid typification string' + + +def libraryTypeUnexpected(): + return 'Attempting to use invalid adaptor for non-RSForm item' + + +def exteorFileVersionNotSupported(): + return 'Некорректный формат файла Экстеор. Сохраните файл в новой версии' + + +def invalidPosition(): + return 'Invalid position: should be positive integer' + + +def constituentaNoStructure(): + return 'Указанная конституента не обладает теоретико-множественной типизацией' + + +def missingFile(): + return 'Отсутствует прикрепленный файл' diff --git a/rsconcept/backend/apps/rsform/migrations/0001_initial.py b/rsconcept/backend/apps/rsform/migrations/0001_initial.py new file mode 100644 index 00000000..8bc733a7 --- /dev/null +++ b/rsconcept/backend/apps/rsform/migrations/0001_initial.py @@ -0,0 +1,72 @@ +# Generated by Django 4.2.4 on 2023-08-26 10:09 + +import apps.rsform.models +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='LibraryItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('item_type', models.CharField(choices=[('rsform', 'Rsform'), ('oss', 'Operations Schema')], max_length=50, verbose_name='Тип')), + ('title', models.TextField(verbose_name='Название')), + ('alias', models.CharField(blank=True, max_length=255, verbose_name='Шифр')), + ('comment', models.TextField(blank=True, verbose_name='Комментарий')), + ('is_common', models.BooleanField(default=False, verbose_name='Общая')), + ('is_canonical', models.BooleanField(default=False, verbose_name='Каноничная')), + ('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), + ('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')), + ('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Владелец')), + ], + options={ + 'verbose_name': 'Схема', + 'verbose_name_plural': 'Схемы', + }, + ), + migrations.CreateModel( + name='Constituenta', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.PositiveIntegerField(default=-1, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Позиция')), + ('alias', models.CharField(default='undefined', max_length=8, verbose_name='Имя')), + ('cst_type', models.CharField(choices=[('basic', 'Base'), ('constant', 'Constant'), ('structure', 'Structured'), ('axiom', 'Axiom'), ('term', 'Term'), ('function', 'Function'), ('predicate', 'Predicate'), ('theorem', 'Theorem')], default='basic', max_length=10, verbose_name='Тип')), + ('convention', models.TextField(blank=True, default='', verbose_name='Комментарий/Конвенция')), + ('term_raw', models.TextField(blank=True, default='', verbose_name='Термин (с отсылками)')), + ('term_resolved', models.TextField(blank=True, default='', verbose_name='Термин')), + ('term_forms', models.JSONField(default=apps.rsform.models._empty_forms, verbose_name='Словоформы')), + ('definition_formal', models.TextField(blank=True, default='', verbose_name='Родоструктурное определение')), + ('definition_raw', models.TextField(blank=True, default='', verbose_name='Текстовое определние (с отсылками)')), + ('definition_resolved', models.TextField(blank=True, default='', verbose_name='Текстовое определние')), + ('schema', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.libraryitem', verbose_name='Концептуальная схема')), + ], + options={ + 'verbose_name': 'Конституента', + 'verbose_name_plural': 'Конституенты', + }, + ), + migrations.CreateModel( + name='Subscription', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.libraryitem', verbose_name='Элемент')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')), + ], + options={ + 'verbose_name': 'Подписки', + 'verbose_name_plural': 'Подписка', + 'unique_together': {('user', 'item')}, + }, + ), + ] diff --git a/rsconcept/backend/apps/rsform/migrations/0002_librarytemplate.py b/rsconcept/backend/apps/rsform/migrations/0002_librarytemplate.py new file mode 100644 index 00000000..00bc2699 --- /dev/null +++ b/rsconcept/backend/apps/rsform/migrations/0002_librarytemplate.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.6 on 2023-10-18 16:12 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('rsform', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='LibraryTemplate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('lib_source', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='rsform.libraryitem', verbose_name='Источник')), + ], + options={ + 'verbose_name': 'Шаблон', + 'verbose_name_plural': 'Шаблоны', + }, + ), + ] diff --git a/rsconcept/backend/apps/rsform/migrations/0003_alter_constituenta_definition_raw_and_more.py b/rsconcept/backend/apps/rsform/migrations/0003_alter_constituenta_definition_raw_and_more.py new file mode 100644 index 00000000..18aaeb15 --- /dev/null +++ b/rsconcept/backend/apps/rsform/migrations/0003_alter_constituenta_definition_raw_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.7 on 2023-12-27 08:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rsform', '0002_librarytemplate'), + ] + + operations = [ + migrations.AlterField( + model_name='constituenta', + name='definition_raw', + field=models.TextField(blank=True, default='', verbose_name='Текстовое определение (с отсылками)'), + ), + migrations.AlterField( + model_name='constituenta', + name='definition_resolved', + field=models.TextField(blank=True, default='', verbose_name='Текстовое определение'), + ), + ] diff --git a/rsconcept/backend/apps/rsform/migrations/0004_version.py b/rsconcept/backend/apps/rsform/migrations/0004_version.py new file mode 100644 index 00000000..db57b43b --- /dev/null +++ b/rsconcept/backend/apps/rsform/migrations/0004_version.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.10 on 2024-03-03 10:57 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('rsform', '0003_alter_constituenta_definition_raw_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Version', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('version', models.CharField(max_length=20, verbose_name='Версия')), + ('description', models.TextField(blank=True, verbose_name='Описание')), + ('data', models.JSONField(verbose_name='Содержание')), + ('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), + ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.libraryitem', verbose_name='Схема')), + ], + options={ + 'verbose_name': 'Версии', + 'verbose_name_plural': 'Версия', + 'unique_together': {('item', 'version')}, + }, + ), + ] diff --git a/rsconcept/backend/apps/rsform/migrations/0005_alter_subscription_options_alter_version_options.py b/rsconcept/backend/apps/rsform/migrations/0005_alter_subscription_options_alter_version_options.py new file mode 100644 index 00000000..e21e509d --- /dev/null +++ b/rsconcept/backend/apps/rsform/migrations/0005_alter_subscription_options_alter_version_options.py @@ -0,0 +1,21 @@ +# Generated by Django 5.0.6 on 2024-05-20 14:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('rsform', '0004_version'), + ] + + operations = [ + migrations.AlterModelOptions( + name='subscription', + options={'verbose_name': 'Подписка', 'verbose_name_plural': 'Подписки'}, + ), + migrations.AlterModelOptions( + name='version', + options={'verbose_name': 'Версия', 'verbose_name_plural': 'Версии'}, + ), + ] diff --git a/rsconcept/backend/apps/rsform/migrations/0006_editor.py b/rsconcept/backend/apps/rsform/migrations/0006_editor.py new file mode 100644 index 00000000..75403aaa --- /dev/null +++ b/rsconcept/backend/apps/rsform/migrations/0006_editor.py @@ -0,0 +1,30 @@ +# Generated by Django 5.0.6 on 2024-05-20 14:40 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rsform', '0005_alter_subscription_options_alter_version_options'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Editor', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата добавления')), + ('editor', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Редактор')), + ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.libraryitem', verbose_name='Схема')), + ], + options={ + 'verbose_name': 'Редактор', + 'verbose_name_plural': 'Редакторы', + 'unique_together': {('item', 'editor')}, + }, + ), + ] diff --git a/rsconcept/backend/apps/rsform/migrations/0007_location_and_flags.py b/rsconcept/backend/apps/rsform/migrations/0007_location_and_flags.py new file mode 100644 index 00000000..2f4f4821 --- /dev/null +++ b/rsconcept/backend/apps/rsform/migrations/0007_location_and_flags.py @@ -0,0 +1,65 @@ +# Hand written migration 20240531 + +from django.db import migrations, models + +from .. import models as m + + +class Migration(migrations.Migration): + + dependencies = [ + ('rsform', '0006_editor'), + ] + + def calculate_location(apps, schema_editor): + LibraryItem = apps.get_model('rsform', 'LibraryItem') + db_alias = schema_editor.connection.alias + for item in LibraryItem.objects.using(db_alias).all(): + if item.is_canonical: + location = m.LocationHead.LIBRARY + elif item.is_common: + location = m.LocationHead.COMMON + else: + location = m.LocationHead.USER + item.location = location + item.save(update_fields=['location']) + + operations = [ + migrations.AddField( + model_name='libraryitem', + name='access_policy', + field=models.CharField( + choices=[ + ('public', 'Public'), + ('protected', 'Protected'), + ('private', 'Private') + ], + default='public', + max_length=500, + verbose_name='Политика доступа'), + ), + migrations.AddField( + model_name='libraryitem', + name='location', + field=models.TextField(default='/U', max_length=500, verbose_name='Расположение'), + ), + migrations.AddField( + model_name='libraryitem', + name='read_only', + field=models.BooleanField(default=False, verbose_name='Запретить редактирование'), + ), + migrations.AddField( + model_name='libraryitem', + name='visible', + field=models.BooleanField(default=True, verbose_name='Отображаемая'), + ), + migrations.RunPython(calculate_location, migrations.RunPython.noop), # type: ignore + migrations.RemoveField( + model_name='libraryitem', + name='is_canonical', + ), + migrations.RemoveField( + model_name='libraryitem', + name='is_common', + ), + ] diff --git a/rsconcept/backend/apps/rsform/migrations/__init__.py b/rsconcept/backend/apps/rsform/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rsconcept/backend/apps/rsform/models/Constituenta.py b/rsconcept/backend/apps/rsform/models/Constituenta.py new file mode 100644 index 00000000..fe09a12f --- /dev/null +++ b/rsconcept/backend/apps/rsform/models/Constituenta.py @@ -0,0 +1,137 @@ +''' Models: Constituenta. ''' +import re + +from django.core.validators import MinValueValidator +from django.db.models import ( + CASCADE, + CharField, + ForeignKey, + JSONField, + Model, + PositiveIntegerField, + TextChoices, + TextField +) +from django.urls import reverse + +from ..utils import apply_pattern + +_REF_ENTITY_PATTERN = re.compile(r'@{([^0-9\-].*?)\|.*?}') +_GLOBAL_ID_PATTERN = re.compile(r'([XCSADFPT][0-9]+)') # cspell:disable-line + + +def _empty_forms(): + return [] + + +class CstType(TextChoices): + ''' Type of constituenta. ''' + BASE = 'basic' + CONSTANT = 'constant' + STRUCTURED = 'structure' + AXIOM = 'axiom' + TERM = 'term' + FUNCTION = 'function' + PREDICATE = 'predicate' + THEOREM = 'theorem' + + +class Constituenta(Model): + ''' Constituenta is the base unit for every conceptual schema. ''' + schema: ForeignKey = ForeignKey( + verbose_name='Концептуальная схема', + to='rsform.LibraryItem', + on_delete=CASCADE + ) + order: PositiveIntegerField = PositiveIntegerField( + verbose_name='Позиция', + validators=[MinValueValidator(1)], + default=-1, + ) + alias: CharField = CharField( + verbose_name='Имя', + max_length=8, + default='undefined' + ) + cst_type: CharField = CharField( + verbose_name='Тип', + max_length=10, + choices=CstType.choices, + default=CstType.BASE + ) + convention: TextField = TextField( + verbose_name='Комментарий/Конвенция', + default='', + blank=True + ) + term_raw: TextField = TextField( + verbose_name='Термин (с отсылками)', + default='', + blank=True + ) + term_resolved: TextField = TextField( + verbose_name='Термин', + default='', + blank=True + ) + term_forms: JSONField = JSONField( + verbose_name='Словоформы', + default=_empty_forms + ) + definition_formal: TextField = TextField( + verbose_name='Родоструктурное определение', + default='', + blank=True + ) + definition_raw: TextField = TextField( + verbose_name='Текстовое определение (с отсылками)', + default='', + blank=True + ) + definition_resolved: TextField = TextField( + verbose_name='Текстовое определение', + default='', + blank=True + ) + + class Meta: + ''' Model metadata. ''' + verbose_name = 'Конституента' + verbose_name_plural = 'Конституенты' + + def get_absolute_url(self): + ''' URL access. ''' + return reverse('constituenta-detail', kwargs={'pk': self.pk}) + + def __str__(self) -> str: + return f'{self.alias}' + + def set_term_resolved(self, new_term: str): + ''' Set term and reset forms if needed. ''' + if new_term == self.term_resolved: + return + self.term_resolved = new_term + self.term_forms = [] + + def apply_mapping(self, mapping: dict[str, str], change_aliases: bool = False): + modified = False + if change_aliases and self.alias in mapping: + modified = True + self.alias = mapping[self.alias] + expression = apply_pattern(self.definition_formal, mapping, _GLOBAL_ID_PATTERN) + if expression != self.definition_formal: + modified = True + self.definition_formal = expression + convention = apply_pattern(self.convention, mapping, _GLOBAL_ID_PATTERN) + if convention != self.convention: + modified = True + self.convention = convention + term = apply_pattern(self.term_raw, mapping, _REF_ENTITY_PATTERN) + if term != self.term_raw: + modified = True + self.term_raw = term + definition = apply_pattern(self.definition_raw, mapping, _REF_ENTITY_PATTERN) + if definition != self.definition_raw: + modified = True + self.definition_raw = definition + return modified diff --git a/rsconcept/backend/apps/rsform/models/Editor.py b/rsconcept/backend/apps/rsform/models/Editor.py new file mode 100644 index 00000000..2f9ce6a0 --- /dev/null +++ b/rsconcept/backend/apps/rsform/models/Editor.py @@ -0,0 +1,71 @@ +''' Models: Editor. ''' +from typing import TYPE_CHECKING + +from django.db import transaction +from django.db.models import CASCADE, DateTimeField, ForeignKey, Model + +from apps.users.models import User + +if TYPE_CHECKING: + from .LibraryItem import LibraryItem + + +class Editor(Model): + ''' Editor list. ''' + item: ForeignKey = ForeignKey( + verbose_name='Схема', + to='rsform.LibraryItem', + on_delete=CASCADE + ) + editor: ForeignKey = ForeignKey( + verbose_name='Редактор', + to=User, + on_delete=CASCADE, + null=True + ) + time_create: DateTimeField = DateTimeField( + verbose_name='Дата добавления', + auto_now_add=True + ) + + class Meta: + ''' Model metadata. ''' + verbose_name = 'Редактор' + verbose_name_plural = 'Редакторы' + unique_together = [['item', 'editor']] + + def __str__(self) -> str: + return f'{self.item}: {self.editor}' + + @staticmethod + def add(item: 'LibraryItem', user: User) -> bool: + ''' Add Editor for item. ''' + if Editor.objects.filter(item=item, editor=user).exists(): + return False + Editor.objects.create(item=item, editor=user) + return True + + @staticmethod + def remove(item: 'LibraryItem', user: User) -> bool: + ''' Remove Editor. ''' + editor = Editor.objects.filter(item=item, editor=user) + if not editor.exists(): + return False + editor.delete() + return True + + @staticmethod + @transaction.atomic + def set(item: 'LibraryItem', users: list[User]): + ''' Set editors for item. ''' + processed: list[User] = [] + for editor_item in Editor.objects.filter(item=item): + if not editor_item.editor in users: + editor_item.delete() + else: + processed.append(editor_item.editor) + + for user in users: + if not user in processed: + processed.append(user) + Editor.objects.create(item=item, editor=user) diff --git a/rsconcept/backend/apps/rsform/models/LibraryItem.py b/rsconcept/backend/apps/rsform/models/LibraryItem.py new file mode 100644 index 00000000..56813796 --- /dev/null +++ b/rsconcept/backend/apps/rsform/models/LibraryItem.py @@ -0,0 +1,133 @@ +''' Models: LibraryItem. ''' +import re + +from django.db import transaction +from django.db.models import ( + SET_NULL, + BooleanField, + CharField, + DateTimeField, + ForeignKey, + Model, + TextChoices, + TextField +) + +from apps.users.models import User + +from .Editor import Editor +from .Subscription import Subscription +from .Version import Version + + +class LibraryItemType(TextChoices): + ''' Type of library items ''' + RSFORM = 'rsform' + OPERATIONS_SCHEMA = 'oss' + + +class AccessPolicy(TextChoices): + ''' Type of item access policy. ''' + PUBLIC = 'public' + PROTECTED = 'protected' + PRIVATE = 'private' + + +class LocationHead(TextChoices): + ''' Location prefixes. ''' + PROJECTS = '/P' + LIBRARY = '/L' + USER = '/U' + COMMON = '/S' + + +_RE_LOCATION = r'^/[PLUS]((/[!\d\w]([!\d\w\- ]*[!\d\w])?)*)?$' # cspell:disable-line + + +def validate_location(target: str) -> bool: + return bool(re.search(_RE_LOCATION, target)) + + +class LibraryItem(Model): + ''' Abstract library item.''' + item_type: CharField = CharField( + verbose_name='Тип', + max_length=50, + choices=LibraryItemType.choices + ) + owner: ForeignKey = ForeignKey( + verbose_name='Владелец', + to=User, + on_delete=SET_NULL, + null=True + ) + title: TextField = TextField( + verbose_name='Название' + ) + alias: CharField = CharField( + verbose_name='Шифр', + max_length=255, + blank=True + ) + comment: TextField = TextField( + verbose_name='Комментарий', + blank=True + ) + visible: BooleanField = BooleanField( + verbose_name='Отображаемая', + default=True + ) + read_only: BooleanField = BooleanField( + verbose_name='Запретить редактирование', + default=False + ) + access_policy: CharField = CharField( + verbose_name='Политика доступа', + max_length=500, + choices=AccessPolicy.choices, + default=AccessPolicy.PUBLIC + ) + location: TextField = TextField( + verbose_name='Расположение', + max_length=500, + default=LocationHead.USER + ) + + time_create: DateTimeField = DateTimeField( + verbose_name='Дата создания', + auto_now_add=True + ) + time_update: DateTimeField = DateTimeField( + verbose_name='Дата изменения', + auto_now=True + ) + + class Meta: + ''' Model metadata. ''' + verbose_name = 'Схема' + verbose_name_plural = 'Схемы' + + def __str__(self) -> str: + return f'{self.alias}' + + def get_absolute_url(self): + return f'/api/library/{self.pk}' + + def subscribers(self) -> list[Subscription]: + ''' Get all subscribers for this item. ''' + return [subscription.user for subscription in Subscription.objects.filter(item=self.pk)] + + def versions(self) -> list[Version]: + ''' Get all Versions of this item. ''' + return list(Version.objects.filter(item=self.pk).order_by('-time_create')) + + def editors(self) -> list[Editor]: + ''' Get all Editors of this item. ''' + return [item.editor for item in Editor.objects.filter(item=self.pk)] + + @transaction.atomic + def save(self, *args, **kwargs): + subscribe = not self.pk and self.owner + super().save(*args, **kwargs) + if subscribe: + Subscription.subscribe(user=self.owner, item=self) diff --git a/rsconcept/backend/apps/rsform/models/LibraryTemplate.py b/rsconcept/backend/apps/rsform/models/LibraryTemplate.py new file mode 100644 index 00000000..c49a5293 --- /dev/null +++ b/rsconcept/backend/apps/rsform/models/LibraryTemplate.py @@ -0,0 +1,17 @@ +''' Models: LibraryTemplate. ''' +from django.db.models import CASCADE, ForeignKey, Model + + +class LibraryTemplate(Model): + ''' Template for library items and constituents. ''' + lib_source: ForeignKey = ForeignKey( + verbose_name='Источник', + to='rsform.LibraryItem', + on_delete=CASCADE, + null=True + ) + + class Meta: + ''' Model metadata. ''' + verbose_name = 'Шаблон' + verbose_name_plural = 'Шаблоны' diff --git a/rsconcept/backend/apps/rsform/models/Subscription.py b/rsconcept/backend/apps/rsform/models/Subscription.py new file mode 100644 index 00000000..6199ad38 --- /dev/null +++ b/rsconcept/backend/apps/rsform/models/Subscription.py @@ -0,0 +1,49 @@ +''' Models: Subscription. ''' +from typing import TYPE_CHECKING + +from django.db.models import CASCADE, ForeignKey, Model + +from apps.users.models import User + +if TYPE_CHECKING: + from .LibraryItem import LibraryItem + + +class Subscription(Model): + ''' User subscription to library item. ''' + user: ForeignKey = ForeignKey( + verbose_name='Пользователь', + to=User, + on_delete=CASCADE + ) + item: ForeignKey = ForeignKey( + verbose_name='Элемент', + to='rsform.LibraryItem', + on_delete=CASCADE + ) + + class Meta: + ''' Model metadata. ''' + verbose_name = 'Подписка' + verbose_name_plural = 'Подписки' + unique_together = [['user', 'item']] + + def __str__(self) -> str: + return f'{self.user} -> {self.item}' + + @staticmethod + def subscribe(user: User, item: 'LibraryItem') -> bool: + ''' Add subscription. ''' + if Subscription.objects.filter(user=user, item=item).exists(): + return False + Subscription.objects.create(user=user, item=item) + return True + + @staticmethod + def unsubscribe(user: User, item: 'LibraryItem') -> bool: + ''' Remove subscription. ''' + sub = Subscription.objects.filter(user=user, item=item) + if not sub.exists(): + return False + sub.delete() + return True diff --git a/rsconcept/backend/apps/rsform/models/Version.py b/rsconcept/backend/apps/rsform/models/Version.py new file mode 100644 index 00000000..0f9174fc --- /dev/null +++ b/rsconcept/backend/apps/rsform/models/Version.py @@ -0,0 +1,44 @@ +''' Models: Version. ''' +from django.db.models import ( + CASCADE, + CharField, + DateTimeField, + ForeignKey, + JSONField, + Model, + TextField +) + + +class Version(Model): + ''' Library item version archive. ''' + item: ForeignKey = ForeignKey( + verbose_name='Схема', + to='rsform.LibraryItem', + on_delete=CASCADE + ) + version = CharField( + verbose_name='Версия', + max_length=20, + blank=False + ) + description: TextField = TextField( + verbose_name='Описание', + blank=True + ) + data: JSONField = JSONField( + verbose_name='Содержание' + ) + time_create: DateTimeField = DateTimeField( + verbose_name='Дата создания', + auto_now_add=True + ) + + class Meta: + ''' Model metadata. ''' + verbose_name = 'Версия' + verbose_name_plural = 'Версии' + unique_together = [['item', 'version']] + + def __str__(self) -> str: + return f'{self.item} v{self.version}' diff --git a/rsconcept/backend/apps/rsform/models/__init__.py b/rsconcept/backend/apps/rsform/models/__init__.py new file mode 100644 index 00000000..3ea203d6 --- /dev/null +++ b/rsconcept/backend/apps/rsform/models/__init__.py @@ -0,0 +1,16 @@ +''' Django: Models. ''' + +from .api_RSForm import RSForm +from .Constituenta import Constituenta, CstType, _empty_forms +from .Editor import Editor +from .LibraryItem import ( + AccessPolicy, + LibraryItem, + LibraryItemType, + LocationHead, + User, + validate_location +) +from .LibraryTemplate import LibraryTemplate +from .Subscription import Subscription +from .Version import Version diff --git a/rsconcept/backend/apps/rsform/models/api_RSForm.py b/rsconcept/backend/apps/rsform/models/api_RSForm.py new file mode 100644 index 00000000..e0c03e12 --- /dev/null +++ b/rsconcept/backend/apps/rsform/models/api_RSForm.py @@ -0,0 +1,616 @@ +''' Models: RSForm API. ''' +from copy import deepcopy +from typing import Optional, Union, cast + +from cctext import Entity, Resolver, TermForm, extract_entities, split_grams +from django.core.exceptions import ValidationError +from django.db import transaction +from django.db.models import QuerySet + +from .. import messages as msg +from ..graph import Graph +from .api_RSLanguage import ( + extract_globals, + generate_structure, + get_type_prefix, + guess_type, + infer_template, + is_base_set, + is_functional, + is_simple_expression, + split_template +) +from .Constituenta import Constituenta, CstType +from .LibraryItem import LibraryItem, LibraryItemType +from .Version import Version + +_INSERT_LAST: int = -1 + + +class RSForm: + ''' RSForm is math form of conceptual schema. ''' + + def __init__(self, item: LibraryItem): + if item.item_type != LibraryItemType.RSFORM: + raise ValueError(msg.libraryTypeUnexpected()) + self.item = item + + @staticmethod + def create(**kwargs) -> 'RSForm': + return RSForm(LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, **kwargs)) + + def constituents(self) -> QuerySet['Constituenta']: + ''' Get QuerySet containing all constituents of current RSForm. ''' + return Constituenta.objects.filter(schema=self.item) + + def resolver(self) -> Resolver: + ''' Create resolver for text references based on schema terms. ''' + result = Resolver({}) + for cst in self.constituents(): + entity = Entity( + alias=cst.alias, + nominal=cst.term_resolved, + manual_forms=[ + TermForm(text=form['text'], grams=split_grams(form['tags'])) + for form in cst.term_forms + ] + ) + result.context[cst.alias] = entity + return result + + def semantic(self) -> 'SemanticInfo': + ''' Access semantic information on constituents. ''' + return SemanticInfo(self) + + @transaction.atomic + def on_term_change(self, changed: list[int]): + ''' Trigger cascade resolutions when term changes. ''' + graph_terms = self._graph_term() + expansion = graph_terms.expand_outputs(changed) + expanded_change = changed + expansion + resolver = self.resolver() + if len(expansion) > 0: + for cst_id in graph_terms.topological_order(): + if cst_id not in expansion: + continue + cst = self.constituents().get(id=cst_id) + resolved = resolver.resolve(cst.term_raw) + if resolved == cst.term_resolved: + continue + cst.set_term_resolved(resolved) + cst.save() + resolver.context[cst.alias] = Entity(cst.alias, resolved) + + graph_defs = self._graph_text() + update_defs = set(expansion + graph_defs.expand_outputs(expanded_change)).union(changed) + if len(update_defs) == 0: + return + for cst_id in update_defs: + cst = self.constituents().get(id=cst_id) + resolved = resolver.resolve(cst.definition_raw) + if resolved == cst.definition_resolved: + continue + cst.definition_resolved = resolved + cst.save() + + def get_max_index(self, cst_type: CstType) -> int: + ''' Get maximum alias index for specific CstType. ''' + result: int = 0 + items = Constituenta.objects \ + .filter(schema=self.item, cst_type=cst_type) \ + .order_by('-alias') \ + .values_list('alias', flat=True) + for alias in items: + result = max(result, int(alias[1:])) + return result + + @transaction.atomic + def insert_new( + self, + alias: str, + cst_type: Union[CstType, None] = None, + position: int = _INSERT_LAST, + **kwargs + ) -> Constituenta: + ''' Insert new constituenta at given position. + All following constituents order is shifted by 1 position. ''' + if self.constituents().filter(alias=alias).exists(): + raise ValidationError(msg.aliasTaken(alias)) + position = self._get_insert_position(position) + if cst_type is None: + cst_type = guess_type(alias) + self._shift_positions(position, 1) + result = Constituenta.objects.create( + schema=self.item, + order=position, + alias=alias, + cst_type=cst_type, + **kwargs + ) + self.item.save() + result.refresh_from_db() + return result + + @transaction.atomic + def insert_copy(self, items: list[Constituenta], position: int = _INSERT_LAST) -> list[Constituenta]: + ''' Insert copy of target constituents updating references. ''' + count = len(items) + if count == 0: + return [] + + position = self._get_insert_position(position) + self._shift_positions(position, count) + + indices: dict[str, int] = {} + for (value, _) in CstType.choices: + indices[value] = self.get_max_index(cast(CstType, value)) + + mapping: dict[str, str] = {} + for cst in items: + indices[cst.cst_type] = indices[cst.cst_type] + 1 + newAlias = f'{get_type_prefix(cst.cst_type)}{indices[cst.cst_type]}' + mapping[cst.alias] = newAlias + + result = deepcopy(items) + for cst in result: + cst.pk = None + cst.schema = self.item + cst.order = position + cst.alias = mapping[cst.alias] + cst.apply_mapping(mapping) + cst.save() + position = position + 1 + self.item.save() + return result + + @transaction.atomic + def move_cst(self, listCst: list[Constituenta], target: int): + ''' Move list of constituents to specific position ''' + count_moved = 0 + count_top = 0 + count_bot = 0 + size = len(listCst) + update_list = [] + for cst in self.constituents().only('id', 'order').order_by('order'): + if cst not in listCst: + if count_top + 1 < target: + cst.order = count_top + 1 + count_top += 1 + else: + cst.order = target + size + count_bot + count_bot += 1 + else: + cst.order = target + count_moved + count_moved += 1 + update_list.append(cst) + Constituenta.objects.bulk_update(update_list, ['order']) + self.item.save() + + @transaction.atomic + def delete_cst(self, listCst): + ''' Delete multiple constituents. Do not check if listCst are from this schema. ''' + for cst in listCst: + cst.delete() + self._reset_order() + self.resolve_all_text() + self.item.save() + + @transaction.atomic + def create_cst(self, data: dict, insert_after: Optional[str] = None) -> Constituenta: + ''' Create new cst from data. ''' + resolver = self.resolver() + cst = self._insert_new(data, insert_after) + cst.convention = data.get('convention', '') + cst.definition_formal = data.get('definition_formal', '') + cst.term_forms = data.get('term_forms', []) + cst.term_raw = data.get('term_raw', '') + if cst.term_raw != '': + resolved = resolver.resolve(cst.term_raw) + cst.term_resolved = resolved + resolver.context[cst.alias] = Entity(cst.alias, resolved) + cst.definition_raw = data.get('definition_raw', '') + if cst.definition_raw != '': + cst.definition_resolved = resolver.resolve(cst.definition_raw) + cst.save() + self.on_term_change([cst.id]) + cst.refresh_from_db() + return cst + + @transaction.atomic + def substitute( + self, + original: Constituenta, + substitution: Constituenta, + transfer_term: bool + ): + ''' Execute constituenta substitution. ''' + assert original.pk != substitution.pk + mapping = {original.alias: substitution.alias} + self.apply_mapping(mapping) + if transfer_term: + substitution.term_raw = original.term_raw + substitution.term_forms = original.term_forms + substitution.term_resolved = original.term_resolved + substitution.save() + original.delete() + self.on_term_change([substitution.id]) + + def restore_order(self): + ''' Restore order based on types and term graph. ''' + manager = _OrderManager(self) + manager.restore_order() + + def reset_aliases(self): + ''' Recreate all aliases based on constituents order. ''' + mapping = self._create_reset_mapping() + self.apply_mapping(mapping, change_aliases=True) + + def _create_reset_mapping(self) -> dict[str, str]: + bases = cast(dict[str, int], {}) + mapping = cast(dict[str, str], {}) + for cst_type in CstType.values: + bases[cst_type] = 1 + cst_list = self.constituents().order_by('order') + for cst in cst_list: + alias = f'{get_type_prefix(cst.cst_type)}{bases[cst.cst_type]}' + bases[cst.cst_type] += 1 + if cst.alias != alias: + mapping[cst.alias] = alias + return mapping + + @transaction.atomic + def apply_mapping(self, mapping: dict[str, str], change_aliases: bool = False): + ''' Apply rename mapping. ''' + cst_list = self.constituents().order_by('order') + for cst in cst_list: + if cst.apply_mapping(mapping, change_aliases): + cst.save() + + @transaction.atomic + def resolve_all_text(self): + ''' Trigger reference resolution for all texts. ''' + graph_terms = self._graph_term() + resolver = Resolver({}) + for cst_id in graph_terms.topological_order(): + cst = self.constituents().get(id=cst_id) + resolved = resolver.resolve(cst.term_raw) + resolver.context[cst.alias] = Entity(cst.alias, resolved) + if resolved != cst.term_resolved: + cst.term_resolved = resolved + cst.save() + for cst in self.constituents(): + resolved = resolver.resolve(cst.definition_raw) + if resolved != cst.definition_resolved: + cst.definition_resolved = resolved + cst.save() + + @transaction.atomic + def create_version(self, version: str, description: str, data) -> Version: + ''' Creates version for current state. ''' + return Version.objects.create( + item=self.item, + version=version, + description=description, + data=data + ) + + @transaction.atomic + def produce_structure(self, target: Constituenta, parse: dict) -> list[int]: + ''' Add constituents for each structural element of the target. ''' + expressions = generate_structure( + alias=target.alias, + expression=target.definition_formal, + parse=parse + ) + count_new = len(expressions) + if count_new == 0: + return [] + position = target.order + 1 + self._shift_positions(position, count_new) + + result = [] + cst_type = CstType.TERM if len(parse['args']) == 0 else CstType.FUNCTION + free_index = self.get_max_index(cst_type) + 1 + prefix = get_type_prefix(cst_type) + for text in expressions: + new_item = Constituenta.objects.create( + schema=self.item, + order=position, + alias=f'{prefix}{free_index}', + definition_formal=text, + cst_type=cst_type + ) + result.append(new_item.id) + free_index = free_index + 1 + position = position + 1 + + self.item.save() + return result + + def _shift_positions(self, start: int, shift: int): + if shift == 0: + return + update_list = \ + Constituenta.objects \ + .only('id', 'order', 'schema') \ + .filter(schema=self.item, order__gte=start) + for cst in update_list: + cst.order += shift + Constituenta.objects.bulk_update(update_list, ['order']) + + def _get_last_position(self): + if self.constituents().exists(): + return self.constituents().count() + else: + return 0 + + def _get_insert_position(self, position: int) -> int: + if position <= 0 and position != _INSERT_LAST: + raise ValidationError(msg.invalidPosition()) + lastPosition = self._get_last_position() + if position == _INSERT_LAST: + position = lastPosition + 1 + else: + position = max(1, min(position, lastPosition + 1)) + return position + + @transaction.atomic + def _reset_order(self): + order = 1 + for cst in self.constituents().only('id', 'order').order_by('order'): + if cst.order != order: + cst.order = order + cst.save() + order += 1 + + def _insert_new(self, data: dict, insert_after: Optional[str] = None) -> Constituenta: + if insert_after is not None: + cst_after = Constituenta.objects.get(pk=insert_after) + return self.insert_new(data['alias'], data['cst_type'], cst_after.order + 1) + else: + return self.insert_new(data['alias'], data['cst_type']) + + def _graph_formal(self) -> Graph[int]: + ''' Graph based on formal definitions. ''' + result: Graph[int] = Graph() + cst_list = \ + self.constituents() \ + .only('id', 'order', 'alias', 'definition_formal') \ + .order_by('order') + for cst in cst_list: + result.add_node(cst.id) + for cst in cst_list: + for alias in extract_globals(cst.definition_formal): + try: + child = cst_list.get(alias=alias) + result.add_edge(src=child.id, dest=cst.id) + except Constituenta.DoesNotExist: + pass + return result + + def _graph_term(self) -> Graph[int]: + ''' Graph based on term texts. ''' + result: Graph[int] = Graph() + cst_list = \ + self.constituents() \ + .only('id', 'order', 'alias', 'term_raw') \ + .order_by('order') + for cst in cst_list: + result.add_node(cst.id) + for cst in cst_list: + for alias in extract_entities(cst.term_raw): + try: + child = cst_list.get(alias=alias) + result.add_edge(src=child.id, dest=cst.id) + except Constituenta.DoesNotExist: + pass + return result + + def _graph_text(self) -> Graph[int]: + ''' Graph based on definition texts. ''' + result: Graph[int] = Graph() + cst_list = \ + self.constituents() \ + .only('id', 'order', 'alias', 'definition_raw') \ + .order_by('order') + for cst in cst_list: + result.add_node(cst.id) + for cst in cst_list: + for alias in extract_entities(cst.definition_raw): + try: + child = cst_list.get(alias=alias) + result.add_edge(src=child.id, dest=cst.id) + except Constituenta.DoesNotExist: + pass + return result + + +class SemanticInfo: + ''' Semantic information derived from constituents. ''' + + def __init__(self, schema: RSForm): + self._graph = schema._graph_formal() + self._items = list( + schema.constituents() + .only('id', 'alias', 'cst_type', 'definition_formal') + .order_by('order') + ) + self._cst_by_alias = {cst.alias: cst for cst in self._items} + self._cst_by_ID = {cst.id: cst for cst in self._items} + self.info = { + cst.id: { + 'is_simple': False, + 'is_template': False, + 'parent': cst.id, + 'children': [] + } + for cst in self._items + } + self._calculate_attributes() + + def __getitem__(self, key: int) -> dict: + return self.info[key] + + def is_simple_expression(self, target: int) -> bool: + ''' Access "is_simple" attribute. ''' + return cast(bool, self.info[target]['is_simple']) + + def is_template(self, target: int) -> bool: + ''' Access "is_template" attribute. ''' + return cast(bool, self.info[target]['is_template']) + + def parent(self, target: int) -> int: + ''' Access "parent" attribute. ''' + return cast(int, self.info[target]['parent']) + + def children(self, target: int) -> list[int]: + ''' Access "children" attribute. ''' + return cast(list[int], self.info[target]['children']) + + def _calculate_attributes(self): + for cst_id in self._graph.topological_order(): + cst = self._cst_by_ID[cst_id] + self.info[cst_id]['is_template'] = infer_template(cst.definition_formal) + self.info[cst_id]['is_simple'] = self._infer_simple_expression(cst) + if not self.info[cst_id]['is_simple'] or cst.cst_type == CstType.STRUCTURED: + continue + parent = self._infer_parent(cst) + self.info[cst_id]['parent'] = parent + if parent != cst_id: + self.info[parent]['children'].append(cst_id) + + def _infer_simple_expression(self, target: Constituenta) -> bool: + if target.cst_type == CstType.STRUCTURED or is_base_set(target.cst_type): + return False + + dependencies = self._graph.inputs[target.id] + has_complex_dependency = any( + self.is_template(cst_id) and + not self.is_simple_expression(cst_id) for cst_id in dependencies + ) + if has_complex_dependency: + return False + + if is_functional(target.cst_type): + return is_simple_expression(split_template(target.definition_formal)['body']) + else: + return is_simple_expression(target.definition_formal) + + def _infer_parent(self, target: Constituenta) -> int: + sources = self._extract_sources(target) + if len(sources) != 1: + return target.id + + parent_id = next(iter(sources)) + parent = self._cst_by_ID[parent_id] + if is_base_set(parent.cst_type): + return target.id + return parent_id + + def _extract_sources(self, target: Constituenta) -> set[int]: + sources: set[int] = set() + if not is_functional(target.cst_type): + for parent_id in self._graph.inputs[target.id]: + parent_info = self[parent_id] + if not parent_info['is_template'] or not parent_info['is_simple']: + sources.add(parent_info['parent']) + return sources + + expression = split_template(target.definition_formal) + body_dependencies = extract_globals(expression['body']) + for alias in body_dependencies: + parent = self._cst_by_alias.get(alias) + if not parent: + continue + + parent_info = self[parent.id] + if not parent_info['is_template'] or not parent_info['is_simple']: + sources.add(parent_info['parent']) + + if self._need_check_head(sources, expression['head']): + head_dependencies = extract_globals(expression['head']) + for alias in head_dependencies: + parent = self._cst_by_alias.get(alias) + if not parent: + continue + + parent_info = self[parent.id] + if not is_base_set(parent.cst_type) and \ + (not parent_info['is_template'] or not parent_info['is_simple']): + sources.add(parent_info['parent']) + return sources + + def _need_check_head(self, sources: set[int], head: str) -> bool: + if len(sources) == 0: + return True + elif len(sources) != 1: + return False + else: + base = self._cst_by_ID[next(iter(sources))] + return not is_functional(base.cst_type) or \ + split_template(base.definition_formal)['head'] != head + + +class _OrderManager: + ''' Ordering helper class ''' + + def __init__(self, schema: RSForm): + self._semantic = schema.semantic() + self._graph = schema._graph_formal() + self._items = list( + schema.constituents() + .only('id', 'order', 'alias', 'cst_type', 'definition_formal') + .order_by('order') + ) + self._cst_by_ID = {cst.id: cst for cst in self._items} + + def restore_order(self) -> None: + ''' Implement order restoration process. ''' + if len(self._items) <= 1: + return + self._fix_kernel() + self._fix_topological() + self._fix_semantic_children() + self._save_order() + + def _fix_topological(self) -> None: + sorted_ids = self._graph.sort_stable([cst.id for cst in self._items]) + sorted_items = [next(cst for cst in self._items if cst.id == id) for id in sorted_ids] + self._items = sorted_items + + def _fix_kernel(self) -> None: + result = [cst for cst in self._items if cst.cst_type == CstType.BASE] + result = result + [cst for cst in self._items if cst.cst_type == CstType.CONSTANT] + kernel = [ + cst.id for cst in self._items if + cst.cst_type in [CstType.STRUCTURED, CstType.AXIOM] or + self._cst_by_ID[self._semantic.parent(cst.id)].cst_type == CstType.STRUCTURED + ] + kernel = kernel + self._graph.expand_inputs(kernel) + result = result + [cst for cst in self._items if result.count(cst) == 0 and cst.id in kernel] + result = result + [cst for cst in self._items if result.count(cst) == 0] + self._items = result + + def _fix_semantic_children(self) -> None: + result: list[Constituenta] = [] + marked: set[Constituenta] = set() + for cst in self._items: + if cst in marked: + continue + result.append(cst) + children = self._semantic[cst.id]['children'] + if len(children) == 0: + continue + for child in self._items: + if child.id in children: + marked.add(child) + result.append(child) + self._items = result + + @transaction.atomic + def _save_order(self) -> None: + order = 1 + for cst in self._items: + cst.order = order + cst.save() + order += 1 diff --git a/rsconcept/backend/apps/rsform/models/api_RSLanguage.py b/rsconcept/backend/apps/rsform/models/api_RSLanguage.py new file mode 100644 index 00000000..d32a8c4e --- /dev/null +++ b/rsconcept/backend/apps/rsform/models/api_RSLanguage.py @@ -0,0 +1,183 @@ +''' Models: Definitions and utility function for RSLanguage. ''' +import json +import re +from enum import IntEnum, unique +from typing import Set, Tuple, cast + +import pyconcept + +from .. import messages as msg +from .Constituenta import CstType + +_RE_GLOBALS = r'[XCSADFPT]\d+' # cspell:disable-line +_RE_TEMPLATE = r'R\d+' +_RE_COMPLEX_SYMBOLS = r'[∀∃×ℬ;|:]' + + +@unique +class TokenType(IntEnum): + ''' Some of grammar token types. Full list seek in frontend / pyconcept ''' + ID_GLOBAL = 259 + ID_RADICAL = 262 + DECART = 287 + BOOLEAN = 292 + BIGPR = 293 + SMALLPR = 294 + REDUCE = 299 + + +def get_type_prefix(cst_type: CstType) -> str: + ''' Get alias prefix. ''' + match cst_type: + case CstType.BASE: return 'X' + case CstType.CONSTANT: return 'C' + case CstType.STRUCTURED: return 'S' + case CstType.AXIOM: return 'A' + case CstType.TERM: return 'D' + case CstType.FUNCTION: return 'F' + case CstType.PREDICATE: return 'P' + case CstType.THEOREM: return 'T' + return 'X' + + +def is_basic_concept(cst_type: CstType) -> bool: + ''' Evaluate if CstType is basic concept.''' + return cst_type in [ + CstType.BASE, + CstType.CONSTANT, + CstType.STRUCTURED, + CstType.AXIOM + ] + + +def is_base_set(cst_type: CstType) -> bool: + ''' Evaluate if CstType is base set or constant set.''' + return cst_type in [ + CstType.BASE, + CstType.CONSTANT + ] + + +def is_functional(cst_type: CstType) -> bool: + ''' Evaluate if CstType is function.''' + return cst_type in [ + CstType.FUNCTION, + CstType.PREDICATE + ] + + +def extract_globals(expression: str) -> Set[str]: + ''' Extract all global aliases from expression. ''' + return set(re.findall(_RE_GLOBALS, expression)) + + +def guess_type(alias: str) -> CstType: + ''' Get CstType for alias. ''' + prefix = alias[0] + for (value, _) in CstType.choices: + if prefix == get_type_prefix(cast(CstType, value)): + return cast(CstType, value) + return CstType.BASE + + +def _get_structure_prefix(alias: str, expression: str, parse: dict) -> Tuple[str, str]: + ''' Generate prefix and alias for structure generation. ''' + args = parse['args'] + if len(args) == 0: + return (alias, '') + prefix = expression[0:expression.find(']')] + '] ' + newAlias = alias + '[' + ','.join([arg['alias'] for arg in args]) + ']' + return (newAlias, prefix) + + +def infer_template(expression: str) -> bool: + ''' Checks if given expression is a template. ''' + return bool(re.search(_RE_TEMPLATE, expression)) + + +def is_simple_expression(expression: str) -> bool: + ''' Checks if given expression is "simple". ''' + return not bool(re.search(_RE_COMPLEX_SYMBOLS, expression)) + + +def split_template(expression: str): + ''' Splits a string containing a template definition into its head and body parts. ''' + start = 0 + for index, char in enumerate(expression): + start = index + if char == '[': + break + if start < len(expression): + counter = 0 + for end in range(start + 1, len(expression)): + if expression[end] == '[': + counter += 1 + elif expression[end] == ']': + if counter != 0: + counter -= 1 + else: + return { + 'head': expression[start + 1:end].strip(), + 'body': expression[end + 1:].strip() + } + return { + 'head': '', + 'body': expression + } + + +def generate_structure(alias: str, expression: str, parse: dict) -> list: + ''' Generate list of expressions for target structure. ''' + ast = json.loads(pyconcept.parse_expression(parse['typification']))['ast'] + if len(ast) == 0: + raise ValueError(msg.typificationInvalidStr()) + if len(ast) == 1: + return [] + (link, prefix) = _get_structure_prefix(alias, expression, parse) + + generated: list = [] + arity: list = [1] * len(ast) + for (n, item) in enumerate(ast): + if n == 0: + generated.append({ + 'text': link, # generated text + 'operation': None, # applied operation. None if text should be skipped + 'is_boolean': False # is the result of operation has an additional boolean + }) + continue + + parent_index = item['parent'] + parent_type = ast[parent_index]['typeID'] + parent_text = generated[parent_index]['text'] + parent_is_boolean = generated[parent_index]['is_boolean'] + assert parent_type in [TokenType.BOOLEAN, TokenType.DECART] + + if parent_is_boolean: + if parent_type == TokenType.BOOLEAN: + generated.append({ + 'text': f'red({parent_text})', + 'operation': TokenType.REDUCE, + 'is_boolean': True + }) + if parent_type == TokenType.DECART: + generated.append({ + 'text': f'Pr{arity[parent_index]}({parent_text})', + 'operation': TokenType.BIGPR, + 'is_boolean': True + }) + arity[parent_index] = arity[parent_index] + 1 + else: + if parent_type == TokenType.BOOLEAN: + generated.append({ + 'text': parent_text, + 'operation': None, + 'is_boolean': True + }) + if parent_type == TokenType.DECART: + generated.append({ + 'text': f'pr{arity[parent_index]}({parent_text})', + 'operation': TokenType.SMALLPR, + 'is_boolean': False + }) + arity[parent_index] = arity[parent_index] + 1 + return [prefix + item['text'] for item in generated if item['operation'] is not None] diff --git a/rsconcept/backend/apps/rsform/permissions.py b/rsconcept/backend/apps/rsform/permissions.py new file mode 100644 index 00000000..7a163dec --- /dev/null +++ b/rsconcept/backend/apps/rsform/permissions.py @@ -0,0 +1,93 @@ +''' Custom Permission classes. + Hierarchy: Anyone -> User -> Editor -> Owner -> Admin +''' +from typing import Any, cast + +from django.core.exceptions import PermissionDenied +from rest_framework.permissions import AllowAny as Anyone # pylint: disable=unused-import +from rest_framework.permissions import BasePermission as _Base +from rest_framework.permissions import \ + IsAuthenticated as GlobalUser # pylint: disable=unused-import +from rest_framework.request import Request +from rest_framework.views import APIView + +from . import models as m + + +def _extract_item(obj: Any) -> m.LibraryItem: + if isinstance(obj, m.LibraryItem): + return obj + elif isinstance(obj, m.Constituenta): + return cast(m.LibraryItem, obj.schema) + elif isinstance(obj, (m.Version, m.Subscription, m.Editor)): + return cast(m.LibraryItem, obj.item) + raise PermissionDenied({ + 'message': 'Invalid type error. Please contact developers', + 'object_id': obj.id + }) + + +class GlobalAdmin(_Base): + ''' Item permission: Admin or higher. ''' + + def has_permission(self, request: Request, view: APIView) -> bool: + if not hasattr(request.user, 'is_staff'): + return False + return request.user.is_staff # type: ignore + + def has_object_permission(self, request: Request, view: APIView, obj: Any) -> bool: + if not hasattr(request.user, 'is_staff'): + return False + return request.user.is_staff # type: ignore + + +class ItemOwner(GlobalAdmin): + ''' Item permission: Owner or higher. ''' + + def has_permission(self, request: Request, view: APIView) -> bool: + return not request.user.is_anonymous + + def has_object_permission(self, request: Request, view: APIView, obj: Any) -> bool: + if request.user == _extract_item(obj).owner: + return True + return super().has_object_permission(request, view, obj) + + +class ItemEditor(ItemOwner): + ''' Item permission: Editor or higher. ''' + + def has_object_permission(self, request: Request, view: APIView, obj: Any) -> bool: + if request.user.is_anonymous: + return False + item = _extract_item(obj) + if m.Editor.objects.filter( + item=item, + editor=cast(m.User, request.user) + ).exists() and item.access_policy != m.AccessPolicy.PRIVATE: + return True + return super().has_object_permission(request, view, obj) + + +class ItemAnyone(ItemEditor): + ''' Item permission: Anyone if public. ''' + + def has_permission(self, request: Request, view: APIView) -> bool: + return True + + def has_object_permission(self, request: Request, view: APIView, obj: Any) -> bool: + item = _extract_item(obj) + if item.access_policy == m.AccessPolicy.PUBLIC: + return True + return super().has_object_permission(request, view, obj) + + +class EditorMixin(APIView): + ''' Editor permissions mixin for API views. ''' + + def get_permissions(self): + result = super().get_permissions() + if self.request.method.upper() == 'GET': + result.append(Anyone()) + else: + result.append(ItemEditor()) + return result diff --git a/rsconcept/backend/apps/rsform/serializers/__init__.py b/rsconcept/backend/apps/rsform/serializers/__init__.py new file mode 100644 index 00000000..12c2a4f5 --- /dev/null +++ b/rsconcept/backend/apps/rsform/serializers/__init__.py @@ -0,0 +1,40 @@ +''' REST API: Serializers. ''' + +from .basics import ( + AccessPolicySerializer, + ASTNodeSerializer, + ExpressionParseSerializer, + ExpressionSerializer, + LocationSerializer, + MultiFormSerializer, + ResolverSerializer, + TextSerializer, + WordFormSerializer +) +from .data_access import ( + CstCreateSerializer, + CstListSerializer, + CstMoveSerializer, + CstRenameSerializer, + CstSerializer, + CstSubstituteSerializer, + CstTargetSerializer, + InlineSynthesisSerializer, + LibraryItemBaseSerializer, + LibraryItemCloneSerializer, + LibraryItemSerializer, + RSFormParseSerializer, + RSFormSerializer, + UsersListSerializer, + UserTargetSerializer, + VersionCreateSerializer, + VersionSerializer +) +from .io_files import FileSerializer, RSFormTRSSerializer, RSFormUploadSerializer +from .io_pyconcept import PyConceptAdapter +from .schema_typing import ( + NewCstResponse, + NewMultiCstResponse, + NewVersionResponse, + ResultTextResponse +) diff --git a/rsconcept/backend/apps/rsform/serializers/basics.py b/rsconcept/backend/apps/rsform/serializers/basics.py new file mode 100644 index 00000000..cc21c83f --- /dev/null +++ b/rsconcept/backend/apps/rsform/serializers/basics.py @@ -0,0 +1,192 @@ +''' Basic serializers that do not interact with database. ''' +from typing import cast + +from cctext import EntityReference, Reference, ReferenceType, Resolver, SyntacticReference +from rest_framework import serializers + +from .. import messages as msg +from ..models import AccessPolicy, validate_location + + +class ExpressionSerializer(serializers.Serializer): + ''' Serializer: RSLang expression. ''' + expression = serializers.CharField() + + +class WordFormSerializer(serializers.Serializer): + ''' Serializer: inflect request. ''' + text = serializers.CharField() + grams = serializers.CharField() + + +class LocationSerializer(serializers.Serializer): + ''' Serializer: Item location. ''' + location = serializers.CharField(max_length=500) + + def validate(self, attrs): + attrs = super().validate(attrs) + if not validate_location(attrs['location']): + raise serializers.ValidationError({ + 'location': msg.invalidLocation() + }) + return attrs + + +class AccessPolicySerializer(serializers.Serializer): + ''' Serializer: Constituenta renaming. ''' + access_policy = serializers.CharField(max_length=500) + + def validate(self, attrs): + attrs = super().validate(attrs) + if not attrs['access_policy'] in AccessPolicy.values: + raise serializers.ValidationError({ + 'access_policy': msg.invalidEnum(attrs['access_policy']) + }) + return attrs + + +class MultiFormSerializer(serializers.Serializer): + ''' Serializer: inflect request. ''' + items = serializers.ListField( + child=WordFormSerializer() + ) + + @staticmethod + def from_list(data: list[tuple[str, str]]) -> dict: + result: dict = {} + result['items'] = [] + for item in data: + result['items'].append({ + 'text': item[0], + 'grams': item[1] + }) + return result + + +class TextSerializer(serializers.Serializer): + ''' Serializer: Text with references. ''' + text = serializers.CharField() + + +class FunctionArgSerializer(serializers.Serializer): + ''' Serializer: RSLang function argument type. ''' + alias = serializers.CharField() + typification = serializers.CharField() + + +class CstParseSerializer(serializers.Serializer): + ''' Serializer: Constituenta parse result. ''' + status = serializers.CharField() + valueClass = serializers.CharField() + typification = serializers.CharField() + syntaxTree = serializers.CharField() + args = serializers.ListField( + child=FunctionArgSerializer() + ) + + +class ErrorDescriptionSerializer(serializers.Serializer): + ''' Serializer: RSError description. ''' + errorType = serializers.IntegerField() + position = serializers.IntegerField() + isCritical = serializers.BooleanField() + params = serializers.ListField( + child=serializers.CharField() + ) + + +class NodeDataSerializer(serializers.Serializer): + ''' Serializer: Node data. ''' + dataType = serializers.CharField() + value = serializers.CharField() + + +class ASTNodeSerializer(serializers.Serializer): + ''' Serializer: Syntax tree node. ''' + uid = serializers.IntegerField() + parent = serializers.IntegerField() # type: ignore + typeID = serializers.IntegerField() + start = serializers.IntegerField() + finish = serializers.IntegerField() + data = NodeDataSerializer() # type: ignore + + +class ExpressionParseSerializer(serializers.Serializer): + ''' Serializer: RSlang expression parse result. ''' + parseResult = serializers.BooleanField() + syntax = serializers.CharField() + typification = serializers.CharField() + valueClass = serializers.CharField() + astText = serializers.CharField() + ast = serializers.ListField( + child=ASTNodeSerializer() + ) + errors = serializers.ListField( # type: ignore + child=ErrorDescriptionSerializer() + ) + args = serializers.ListField( + child=FunctionArgSerializer() + ) + + +class TextPositionSerializer(serializers.Serializer): + ''' Serializer: Text position. ''' + start = serializers.IntegerField() + finish = serializers.IntegerField() + + +class ReferenceDataSerializer(serializers.Serializer): + ''' Serializer: Reference data - Union of all references. ''' + offset = serializers.IntegerField() + nominal = serializers.CharField() + entity = serializers.CharField() + form = serializers.CharField() + + +class ReferenceSerializer(serializers.Serializer): + ''' Serializer: Language reference. ''' + type = serializers.CharField() + data = ReferenceDataSerializer() # type: ignore + pos_input = TextPositionSerializer() + pos_output = TextPositionSerializer() + + +class ResolverSerializer(serializers.Serializer): + ''' Serializer: Resolver results serializer. ''' + input = serializers.CharField() + output = serializers.CharField() + refs = serializers.ListField( + child=ReferenceSerializer() + ) + + def to_representation(self, instance: Resolver) -> dict: + return { + 'input': instance.input, + 'output': instance.output, + 'refs': [{ + 'type': ref.ref.get_type().value, + 'data': self._get_reference_data(ref.ref), + 'resolved': ref.resolved, + 'pos_input': { + 'start': ref.pos_input.start, + 'finish': ref.pos_input.finish + }, + 'pos_output': { + 'start': ref.pos_output.start, + 'finish': ref.pos_output.finish + } + } for ref in instance.refs] + } + + @staticmethod + def _get_reference_data(ref: Reference) -> dict: + if ref.get_type() == ReferenceType.entity: + return { + 'entity': cast(EntityReference, ref).entity, + 'form': cast(EntityReference, ref).form + } + else: + return { + 'offset': cast(SyntacticReference, ref).offset, + 'nominal': cast(SyntacticReference, ref).nominal + } diff --git a/rsconcept/backend/apps/rsform/serializers/data_access.py b/rsconcept/backend/apps/rsform/serializers/data_access.py new file mode 100644 index 00000000..2861eff7 --- /dev/null +++ b/rsconcept/backend/apps/rsform/serializers/data_access.py @@ -0,0 +1,446 @@ +''' Serializers for persistent data manipulation. ''' +from typing import Optional, cast + +from django.contrib.auth.models import User +from django.core.exceptions import PermissionDenied +from django.db import transaction +from rest_framework import serializers +from rest_framework.serializers import PrimaryKeyRelatedField as PKField + +from .. import messages as msg +from ..models import Constituenta, CstType, LibraryItem, RSForm, Version +from .basics import CstParseSerializer +from .io_pyconcept import PyConceptAdapter + + +class LibraryItemBaseSerializer(serializers.ModelSerializer): + ''' Serializer: LibraryItem entry full access. ''' + class Meta: + ''' serializer metadata. ''' + model = LibraryItem + fields = '__all__' + read_only_fields = ('id',) + + +class LibraryItemSerializer(serializers.ModelSerializer): + ''' Serializer: LibraryItem entry limited access. ''' + class Meta: + ''' serializer metadata. ''' + model = LibraryItem + fields = '__all__' + read_only_fields = ('id', 'item_type', 'owner', 'location', 'access_policy') + + +class LibraryItemCloneSerializer(serializers.ModelSerializer): + ''' Serializer: LibraryItem cloning. ''' + items = PKField(many=True, required=False, queryset=Constituenta.objects.all()) + + class Meta: + ''' serializer metadata. ''' + model = LibraryItem + exclude = ['id', 'item_type', 'owner'] + + +class VersionSerializer(serializers.ModelSerializer): + ''' Serializer: Version data. ''' + class Meta: + ''' serializer metadata. ''' + model = Version + fields = 'id', 'version', 'item', 'description', 'time_create' + read_only_fields = ('id', 'item', 'time_create') + + +class VersionInnerSerializer(serializers.ModelSerializer): + ''' Serializer: Version data for list of versions. ''' + class Meta: + ''' serializer metadata. ''' + model = Version + fields = 'id', 'version', 'description', 'time_create' + read_only_fields = ('id', 'item', 'time_create') + + +class VersionCreateSerializer(serializers.ModelSerializer): + ''' Serializer: Version create data. ''' + class Meta: + ''' serializer metadata. ''' + model = Version + fields = 'version', 'description' + + +class LibraryItemDetailsSerializer(serializers.ModelSerializer): + ''' Serializer: LibraryItem detailed data. ''' + subscribers = serializers.SerializerMethodField() + editors = serializers.SerializerMethodField() + versions = serializers.SerializerMethodField() + + class Meta: + ''' serializer metadata. ''' + model = LibraryItem + fields = '__all__' + read_only_fields = ('owner', 'id', 'item_type') + + def get_subscribers(self, instance: LibraryItem) -> list[int]: + return [item.pk for item in instance.subscribers()] + + def get_editors(self, instance: LibraryItem) -> list[int]: + return [item.pk for item in instance.editors()] + + def get_versions(self, instance: LibraryItem) -> list: + return [VersionInnerSerializer(item).data for item in instance.versions()] + + +class CstBaseSerializer(serializers.ModelSerializer): + ''' Serializer: Constituenta all data. ''' + class Meta: + ''' serializer metadata. ''' + model = Constituenta + fields = '__all__' + read_only_fields = ('id',) + + +class CstSerializer(serializers.ModelSerializer): + ''' Serializer: Constituenta data. ''' + class Meta: + ''' serializer metadata. ''' + model = Constituenta + fields = '__all__' + read_only_fields = ('id', 'order', 'alias', 'cst_type', 'definition_resolved', 'term_resolved') + + def update(self, instance: Constituenta, validated_data) -> Constituenta: + data = validated_data # Note: use alias for better code readability + schema = RSForm(instance.schema) + definition: Optional[str] = data['definition_raw'] if 'definition_raw' in data else None + term: Optional[str] = data['term_raw'] if 'term_raw' in data else None + term_changed = 'term_forms' in data + if definition is not None and definition != instance.definition_raw: + data['definition_resolved'] = schema.resolver().resolve(definition) + if term is not None and term != instance.term_raw: + data['term_resolved'] = schema.resolver().resolve(term) + if data['term_resolved'] != instance.term_resolved and 'term_forms' not in data: + data['term_forms'] = [] + term_changed = data['term_resolved'] != instance.term_resolved + result: Constituenta = super().update(instance, data) + if term_changed: + schema.on_term_change([result.id]) + result.refresh_from_db() + schema.item.save() + return result + + +class CstDetailsSerializer(serializers.ModelSerializer): + ''' Serializer: Constituenta data including parse. ''' + parse = CstParseSerializer() + + class Meta: + ''' serializer metadata. ''' + model = Constituenta + fields = '__all__' + + +class CstCreateSerializer(serializers.ModelSerializer): + ''' Serializer: Constituenta creation. ''' + insert_after = serializers.IntegerField(required=False, allow_null=True) + + class Meta: + ''' serializer metadata. ''' + model = Constituenta + fields = \ + 'alias', 'cst_type', 'convention', \ + 'term_raw', 'definition_raw', 'definition_formal', \ + 'insert_after', 'term_forms' + + +class RSFormSerializer(serializers.ModelSerializer): + ''' Serializer: Detailed data for RSForm. ''' + subscribers = serializers.ListField( + child=serializers.IntegerField() + ) + editors = serializers.ListField( + child=serializers.IntegerField() + ) + items = serializers.ListField( + child=CstSerializer() + ) + + class Meta: + ''' serializer metadata. ''' + model = LibraryItem + fields = '__all__' + + def to_representation(self, instance: LibraryItem) -> dict: + result = LibraryItemDetailsSerializer(instance).data + schema = RSForm(instance) + result['items'] = [] + for cst in schema.constituents().order_by('order'): + result['items'].append(CstSerializer(cst).data) + return result + + def to_versioned_data(self) -> dict: + ''' Create serializable version representation without redundant data. ''' + result = self.to_representation(cast(LibraryItem, self.instance)) + del result['versions'] + del result['subscribers'] + del result['editors'] + + del result['owner'] + del result['visible'] + del result['read_only'] + del result['access_policy'] + del result['location'] + + del result['time_create'] + del result['time_update'] + return result + + def from_versioned_data(self, version: int, data: dict) -> dict: + ''' Load data from version. ''' + result = self.to_representation(cast(LibraryItem, self.instance)) + result['version'] = version + return result | data + + @transaction.atomic + def restore_from_version(self, data: dict): + ''' Load data from version. ''' + schema = RSForm(cast(LibraryItem, self.instance)) + items: list[dict] = data['items'] + ids: list[int] = [item['id'] for item in items] + processed: list[int] = [] + + for cst in schema.constituents(): + if not cst.pk in ids: + cst.delete() + else: + cst_data = next(x for x in items if x['id'] == cst.pk) + new_cst = CstBaseSerializer(data=cst_data) + new_cst.is_valid(raise_exception=True) + new_cst.update( + instance=cst, + validated_data=new_cst.validated_data + ) + processed.append(cst.pk) + + for cst_data in items: + if cst_data['id'] not in processed: + cst = schema.insert_new(cst_data['alias']) + cst_data['id'] = cst.pk + new_cst = CstBaseSerializer(data=cst_data) + new_cst.is_valid(raise_exception=True) + new_cst.update( + instance=cst, + validated_data=new_cst.validated_data + ) + + loaded_item = LibraryItemBaseSerializer(data=data) + loaded_item.is_valid(raise_exception=True) + loaded_item.update( + instance=cast(LibraryItem, self.instance), + validated_data=loaded_item.validated_data + ) + + +class RSFormParseSerializer(serializers.ModelSerializer): + ''' Serializer: Detailed data for RSForm including parse. ''' + subscribers = serializers.ListField( + child=serializers.IntegerField() + ) + editors = serializers.ListField( + child=serializers.IntegerField() + ) + items = serializers.ListField( + child=CstDetailsSerializer() + ) + + class Meta: + ''' serializer metadata. ''' + model = LibraryItem + fields = '__all__' + + def to_representation(self, instance: LibraryItem): + result = RSFormSerializer(instance).data + return self._parse_data(result) + + def from_versioned_data(self, version: int, data: dict) -> dict: + ''' Load data from version and parse. ''' + item = cast(LibraryItem, self.instance) + result = RSFormSerializer(item).from_versioned_data(version, data) + return self._parse_data(result) + + def _parse_data(self, data: dict) -> dict: + parse = PyConceptAdapter(data).parse() + for cst_data in data['items']: + cst_data['parse'] = next( + cst['parse'] for cst in parse['items'] + if cst['id'] == cst_data['id'] + ) + return data + + +class CstTargetSerializer(serializers.Serializer): + ''' Serializer: Target single Constituenta. ''' + target = PKField(many=False, queryset=Constituenta.objects.all()) + + def validate(self, attrs): + schema = cast(LibraryItem, self.context['schema']) + cst = cast(Constituenta, attrs['target']) + if schema and cst.schema != schema: + raise serializers.ValidationError({ + f'{cst.id}': msg.constituentaNotOwned(schema.title) + }) + if cst.cst_type not in [CstType.FUNCTION, CstType.STRUCTURED, CstType.TERM]: + raise serializers.ValidationError({ + f'{cst.id}': msg.constituentaNoStructure() + }) + self.instance = cst + return attrs + + +class UserTargetSerializer(serializers.Serializer): + ''' Serializer: Target single User. ''' + user = PKField(many=False, queryset=User.objects.all()) + + +class UsersListSerializer(serializers.Serializer): + ''' Serializer: List of Users. ''' + users = PKField(many=True, queryset=User.objects.all()) + + +class CstRenameSerializer(serializers.Serializer): + ''' Serializer: Constituenta renaming. ''' + target = PKField(many=False, queryset=Constituenta.objects.all()) + alias = serializers.CharField() + cst_type = serializers.CharField() + + def validate(self, attrs): + attrs = super().validate(attrs) + schema = cast(LibraryItem, self.context['schema']) + cst = cast(Constituenta, attrs['target']) + if cst.schema != schema: + raise serializers.ValidationError({ + f'{cst.id}': msg.constituentaNotOwned(schema.title) + }) + new_alias = self.initial_data['alias'] + if cst.alias == new_alias: + raise serializers.ValidationError({ + 'alias': msg.renameTrivial(new_alias) + }) + if RSForm(schema).constituents().filter(alias=new_alias).exists(): + raise serializers.ValidationError({ + 'alias': msg.aliasTaken(new_alias) + }) + return attrs + + +class CstListSerializer(serializers.Serializer): + ''' Serializer: List of constituents from one origin. ''' + items = PKField(many=True, queryset=Constituenta.objects.all()) + + def validate(self, attrs): + schema = cast(LibraryItem, self.context['schema']) + if not schema: + return attrs + + for item in attrs['items']: + if item.schema != schema: + raise serializers.ValidationError({ + f'{item.id}': msg.constituentaNotOwned(schema.title) + }) + return attrs + + +class CstMoveSerializer(CstListSerializer): + ''' Serializer: Change constituenta position. ''' + move_to = serializers.IntegerField() + + +class CstSubstituteSerializerBase(serializers.Serializer): + ''' Serializer: Basic substitution. ''' + original = PKField(many=False, queryset=Constituenta.objects.all()) + substitution = PKField(many=False, queryset=Constituenta.objects.all()) + transfer_term = serializers.BooleanField(required=False, default=False) + + +class CstSubstituteSerializer(serializers.Serializer): + ''' Serializer: Constituenta substitution. ''' + substitutions = serializers.ListField( + child=CstSubstituteSerializerBase(), + min_length=1 + ) + + def validate(self, attrs): + schema = cast(LibraryItem, self.context['schema']) + deleted = set() + for item in attrs['substitutions']: + original_cst = cast(Constituenta, item['original']) + substitution_cst = cast(Constituenta, item['substitution']) + if original_cst.pk in deleted: + raise serializers.ValidationError({ + f'{original_cst.id}': msg.substituteDouble(original_cst.alias) + }) + if original_cst.alias == substitution_cst.alias: + raise serializers.ValidationError({ + 'alias': msg.substituteTrivial(original_cst.alias) + }) + if original_cst.schema != schema: + raise serializers.ValidationError({ + 'original': msg.constituentaNotOwned(schema.title) + }) + if substitution_cst.schema != schema: + raise serializers.ValidationError({ + 'substitution': msg.constituentaNotOwned(schema.title) + }) + deleted.add(original_cst.pk) + return attrs + + +class InlineSynthesisSerializer(serializers.Serializer): + ''' Serializer: Inline synthesis operation input. ''' + receiver = PKField(many=False, queryset=LibraryItem.objects.all()) + source = PKField(many=False, queryset=LibraryItem.objects.all()) # type: ignore + items = PKField(many=True, queryset=Constituenta.objects.all()) + substitutions = serializers.ListField( + child=CstSubstituteSerializerBase() + ) + + def validate(self, attrs): + user = cast(User, self.context['user']) + schema_in = cast(LibraryItem, attrs['source']) + schema_out = cast(LibraryItem, attrs['receiver']) + if user.is_anonymous or (schema_out.owner != user and not user.is_staff): + raise PermissionDenied({ + 'message': msg.schemaNotOwned(), + 'object_id': schema_in.id + }) + constituents = cast(list[Constituenta], attrs['items']) + for cst in constituents: + if cst.schema != schema_in: + raise serializers.ValidationError({ + f'{cst.id}': msg.constituentaNotOwned(schema_in.title) + }) + deleted = set() + for item in attrs['substitutions']: + original_cst = cast(Constituenta, item['original']) + substitution_cst = cast(Constituenta, item['substitution']) + if original_cst.schema == schema_in: + if original_cst not in constituents: + raise serializers.ValidationError({ + f'{original_cst.id}': msg.substitutionNotInList() + }) + if substitution_cst.schema != schema_out: + raise serializers.ValidationError({ + f'{substitution_cst.id}': msg.constituentaNotOwned(schema_out.title) + }) + else: + if substitution_cst not in constituents: + raise serializers.ValidationError({ + f'{substitution_cst.id}': msg.substitutionNotInList() + }) + if original_cst.schema != schema_out: + raise serializers.ValidationError({ + f'{original_cst.id}': msg.constituentaNotOwned(schema_out.title) + }) + if original_cst.pk in deleted: + raise serializers.ValidationError({ + f'{original_cst.id}': msg.substituteDouble(original_cst.alias) + }) + deleted.add(original_cst.pk) + return attrs diff --git a/rsconcept/backend/apps/rsform/serializers/io_files.py b/rsconcept/backend/apps/rsform/serializers/io_files.py new file mode 100644 index 00000000..00065236 --- /dev/null +++ b/rsconcept/backend/apps/rsform/serializers/io_files.py @@ -0,0 +1,221 @@ +''' Serializers for file interaction. ''' +from django.db import transaction +from rest_framework import serializers + +from .. import messages as msg +from ..models import Constituenta, LibraryItem, RSForm +from ..utils import fix_old_references + +_CST_TYPE = 'constituenta' +_TRS_TYPE = 'rsform' +_TRS_VERSION_MIN = 16 +_TRS_VERSION = 16 +_TRS_HEADER = 'Exteor 4.8.13.1000 - 30/05/2022' + + +class FileSerializer(serializers.Serializer): + ''' Serializer: File input. ''' + file = serializers.FileField(allow_empty_file=False) + + +class RSFormUploadSerializer(serializers.Serializer): + ''' Upload data for RSForm serializer. ''' + file = serializers.FileField() + load_metadata = serializers.BooleanField() + + +class RSFormTRSSerializer(serializers.Serializer): + ''' Serializer: TRS file production and loading for RSForm. ''' + + def to_representation(self, instance: RSForm) -> dict: + result = self._prepare_json_rsform(instance) + items = instance.constituents().order_by('order') + for cst in items: + result['items'].append(self._prepare_json_constituenta(cst)) + return result + + @staticmethod + def _prepare_json_rsform(schema: RSForm) -> dict: + return { + 'type': _TRS_TYPE, + 'title': schema.item.title, + 'alias': schema.item.alias, + 'comment': schema.item.comment, + 'items': [], + 'claimed': False, + 'selection': [], + 'version': _TRS_VERSION, + 'versionInfo': _TRS_HEADER + } + + @staticmethod + def _prepare_json_constituenta(cst: Constituenta) -> dict: + return { + 'entityUID': cst.pk, + 'type': _CST_TYPE, + 'cstType': cst.cst_type, + 'alias': cst.alias, + 'convention': cst.convention, + 'term': { + 'raw': cst.term_raw, + 'resolved': cst.term_resolved, + 'forms': cst.term_forms + }, + 'definition': { + 'formal': cst.definition_formal, + 'text': { + 'raw': cst.definition_raw, + 'resolved': cst.definition_resolved + }, + }, + } + + def from_versioned_data(self, data: dict) -> dict: + ''' Load data from version. ''' + result = { + 'type': _TRS_TYPE, + 'title': data['title'], + 'alias': data['alias'], + 'comment': data['comment'], + 'items': [], + 'claimed': False, + 'selection': [], + 'version': _TRS_VERSION, + 'versionInfo': _TRS_HEADER + } + for cst in data['items']: + result['items'].append({ + 'entityUID': cst['id'], + 'type': _CST_TYPE, + 'cstType': cst['cst_type'], + 'alias': cst['alias'], + 'convention': cst['convention'], + 'term': { + 'raw': cst['term_raw'], + 'resolved': cst['term_resolved'], + 'forms': cst['term_forms'] + }, + 'definition': { + 'formal': cst['definition_formal'], + 'text': { + 'raw': cst['definition_raw'], + 'resolved': cst['definition_resolved'] + }, + }, + }) + return result + + def to_internal_value(self, data): + result = super().to_internal_value(data) + if 'owner' in data: + result['owner'] = data['owner'] + if 'visible' in data: + result['visible'] = data['visible'] + if 'read_only' in data: + result['read_only'] = data['read_only'] + if 'access_policy' in data: + result['access_policy'] = data['access_policy'] + if 'location' in data: + result['location'] = data['location'] + result['items'] = data.get('items', []) + if self.context['load_meta']: + result['title'] = data.get('title', 'Без названия') + result['alias'] = data.get('alias', '') + result['comment'] = data.get('comment', '') + if 'id' in data: + result['id'] = data['id'] + self.instance = RSForm(LibraryItem.objects.get(pk=result['id'])) + return result + + def validate(self, attrs: dict): + if 'version' not in self.initial_data \ + or self.initial_data['version'] < _TRS_VERSION_MIN \ + or self.initial_data['version'] > _TRS_VERSION: + raise serializers.ValidationError({ + 'version': msg.exteorFileVersionNotSupported() + }) + return attrs + + @transaction.atomic + def create(self, validated_data: dict) -> RSForm: + self.instance: RSForm = RSForm.create( + owner=validated_data.get('owner', None), + alias=validated_data['alias'], + title=validated_data['title'], + comment=validated_data['comment'], + visible=validated_data['visible'], + read_only=validated_data['read_only'], + access_policy=validated_data['access_policy'], + location=validated_data['location'] + ) + self.instance.item.save() + order = 1 + for cst_data in validated_data['items']: + cst = Constituenta( + alias=cst_data['alias'], + schema=self.instance.item, + order=order, + cst_type=cst_data['cstType'], + ) + self._load_cst_texts(cst, cst_data) + cst.save() + order += 1 + self.instance.resolve_all_text() + return self.instance + + @transaction.atomic + def update(self, instance: RSForm, validated_data) -> RSForm: + if 'alias' in validated_data: + instance.item.alias = validated_data['alias'] + if 'title' in validated_data: + instance.item.title = validated_data['title'] + if 'comment' in validated_data: + instance.item.comment = validated_data['comment'] + + order = 1 + prev_constituents = instance.constituents() + loaded_ids = set() + for cst_data in validated_data['items']: + uid = int(cst_data['entityUID']) + if prev_constituents.filter(pk=uid).exists(): + cst: Constituenta = prev_constituents.get(pk=uid) + cst.order = order + cst.alias = cst_data['alias'] + cst.cst_type = cst_data['cstType'] + self._load_cst_texts(cst, cst_data) + cst.save() + else: + cst = Constituenta( + alias=cst_data['alias'], + schema=instance.item, + order=order, + cst_type=cst_data['cstType'], + ) + self._load_cst_texts(cst, cst_data) + cst.save() + uid = cst.pk + loaded_ids.add(uid) + order += 1 + for prev_cst in prev_constituents: + if prev_cst.pk not in loaded_ids: + prev_cst.delete() + + instance.resolve_all_text() + instance.item.save() + return instance + + @staticmethod + def _load_cst_texts(cst: Constituenta, data: dict): + cst.convention = data.get('convention', '') + if 'definition' in data: + cst.definition_formal = data['definition'].get('formal', '') + if 'text' in data['definition']: + cst.definition_raw = fix_old_references(data['definition']['text'].get('raw', '')) + else: + cst.definition_raw = '' + if 'term' in data: + cst.term_raw = fix_old_references(data['term'].get('raw', '')) + cst.term_forms = data['term'].get('forms', []) + else: + cst.term_raw = '' + cst.term_forms = [] diff --git a/rsconcept/backend/apps/rsform/serializers/io_pyconcept.py b/rsconcept/backend/apps/rsform/serializers/io_pyconcept.py new file mode 100644 index 00000000..1aa79820 --- /dev/null +++ b/rsconcept/backend/apps/rsform/serializers/io_pyconcept.py @@ -0,0 +1,80 @@ +''' Data adapter to interface with pyconcept module. ''' +import json +from typing import Optional, Union, cast + +import pyconcept + +from .. import messages as msg +from ..models import RSForm + + +class PyConceptAdapter: + ''' RSForm adapter for interacting with pyconcept module. ''' + + def __init__(self, data: Union[RSForm, dict]): + try: + if 'items' in cast(dict, data): + self.data = self._prepare_request_raw(cast(dict, data)) + else: + self.data = self._prepare_request(cast(RSForm, data)) + except TypeError: + self.data = self._prepare_request(cast(RSForm, data)) + self._checked_data: Optional[dict] = None + + def parse(self) -> dict: + ''' Check RSForm and return check results. + Warning! Does not include texts. ''' + self._produce_response() + if self._checked_data is None: + raise ValueError(msg.pyconceptFailure()) + return self._checked_data + + def _prepare_request(self, schema: RSForm) -> dict: + result: dict = { + 'items': [] + } + items = schema.constituents().order_by('order') + for cst in items: + result['items'].append({ + 'entityUID': cst.pk, + 'cstType': cst.cst_type, + 'alias': cst.alias, + 'definition': { + 'formal': cst.definition_formal + } + }) + return result + + def _prepare_request_raw(self, data: dict) -> dict: + result: dict = { + 'items': [] + } + for cst in data['items']: + result['items'].append({ + 'entityUID': cst['id'], + 'cstType': cst['cst_type'], + 'alias': cst['alias'], + 'definition': { + 'formal': cst['definition_formal'] + } + }) + return result + + def _produce_response(self): + if self._checked_data is not None: + return + response = pyconcept.check_schema(json.dumps(self.data)) + data = json.loads(response) + self._checked_data = { + 'items': [] + } + for cst in data['items']: + self._checked_data['items'].append({ + 'id': cst['entityUID'], + 'cstType': cst['cstType'], + 'alias': cst['alias'], + 'definition': { + 'formal': cst['definition']['formal'] + }, + 'parse': cst['parse'] + }) diff --git a/rsconcept/backend/apps/rsform/serializers/schema_typing.py b/rsconcept/backend/apps/rsform/serializers/schema_typing.py new file mode 100644 index 00000000..43ff4e53 --- /dev/null +++ b/rsconcept/backend/apps/rsform/serializers/schema_typing.py @@ -0,0 +1,29 @@ +''' Utility serializers for REST API schema - SHOULD NOT BE ACCESSED DIRECTLY. ''' +from rest_framework import serializers + +from .data_access import RSFormParseSerializer + + +class ResultTextResponse(serializers.Serializer): + ''' Serializer: Text result of a function call. ''' + result = serializers.CharField() + + +class NewCstResponse(serializers.Serializer): + ''' Serializer: Create cst response. ''' + new_cst = serializers.IntegerField() + schema = RSFormParseSerializer() + + +class NewMultiCstResponse(serializers.Serializer): + ''' Serializer: Create multiple cst response. ''' + cst_list = serializers.ListField( + child=serializers.IntegerField() + ) + schema = RSFormParseSerializer() + + +class NewVersionResponse(serializers.Serializer): + ''' Serializer: Create cst response. ''' + version = serializers.IntegerField() + schema = RSFormParseSerializer() diff --git a/rsconcept/backend/apps/rsform/tests/EndpointTester.py b/rsconcept/backend/apps/rsform/tests/EndpointTester.py new file mode 100644 index 00000000..e2436cc4 --- /dev/null +++ b/rsconcept/backend/apps/rsform/tests/EndpointTester.py @@ -0,0 +1,177 @@ +''' Utils: base tester class for endpoints. ''' +from rest_framework import status +from rest_framework.test import APIClient, APIRequestFactory, APITestCase + +from apps.rsform.models import Editor, LibraryItem +from apps.users.models import User + + +def decl_endpoint(endpoint: str, method: str): + ''' Decorator for EndpointTester methods to provide API attributes. ''' + def set_endpoint_inner(function): + def wrapper(*args, **kwargs): + if '{' in endpoint: + args[0].endpoint = 'UNRESOLVED' + args[0].endpoint_mask = endpoint + else: + args[0].endpoint_mask = None + args[0].endpoint = endpoint + args[0].method = method + return function(*args, **kwargs) + return wrapper + return set_endpoint_inner + + +class EndpointTester(APITestCase): + ''' Abstract base class for Testing endpoints. ''' + + def setUp(self): + self.factory = APIRequestFactory() + self.user = User.objects.create_user( + username='UserTest', + email='blank@test.com', + password='password' + ) + self.user2 = User.objects.create_user( + username='UserTest2', + email='another@test.com', + password='password' + ) + self.client = APIClient() + self.client.force_authenticate(user=self.user) + + def toggle_admin(self, value: bool = True): + self.user.is_staff = value + self.user.save() + + def toggle_editor(self, item: LibraryItem, value: bool = True): + if value: + Editor.add(item, self.user) + else: + Editor.remove(item, self.user) + + def login(self): + self.client.force_authenticate(user=self.user) + + def logout(self): + self.client.logout() + + def set_params(self, **kwargs): + ''' Given named argument values resolve current endpoint_mask. ''' + if self.endpoint_mask and len(kwargs) > 0: + self.endpoint = _resolve_url(self.endpoint_mask, **kwargs) + + def get(self, endpoint: str = '', **kwargs): + if endpoint != '': + return self.client.get(endpoint) + else: + self.set_params(**kwargs) + return self.client.get(self.endpoint) + + def post(self, data=None, **kwargs): + self.set_params(**kwargs) + if not data is None: + return self.client.post(self.endpoint, data=data, format='json') + else: + return self.client.post(self.endpoint) + + def patch(self, data=None, **kwargs): + self.set_params(**kwargs) + if not data is None: + return self.client.patch(self.endpoint, data=data, format='json') + else: + return self.client.patch(self.endpoint) + + def put(self, data, **kwargs): + self.set_params(**kwargs) + return self.client.get(self.endpoint, data=data, format='json') + + def delete(self, data=None, **kwargs): + self.set_params(**kwargs) + if not data is None: + return self.client.delete(self.endpoint, data=data, format='json') + else: + return self.client.delete(self.endpoint) + + def execute(self, data=None, **kwargs): + if self.method == 'get': + return self.get(**kwargs) + if self.method == 'post': + return self.post(data, **kwargs) + if self.method == 'put': + return self.put(data, **kwargs) + if self.method == 'patch': + return self.patch(data, **kwargs) + if self.method == 'delete': + return self.delete(data, **kwargs) + return None + + def executeOK(self, data=None, **kwargs): + response = self.execute(data, **kwargs) + self.assertEqual(response.status_code, status.HTTP_200_OK) + return response + + def executeCreated(self, data=None, **kwargs): + response = self.execute(data, **kwargs) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + return response + + def executeAccepted(self, data=None, **kwargs): + response = self.execute(data, **kwargs) + self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) + return response + + def executeNoContent(self, data=None, **kwargs): + response = self.execute(data, **kwargs) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + return response + + def executeBadData(self, data=None, **kwargs): + response = self.execute(data, **kwargs) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + return response + + def executeForbidden(self, data=None, **kwargs): + response = self.execute(data, **kwargs) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + return response + + def executeNotModified(self, data=None, **kwargs): + response = self.execute(data, **kwargs) + self.assertEqual(response.status_code, status.HTTP_304_NOT_MODIFIED) + return response + + def executeNotFound(self, data=None, **kwargs): + response = self.execute(data, **kwargs) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + return response + + +def _resolve_url(url: str, **kwargs) -> str: + if url == '' or len(kwargs) == 0: + return url + pos_input: int = 0 + pos_start: int = 0 + pos_end: int = 0 + arg_names = set() + output: str = '' + while True: + pos_start = url.find('{', pos_input) + if pos_start == -1: + break + pos_end = url.find('}', pos_start) + if pos_end == -1: + break + name = url[(pos_start + 1): pos_end] + arg_names.add(name) + if not name in kwargs: + raise KeyError(f'Missing argument: {name} | Mask: {url}') + output += url[pos_input: pos_start] + output += str(kwargs[name]) + pos_input = pos_end + 1 + if pos_input < len(url): + output += url[pos_input: len(url)] + for (key, _) in kwargs.items(): + if key not in arg_names: + raise KeyError(f'Unused argument: {name} | Mask: {url}') + return output diff --git a/rsconcept/backend/apps/rsform/tests/__init__.py b/rsconcept/backend/apps/rsform/tests/__init__.py new file mode 100644 index 00000000..66f814e6 --- /dev/null +++ b/rsconcept/backend/apps/rsform/tests/__init__.py @@ -0,0 +1,7 @@ +''' Tests. ''' +from .s_models.t_RSForm import * +from .s_views import * +from .t_graph import * +from .t_imports import * +from .t_serializers import * +from .t_utils import * diff --git a/rsconcept/backend/apps/rsform/tests/s_models/__init__.py b/rsconcept/backend/apps/rsform/tests/s_models/__init__.py new file mode 100644 index 00000000..f2ec99ed --- /dev/null +++ b/rsconcept/backend/apps/rsform/tests/s_models/__init__.py @@ -0,0 +1,6 @@ +''' Tests for REST API. ''' +from .t_Constituenta import * +from .t_Editor import * +from .t_LibraryItem import * +from .t_RSForm import * +from .t_Subscription import * diff --git a/rsconcept/backend/apps/rsform/tests/s_models/t_Constituenta.py b/rsconcept/backend/apps/rsform/tests/s_models/t_Constituenta.py new file mode 100644 index 00000000..ad0ca54b --- /dev/null +++ b/rsconcept/backend/apps/rsform/tests/s_models/t_Constituenta.py @@ -0,0 +1,66 @@ +''' Testing models: Constituenta. ''' +from django.db.utils import IntegrityError +from django.forms import ValidationError +from django.test import TestCase + +from apps.rsform.models import Constituenta, CstType, LibraryItem, LibraryItemType + + +class TestConstituenta(TestCase): + ''' Testing Constituenta model. ''' + + def setUp(self): + self.schema1 = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test1') + self.schema2 = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test2') + + + def test_str(self): + testStr = 'X1' + cst = Constituenta.objects.create(alias=testStr, schema=self.schema1, order=1, convention='Test') + self.assertEqual(str(cst), testStr) + + + def test_url(self): + testStr = 'X1' + cst = Constituenta.objects.create(alias=testStr, schema=self.schema1, order=1, convention='Test') + self.assertEqual(cst.get_absolute_url(), f'/api/constituents/{cst.pk}') + + + def test_order_not_null(self): + with self.assertRaises(IntegrityError): + Constituenta.objects.create(alias='X1', schema=self.schema1) + + + def test_order_positive_integer(self): + with self.assertRaises(IntegrityError): + Constituenta.objects.create(alias='X1', schema=self.schema1, order=-1) + + + def test_order_min_value(self): + with self.assertRaises(ValidationError): + cst = Constituenta.objects.create(alias='X1', schema=self.schema1, order=0) + cst.full_clean() + + + def test_schema_not_null(self): + with self.assertRaises(IntegrityError): + Constituenta.objects.create(alias='X1', order=1) + + + def test_create_default(self): + cst = Constituenta.objects.create( + alias='X1', + schema=self.schema1, + order=1 + ) + self.assertEqual(cst.schema, self.schema1) + self.assertEqual(cst.order, 1) + self.assertEqual(cst.alias, 'X1') + self.assertEqual(cst.cst_type, CstType.BASE) + self.assertEqual(cst.convention, '') + self.assertEqual(cst.definition_formal, '') + self.assertEqual(cst.term_raw, '') + self.assertEqual(cst.term_resolved, '') + self.assertEqual(cst.term_forms, []) + self.assertEqual(cst.definition_resolved, '') + self.assertEqual(cst.definition_raw, '') diff --git a/rsconcept/backend/apps/rsform/tests/s_models/t_Editor.py b/rsconcept/backend/apps/rsform/tests/s_models/t_Editor.py new file mode 100644 index 00000000..1f25ad4d --- /dev/null +++ b/rsconcept/backend/apps/rsform/tests/s_models/t_Editor.py @@ -0,0 +1,76 @@ +''' Testing models: Editor. ''' +from django.test import TestCase + +from apps.rsform.models import Editor, LibraryItem, LibraryItemType, User + + +class TestEditor(TestCase): + ''' Testing Editor model. ''' + + def setUp(self): + self.user1 = User.objects.create(username='User1') + self.user2 = User.objects.create(username='User2') + self.item = LibraryItem.objects.create( + item_type=LibraryItemType.RSFORM, + title='Test', + alias='КС1', + owner=self.user1 + ) + + + def test_default(self): + editors = list(Editor.objects.filter(item=self.item)) + self.assertEqual(len(editors), 0) + + + def test_str(self): + testStr = 'КС1: User2' + item = Editor.objects.create( + editor=self.user2, + item=self.item + ) + self.assertEqual(str(item), testStr) + + + def test_add_editor(self): + self.assertTrue(Editor.add(self.item, self.user1)) + self.assertEqual(len(self.item.editors()), 1) + self.assertTrue(self.user1 in self.item.editors()) + + self.assertFalse(Editor.add(self.item, self.user1)) + self.assertEqual(len(self.item.editors()), 1) + + self.assertTrue(Editor.add(self.item, self.user2)) + self.assertEqual(len(self.item.editors()), 2) + self.assertTrue(self.user1 in self.item.editors()) + self.assertTrue(self.user2 in self.item.editors()) + + self.user1.delete() + self.assertEqual(len(self.item.editors()), 1) + + + def test_remove_editor(self): + self.assertFalse(Editor.remove(self.item, self.user1)) + Editor.add(self.item, self.user1) + Editor.add(self.item, self.user2) + self.assertEqual(len(self.item.editors()), 2) + + self.assertTrue(Editor.remove(self.item, self.user1)) + self.assertEqual(len(self.item.editors()), 1) + self.assertTrue(self.user2 in self.item.editors()) + + self.assertFalse(Editor.remove(self.item, self.user1)) + + + def test_set_editors(self): + Editor.set(self.item, [self.user1]) + self.assertEqual(self.item.editors(), [self.user1]) + + Editor.set(self.item, [self.user1, self.user1]) + self.assertEqual(self.item.editors(), [self.user1]) + + Editor.set(self.item, []) + self.assertEqual(self.item.editors(), []) + + Editor.set(self.item, [self.user1, self.user2]) + self.assertEqual(set(self.item.editors()), set([self.user1, self.user2])) diff --git a/rsconcept/backend/apps/rsform/tests/s_models/t_LibraryItem.py b/rsconcept/backend/apps/rsform/tests/s_models/t_LibraryItem.py new file mode 100644 index 00000000..e9922a9c --- /dev/null +++ b/rsconcept/backend/apps/rsform/tests/s_models/t_LibraryItem.py @@ -0,0 +1,103 @@ +''' Testing models: LibraryItem. ''' +from django.test import TestCase + +from apps.rsform.models import ( + AccessPolicy, + LibraryItem, + LibraryItemType, + LocationHead, + Subscription, + User, + validate_location +) + + +class TestLibraryItem(TestCase): + ''' Testing LibraryItem model. ''' + + def setUp(self): + self.user1 = User.objects.create(username='User1') + self.user2 = User.objects.create(username='User2') + + + def test_str(self): + testStr = 'Test123' + item = LibraryItem.objects.create( + item_type=LibraryItemType.RSFORM, + title='Title', + owner=self.user1, + alias=testStr + ) + self.assertEqual(str(item), testStr) + + + def test_url(self): + testStr = 'Test123' + item = LibraryItem.objects.create( + item_type=LibraryItemType.RSFORM, + title=testStr, + owner=self.user1, + alias='КС1' + ) + self.assertEqual(item.get_absolute_url(), f'/api/library/{item.pk}') + + + def test_create_default(self): + item = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test') + self.assertIsNone(item.owner) + self.assertEqual(item.title, 'Test') + self.assertEqual(item.alias, '') + self.assertEqual(item.comment, '') + self.assertEqual(item.visible, True) + self.assertEqual(item.read_only, False) + self.assertEqual(item.access_policy, AccessPolicy.PUBLIC) + self.assertEqual(item.location, LocationHead.USER) + + + def test_create(self): + item = LibraryItem.objects.create( + item_type=LibraryItemType.RSFORM, + title='Test', + owner=self.user1, + alias='KS1', + comment='Test comment', + location=LocationHead.COMMON + ) + self.assertEqual(item.owner, self.user1) + self.assertEqual(item.title, 'Test') + self.assertEqual(item.alias, 'KS1') + self.assertEqual(item.comment, 'Test comment') + self.assertEqual(item.location, LocationHead.COMMON) + self.assertTrue(Subscription.objects.filter(user=item.owner, item=item).exists()) + + +class TestLocation(TestCase): + ''' Testing Location model. ''' + + def test_validate_location(self): + self.assertFalse(validate_location('')) + self.assertFalse(validate_location('/A')) + self.assertFalse(validate_location('U/U')) + self.assertFalse(validate_location('/U/')) + self.assertFalse(validate_location('/U/user@mail')) + self.assertFalse(validate_location('/U/u\\asdf')) + self.assertFalse(validate_location('/U/ asdf')) + self.assertFalse(validate_location('/User')) + self.assertFalse(validate_location('//')) + self.assertFalse(validate_location('/S/1/')) + self.assertFalse(validate_location('/S/1 ')) + self.assertFalse(validate_location('/S/1/2 /3')) + self.assertFalse(validate_location('/S/-')) + self.assertFalse(validate_location('/S/1-')) + + self.assertTrue(validate_location('/P')) + self.assertTrue(validate_location('/L')) + self.assertTrue(validate_location('/U')) + self.assertTrue(validate_location('/S')) + self.assertTrue(validate_location('/S/1')) + self.assertTrue(validate_location('/S/1-2')) + self.assertTrue(validate_location('/S/20210101 asdf-a/2')) + self.assertTrue(validate_location('/S/12')) + self.assertTrue(validate_location('/S/12/3')) + self.assertTrue(validate_location('/S/Вася шофер')) + self.assertTrue(validate_location('/S/1/!asdf/тест тест')) diff --git a/rsconcept/backend/apps/rsform/tests/s_models/t_RSForm.py b/rsconcept/backend/apps/rsform/tests/s_models/t_RSForm.py new file mode 100644 index 00000000..7881f2b9 --- /dev/null +++ b/rsconcept/backend/apps/rsform/tests/s_models/t_RSForm.py @@ -0,0 +1,364 @@ +''' Testing models: api_RSForm. ''' +from django.forms import ValidationError +from django.test import TestCase + +from apps.rsform.models import Constituenta, CstType, RSForm, User + + +class TestRSForm(TestCase): + ''' Testing RSForm wrapper. ''' + + def setUp(self): + self.user1 = User.objects.create(username='User1') + self.user2 = User.objects.create(username='User2') + self.schema = RSForm.create(title='Test') + self.assertNotEqual(self.user1, self.user2) + + + def test_constituents(self): + schema1 = RSForm.create(title='Test1') + schema2 = RSForm.create(title='Test2') + self.assertFalse(schema1.constituents().exists()) + self.assertFalse(schema2.constituents().exists()) + + Constituenta.objects.create(alias='X1', schema=schema1.item, order=1) + Constituenta.objects.create(alias='X2', schema=schema1.item, order=2) + self.assertTrue(schema1.constituents().exists()) + self.assertFalse(schema2.constituents().exists()) + self.assertEqual(schema1.constituents().count(), 2) + + + def test_get_max_index(self): + schema1 = RSForm.create(title='Test1') + Constituenta.objects.create(alias='X1', schema=schema1.item, order=1) + Constituenta.objects.create(alias='D2', cst_type=CstType.TERM, schema=schema1.item, order=2) + self.assertEqual(schema1.get_max_index(CstType.BASE), 1) + self.assertEqual(schema1.get_max_index(CstType.TERM), 2) + self.assertEqual(schema1.get_max_index(CstType.AXIOM), 0) + + + def test_insert_at(self): + schema = RSForm.create(title='Test') + x1 = schema.insert_new('X1') + self.assertEqual(x1.order, 1) + self.assertEqual(x1.schema, schema.item) + + x2 = schema.insert_new('X2', position=1) + x1.refresh_from_db() + self.assertEqual(x2.order, 1) + self.assertEqual(x2.schema, schema.item) + self.assertEqual(x1.order, 2) + + x3 = schema.insert_new('X3', position=4) + x2.refresh_from_db() + x1.refresh_from_db() + self.assertEqual(x3.order, 3) + self.assertEqual(x3.schema, schema.item) + self.assertEqual(x2.order, 1) + self.assertEqual(x1.order, 2) + + x4 = schema.insert_new('X4', position=3) + x3.refresh_from_db() + x2.refresh_from_db() + x1.refresh_from_db() + self.assertEqual(x4.order, 3) + self.assertEqual(x4.schema, schema.item) + self.assertEqual(x3.order, 4) + self.assertEqual(x2.order, 1) + self.assertEqual(x1.order, 2) + + + def test_insert_at_invalid_position(self): + with self.assertRaises(ValidationError): + self.schema.insert_new('X5', position=0) + + + def test_insert_at_invalid_alias(self): + self.schema.insert_new('X1') + with self.assertRaises(ValidationError): + self.schema.insert_new('X1') + + + def test_insert_at_reorder(self): + self.schema.insert_new('X1') + d1 = self.schema.insert_new('D1') + d2 = self.schema.insert_new('D2', position=1) + d1.refresh_from_db() + self.assertEqual(d1.order, 3) + self.assertEqual(d2.order, 1) + + x2 = self.schema.insert_new('X2', position=4) + self.assertEqual(x2.order, 4) + + + def test_insert_last(self): + x1 = self.schema.insert_new('X1') + self.assertEqual(x1.order, 1) + self.assertEqual(x1.schema, self.schema.item) + + x2 = self.schema.insert_new('X2') + self.assertEqual(x2.order, 2) + self.assertEqual(x2.schema, self.schema.item) + self.assertEqual(x1.order, 1) + + + def test_create_cst_resolve(self): + x1 = self.schema.insert_new( + alias='X1', + term_raw='@{X2|datv}', + definition_raw='@{X1|datv} @{X2|datv}' + ) + x2 = self.schema.create_cst({ + 'alias': 'X2', + 'cst_type': CstType.BASE, + 'term_raw': 'слон', + 'definition_raw': '@{X1|plur} @{X2|plur}' + }) + x1.refresh_from_db() + self.assertEqual(x1.term_resolved, 'слону') + self.assertEqual(x1.definition_resolved, 'слону слону') + self.assertEqual(x2.term_resolved, 'слон') + self.assertEqual(x2.definition_resolved, 'слонам слоны') + + + def test_insert_copy(self): + x1 = self.schema.insert_new( + alias='X10', + convention='Test' + ) + s1 = self.schema.insert_new( + alias='S11', + definition_formal=x1.alias, + definition_raw='@{X10|plur}' + ) + + result = self.schema.insert_copy([s1, x1], 2) + self.assertEqual(len(result), 2) + + s1.refresh_from_db() + self.assertEqual(s1.order, 4) + + x2 = result[1] + self.assertEqual(x2.order, 3) + self.assertEqual(x2.alias, 'X11') + self.assertEqual(x2.cst_type, CstType.BASE) + self.assertEqual(x2.convention, x1.convention) + + s2 = result[0] + self.assertEqual(s2.order, 2) + self.assertEqual(s2.alias, 'S12') + self.assertEqual(s2.cst_type, CstType.STRUCTURED) + self.assertEqual(s2.definition_formal, x2.alias) + self.assertEqual(s2.definition_raw, '@{X11|plur}') + + + def test_apply_mapping(self): + x1 = self.schema.insert_new('X1') + x2 = self.schema.insert_new('X11') + d1 = self.schema.insert_new( + alias='D1', + definition_formal='X1 = X11 = X2', + definition_raw='@{X11|sing}', + convention='X1', + term_raw='@{X1|plur}' + ) + + self.schema.apply_mapping({x1.alias: 'X3', x2.alias: 'X4'}) + d1.refresh_from_db() + self.assertEqual(d1.definition_formal, 'X3 = X4 = X2', msg='Map IDs in expression') + self.assertEqual(d1.definition_raw, '@{X4|sing}', msg='Map IDs in definition') + self.assertEqual(d1.convention, 'X3', msg='Map IDs in convention') + self.assertEqual(d1.term_raw, '@{X3|plur}', msg='Map IDs in term') + self.assertEqual(d1.term_resolved, '', msg='Do not run resolve on mapping') + self.assertEqual(d1.definition_resolved, '', msg='Do not run resolve on mapping') + + + def test_substitute(self): + x1 = self.schema.insert_new( + alias='X1', + term_raw='Test' + ) + x2 = self.schema.insert_new( + alias='X2', + term_raw='Test2' + ) + d1 = self.schema.insert_new( + alias='D1', + definition_formal=x1.alias + ) + + self.schema.substitute(x1, x2, True) + x2.refresh_from_db() + d1.refresh_from_db() + self.assertEqual(self.schema.constituents().count(), 2) + self.assertEqual(x2.term_raw, 'Test') + self.assertEqual(d1.definition_formal, x2.alias) + + + def test_move_cst(self): + x1 = self.schema.insert_new('X1') + x2 = self.schema.insert_new('X2') + d1 = self.schema.insert_new('D1') + d2 = self.schema.insert_new('D2') + self.schema.move_cst([x2, d2], 1) + x1.refresh_from_db() + x2.refresh_from_db() + d1.refresh_from_db() + d2.refresh_from_db() + self.assertEqual(x1.order, 3) + self.assertEqual(x2.order, 1) + self.assertEqual(d1.order, 4) + self.assertEqual(d2.order, 2) + + + def test_move_cst_down(self): + x1 = self.schema.insert_new('X1') + x2 = self.schema.insert_new('X2') + self.schema.move_cst([x1], 2) + x1.refresh_from_db() + x2.refresh_from_db() + self.assertEqual(x1.order, 2) + self.assertEqual(x2.order, 1) + + + def test_restore_order(self): + d2 = self.schema.insert_new( + alias='D2', + definition_formal=r'D{ξ∈S1 | 1=1}', + ) + d1 = self.schema.insert_new( + alias='D1', + definition_formal=r'Pr1(S1)\X1', + ) + x1 = self.schema.insert_new('X1') + x2 = self.schema.insert_new('X2') + s1 = self.schema.insert_new( + alias='S1', + definition_formal='ℬ(X1×X1)' + ) + c1 = self.schema.insert_new('C1') + s2 = self.schema.insert_new( + alias='S2', + definition_formal='ℬ(X2×D1)' + ) + a1 = self.schema.insert_new( + alias='A1', + definition_formal=r'D3=∅', + ) + d3 = self.schema.insert_new( + alias='D3', + definition_formal=r'Pr2(S2)', + ) + f1 = self.schema.insert_new( + alias='F1', + definition_formal=r'[α∈ℬ(X1)] D{σ∈S1 | α⊆pr1(σ)}', + ) + d4 = self.schema.insert_new( + alias='D4', + definition_formal=r'Pr2(D3)', + ) + f2 = self.schema.insert_new( + alias='F2', + definition_formal=r'[α∈ℬ(X1)] X1\α', + ) + + self.schema.restore_order() + x1.refresh_from_db() + x2.refresh_from_db() + c1.refresh_from_db() + s1.refresh_from_db() + s2.refresh_from_db() + d1.refresh_from_db() + d2.refresh_from_db() + d3.refresh_from_db() + d4.refresh_from_db() + f1.refresh_from_db() + f2.refresh_from_db() + a1.refresh_from_db() + + self.assertEqual(x1.order, 1) + self.assertEqual(x2.order, 2) + self.assertEqual(c1.order, 3) + self.assertEqual(s1.order, 4) + self.assertEqual(d1.order, 5) + self.assertEqual(s2.order, 6) + self.assertEqual(d3.order, 7) + self.assertEqual(a1.order, 8) + self.assertEqual(d4.order, 9) + self.assertEqual(d2.order, 10) + self.assertEqual(f1.order, 11) + self.assertEqual(f2.order, 12) + + + def test_reset_aliases(self): + x1 = self.schema.insert_new( + alias='X11', + term_raw='человек', + term_resolved='человек' + ) + x2 = self.schema.insert_new('X21') + d1 = self.schema.insert_new( + alias='D11', + convention='D11 - cool', + definition_formal='X21=X21', + term_raw='@{X21|sing}', + definition_raw='@{X11|datv}', + definition_resolved='test' + ) + + self.schema.reset_aliases() + x1.refresh_from_db() + x2.refresh_from_db() + d1.refresh_from_db() + + self.assertEqual(x1.alias, 'X1') + self.assertEqual(x2.alias, 'X2') + self.assertEqual(d1.alias, 'D1') + self.assertEqual(d1.convention, 'D1 - cool') + self.assertEqual(d1.term_raw, '@{X2|sing}') + self.assertEqual(d1.definition_raw, '@{X1|datv}') + self.assertEqual(d1.definition_resolved, 'test') + + + def test_on_term_change(self): + x1 = self.schema.insert_new( + alias='X1', + term_raw='человек', + term_resolved='человек', + definition_raw='одному @{X1|datv}', + definition_resolved='одному человеку', + ) + x2 = self.schema.insert_new( + alias='X2', + term_raw='сильный @{X1|sing}', + term_resolved='сильный человек', + definition_raw=x1.definition_raw, + definition_resolved=x1.definition_resolved + ) + x3 = self.schema.insert_new( + alias='X3', + definition_raw=x1.definition_raw, + definition_resolved=x1.definition_resolved + ) + d1 = self.schema.insert_new( + alias='D1', + definition_raw='очень @{X2|sing}', + definition_resolved='очень сильный человек' + ) + + x1.term_raw = 'слон' + x1.term_resolved = 'слон' + x1.save() + + self.schema.on_term_change([x1.pk]) + x1.refresh_from_db() + x2.refresh_from_db() + x3.refresh_from_db() + d1.refresh_from_db() + + self.assertEqual(x1.term_raw, 'слон') + self.assertEqual(x1.term_resolved, 'слон') + self.assertEqual(x1.definition_resolved, 'одному слону') + self.assertEqual(x2.definition_resolved, x1.definition_resolved) + self.assertEqual(x3.definition_resolved, x1.definition_resolved) + self.assertEqual(d1.definition_resolved, 'очень сильный слон') diff --git a/rsconcept/backend/apps/rsform/tests/s_models/t_Subscription.py b/rsconcept/backend/apps/rsform/tests/s_models/t_Subscription.py new file mode 100644 index 00000000..95ee8380 --- /dev/null +++ b/rsconcept/backend/apps/rsform/tests/s_models/t_Subscription.py @@ -0,0 +1,68 @@ +''' Testing models: Subscription. ''' +from django.test import TestCase + +from apps.rsform.models import LibraryItem, LibraryItemType, Subscription, User + + +class TestSubscription(TestCase): + ''' Testing Subscription model. ''' + + def setUp(self): + self.user1 = User.objects.create(username='User1') + self.user2 = User.objects.create(username='User2') + self.item = LibraryItem.objects.create( + item_type=LibraryItemType.RSFORM, + title='Test', + alias='КС1', + owner=self.user1 + ) + + + def test_default(self): + subs = list(Subscription.objects.filter(item=self.item)) + self.assertEqual(len(subs), 1) + self.assertEqual(subs[0].item, self.item) + self.assertEqual(subs[0].user, self.user1) + + + def test_str(self): + testStr = 'User2 -> КС1' + item = Subscription.objects.create( + user=self.user2, + item=self.item + ) + self.assertEqual(str(item), testStr) + + + def test_subscribe(self): + item = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test') + self.assertEqual(len(item.subscribers()), 0) + + self.assertTrue(Subscription.subscribe(self.user1, item)) + self.assertEqual(len(item.subscribers()), 1) + self.assertTrue(self.user1 in item.subscribers()) + + self.assertFalse(Subscription.subscribe(self.user1, item)) + self.assertEqual(len(item.subscribers()), 1) + + self.assertTrue(Subscription.subscribe(self.user2, item)) + self.assertEqual(len(item.subscribers()), 2) + self.assertTrue(self.user1 in item.subscribers()) + self.assertTrue(self.user2 in item.subscribers()) + + self.user1.delete() + self.assertEqual(len(item.subscribers()), 1) + + + def test_unsubscribe(self): + item = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test') + self.assertFalse(Subscription.unsubscribe(self.user1, item)) + Subscription.subscribe(self.user1, item) + Subscription.subscribe(self.user2, item) + self.assertEqual(len(item.subscribers()), 2) + + self.assertTrue(Subscription.unsubscribe(self.user1, item)) + self.assertEqual(len(item.subscribers()), 1) + self.assertTrue(self.user2 in item.subscribers()) + + self.assertFalse(Subscription.unsubscribe(self.user1, item)) diff --git a/rsconcept/backend/apps/rsform/tests/s_views/__init__.py b/rsconcept/backend/apps/rsform/tests/s_views/__init__.py new file mode 100644 index 00000000..376add39 --- /dev/null +++ b/rsconcept/backend/apps/rsform/tests/s_views/__init__.py @@ -0,0 +1,9 @@ +''' Tests for REST API. ''' +from .t_library import * +from .t_constituents import * +from .t_operations import * +from .t_rsforms import * +from .t_versions import * + +from .t_cctext import * +from .t_rslang import * diff --git a/rsconcept/backend/apps/rsform/tests/s_views/data/sample-rsform.trs b/rsconcept/backend/apps/rsform/tests/s_views/data/sample-rsform.trs new file mode 100644 index 00000000..fb49cdd8 Binary files /dev/null and b/rsconcept/backend/apps/rsform/tests/s_views/data/sample-rsform.trs differ diff --git a/rsconcept/backend/apps/rsform/tests/s_views/t_cctext.py b/rsconcept/backend/apps/rsform/tests/s_views/t_cctext.py new file mode 100644 index 00000000..33c91811 --- /dev/null +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_cctext.py @@ -0,0 +1,33 @@ +''' Testing views ''' +from cctext import split_grams + +from ..EndpointTester import EndpointTester, decl_endpoint + + +class TestNaturalLanguageViews(EndpointTester): + ''' Test natural language endpoints. ''' + + def _assert_tags(self, actual: str, expected: str): + self.assertEqual(set(split_grams(actual)), set(split_grams(expected))) + + + @decl_endpoint(endpoint='/api/cctext/parse', method='post') + def test_parse_text(self): + data = {'text': 'синим слонам'} + response = self.executeOK(data) + self._assert_tags(response.data['result'], 'datv,NOUN,plur,anim,masc') + + + @decl_endpoint(endpoint='/api/cctext/inflect', method='post') + def test_inflect(self): + data = {'text': 'синий слон', 'grams': 'plur,datv'} + response = self.executeOK(data) + self.assertEqual(response.data['result'], 'синим слонам') + + + @decl_endpoint(endpoint='/api/cctext/generate-lexeme', method='post') + def test_generate_lexeme(self): + data = {'text': 'синий слон'} + response = self.executeOK(data) + self.assertEqual(len(response.data['items']), 12) + self.assertEqual(response.data['items'][0]['text'], 'синий слон') diff --git a/rsconcept/backend/apps/rsform/tests/s_views/t_constituents.py b/rsconcept/backend/apps/rsform/tests/s_views/t_constituents.py new file mode 100644 index 00000000..a19cfc49 --- /dev/null +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_constituents.py @@ -0,0 +1,103 @@ +''' Testing API: Constituents. ''' +from apps.rsform.models import Constituenta, CstType, RSForm + +from ..EndpointTester import EndpointTester, decl_endpoint + + +class TestConstituentaAPI(EndpointTester): + ''' Testing Constituenta view. ''' + + def setUp(self): + super().setUp() + self.rsform_owned = RSForm.create(title='Test', alias='T1', owner=self.user) + self.rsform_unowned = RSForm.create(title='Test2', alias='T2') + self.cst1 = Constituenta.objects.create( + alias='X1', + cst_type=CstType.BASE, + schema=self.rsform_owned.item, + order=1, + convention='Test', + term_raw='Test1', + term_resolved='Test1R', + term_forms=[{'text': 'form1', 'tags': 'sing,datv'}]) + self.cst2 = Constituenta.objects.create( + alias='X2', + cst_type=CstType.BASE, + schema=self.rsform_unowned.item, + order=1, + convention='Test1', + term_raw='Test2', + term_resolved='Test2R' + ) + self.cst3 = Constituenta.objects.create( + alias='X3', + schema=self.rsform_owned.item, + order=2, + term_raw='Test3', + term_resolved='Test3', + definition_raw='Test1', + definition_resolved='Test2' + ) + self.invalid_cst = self.cst3.pk + 1337 + + + @decl_endpoint('/api/constituents/{item}', method='get') + def test_retrieve(self): + self.executeNotFound(item=self.invalid_cst) + response = self.executeOK(item=self.cst1.pk) + self.assertEqual(response.data['alias'], self.cst1.alias) + self.assertEqual(response.data['convention'], self.cst1.convention) + + + @decl_endpoint('/api/constituents/{item}', method='patch') + def test_partial_update(self): + data = {'convention': 'tt'} + self.executeForbidden(data, item=self.cst2.pk) + + self.logout() + self.executeForbidden(data, item=self.cst1.pk) + + self.login() + response = self.executeOK(data, item=self.cst1.pk) + self.cst1.refresh_from_db() + self.assertEqual(response.data['convention'], 'tt') + self.assertEqual(self.cst1.convention, 'tt') + + self.executeOK(data, item=self.cst1.pk) + + + @decl_endpoint('/api/constituents/{item}', method='patch') + def test_update_resolved_no_refs(self): + data = { + 'term_raw': 'New term', + 'definition_raw': 'New def' + } + response = self.executeOK(data, item=self.cst3.pk) + self.cst3.refresh_from_db() + self.assertEqual(response.data['term_resolved'], 'New term') + self.assertEqual(self.cst3.term_resolved, 'New term') + self.assertEqual(response.data['definition_resolved'], 'New def') + self.assertEqual(self.cst3.definition_resolved, 'New def') + + + @decl_endpoint('/api/constituents/{item}', method='patch') + def test_update_resolved_refs(self): + data = { + 'term_raw': '@{X1|nomn,sing}', + 'definition_raw': '@{X1|nomn,sing} @{X1|sing,datv}' + } + response = self.executeOK(data, item=self.cst3.pk) + self.cst3.refresh_from_db() + self.assertEqual(self.cst3.term_resolved, self.cst1.term_resolved) + self.assertEqual(response.data['term_resolved'], self.cst1.term_resolved) + self.assertEqual(self.cst3.definition_resolved, f'{self.cst1.term_resolved} form1') + self.assertEqual(response.data['definition_resolved'], f'{self.cst1.term_resolved} form1') + + + @decl_endpoint('/api/constituents/{item}', method='patch') + def test_readonly_cst_fields(self): + data = {'alias': 'X33', 'order': 10} + response = self.executeOK(data, item=self.cst1.pk) + self.assertEqual(response.data['alias'], 'X1') + self.assertEqual(response.data['alias'], self.cst1.alias) + self.assertEqual(response.data['order'], self.cst1.order) diff --git a/rsconcept/backend/apps/rsform/tests/s_views/t_library.py b/rsconcept/backend/apps/rsform/tests/s_views/t_library.py new file mode 100644 index 00000000..3c53d80e --- /dev/null +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_library.py @@ -0,0 +1,403 @@ +''' Testing API: Library. ''' +from rest_framework import status + +from apps.rsform.models import ( + AccessPolicy, + Editor, + LibraryItem, + LibraryItemType, + LibraryTemplate, + LocationHead, + RSForm, + Subscription +) + +from ..EndpointTester import EndpointTester, decl_endpoint +from ..testing_utils import response_contains + + +class TestLibraryViewset(EndpointTester): + ''' Testing Library view. ''' + + def setUp(self): + super().setUp() + self.owned = LibraryItem.objects.create( + item_type=LibraryItemType.RSFORM, + title='Test', + alias='T1', + owner=self.user + ) + self.schema = RSForm(self.owned) + self.unowned = LibraryItem.objects.create( + item_type=LibraryItemType.RSFORM, + title='Test2', + alias='T2' + ) + self.common = LibraryItem.objects.create( + item_type=LibraryItemType.RSFORM, + title='Test3', + alias='T3', + location=LocationHead.COMMON + ) + self.invalid_user = 1337 + self.user2.pk + self.invalid_item = 1337 + self.common.pk + + + @decl_endpoint('/api/library', method='post') + def test_create(self): + data = { + 'title': 'Title', + 'alias': 'alias', + } + self.executeBadData(data) + + data = { + 'item_type': LibraryItemType.OPERATIONS_SCHEMA, + 'title': 'Title', + 'alias': 'alias', + 'access_policy': AccessPolicy.PROTECTED, + 'visible': False, + 'read_only': True + } + response = self.executeCreated(data) + self.assertEqual(response.data['owner'], self.user.pk) + self.assertEqual(response.data['item_type'], data['item_type']) + self.assertEqual(response.data['title'], data['title']) + self.assertEqual(response.data['alias'], data['alias']) + self.assertEqual(response.data['access_policy'], data['access_policy']) + self.assertEqual(response.data['visible'], data['visible']) + self.assertEqual(response.data['read_only'], data['read_only']) + + self.logout() + data = {'title': 'Title2'} + self.executeForbidden(data) + + + @decl_endpoint('/api/library/{item}', method='patch') + def test_update(self): + data = {'id': self.unowned.pk, 'title': 'New Title'} + self.executeNotFound(data, item=self.invalid_item) + self.executeForbidden(data, item=self.unowned.pk) + + self.toggle_editor(self.unowned, True) + response = self.executeOK(data, item=self.unowned.pk) + self.assertEqual(response.data['title'], data['title']) + + self.unowned.access_policy = AccessPolicy.PRIVATE + self.unowned.save() + self.executeForbidden(data, item=self.unowned.pk) + + data = {'id': self.owned.pk, 'title': 'New Title'} + response = self.executeOK(data, item=self.owned.pk) + self.assertEqual(response.data['title'], data['title']) + self.assertEqual(response.data['alias'], self.owned.alias) + + data = { + 'id': self.owned.pk, + 'title': 'Another Title', + 'owner': self.user2.pk, + 'access_policy': AccessPolicy.PROTECTED, + 'location': LocationHead.LIBRARY + } + response = self.executeOK(data, item=self.owned.pk) + self.assertEqual(response.data['title'], data['title']) + self.assertEqual(response.data['owner'], self.owned.owner.pk) + self.assertEqual(response.data['access_policy'], self.owned.access_policy) + self.assertEqual(response.data['location'], self.owned.location) + self.assertNotEqual(response.data['location'], LocationHead.LIBRARY) + + + @decl_endpoint('/api/library/{item}/set-owner', method='patch') + def test_set_owner(self): + time_update = self.owned.time_update + + data = {'user': self.user.pk} + self.executeNotFound(data, item=self.invalid_item) + self.executeForbidden(data, item=self.unowned.pk) + self.executeOK(data, item=self.owned.pk) + self.owned.refresh_from_db() + self.assertEqual(self.owned.owner, self.user) + + data = {'user': self.user2.pk} + self.executeOK(data, item=self.owned.pk) + self.owned.refresh_from_db() + self.assertEqual(self.owned.owner, self.user2) + self.assertEqual(self.owned.time_update, time_update) + self.executeForbidden(data, item=self.owned.pk) + + self.toggle_admin(True) + data = {'user': self.user.pk} + self.executeOK(data, item=self.owned.pk) + self.owned.refresh_from_db() + self.assertEqual(self.owned.owner, self.user) + + @decl_endpoint('/api/library/{item}/set-access-policy', method='patch') + def test_set_access_policy(self): + time_update = self.owned.time_update + + data = {'access_policy': 'invalid'} + self.executeBadData(data, item=self.owned.pk) + + data = {'access_policy': AccessPolicy.PRIVATE} + self.executeNotFound(data, item=self.invalid_item) + self.executeForbidden(data, item=self.unowned.pk) + self.executeOK(data, item=self.owned.pk) + self.owned.refresh_from_db() + self.assertEqual(self.owned.access_policy, data['access_policy']) + + self.toggle_editor(self.unowned, True) + self.executeForbidden(data, item=self.unowned.pk) + + self.toggle_admin(True) + self.executeOK(data, item=self.unowned.pk) + self.unowned.refresh_from_db() + self.assertEqual(self.unowned.access_policy, data['access_policy']) + + @decl_endpoint('/api/library/{item}/set-location', method='patch') + def test_set_location(self): + time_update = self.owned.time_update + + data = {'location': 'invalid'} + self.executeBadData(data, item=self.owned.pk) + + data = {'location': '/U/temp'} + self.executeNotFound(data, item=self.invalid_item) + self.executeForbidden(data, item=self.unowned.pk) + self.executeOK(data, item=self.owned.pk) + self.owned.refresh_from_db() + self.assertEqual(self.owned.location, data['location']) + + data = {'location': LocationHead.LIBRARY} + self.executeForbidden(data, item=self.owned.pk) + + data = {'location': '/U/temp'} + self.toggle_editor(self.unowned, True) + self.executeForbidden(data, item=self.unowned.pk) + + self.toggle_admin(True) + data = {'location': LocationHead.LIBRARY} + self.executeOK(data, item=self.owned.pk) + self.owned.refresh_from_db() + self.assertEqual(self.owned.location, data['location']) + + self.executeOK(data, item=self.unowned.pk) + self.unowned.refresh_from_db() + self.assertEqual(self.unowned.location, data['location']) + + @decl_endpoint('/api/library/{item}/editors-add', method='patch') + def test_add_editor(self): + time_update = self.owned.time_update + + data = {'user': self.invalid_user} + self.executeBadData(data, item=self.owned.pk) + + data = {'user': self.user.pk} + self.executeNotFound(data, item=self.invalid_item) + self.executeForbidden(data, item=self.unowned.pk) + + self.executeOK(data, item=self.owned.pk) + self.owned.refresh_from_db() + self.assertEqual(self.owned.time_update, time_update) + self.assertEqual(self.owned.editors(), [self.user]) + + self.executeOK(data) + self.assertEqual(self.owned.editors(), [self.user]) + + data = {'user': self.user2.pk} + self.executeOK(data) + self.assertEqual(set(self.owned.editors()), set([self.user, self.user2])) + + + @decl_endpoint('/api/library/{item}/editors-remove', method='patch') + def test_remove_editor(self): + time_update = self.owned.time_update + + data = {'user': self.invalid_user} + self.executeBadData(data, item=self.owned.pk) + + data = {'user': self.user.pk} + self.executeNotFound(data, item=self.invalid_item) + self.executeForbidden(data, item=self.unowned.pk) + + self.executeOK(data, item=self.owned.pk) + self.owned.refresh_from_db() + self.assertEqual(self.owned.time_update, time_update) + self.assertEqual(self.owned.editors(), []) + + Editor.add(item=self.owned, user=self.user) + self.executeOK(data) + self.assertEqual(self.owned.editors(), []) + + Editor.add(item=self.owned, user=self.user) + Editor.add(item=self.owned, user=self.user2) + data = {'user': self.user2.pk} + self.executeOK(data) + self.assertEqual(self.owned.editors(), [self.user]) + + + @decl_endpoint('/api/library/{item}/editors-set', method='patch') + def test_set_editors(self): + time_update = self.owned.time_update + + data = {'users': [self.invalid_user]} + self.executeBadData(data, item=self.owned.pk) + + data = {'users': [self.user.pk]} + self.executeNotFound(data, item=self.invalid_item) + self.executeForbidden(data, item=self.unowned.pk) + + self.executeOK(data, item=self.owned.pk) + self.owned.refresh_from_db() + self.assertEqual(self.owned.time_update, time_update) + self.assertEqual(self.owned.editors(), [self.user]) + + self.executeOK(data) + self.assertEqual(self.owned.editors(), [self.user]) + + data = {'users': [self.user2.pk]} + self.executeOK(data) + self.assertEqual(self.owned.editors(), [self.user2]) + + data = {'users': []} + self.executeOK(data) + self.assertEqual(self.owned.editors(), []) + + data = {'users': [self.user2.pk, self.user.pk]} + self.executeOK(data) + self.assertEqual(set(self.owned.editors()), set([self.user2, self.user])) + + + @decl_endpoint('/api/library/{item}', method='delete') + def test_destroy(self): + response = self.execute(item=self.owned.pk) + self.assertTrue(response.status_code in [status.HTTP_202_ACCEPTED, status.HTTP_204_NO_CONTENT]) + + self.executeForbidden(item=self.unowned.pk) + self.toggle_admin(True) + response = self.execute(item=self.unowned.pk) + self.assertTrue(response.status_code in [status.HTTP_202_ACCEPTED, status.HTTP_204_NO_CONTENT]) + + + @decl_endpoint('/api/library/active', method='get') + def test_retrieve_common(self): + response = self.executeOK() + self.assertTrue(response_contains(response, self.common)) + self.assertFalse(response_contains(response, self.unowned)) + self.assertTrue(response_contains(response, self.owned)) + + self.logout() + response = self.executeOK() + self.assertTrue(response_contains(response, self.common)) + self.assertFalse(response_contains(response, self.unowned)) + self.assertFalse(response_contains(response, self.owned)) + + @decl_endpoint('/api/library', method='get') + def test_library_get(self): + non_schema = LibraryItem.objects.create( + item_type=LibraryItemType.OPERATIONS_SCHEMA, + title='Test4' + ) + response = self.executeOK() + self.assertTrue(response_contains(response, non_schema)) + self.assertTrue(response_contains(response, self.unowned)) + self.assertTrue(response_contains(response, self.owned)) + + @decl_endpoint('/api/library/all', method='get') + def test_retrieve_all(self): + self.toggle_admin(False) + self.executeForbidden() + self.toggle_admin(True) + response = self.executeOK() + self.assertTrue(response_contains(response, self.common)) + self.assertTrue(response_contains(response, self.unowned)) + self.assertTrue(response_contains(response, self.owned)) + + self.logout() + self.executeForbidden() + + + @decl_endpoint('/api/library/active', method='get') + def test_retrieve_subscribed(self): + response = self.executeOK() + self.assertFalse(response_contains(response, self.unowned)) + + Subscription.subscribe(user=self.user, item=self.unowned) + Subscription.subscribe(user=self.user2, item=self.unowned) + Subscription.subscribe(user=self.user2, item=self.owned) + + response = self.executeOK() + self.assertTrue(response_contains(response, self.unowned)) + self.assertEqual(len(response.data), 3) + + + @decl_endpoint('/api/library/{item}/subscribe', method='post') + def test_subscriptions(self): + self.executeNotFound(item=self.invalid_item) + response = self.client.delete(f'/api/library/{self.unowned.pk}/unsubscribe') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertFalse(self.user in self.unowned.subscribers()) + + response = self.executeOK(item=self.unowned.pk) + self.assertTrue(self.user in self.unowned.subscribers()) + + response = self.executeOK(item=self.unowned.pk) + self.assertTrue(self.user in self.unowned.subscribers()) + + response = self.client.delete(f'/api/library/{self.unowned.pk}/unsubscribe') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertFalse(self.user in self.unowned.subscribers()) + + + @decl_endpoint('/api/library/templates', method='get') + def test_retrieve_templates(self): + response = self.executeOK() + self.assertFalse(response_contains(response, self.common)) + self.assertFalse(response_contains(response, self.unowned)) + self.assertFalse(response_contains(response, self.owned)) + + LibraryTemplate.objects.create(lib_source=self.unowned) + response = self.executeOK() + self.assertFalse(response_contains(response, self.common)) + self.assertTrue(response_contains(response, self.unowned)) + self.assertFalse(response_contains(response, self.owned)) + + + @decl_endpoint('/api/library/{item}/clone', method='post') + def test_clone_rsform(self): + x12 = self.schema.insert_new( + alias='X12', + term_raw='человек', + term_resolved='человек' + ) + d2 = self.schema.insert_new( + alias='D2', + term_raw='@{X12|plur}', + term_resolved='люди' + ) + + data = {'title': 'Title1337'} + self.executeNotFound(data, item=self.invalid_item) + self.executeCreated(data, item=self.unowned.pk) + + data = {'title': 'Title1338'} + response = self.executeCreated(data, item=self.owned.pk) + self.assertEqual(response.data['title'], data['title']) + self.assertEqual(len(response.data['items']), 2) + self.assertEqual(response.data['items'][0]['alias'], x12.alias) + self.assertEqual(response.data['items'][0]['term_raw'], x12.term_raw) + self.assertEqual(response.data['items'][0]['term_resolved'], x12.term_resolved) + self.assertEqual(response.data['items'][1]['term_raw'], d2.term_raw) + self.assertEqual(response.data['items'][1]['term_resolved'], d2.term_resolved) + + data = {'title': 'Title1340', 'items': []} + response = self.executeCreated(data, item=self.owned.pk) + self.assertEqual(response.data['title'], data['title']) + self.assertEqual(len(response.data['items']), 0) + + data = {'title': 'Title1341', 'items': [x12.pk]} + response = self.executeCreated(data, item=self.owned.pk) + self.assertEqual(response.data['title'], data['title']) + self.assertEqual(len(response.data['items']), 1) + self.assertEqual(response.data['items'][0]['alias'], x12.alias) + self.assertEqual(response.data['items'][0]['term_raw'], x12.term_raw) + self.assertEqual(response.data['items'][0]['term_resolved'], x12.term_resolved) diff --git a/rsconcept/backend/apps/rsform/tests/s_views/t_operations.py b/rsconcept/backend/apps/rsform/tests/s_views/t_operations.py new file mode 100644 index 00000000..0cd9867f --- /dev/null +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_operations.py @@ -0,0 +1,83 @@ +''' Testing API: Operations. ''' +from apps.rsform.models import Constituenta, CstType, RSForm + +from ..EndpointTester import EndpointTester, decl_endpoint + + +class TestInlineSynthesis(EndpointTester): + ''' Testing Operations endpoints. ''' + + + @decl_endpoint('/api/operations/inline-synthesis', method='patch') + def setUp(self): + super().setUp() + self.schema1 = RSForm.create(title='Test1', alias='T1', owner=self.user) + self.schema2 = RSForm.create(title='Test2', alias='T2', owner=self.user) + self.unowned = RSForm.create(title='Test3', alias='T3') + + + def test_inline_synthesis_inputs(self): + invalid_id = 1338 + data = { + 'receiver': self.unowned.item.pk, + 'source': self.schema1.item.pk, + 'items': [], + 'substitutions': [] + } + self.executeForbidden(data) + + data['receiver'] = invalid_id + self.executeBadData(data) + + data['receiver'] = self.schema1.item.pk + data['source'] = invalid_id + self.executeBadData(data) + + data['source'] = self.schema1.item.pk + self.executeOK(data) + + data['items'] = [invalid_id] + self.executeBadData(data) + + + def test_inline_synthesis(self): + ks1_x1 = self.schema1.insert_new('X1', term_raw='KS1X1') # -> delete + ks1_x2 = self.schema1.insert_new('X2', term_raw='KS1X2') # -> X2 + ks1_s1 = self.schema1.insert_new('S1', definition_formal='X2', term_raw='KS1S1') # -> S1 + ks1_d1 = self.schema1.insert_new('D1', definition_formal=r'S1\X1\X2') # -> D1 + ks2_x1 = self.schema2.insert_new('X1', term_raw='KS2X1') # -> delete + ks2_x2 = self.schema2.insert_new('X2', term_raw='KS2X2') # -> X4 + ks2_s1 = self.schema2.insert_new('S1', definition_formal='X2×X2', term_raw='KS2S1') # -> S2 + ks2_d1 = self.schema2.insert_new('D1', definition_formal=r'S1\X1\X2') # -> D2 + ks2_a1 = self.schema2.insert_new('A1', definition_formal='1=1') # -> not included in items + + data = { + 'receiver': self.schema1.item.pk, + 'source': self.schema2.item.pk, + 'items': [ks2_x1.pk, ks2_x2.pk, ks2_s1.pk, ks2_d1.pk], + 'substitutions': [ + { + 'original': ks1_x1.pk, + 'substitution': ks2_s1.pk, + 'transfer_term': False + }, + { + 'original': ks2_x1.pk, + 'substitution': ks1_s1.pk, + 'transfer_term': True + } + ] + } + response = self.executeOK(data) + result = {item['alias']: item for item in response.data['items']} + self.assertEqual(len(result), 6) + self.assertEqual(result['X2']['term_raw'], ks1_x2.term_raw) + self.assertEqual(result['X2']['order'], 1) + self.assertEqual(result['X4']['term_raw'], ks2_x2.term_raw) + self.assertEqual(result['X4']['order'], 2) + self.assertEqual(result['S1']['term_raw'], ks2_x1.term_raw) + self.assertEqual(result['S2']['term_raw'], ks2_s1.term_raw) + self.assertEqual(result['S1']['definition_formal'], 'X2') + self.assertEqual(result['S2']['definition_formal'], 'X4×X4') + self.assertEqual(result['D1']['definition_formal'], r'S1\S2\X2') + self.assertEqual(result['D2']['definition_formal'], r'S2\S1\X4') diff --git a/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py b/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py new file mode 100644 index 00000000..dc735490 --- /dev/null +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py @@ -0,0 +1,518 @@ +''' Testing API: RSForms. ''' +import io +import os +from zipfile import ZipFile + +from cctext import ReferenceType +from rest_framework import status + +from apps.rsform.models import ( + AccessPolicy, + Constituenta, + CstType, + LibraryItem, + LibraryItemType, + LocationHead, + RSForm +) + +from ..EndpointTester import EndpointTester, decl_endpoint +from ..testing_utils import response_contains + + +class TestRSFormViewset(EndpointTester): + ''' Testing RSForm view. ''' + + def setUp(self): + super().setUp() + self.owned = RSForm.create(title='Test', alias='T1', owner=self.user) + self.owned_id = self.owned.item.pk + self.unowned = RSForm.create(title='Test2', alias='T2') + self.unowned_id = self.unowned.item.pk + self.private = RSForm.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE) + self.private_id = self.private.item.pk + + + @decl_endpoint('/api/rsforms/create-detailed', method='post') + def test_create_rsform_file(self): + work_dir = os.path.dirname(os.path.abspath(__file__)) + data = { + 'title': 'Test123', + 'comment': '123', + 'alias': 'ks1', + 'location': LocationHead.PROJECTS, + 'access_policy': AccessPolicy.PROTECTED, + 'visible': False + } + self.executeBadData(data) + + with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file: + data['file'] = file + response = self.client.post(self.endpoint, data=data, format='multipart') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(response.data['owner'], self.user.pk) + self.assertEqual(response.data['title'], data['title']) + self.assertEqual(response.data['alias'], data['alias']) + self.assertEqual(response.data['comment'], data['comment']) + + + @decl_endpoint('/api/rsforms', method='get') + def test_list_rsforms(self): + non_schema = LibraryItem.objects.create( + item_type=LibraryItemType.OPERATIONS_SCHEMA, + title='Test3' + ) + response = self.executeOK() + self.assertFalse(response_contains(response, non_schema)) + self.assertTrue(response_contains(response, self.unowned.item)) + self.assertTrue(response_contains(response, self.owned.item)) + + + @decl_endpoint('/api/rsforms/{item}/contents', method='get') + def test_contents(self): + response = self.executeOK(item=self.owned_id) + self.assertEqual(response.data['owner'], self.owned.item.owner.pk) + self.assertEqual(response.data['title'], self.owned.item.title) + self.assertEqual(response.data['alias'], self.owned.item.alias) + self.assertEqual(response.data['location'], self.owned.item.location) + self.assertEqual(response.data['access_policy'], self.owned.item.access_policy) + self.assertEqual(response.data['visible'], self.owned.item.visible) + + + @decl_endpoint('/api/rsforms/{item}/details', method='get') + def test_details(self): + x1 = self.owned.insert_new( + alias='X1', + term_raw='человек', + term_resolved='человек' + ) + x2 = self.owned.insert_new( + alias='X2', + term_raw='@{X1|plur}', + term_resolved='люди' + ) + + response = self.executeOK(item=self.owned_id) + self.assertEqual(response.data['owner'], self.owned.item.owner.pk) + self.assertEqual(response.data['title'], self.owned.item.title) + self.assertEqual(response.data['alias'], self.owned.item.alias) + self.assertEqual(response.data['location'], self.owned.item.location) + self.assertEqual(response.data['access_policy'], self.owned.item.access_policy) + self.assertEqual(response.data['visible'], self.owned.item.visible) + + self.assertEqual(len(response.data['items']), 2) + self.assertEqual(response.data['items'][0]['id'], x1.pk) + self.assertEqual(response.data['items'][0]['parse']['status'], 'verified') + self.assertEqual(response.data['items'][0]['term_raw'], x1.term_raw) + self.assertEqual(response.data['items'][0]['term_resolved'], x1.term_resolved) + self.assertEqual(response.data['items'][1]['id'], x2.pk) + self.assertEqual(response.data['items'][1]['term_raw'], x2.term_raw) + self.assertEqual(response.data['items'][1]['term_resolved'], x2.term_resolved) + self.assertEqual(response.data['subscribers'], [self.user.pk]) + self.assertEqual(response.data['editors'], []) + + self.executeOK(item=self.unowned_id) + self.executeForbidden(item=self.private_id) + + self.logout() + self.executeOK(item=self.owned_id) + self.executeOK(item=self.unowned_id) + self.executeForbidden(item=self.private_id) + + + @decl_endpoint('/api/rsforms/{item}/check', method='post') + def test_check(self): + self.owned.insert_new('X1') + data = {'expression': 'X1=X1'} + response = self.executeOK(data, item=self.owned_id) + self.assertEqual(response.data['parseResult'], True) + self.assertEqual(response.data['syntax'], 'math') + self.assertEqual(response.data['astText'], '[=[X1][X1]]') + self.assertEqual(response.data['typification'], 'LOGIC') + self.assertEqual(response.data['valueClass'], 'value') + + self.executeOK(data, item=self.unowned_id) + + + @decl_endpoint('/api/rsforms/{item}/resolve', method='post') + def test_resolve(self): + x1 = self.owned.insert_new( + alias='X1', + term_resolved='синий слон' + ) + + data = {'text': '@{1|редкий} @{X1|plur,datv}'} + response = self.executeOK(data, item=self.owned_id) + self.assertEqual(response.data['input'], '@{1|редкий} @{X1|plur,datv}') + self.assertEqual(response.data['output'], 'редким синим слонам') + self.assertEqual(len(response.data['refs']), 2) + self.assertEqual(response.data['refs'][0]['type'], ReferenceType.syntactic.value) + self.assertEqual(response.data['refs'][0]['resolved'], 'редким') + self.assertEqual(response.data['refs'][0]['data']['offset'], 1) + self.assertEqual(response.data['refs'][0]['data']['nominal'], 'редкий') + self.assertEqual(response.data['refs'][0]['pos_input']['start'], 0) + self.assertEqual(response.data['refs'][0]['pos_input']['finish'], 11) + self.assertEqual(response.data['refs'][0]['pos_output']['start'], 0) + self.assertEqual(response.data['refs'][0]['pos_output']['finish'], 6) + self.assertEqual(response.data['refs'][1]['type'], ReferenceType.entity.value) + self.assertEqual(response.data['refs'][1]['resolved'], 'синим слонам') + self.assertEqual(response.data['refs'][1]['data']['entity'], 'X1') + self.assertEqual(response.data['refs'][1]['data']['form'], 'plur,datv') + self.assertEqual(response.data['refs'][1]['pos_input']['start'], 12) + self.assertEqual(response.data['refs'][1]['pos_input']['finish'], 27) + self.assertEqual(response.data['refs'][1]['pos_output']['start'], 7) + self.assertEqual(response.data['refs'][1]['pos_output']['finish'], 19) + + + @decl_endpoint('/api/rsforms/import-trs', method='post') + def test_import_trs(self): + work_dir = os.path.dirname(os.path.abspath(__file__)) + with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file: + data = {'file': file} + response = self.client.post(self.endpoint, data=data, format='multipart') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(response.data['owner'], self.user.pk) + self.assertTrue(response.data['title'] != '') + + + @decl_endpoint('/api/rsforms/{item}/export-trs', method='get') + def test_export_trs(self): + schema = RSForm.create(title='Test') + schema.insert_new('X1') + response = self.executeOK(item=schema.item.pk) + self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename=Schema.trs') + with io.BytesIO(response.content) as stream: + with ZipFile(stream, 'r') as zipped_file: + self.assertIsNone(zipped_file.testzip()) + self.assertIn('document.json', zipped_file.namelist()) + + + @decl_endpoint('/api/rsforms/{item}/cst-create', method='post') + def test_create_constituenta(self): + data = {'alias': 'X3', 'cst_type': CstType.BASE} + self.executeForbidden(data, item=self.unowned_id) + + self.owned.insert_new('X1') + x2 = self.owned.insert_new('X2') + + response = self.executeCreated(data, item=self.owned_id) + self.assertEqual(response.data['new_cst']['alias'], 'X3') + x3 = Constituenta.objects.get(alias=response.data['new_cst']['alias']) + self.assertEqual(x3.order, 3) + + data = { + 'alias': 'X4', + 'cst_type': CstType.BASE, + 'insert_after': x2.pk, + 'term_raw': 'test', + 'term_forms': [{'text': 'form1', 'tags': 'sing,datv'}] + } + response = self.executeCreated(data, item=self.owned_id) + self.assertEqual(response.data['new_cst']['alias'], data['alias']) + x4 = Constituenta.objects.get(alias=response.data['new_cst']['alias']) + self.assertEqual(x4.order, 3) + self.assertEqual(x4.term_raw, data['term_raw']) + self.assertEqual(x4.term_forms, data['term_forms']) + + + @decl_endpoint('/api/rsforms/{item}/cst-rename', method='patch') + def test_rename_constituenta(self): + x1 = self.owned.insert_new( + alias='X1', + convention='Test', + term_raw='Test1', + term_resolved='Test1', + term_forms=[{'text': 'form1', 'tags': 'sing,datv'}] + ) + x2_2 = self.unowned.insert_new('X2') + x3 = self.owned.insert_new( + alias='X3', + term_raw='Test3', + term_resolved='Test3', + definition_raw='Test1', + definition_resolved='Test2' + ) + + data = {'target': x2_2.pk, 'alias': 'D2', 'cst_type': CstType.TERM} + self.executeForbidden(data, item=self.unowned_id) + self.executeBadData(data, item=self.owned_id) + + data = {'target': x1.pk, 'alias': x1.alias, 'cst_type': CstType.TERM} + self.executeBadData(data, item=self.owned_id) + + data = {'target': x1.pk, 'alias': x3.alias} + self.executeBadData(data, item=self.owned_id) + + d1 = self.owned.insert_new( + alias='D1', + term_raw='@{X1|plur}', + definition_formal='X1' + ) + self.assertEqual(x1.order, 1) + self.assertEqual(x1.alias, 'X1') + self.assertEqual(x1.cst_type, CstType.BASE) + + data = {'target': x1.pk, 'alias': 'D2', 'cst_type': CstType.TERM} + response = self.executeOK(data, item=self.owned_id) + self.assertEqual(response.data['new_cst']['alias'], 'D2') + self.assertEqual(response.data['new_cst']['cst_type'], CstType.TERM) + d1.refresh_from_db() + x1.refresh_from_db() + self.assertEqual(d1.term_resolved, '') + self.assertEqual(d1.term_raw, '@{D2|plur}') + self.assertEqual(x1.order, 1) + self.assertEqual(x1.alias, 'D2') + self.assertEqual(x1.cst_type, CstType.TERM) + + + @decl_endpoint('/api/rsforms/{item}/cst-substitute', method='patch') + def test_substitute_single(self): + x1 = self.owned.insert_new( + alias='X1', + term_raw='Test1', + term_resolved='Test1', + term_forms=[{'text': 'form1', 'tags': 'sing,datv'}] + ) + x2 = self.owned.insert_new( + alias='X2', + term_raw='Test2' + ) + unowned = self.unowned.insert_new('X2') + + data = {'substitutions': [{'original': x1.pk, 'substitution': unowned.pk, 'transfer_term': True}]} + self.executeForbidden(data, item=self.unowned_id) + self.executeBadData(data, item=self.owned_id) + + data = {'substitutions': [{'original': unowned.pk, 'substitution': x1.pk, 'transfer_term': True}]} + self.executeBadData(data, item=self.owned_id) + + data = {'substitutions': [{'original': x1.pk, 'substitution': x1.pk, 'transfer_term': True}]} + self.executeBadData(data, item=self.owned_id) + + d1 = self.owned.insert_new( + alias='D1', + term_raw='@{X2|sing,datv}', + definition_formal='X1' + ) + data = {'substitutions': [{'original': x1.pk, 'substitution': x2.pk, 'transfer_term': True}]} + response = self.executeOK(data, item=self.owned_id) + d1.refresh_from_db() + x2.refresh_from_db() + self.assertEqual(x2.term_raw, 'Test1') + self.assertEqual(d1.term_resolved, 'form1') + self.assertEqual(d1.definition_formal, 'X2') + + @decl_endpoint('/api/rsforms/{item}/cst-substitute', method='patch') + def test_substitute_multiple(self): + self.set_params(item=self.owned_id) + x1 = self.owned.insert_new('X1') + x2 = self.owned.insert_new('X2') + d1 = self.owned.insert_new('D1') + d2 = self.owned.insert_new('D2') + d3 = self.owned.insert_new( + alias='D3', + definition_formal=r'X1 \ X2' + ) + + data = {'substitutions': []} + self.executeBadData(data) + + data = {'substitutions': [ + { + 'original': x1.pk, + 'substitution': d1.pk, + 'transfer_term': True + }, + { + 'original': x1.pk, + 'substitution': d2.pk, + 'transfer_term': True + } + ]} + self.executeBadData(data) + + data = {'substitutions': [ + { + 'original': x1.pk, + 'substitution': d1.pk, + 'transfer_term': True + }, + { + 'original': x2.pk, + 'substitution': d2.pk, + 'transfer_term': True + } + ]} + response = self.executeOK(data, item=self.owned_id) + d3.refresh_from_db() + self.assertEqual(d3.definition_formal, r'D1 \ D2') + + + @decl_endpoint('/api/rsforms/{item}/cst-create', method='post') + def test_create_constituenta_data(self): + data = { + 'alias': 'X3', + 'cst_type': CstType.BASE, + 'convention': '1', + 'term_raw': '2', + 'definition_formal': '3', + 'definition_raw': '4' + } + response = self.executeCreated(data, item=self.owned_id) + self.assertEqual(response.data['new_cst']['alias'], 'X3') + self.assertEqual(response.data['new_cst']['cst_type'], CstType.BASE) + self.assertEqual(response.data['new_cst']['convention'], '1') + self.assertEqual(response.data['new_cst']['term_raw'], '2') + self.assertEqual(response.data['new_cst']['term_resolved'], '2') + self.assertEqual(response.data['new_cst']['definition_formal'], '3') + self.assertEqual(response.data['new_cst']['definition_raw'], '4') + self.assertEqual(response.data['new_cst']['definition_resolved'], '4') + + + @decl_endpoint('/api/rsforms/{item}/cst-delete-multiple', method='patch') + def test_delete_constituenta(self): + self.set_params(item=self.owned_id) + + data = {'items': [1337]} + self.executeBadData(data) + + x1 = self.owned.insert_new('X1') + x2 = self.owned.insert_new('X2') + + data = {'items': [x1.pk]} + response = self.executeOK(data) + x2.refresh_from_db() + self.owned.item.refresh_from_db() + self.assertEqual(len(response.data['items']), 1) + self.assertEqual(self.owned.constituents().count(), 1) + self.assertEqual(x2.alias, 'X2') + self.assertEqual(x2.order, 1) + + x3 = self.unowned.insert_new('X1') + data = {'items': [x3.pk]} + self.executeBadData(data, item=self.owned_id) + + + @decl_endpoint('/api/rsforms/{item}/cst-moveto', method='patch') + def test_move_constituenta(self): + self.set_params(item=self.owned_id) + + data = {'items': [1337], 'move_to': 1} + self.executeBadData(data) + + x1 = self.owned.insert_new('X1') + x2 = self.owned.insert_new('X2') + + data = {'items': [x2.pk], 'move_to': 1} + response = self.executeOK(data) + x1.refresh_from_db() + x2.refresh_from_db() + self.assertEqual(response.data['id'], self.owned_id) + self.assertEqual(x1.order, 2) + self.assertEqual(x2.order, 1) + + x3 = self.unowned.insert_new('X1') + data = {'items': [x3.pk], 'move_to': 1} + self.executeBadData(data) + + + @decl_endpoint('/api/rsforms/{item}/reset-aliases', method='patch') + def test_reset_aliases(self): + self.set_params(item=self.owned_id) + + response = self.executeOK() + self.assertEqual(response.data['id'], self.owned_id) + + x2 = self.owned.insert_new('X2') + x1 = self.owned.insert_new('X1') + d11 = self.owned.insert_new('D11') + + response = self.executeOK() + x1.refresh_from_db() + x2.refresh_from_db() + d11.refresh_from_db() + self.assertEqual(x2.order, 1) + self.assertEqual(x2.alias, 'X1') + self.assertEqual(x1.order, 2) + self.assertEqual(x1.alias, 'X2') + self.assertEqual(d11.order, 3) + self.assertEqual(d11.alias, 'D1') + + self.executeOK() + + + @decl_endpoint('/api/rsforms/{item}/load-trs', method='patch') + def test_load_trs(self): + self.set_params(item=self.owned_id) + self.owned.item.title = 'Test11' + self.owned.item.save() + x1 = self.owned.insert_new('X1') + work_dir = os.path.dirname(os.path.abspath(__file__)) + with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file: + data = {'file': file, 'load_metadata': False} + response = self.client.patch(self.endpoint, data=data, format='multipart') + self.owned.item.refresh_from_db() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(self.owned.item.title, 'Test11') + self.assertEqual(len(response.data['items']), 25) + self.assertEqual(self.owned.constituents().count(), 25) + self.assertFalse(Constituenta.objects.filter(pk=x1.pk).exists()) + + + @decl_endpoint('/api/rsforms/{item}/cst-produce-structure', method='patch') + def test_produce_structure(self): + self.set_params(item=self.owned_id) + x1 = self.owned.insert_new('X1') + s1 = self.owned.insert_new( + alias='S1', + definition_formal='ℬ(X1×X1)' + ) + s2 = self.owned.insert_new( + alias='S2', + definition_formal='invalid' + ) + s3 = self.owned.insert_new( + alias='S3', + definition_formal='X1×(X1×ℬℬ(X1))×ℬ(X1×X1)' + ) + a1 = self.owned.insert_new( + alias='A1', + definition_formal='1=1' + ) + f1 = self.owned.insert_new( + alias='F10', + definition_formal='[α∈X1, β∈X1] Fi1[{α,β}](S1)' + ) + invalid_id = f1.pk + 1337 + + self.executeBadData({'target': invalid_id}) + self.executeBadData({'target': x1.pk}) + self.executeBadData({'target': s2.pk}) + + # Testing simple structure + response = self.executeOK(data={'target': s1.pk}) + result = response.data['schema'] + items = [item for item in result['items'] if item['id'] in response.data['cst_list']] + self.assertEqual(len(items), 2) + self.assertEqual(items[0]['order'], s1.order + 1) + self.assertEqual(items[0]['definition_formal'], 'Pr1(S1)') + self.assertEqual(items[1]['order'], s1.order + 2) + self.assertEqual(items[1]['definition_formal'], 'Pr2(S1)') + + # Testing complex structure + s3.refresh_from_db() + response = self.executeOK(data={'target': s3.pk}) + result = response.data['schema'] + items = [item for item in result['items'] if item['id'] in response.data['cst_list']] + self.assertEqual(len(items), 8) + self.assertEqual(items[0]['order'], s3.order + 1) + self.assertEqual(items[0]['definition_formal'], 'pr1(S3)') + + # Testing function + f1.refresh_from_db() + response = self.executeOK(data={'target': f1.pk}) + result = response.data['schema'] + items = [item for item in result['items'] if item['id'] in response.data['cst_list']] + self.assertEqual(len(items), 2) + self.assertEqual(items[0]['order'], f1.order + 1) + self.assertEqual(items[0]['definition_formal'], '[α∈X1, β∈X1] Pr1(F10[α,β])') diff --git a/rsconcept/backend/apps/rsform/tests/s_views/t_rslang.py b/rsconcept/backend/apps/rsform/tests/s_views/t_rslang.py new file mode 100644 index 00000000..910e852e --- /dev/null +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_rslang.py @@ -0,0 +1,37 @@ +''' Testing views ''' +from ..EndpointTester import EndpointTester, decl_endpoint + + +class TestRSLanguageViews(EndpointTester): + ''' Test RS language endpoints. ''' + + @decl_endpoint('/api/rslang/to-ascii', method='post') + def test_convert_to_ascii(self): + data = {'data': '1=1'} + self.executeBadData(data) + + data = {'expression': '1=1'} + response = self.executeOK(data) + self.assertEqual(response.data['result'], r'1 \eq 1') + + + @decl_endpoint('/api/rslang/to-math', method='post') + def test_convert_to_math(self): + data = {'data': r'1 \eq 1'} + self.executeBadData(data) + + data = {'expression': r'1 \eq 1'} + response = self.executeOK(data) + self.assertEqual(response.data['result'], r'1=1') + + + @decl_endpoint('/api/rslang/parse-expression', method='post') + def test_parse_expression(self): + data = {'data': r'1=1'} + self.executeBadData(data) + + data = {'expression': r'1=1'} + response = self.executeOK(data) + self.assertEqual(response.data['parseResult'], True) + self.assertEqual(response.data['syntax'], 'math') + self.assertEqual(response.data['astText'], '[=[1][1]]') diff --git a/rsconcept/backend/apps/rsform/tests/s_views/t_versions.py b/rsconcept/backend/apps/rsform/tests/s_views/t_versions.py new file mode 100644 index 00000000..d2b273a7 --- /dev/null +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_versions.py @@ -0,0 +1,174 @@ +''' Testing API: Versions. ''' +import io +from sys import version +from typing import cast +from zipfile import ZipFile + +from rest_framework import status + +from apps.rsform.models import Constituenta, RSForm + +from ..EndpointTester import EndpointTester, decl_endpoint + + +class TestVersionViews(EndpointTester): + ''' Testing versioning endpoints. ''' + + def setUp(self): + super().setUp() + self.owned = RSForm.create(title='Test', alias='T1', owner=self.user).item + self.schema = RSForm(self.owned) + self.unowned = RSForm.create(title='Test2', alias='T2').item + self.x1 = self.schema.insert_new( + alias='X1', + convention='testStart' + ) + + + @decl_endpoint('/api/rsforms/{schema}/versions/create', method='post') + def test_create_version(self): + invalid_data = {'description': 'test'} + invalid_id = 1338 + data = {'version': '1.0.0', 'description': 'test'} + + self.executeNotFound(data, schema=invalid_id) + self.executeForbidden(data, schema=self.unowned.pk) + self.executeBadData(invalid_data, schema=self.owned.pk) + + response = self.executeCreated(data, schema=self.owned.pk) + self.assertTrue('version' in response.data) + self.assertTrue('schema' in response.data) + self.assertTrue(response.data['version'] in [v['id'] for v in response.data['schema']['versions']]) + + + @decl_endpoint('/api/rsforms/{schema}/versions/{version}', method='get') + def test_retrieve_version(self): + version_id = self._create_version({'version': '1.0.0', 'description': 'test'}) + invalid_id = version_id + 1337 + + self.executeNotFound(schema=invalid_id, version=invalid_id) + self.executeNotFound(schema=self.owned.pk, version=invalid_id) + self.executeNotFound(schema=invalid_id, version=version_id) + self.executeNotFound(schema=self.unowned.pk, version=version_id) + + self.owned.alias = 'NewName' + self.owned.save() + self.x1.alias = 'X33' + self.x1.save() + + response = self.executeOK(schema=self.owned.pk, version=version_id) + self.assertNotEqual(response.data['alias'], self.owned.alias) + self.assertNotEqual(response.data['items'][0]['alias'], self.x1.alias) + self.assertEqual(response.data['version'], version_id) + + + @decl_endpoint('/api/versions/{version}', method='get') + def test_access_version(self): + data = {'version': '1.0.0', 'description': 'test'} + version_id = self._create_version(data) + invalid_id = version_id + 1337 + + self.executeNotFound(version=invalid_id) + + self.set_params(version=version_id) + self.logout() + response = self.executeOK() + self.assertEqual(response.data['version'], data['version']) + self.assertEqual(response.data['description'], data['description']) + self.assertEqual(response.data['item'], self.owned.pk) + + data = {'version': '1.2.0', 'description': 'test1'} + self.method = 'patch' + self.executeForbidden(data) + + self.method = 'delete' + self.executeForbidden() + + self.client.force_authenticate(user=self.user) + self.method = 'patch' + self.executeOK(data) + response = self.get() + self.assertEqual(response.data['version'], data['version']) + self.assertEqual(response.data['description'], data['description']) + + response = self.delete() + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + response = self.get() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + + @decl_endpoint('/api/rsforms/{schema}/versions/{version}', method='get') + def test_retrieve_version_details(self): + a1 = Constituenta.objects.create( + schema=self.owned, + alias='A1', + cst_type='axiom', + definition_formal='X1=X1', + order=2 + ) + version_id = self._create_version({'version': '1.0.0', 'description': 'test'}) + a1.definition_formal = 'X1=X2' + a1.save() + + response = self.executeOK(schema=self.owned.pk, version=version_id) + loaded_a1 = response.data['items'][1] + self.assertEqual(loaded_a1['definition_formal'], 'X1=X1') + self.assertEqual(loaded_a1['parse']['status'], 'verified') + + + @decl_endpoint('/api/versions/{version}/export-file', method='get') + def test_export_version(self): + invalid_id = 1338 + self.executeNotFound(version=invalid_id) + + version_id = self._create_version({'version': '1.0.0', 'description': 'test'}) + response = self.executeOK(version=version_id) + self.assertEqual( + response.headers['Content-Disposition'], + f'attachment; filename={self.owned.alias}.trs' + ) + with io.BytesIO(response.content) as stream: + with ZipFile(stream, 'r') as zipped_file: + self.assertIsNone(zipped_file.testzip()) + self.assertIn('document.json', zipped_file.namelist()) + + + @decl_endpoint('/api/versions/{version}/restore', method='patch') + def test_restore_version(self): + x1 = self.x1 + x2 = self.schema.insert_new('X2') + d1 = self.schema.insert_new('D1', term_raw='TestTerm') + data = {'version': '1.0.0', 'description': 'test'} + version_id = self._create_version(data) + invalid_id = version_id + 1337 + + d1.delete() + x3 = self.schema.insert_new('X3') + x1.order = x3.order + x1.convention = 'Test2' + x1.term_raw = 'Test' + x1.save() + x3.order = 1 + x3.save() + + self.executeNotFound(version=invalid_id) + + response = self.executeOK(version=version_id) + x1.refresh_from_db() + x2.refresh_from_db() + self.assertEqual(len(response.data['items']), 3) + self.assertEqual(x1.order, 1) + self.assertEqual(x1.convention, 'testStart') + self.assertEqual(x1.term_raw, '') + self.assertEqual(x2.order, 2) + self.assertEqual(response.data['items'][2]['alias'], 'D1') + self.assertEqual(response.data['items'][2]['term_raw'], 'TestTerm') + + + def _create_version(self, data) -> int: + response = self.client.post( + f'/api/rsforms/{self.owned.pk}/versions/create', + data=data, format='json' + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + return response.data['version'] # type: ignore diff --git a/rsconcept/backend/apps/rsform/tests/t_graph.py b/rsconcept/backend/apps/rsform/tests/t_graph.py new file mode 100644 index 00000000..63713f95 --- /dev/null +++ b/rsconcept/backend/apps/rsform/tests/t_graph.py @@ -0,0 +1,115 @@ +''' Unit tests: graph. ''' +import unittest + +from apps.rsform.graph import Graph + + +class TestGraph(unittest.TestCase): + ''' Test class for graph. ''' + + def test_construction(self): + graph = Graph() + self.assertFalse(graph.contains(1)) + + graph.add_node(1) + self.assertTrue(graph.contains(1)) + + graph.add_edge(2, 3) + self.assertTrue(graph.contains(2)) + self.assertTrue(graph.contains(3)) + self.assertTrue(graph.has_edge(2, 3)) + self.assertFalse(graph.has_edge(3, 2)) + + graph = Graph({1: [3, 4], 2: [1], 3: [], 4: [], 5: []}) + self.assertTrue(graph.contains(1)) + self.assertTrue(graph.contains(5)) + self.assertTrue(graph.has_edge(1, 3)) + self.assertTrue(graph.has_edge(2, 1)) + + + def test_expand_outputs(self): + graph = Graph({ + 1: [2], + 2: [3, 5], + 3: [], + 5: [6], + 6: [1], + 7: [] + }) + self.assertEqual(graph.expand_outputs([]), []) + self.assertEqual(graph.expand_outputs([3]), []) + self.assertEqual(graph.expand_outputs([7]), []) + self.assertEqual(graph.expand_outputs([2, 5]), [3, 6, 1]) + + def test_expand_inputs(self): + graph = Graph({ + 1: [2], + 2: [3, 5], + 3: [], + 5: [6], + 6: [1], + 7: [] + }) + self.assertEqual(graph.expand_inputs([]), []) + self.assertEqual(graph.expand_inputs([1]), [6, 5, 2]) + self.assertEqual(graph.expand_inputs([7]), []) + self.assertEqual(graph.expand_inputs([3]), [2, 1, 6, 5]) + self.assertEqual(graph.expand_inputs([2, 5]), [1, 6]) + + + def test_transitive_closure(self): + graph = Graph({ + 1: [2], + 2: [3, 5], + 3: [], + 5: [6], + 6: [], + 7: [6] + }) + self.assertEqual(graph.transitive_closure(), { + 1: [2, 3, 5, 6], + 2: [3, 5, 6], + 3: [], + 5: [6], + 6: [], + 7: [6] + }) + + def test_topological_order(self): + self.assertEqual(Graph().topological_order(), []) + graph = Graph({ + 1: [], + 2: [1], + 3: [], + 4: [3], + 5: [6], + 6: [1, 2] + }) + self.assertEqual(graph.topological_order(), [5, 6, 4, 3, 2, 1]) + + graph = Graph({ + 1: [1], + 2: [4], + 3: [2], + 4: [], + 5: [2], + }) + self.assertEqual(graph.topological_order(), [5, 3, 2, 4, 1]) + + def test_sort_stable(self): + graph = Graph({ + 1: [2], + 2: [3, 5], + 3: [], + 5: [6], + 6: [], + 7: [6] + }) + self.assertEqual(graph.sort_stable([]), []) + self.assertEqual(graph.sort_stable([1]), [1]) + self.assertEqual(graph.sort_stable([1, 2]), [1, 2]) + self.assertEqual(graph.sort_stable([7, 2, 1]), [7, 1, 2]) + self.assertEqual(graph.sort_stable([2, 1, 7]), [1, 2, 7]) + self.assertEqual(graph.sort_stable([1, 2, 7]), [1, 2, 7]) + self.assertEqual(graph.sort_stable([2, 1, 3, 6, 7]), [1, 2, 3, 7, 6]) + self.assertEqual(graph.sort_stable([2, 1, 6, 7, 3]), [1, 2, 7, 6, 3]) diff --git a/rsconcept/backend/apps/rsform/tests/t_imports.py b/rsconcept/backend/apps/rsform/tests/t_imports.py new file mode 100644 index 00000000..442e3874 --- /dev/null +++ b/rsconcept/backend/apps/rsform/tests/t_imports.py @@ -0,0 +1,142 @@ +''' Testing imported pyconcept functionality ''' +import json +from unittest import TestCase as RegularTest + +import pyconcept as pc + + +class TestIntegrations(RegularTest): + + + def test_convert_to_ascii(self): + ''' Test converting to ASCII syntax ''' + self.assertEqual(pc.convert_to_ascii(''), '') + self.assertEqual(pc.convert_to_ascii('\u212c(X1)'), r'B(X1)') + + + def test_convert_to_math(self): + ''' Test converting to MATH syntax ''' + self.assertEqual(pc.convert_to_math(''), '') + self.assertEqual(pc.convert_to_math(r'B(X1)'), '\u212c(X1)') + + + def test_parse_expression(self): + ''' Test parsing expression ''' + out = json.loads(pc.parse_expression('X1=X2')) + self.assertEqual(out['parseResult'], True) + self.assertEqual(out['syntax'], 'math') + + + def test_empty_schema(self): + with self.assertRaises(RuntimeError): + pc.check_schema('') + + + def test_check_schema(self): + schema = self._default_schema() + self.assertTrue(pc.check_schema(schema) != '') + + + def test_check_expression(self): + schema = self._default_schema() + out1 = json.loads(pc.check_expression(schema, 'X1=X1')) + self.assertTrue(out1['parseResult']) + self.assertEqual(len(out1['args']), 0) + + out2 = json.loads(pc.check_expression(schema, 'X1=X2')) + self.assertFalse(out2['parseResult']) + + + def test_reset_aliases(self): + ''' Test reset aliases in schema ''' + schema = self._default_schema() + fixedSchema = json.loads(pc.reset_aliases(schema)) + self.assertTrue(len(fixedSchema['items']) > 2) + self.assertEqual(fixedSchema['items'][2]['alias'], 'S1') + + + def _default_schema(self): + return '''{ + "type": "rsform", + "title": "default", + "alias": "default", + "comment": "", + "items": [ + { + "entityUID": 1023383816, + "type": "constituenta", + "cstType": "basic", + "alias": "X1", + "convention": "", + "term": { + "raw": "", + "resolved": "", + "forms": [] + }, + "definition": { + "formal": "", + "text": { + "raw": "", + "resolved": "" + } + } + }, + { + "entityUID": 1877659352, + "type": "constituenta", + "cstType": "basic", + "alias": "X2", + "convention": "", + "term": { + "raw": "", + "resolved": "", + "forms": [] + }, + "definition": { + "formal": "", + "text": { + "raw": "", + "resolved": "" + } + } + }, + { + "entityUID": 1115937389, + "type": "constituenta", + "cstType": "structure", + "alias": "S2", + "convention": "", + "term": { + "raw": "", + "resolved": "", + "forms": [] + }, + "definition": { + "formal": "ℬ(X1×X1)", + "text": { + "raw": "", + "resolved": "" + } + } + }, + { + "entityUID": 94433573, + "type": "constituenta", + "cstType": "structure", + "alias": "S3", + "convention": "", + "term": { + "raw": "", + "resolved": "", + "forms": [] + }, + "definition": { + "formal": "ℬ(X1×X2)", + "text": { + "raw": "", + "resolved": "" + } + } + } + ] +}''' diff --git a/rsconcept/backend/apps/rsform/tests/t_serializers.py b/rsconcept/backend/apps/rsform/tests/t_serializers.py new file mode 100644 index 00000000..b7ccb781 --- /dev/null +++ b/rsconcept/backend/apps/rsform/tests/t_serializers.py @@ -0,0 +1,22 @@ +''' Testing serializers ''' +from django.test import TestCase + +from apps.rsform.serializers import ExpressionSerializer + + +class TestExpressionSerializer(TestCase): + def setUp(self): + pass + + + def test_validate(self): + serializer = ExpressionSerializer(data={'expression': 'X1=X1'}) + self.assertTrue(serializer.is_valid(raise_exception=False)) + self.assertEqual(serializer.validated_data['expression'], 'X1=X1') + + + def test_missing_data(self): + serializer = ExpressionSerializer(data={}) + self.assertFalse(serializer.is_valid(raise_exception=False)) + serializer = ExpressionSerializer(data={'schema': 1}) + self.assertFalse(serializer.is_valid(raise_exception=False)) diff --git a/rsconcept/backend/apps/rsform/tests/t_utils.py b/rsconcept/backend/apps/rsform/tests/t_utils.py new file mode 100644 index 00000000..27f0d1de --- /dev/null +++ b/rsconcept/backend/apps/rsform/tests/t_utils.py @@ -0,0 +1,25 @@ +''' Unit tests: utils. ''' +import re +import unittest + +from apps.rsform.utils import apply_pattern, fix_old_references + + +class TestUtils(unittest.TestCase): + ''' Test various utility functions. ''' + + def test_apply_mapping_patter(self): + mapping = {'X101': 'X20'} + pattern = re.compile(r'(X[0-9]+)') + self.assertEqual(apply_pattern('', mapping, pattern), '') + self.assertEqual(apply_pattern('X20', mapping, pattern), 'X20') + self.assertEqual(apply_pattern('X101', mapping, pattern), 'X20') + self.assertEqual(apply_pattern('asdf X101 asdf', mapping, pattern), 'asdf X20 asdf') + + + def test_fix_old_references(self): + self.assertEqual(fix_old_references(''), '') + self.assertEqual(fix_old_references('X20'), 'X20') + self.assertEqual(fix_old_references('@{X1|nomn,sing}'), '@{X1|nomn,sing}') + self.assertEqual(fix_old_references('@{X1|sing,ablt} @{X1|sing,ablt}'), '@{X1|sing,ablt} @{X1|sing,ablt}') + self.assertEqual(fix_old_references('@{X1|nomn|sing}'), '@{X1|nomn,sing}') diff --git a/rsconcept/backend/apps/rsform/tests/testing_utils.py b/rsconcept/backend/apps/rsform/tests/testing_utils.py new file mode 100644 index 00000000..637b2032 --- /dev/null +++ b/rsconcept/backend/apps/rsform/tests/testing_utils.py @@ -0,0 +1,8 @@ +''' Utilities for testing. ''' + +from apps.rsform.models import LibraryItem + + +def response_contains(response, item: LibraryItem) -> bool: + ''' Check if response contains specific item. ''' + return any(x for x in response.data if x['id'] == item.pk) diff --git a/rsconcept/backend/apps/rsform/urls.py b/rsconcept/backend/apps/rsform/urls.py new file mode 100644 index 00000000..d4df9cb0 --- /dev/null +++ b/rsconcept/backend/apps/rsform/urls.py @@ -0,0 +1,35 @@ +''' Routing: RSForms for conceptual schemas. ''' +from django.urls import include, path +from rest_framework import routers + +from . import views + +library_router = routers.SimpleRouter(trailing_slash=False) +library_router.register('library', views.LibraryViewSet, 'Library') +library_router.register('rsforms', views.RSFormViewSet, 'RSForm') +library_router.register('versions', views.VersionViewset, 'Version') + +urlpatterns = [ + path('library/active', views.LibraryActiveView.as_view()), + path('library/all', views.LibraryAdminView.as_view()), + path('library/templates', views.LibraryTemplatesView.as_view(), name='templates'), + path('constituents/', views.ConstituentAPIView.as_view(), name='constituenta-detail'), + path('rsforms/import-trs', views.TrsImportView.as_view()), + path('rsforms/create-detailed', views.create_rsform), + + path('versions//export-file', views.export_file), + path('rsforms//versions/create', views.create_version), + path('rsforms//versions/', views.retrieve_version), + + path('operations/inline-synthesis', views.inline_synthesis), + + path('rslang/parse-expression', views.parse_expression), + path('rslang/to-ascii', views.convert_to_ascii), + path('rslang/to-math', views.convert_to_math), + + path('cctext/inflect', views.inflect), + path('cctext/generate-lexeme', views.generate_lexeme), + path('cctext/parse', views.parse_text), + + path('', include(library_router.urls)), +] diff --git a/rsconcept/backend/apps/rsform/utils.py b/rsconcept/backend/apps/rsform/utils.py new file mode 100644 index 00000000..5b3e1d80 --- /dev/null +++ b/rsconcept/backend/apps/rsform/utils.py @@ -0,0 +1,68 @@ +''' Utility functions ''' +import json +import re +from io import BytesIO +from zipfile import ZipFile + +# Name for JSON inside Exteor files archive +EXTEOR_INNER_FILENAME = 'document.json' + +# Old style reference pattern +_REF_OLD_PATTERN = re.compile(r'@{([^0-9\-][^\}\|\{]*?)\|([^\}\|\{]*?)\|([^\}\|\{]*?)}') + + +def read_zipped_json(data, json_filename: str) -> dict: + ''' Read JSON from zipped data ''' + with ZipFile(data, 'r') as archive: + json_data = archive.read(json_filename) + result: dict = json.loads(json_data) + return result + + +def write_zipped_json(json_data: dict, json_filename: str) -> bytes: + ''' Write json JSON to bytes buffer ''' + content = BytesIO() + data = json.dumps(json_data, indent=4, ensure_ascii=False) + with ZipFile(content, 'w') as archive: + archive.writestr(json_filename, data=data) + return content.getvalue() + + +def apply_pattern(text: str, mapping: dict[str, str], pattern: re.Pattern[str]) -> str: + ''' Apply mapping to matching in regular expression pattern subgroup 1 ''' + if text == '' or pattern == '': + return text + pos_input: int = 0 + output: str = '' + for segment in re.finditer(pattern, text): + entity = segment.group(1) + if entity in mapping: + output += text[pos_input: segment.start(1)] + output += mapping[entity] + output += text[segment.end(1): segment.end(0)] + pos_input = segment.end(0) + output += text[pos_input: len(text)] + return output + + +def fix_old_references(text: str) -> str: + ''' Fix reference format: @{X1|nomn|sing} -> {X1|nomn,sing} ''' + if text == '': + return text + pos_input: int = 0 + output: str = '' + for segment in re.finditer(_REF_OLD_PATTERN, text): + output += text[pos_input: segment.start(0)] + output += f'@{{{segment.group(1)}|{segment.group(2)},{segment.group(3)}}}' + pos_input = segment.end(0) + output += text[pos_input: len(text)] + return output + + +def filename_for_schema(alias: str) -> str: + ''' Generate filename for schema from alias. ''' + if alias == '' or not alias.isascii(): + # Note: non-ascii symbols in Content-Disposition + # are not supported by some browsers + return 'Schema.trs' + return alias + '.trs' diff --git a/rsconcept/backend/apps/rsform/views/__init__.py b/rsconcept/backend/apps/rsform/views/__init__.py new file mode 100644 index 00000000..61c0a1d0 --- /dev/null +++ b/rsconcept/backend/apps/rsform/views/__init__.py @@ -0,0 +1,8 @@ +''' REST API: Endpoint processors. ''' +from .cctext import generate_lexeme, inflect, parse_text +from .constituents import ConstituentAPIView +from .library import LibraryActiveView, LibraryAdminView, LibraryTemplatesView, LibraryViewSet +from .operations import inline_synthesis +from .rsforms import RSFormViewSet, TrsImportView, create_rsform +from .rslang import convert_to_ascii, convert_to_math, parse_expression +from .versions import VersionViewset, create_version, export_file, retrieve_version diff --git a/rsconcept/backend/apps/rsform/views/cctext.py b/rsconcept/backend/apps/rsform/views/cctext.py new file mode 100644 index 00000000..ed0e60a3 --- /dev/null +++ b/rsconcept/backend/apps/rsform/views/cctext.py @@ -0,0 +1,70 @@ +''' Endpoints for cctext. ''' +import cctext as ct +from drf_spectacular.utils import extend_schema +from rest_framework import status as c +from rest_framework.decorators import api_view +from rest_framework.request import Request +from rest_framework.response import Response + +from .. import serializers as s + + +@extend_schema( + summary='generate wordform', + tags=['NaturalLanguage'], + request=s.WordFormSerializer, + responses={200: s.ResultTextResponse}, + auth=None +) +@api_view(['POST']) +def inflect(request: Request): + ''' Endpoint: Generate wordform with set grammemes. ''' + serializer = s.WordFormSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + text = serializer.validated_data['text'] + grams = serializer.validated_data['grams'] + result = ct.inflect(text, grams) + return Response( + status=c.HTTP_200_OK, + data={'result': result} + ) + + +@extend_schema( + summary='all wordforms for current lexeme', + tags=['NaturalLanguage'], + request=s.TextSerializer, + responses={200: s.MultiFormSerializer}, + auth=None +) +@api_view(['POST']) +def generate_lexeme(request: Request): + ''' Endpoint: Generate complete set of wordforms for lexeme. ''' + serializer = s.TextSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + nominal = serializer.validated_data['text'] + result = ct.generate_lexeme(nominal) + return Response( + status=c.HTTP_200_OK, + data=s.MultiFormSerializer.from_list(result) + ) + + +@extend_schema( + summary='get likely parse grammemes', + tags=['NaturalLanguage'], + request=s.TextSerializer, + responses={200: s.ResultTextResponse}, + auth=None +) +@api_view(['POST']) +def parse_text(request: Request): + ''' Endpoint: Get likely vocabulary parse. ''' + serializer = s.TextSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + text = serializer.validated_data['text'] + result = ct.parse(text) + return Response( + status=c.HTTP_200_OK, + data={'result': result} + ) diff --git a/rsconcept/backend/apps/rsform/views/constituents.py b/rsconcept/backend/apps/rsform/views/constituents.py new file mode 100644 index 00000000..cd772f1c --- /dev/null +++ b/rsconcept/backend/apps/rsform/views/constituents.py @@ -0,0 +1,15 @@ +''' Endpoints for Constituenta. ''' +from drf_spectacular.utils import extend_schema, extend_schema_view +from rest_framework import generics + +from .. import models as m +from .. import permissions +from .. import serializers as s + + +@extend_schema(tags=['Constituenta']) +@extend_schema_view() +class ConstituentAPIView(generics.RetrieveUpdateAPIView, permissions.EditorMixin): + ''' Endpoint: Get / Update Constituenta. ''' + queryset = m.Constituenta.objects.all() + serializer_class = s.CstSerializer diff --git a/rsconcept/backend/apps/rsform/views/library.py b/rsconcept/backend/apps/rsform/views/library.py new file mode 100644 index 00000000..4ce592af --- /dev/null +++ b/rsconcept/backend/apps/rsform/views/library.py @@ -0,0 +1,313 @@ +''' Endpoints for library. ''' +from copy import deepcopy +from typing import cast + +from django.db import transaction +from django.db.models import Q +from django_filters.rest_framework import DjangoFilterBackend +from drf_spectacular.utils import extend_schema, extend_schema_view +from rest_framework import filters, generics +from rest_framework import status as c +from rest_framework import viewsets +from rest_framework.decorators import action +from rest_framework.request import Request +from rest_framework.response import Response + +from .. import models as m +from .. import permissions +from .. import serializers as s + + +@extend_schema(tags=['Library']) +@extend_schema_view() +class LibraryViewSet(viewsets.ModelViewSet): + ''' Endpoint: Library operations. ''' + queryset = m.LibraryItem.objects.all() + + filter_backends = (DjangoFilterBackend, filters.OrderingFilter) + filterset_fields = ['item_type', 'owner'] + ordering_fields = ('item_type', 'owner', 'alias', 'title', 'time_update') + ordering = '-time_update' + + def get_serializer_class(self): + if self.action == 'create': + return s.LibraryItemBaseSerializer + return s.LibraryItemSerializer + + def perform_create(self, serializer): + if not self.request.user.is_anonymous and 'owner' not in self.request.POST: + return serializer.save(owner=self.request.user) + else: + return serializer.save() + + def get_permissions(self): + if self.action in ['update', 'partial_update']: + permission_list = [permissions.ItemEditor] + elif self.action in [ + 'destroy', 'set_owner', 'set_access_policy', 'set_location', + 'editors_add', 'editors_remove', 'editors_set' + ]: + permission_list = [permissions.ItemOwner] + elif self.action in ['create', 'clone', 'subscribe', 'unsubscribe']: + permission_list = [permissions.GlobalUser] + else: + permission_list = [permissions.ItemAnyone] + return [permission() for permission in permission_list] + + def _get_item(self) -> m.LibraryItem: + return cast(m.LibraryItem, self.get_object()) + + @extend_schema( + summary='clone item including contents', + tags=['Library'], + request=s.LibraryItemCloneSerializer, + responses={ + c.HTTP_201_CREATED: s.RSFormParseSerializer, + c.HTTP_400_BAD_REQUEST: None, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } + ) + @transaction.atomic + @action(detail=True, methods=['post'], url_path='clone') + def clone(self, request: Request, pk): + ''' Endpoint: Create deep copy of library item. ''' + serializer = s.LibraryItemCloneSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + item = self._get_item() + clone = deepcopy(item) + clone.pk = None + clone.owner = self.request.user + clone.title = serializer.validated_data['title'] + clone.alias = serializer.validated_data.get('alias', '') + clone.comment = serializer.validated_data.get('comment', '') + clone.visible = serializer.validated_data.get('visible', True) + clone.read_only = False + clone.access_policy = serializer.validated_data.get('access_policy', m.AccessPolicy.PUBLIC) + clone.location = serializer.validated_data.get('location', m.LocationHead.USER) + clone.save() + + if clone.item_type == m.LibraryItemType.RSFORM: + need_filter = 'items' in request.data + for cst in m.RSForm(item).constituents(): + if not need_filter or cst.pk in request.data['items']: + cst.pk = None + cst.schema = clone + cst.save() + return Response( + status=c.HTTP_201_CREATED, + data=s.RSFormParseSerializer(clone).data + ) + return Response(status=c.HTTP_400_BAD_REQUEST) + + @extend_schema( + summary='subscribe to item', + tags=['Library'], + request=None, + responses={ + c.HTTP_200_OK: None, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=True, methods=['post']) + def subscribe(self, request: Request, pk): + ''' Endpoint: Subscribe current user to item. ''' + item = self._get_item() + m.Subscription.subscribe(user=cast(m.User, self.request.user), item=item) + return Response(status=c.HTTP_200_OK) + + @extend_schema( + summary='unsubscribe from item', + tags=['Library'], + request=None, + responses={ + c.HTTP_200_OK: None, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + }, + ) + @action(detail=True, methods=['delete']) + def unsubscribe(self, request: Request, pk): + ''' Endpoint: Unsubscribe current user from item. ''' + item = self._get_item() + m.Subscription.unsubscribe(user=cast(m.User, self.request.user), item=item) + return Response(status=c.HTTP_200_OK) + + @extend_schema( + summary='set owner for item', + tags=['Library'], + request=s.UserTargetSerializer, + responses={ + c.HTTP_200_OK: None, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=True, methods=['patch'], url_path='set-owner') + def set_owner(self, request: Request, pk): + ''' Endpoint: Set item owner. ''' + item = self._get_item() + serializer = s.UserTargetSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + new_owner = serializer.validated_data['user'] + m.LibraryItem.objects.filter(pk=item.pk).update(owner=new_owner) + return Response(status=c.HTTP_200_OK) + + @extend_schema( + summary='set AccessPolicy for item', + tags=['Library'], + request=s.AccessPolicySerializer, + responses={ + c.HTTP_200_OK: None, + c.HTTP_400_BAD_REQUEST: None, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=True, methods=['patch'], url_path='set-access-policy') + def set_access_policy(self, request: Request, pk): + ''' Endpoint: Set item AccessPolicy. ''' + item = self._get_item() + serializer = s.AccessPolicySerializer(data=request.data) + serializer.is_valid(raise_exception=True) + m.LibraryItem.objects.filter(pk=item.pk).update(access_policy=serializer.validated_data['access_policy']) + return Response(status=c.HTTP_200_OK) + + @extend_schema( + summary='set location for item', + tags=['Library'], + request=s.LocationSerializer, + responses={ + c.HTTP_200_OK: None, + c.HTTP_400_BAD_REQUEST: None, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=True, methods=['patch'], url_path='set-location') + def set_location(self, request: Request, pk): + ''' Endpoint: Set item location. ''' + item = self._get_item() + serializer = s.LocationSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + location: str = serializer.validated_data['location'] + if location.startswith(m.LocationHead.LIBRARY) and not self.request.user.is_staff: + return Response(status=c.HTTP_403_FORBIDDEN) + m.LibraryItem.objects.filter(pk=item.pk).update(location=location) + return Response(status=c.HTTP_200_OK) + + @extend_schema( + summary='add editor for item', + tags=['Library'], + request=s.UserTargetSerializer, + responses={ + c.HTTP_200_OK: None, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=True, methods=['patch'], url_path='editors-add') + def editors_add(self, request: Request, pk): + ''' Endpoint: Add editor for item. ''' + item = self._get_item() + serializer = s.UserTargetSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + new_editor = serializer.validated_data['user'] + m.Editor.add(item=item, user=new_editor) + return Response(status=c.HTTP_200_OK) + + @extend_schema( + summary='remove editor for item', + tags=['Library'], + request=s.UserTargetSerializer, + responses={ + c.HTTP_200_OK: None, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=True, methods=['patch'], url_path='editors-remove') + def editors_remove(self, request: Request, pk): + ''' Endpoint: Remove editor for item. ''' + item = self._get_item() + serializer = s.UserTargetSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + editor = serializer.validated_data['user'] + m.Editor.remove(item=item, user=editor) + return Response(status=c.HTTP_200_OK) + + @extend_schema( + summary='set list of editors for item', + tags=['Library'], + request=s.UsersListSerializer, + responses={ + c.HTTP_200_OK: None, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=True, methods=['patch'], url_path='editors-set') + def editors_set(self, request: Request, pk): + ''' Endpoint: Set list of editors for item. ''' + item = self._get_item() + serializer = s.UsersListSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + editors = serializer.validated_data['users'] + m.Editor.set(item=item, users=editors) + return Response(status=c.HTTP_200_OK) + + +@extend_schema(tags=['Library']) +@extend_schema_view() +class LibraryActiveView(generics.ListAPIView): + ''' Endpoint: Get list of library items available for active user. ''' + permission_classes = (permissions.Anyone,) + serializer_class = s.LibraryItemSerializer + + def get_queryset(self): + if self.request.user.is_anonymous: + return m.LibraryItem.objects.filter( + Q(access_policy=m.AccessPolicy.PUBLIC), + ).filter( + Q(location__startswith=m.LocationHead.COMMON) | + Q(location__startswith=m.LocationHead.LIBRARY) + ).order_by('-time_update') + else: + user = cast(m.User, self.request.user) + # pylint: disable=unsupported-binary-operation + return m.LibraryItem.objects.filter( + ( + Q(access_policy=m.AccessPolicy.PUBLIC) & + ( + Q(location__startswith=m.LocationHead.COMMON) | + Q(location__startswith=m.LocationHead.LIBRARY) + ) + ) | + Q(owner=user) | + Q(editor__editor=user) | + Q(subscription__user=user) + ).distinct().order_by('-time_update') + + +@extend_schema(tags=['Library']) +@extend_schema_view() +class LibraryAdminView(generics.ListAPIView): + ''' Endpoint: Get list of all library items. Admin only ''' + permission_classes = (permissions.GlobalAdmin,) + serializer_class = s.LibraryItemSerializer + + def get_queryset(self): + return m.LibraryItem.objects.all().order_by('-time_update') + + +@extend_schema(tags=['Library']) +@extend_schema_view() +class LibraryTemplatesView(generics.ListAPIView): + ''' Endpoint: Get list of templates. ''' + permission_classes = (permissions.Anyone,) + serializer_class = s.LibraryItemSerializer + + def get_queryset(self): + template_ids = m.LibraryTemplate.objects.values_list('lib_source', flat=True) + return m.LibraryItem.objects.filter(pk__in=template_ids) diff --git a/rsconcept/backend/apps/rsform/views/operations.py b/rsconcept/backend/apps/rsform/views/operations.py new file mode 100644 index 00000000..4520d29f --- /dev/null +++ b/rsconcept/backend/apps/rsform/views/operations.py @@ -0,0 +1,50 @@ +''' Endpoints for RSForm. ''' +from typing import cast + +from django.db import transaction +from drf_spectacular.utils import extend_schema +from rest_framework import status as c +from rest_framework.decorators import api_view +from rest_framework.request import Request +from rest_framework.response import Response + +from .. import models as m +from .. import serializers as s + + +@extend_schema( + summary='Inline synthesis: merge one schema into another', + tags=['Operations'], + request=s.InlineSynthesisSerializer, + responses={c.HTTP_200_OK: s.RSFormParseSerializer} +) +@transaction.atomic +@api_view(['PATCH']) +def inline_synthesis(request: Request): + ''' Endpoint: Inline synthesis. ''' + serializer = s.InlineSynthesisSerializer( + data=request.data, + context={'user': request.user} + ) + serializer.is_valid(raise_exception=True) + + schema = m.RSForm(serializer.validated_data['receiver']) + items = cast(list[m.Constituenta], serializer.validated_data['items']) + new_items = schema.insert_copy(items) + + for substitution in serializer.validated_data['substitutions']: + original = cast(m.Constituenta, substitution['original']) + replacement = cast(m.Constituenta, substitution['substitution']) + if original in items: + index = next(i for (i, cst) in enumerate(items) if cst == original) + original = new_items[index] + else: + index = next(i for (i, cst) in enumerate(items) if cst == replacement) + replacement = new_items[index] + schema.substitute(original, replacement, substitution['transfer_term']) + + schema.restore_order() + return Response( + status=c.HTTP_200_OK, + data=s.RSFormParseSerializer(schema.item).data + ) diff --git a/rsconcept/backend/apps/rsform/views/rsforms.py b/rsconcept/backend/apps/rsform/views/rsforms.py new file mode 100644 index 00000000..1af946f1 --- /dev/null +++ b/rsconcept/backend/apps/rsform/views/rsforms.py @@ -0,0 +1,492 @@ +''' Endpoints for RSForm. ''' +import json +from typing import Union, cast + +import pyconcept +from django.db import transaction +from django.http import HttpResponse +from drf_spectacular.utils import extend_schema, extend_schema_view +from rest_framework import generics +from rest_framework import status as c +from rest_framework import views, viewsets +from rest_framework.decorators import action, api_view +from rest_framework.request import Request +from rest_framework.response import Response + +from .. import messages as msg +from .. import models as m +from .. import permissions +from .. import serializers as s +from .. import utils + + +@extend_schema(tags=['RSForm']) +@extend_schema_view() +class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView): + ''' Endpoint: RSForm operations. ''' + queryset = m.LibraryItem.objects.filter(item_type=m.LibraryItemType.RSFORM) + serializer_class = s.LibraryItemSerializer + + def _get_schema(self) -> m.RSForm: + return m.RSForm(cast(m.LibraryItem, self.get_object())) + + def get_permissions(self): + ''' Determine permission class. ''' + if self.action in [ + 'load_trs', 'cst_create', 'cst_delete_multiple', + 'reset_aliases', 'cst_rename', 'cst_substitute' + ]: + permission_list = [permissions.ItemEditor] + elif self.action in ['contents', 'details', 'export_trs', 'resolve', 'check']: + permission_list = [permissions.ItemAnyone] + else: + permission_list = [permissions.Anyone] + return [permission() for permission in permission_list] + + @extend_schema( + summary='create constituenta', + tags=['Constituenta'], + request=s.CstCreateSerializer, + responses={ + c.HTTP_201_CREATED: s.NewCstResponse, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=True, methods=['post'], url_path='cst-create') + def cst_create(self, request: Request, pk): + ''' Create new constituenta. ''' + schema = self._get_schema() + serializer = s.CstCreateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + data = serializer.validated_data + new_cst = schema.create_cst( + data=data, + insert_after=data['insert_after'] if 'insert_after' in data else None + ) + schema.item.refresh_from_db() + response = Response( + status=c.HTTP_201_CREATED, + data={ + 'new_cst': s.CstSerializer(new_cst).data, + 'schema': s.RSFormParseSerializer(schema.item).data + } + ) + response['Location'] = new_cst.get_absolute_url() + return response + + @extend_schema( + summary='produce the structure of a given constituenta', + tags=['RSForm'], + request=s.CstTargetSerializer, + responses={ + c.HTTP_200_OK: s.NewMultiCstResponse, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=True, methods=['patch'], url_path='cst-produce-structure') + def produce_structure(self, request: Request, pk): + ''' Produce a term for every element of the target constituenta typification. ''' + schema = self._get_schema() + + serializer = s.CstTargetSerializer(data=request.data, context={'schema': schema.item}) + serializer.is_valid(raise_exception=True) + cst = cast(m.Constituenta, serializer.validated_data['target']) + + schema_details = s.RSFormParseSerializer(schema.item).data['items'] + cst_parse = next(item for item in schema_details if item['id'] == cst.id)['parse'] + if not cst_parse['typification']: + return Response( + status=c.HTTP_400_BAD_REQUEST, + data={f'{cst.id}': msg.constituentaNoStructure()} + ) + + result = schema.produce_structure(cst, cst_parse) + return Response( + status=c.HTTP_200_OK, + data={ + 'cst_list': result, + 'schema': s.RSFormParseSerializer(schema.item).data + } + ) + + @extend_schema( + summary='rename constituenta', + tags=['Constituenta'], + request=s.CstRenameSerializer, + responses={ + c.HTTP_200_OK: s.NewCstResponse, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } + ) + @transaction.atomic + @action(detail=True, methods=['patch'], url_path='cst-rename') + def cst_rename(self, request: Request, pk): + ''' Rename constituenta possibly changing type. ''' + schema = self._get_schema() + serializer = s.CstRenameSerializer(data=request.data, context={'schema': schema.item}) + serializer.is_valid(raise_exception=True) + + cst = cast(m.Constituenta, serializer.validated_data['target']) + old_alias = cst.alias + + cst.alias = serializer.validated_data['alias'] + cst.cst_type = serializer.validated_data['cst_type'] + cst.save() + + mapping = {old_alias: cst.alias} + schema.apply_mapping(mapping, change_aliases=False) + schema.item.refresh_from_db() + cst.refresh_from_db() + + return Response( + status=c.HTTP_200_OK, + data={ + 'new_cst': s.CstSerializer(cst).data, + 'schema': s.RSFormParseSerializer(schema.item).data + } + ) + + @extend_schema( + summary='substitute constituenta', + tags=['RSForm'], + request=s.CstSubstituteSerializer, + responses={ + c.HTTP_200_OK: s.RSFormParseSerializer, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } + ) + @transaction.atomic + @action(detail=True, methods=['patch'], url_path='cst-substitute') + def cst_substitute(self, request: Request, pk): + ''' Substitute occurrences of constituenta with another one. ''' + schema = self._get_schema() + serializer = s.CstSubstituteSerializer( + data=request.data, + context={'schema': schema.item} + ) + serializer.is_valid(raise_exception=True) + for substitution in serializer.validated_data['substitutions']: + original = cast(m.Constituenta, substitution['original']) + replacement = cast(m.Constituenta, substitution['substitution']) + schema.substitute(original, replacement, substitution['transfer_term']) + schema.item.refresh_from_db() + return Response( + status=c.HTTP_200_OK, + data=s.RSFormParseSerializer(schema.item).data + ) + + @extend_schema( + summary='delete constituents', + tags=['RSForm'], + request=s.CstListSerializer, + responses={ + c.HTTP_200_OK: s.RSFormParseSerializer, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=True, methods=['patch'], url_path='cst-delete-multiple') + def cst_delete_multiple(self, request: Request, pk): + ''' Endpoint: Delete multiple constituents. ''' + schema = self._get_schema() + serializer = s.CstListSerializer( + data=request.data, + context={'schema': schema.item} + ) + serializer.is_valid(raise_exception=True) + schema.delete_cst(serializer.validated_data['items']) + schema.item.refresh_from_db() + return Response( + status=c.HTTP_200_OK, + data=s.RSFormParseSerializer(schema.item).data + ) + + @extend_schema( + summary='move constituenta', + tags=['RSForm'], + request=s.CstMoveSerializer, + responses={ + c.HTTP_200_OK: s.RSFormParseSerializer, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=True, methods=['patch'], url_path='cst-moveto') + def cst_moveto(self, request: Request, pk): + ''' Endpoint: Move multiple constituents. ''' + schema = self._get_schema() + serializer = s.CstMoveSerializer( + data=request.data, + context={'schema': schema.item} + ) + serializer.is_valid(raise_exception=True) + schema.move_cst( + listCst=serializer.validated_data['items'], + target=serializer.validated_data['move_to'] + ) + return Response( + status=c.HTTP_200_OK, + data=s.RSFormParseSerializer(schema.item).data + ) + + @extend_schema( + summary='reset aliases, update expressions and references', + tags=['RSForm'], + request=None, + responses={ + c.HTTP_200_OK: s.RSFormParseSerializer, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=True, methods=['patch'], url_path='reset-aliases') + def reset_aliases(self, request: Request, pk): + ''' Endpoint: Recreate all aliases based on order. ''' + schema = self._get_schema() + schema.reset_aliases() + return Response( + status=c.HTTP_200_OK, + data=s.RSFormParseSerializer(schema.item).data + ) + + @extend_schema( + summary='restore order based on types and term graph', + tags=['RSForm'], + request=None, + responses={ + c.HTTP_200_OK: s.RSFormParseSerializer, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=True, methods=['patch'], url_path='restore-order') + def restore_order(self, request: Request, pk): + ''' Endpoint: Restore order based on types and term graph. ''' + schema = self._get_schema() + schema.restore_order() + return Response( + status=c.HTTP_200_OK, + data=s.RSFormParseSerializer(schema.item).data + ) + + @extend_schema( + summary='load data from TRS file', + tags=['RSForm'], + request=s.RSFormUploadSerializer, + responses={ + c.HTTP_200_OK: s.RSFormParseSerializer, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=True, methods=['patch'], url_path='load-trs') + def load_trs(self, request: Request, pk): + ''' Endpoint: Load data from file and replace current schema. ''' + input_serializer = s.RSFormUploadSerializer(data=request.data) + input_serializer.is_valid(raise_exception=True) + schema = self._get_schema() + load_metadata = input_serializer.validated_data['load_metadata'] + data = utils.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME) + data['id'] = schema.item.pk + + serializer = s.RSFormTRSSerializer( + data=data, + context={'load_meta': load_metadata} + ) + serializer.is_valid(raise_exception=True) + result = serializer.save() + return Response( + status=c.HTTP_200_OK, + data=s.RSFormParseSerializer(result.item).data + ) + + @extend_schema( + summary='get all constituents data from DB', + tags=['RSForm'], + request=None, + responses={ + c.HTTP_200_OK: s.RSFormSerializer, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=True, methods=['get']) + def contents(self, request: Request, pk): + ''' Endpoint: View schema db contents (including constituents). ''' + schema = s.RSFormSerializer(self.get_object()) + return Response( + status=c.HTTP_200_OK, + data=schema.data + ) + + @extend_schema( + summary='get all constituents data and parses', + tags=['RSForm'], + request=None, + responses={ + c.HTTP_200_OK: s.RSFormParseSerializer, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=True, methods=['get']) + def details(self, request: Request, pk): + ''' Endpoint: Detailed schema view including statuses and parse. ''' + serializer = s.RSFormParseSerializer(cast(m.LibraryItem, self.get_object())) + return Response( + status=c.HTTP_200_OK, + data=serializer.data + ) + + @extend_schema( + summary='check RSLang expression', + tags=['RSForm', 'FormalLanguage'], + request=s.ExpressionSerializer, + responses={ + c.HTTP_200_OK: s.ExpressionParseSerializer, + c.HTTP_404_NOT_FOUND: None + }, + ) + @action(detail=True, methods=['post']) + def check(self, request: Request, pk): + ''' Endpoint: Check RSLang expression against schema context. ''' + serializer = s.ExpressionSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + expression = serializer.validated_data['expression'] + schema = s.PyConceptAdapter(self._get_schema()) + result = pyconcept.check_expression(json.dumps(schema.data), expression) + return Response( + status=c.HTTP_200_OK, + data=json.loads(result) + ) + + @extend_schema( + summary='resolve text with references', + tags=['RSForm', 'NaturalLanguage'], + request=s.TextSerializer, + responses={ + c.HTTP_200_OK: s.ResolverSerializer, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=True, methods=['post']) + def resolve(self, request: Request, pk): + ''' Endpoint: Resolve references in text against schema terms context. ''' + serializer = s.TextSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + text = serializer.validated_data['text'] + resolver = self._get_schema().resolver() + resolver.resolve(text) + return Response( + status=c.HTTP_200_OK, + data=s.ResolverSerializer(resolver).data + ) + + @extend_schema( + summary='export as TRS file', + tags=['RSForm'], + request=None, + responses={ + (c.HTTP_200_OK, 'application/zip'): bytes, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=True, methods=['get'], url_path='export-trs') + def export_trs(self, request: Request, pk): + ''' Endpoint: Download Exteor compatible file. ''' + data = s.RSFormTRSSerializer(self._get_schema()).data + file = utils.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME) + filename = utils.filename_for_schema(self._get_schema().item.alias) + response = HttpResponse(file, content_type='application/zip') + response['Content-Disposition'] = f'attachment; filename={filename}' + return response + + +class TrsImportView(views.APIView): + ''' Endpoint: Upload RS form in Exteor format. ''' + serializer_class = s.FileSerializer + permission_classes = [permissions.GlobalUser] + + @extend_schema( + summary='import TRS file into RSForm', + tags=['RSForm'], + request=s.FileSerializer, + responses={ + c.HTTP_201_CREATED: s.LibraryItemSerializer, + c.HTTP_403_FORBIDDEN: None + } + ) + def post(self, request: Request): + data = utils.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME) + owner = cast(m.User, self.request.user) + _prepare_rsform_data(data, request, owner) + serializer = s.RSFormTRSSerializer( + data=data, + context={'load_meta': True} + ) + serializer.is_valid(raise_exception=True) + schema = serializer.save() + result = s.LibraryItemSerializer(schema.item) + return Response( + status=c.HTTP_201_CREATED, + data=result.data + ) + + +@extend_schema( + summary='create new RSForm empty or from file', + tags=['RSForm'], + request=s.LibraryItemSerializer, + responses={ + c.HTTP_201_CREATED: s.LibraryItemSerializer, + c.HTTP_400_BAD_REQUEST: None, + c.HTTP_403_FORBIDDEN: None + } +) +@api_view(['POST']) +def create_rsform(request: Request): + ''' Endpoint: Create RSForm from user input and/or trs file. ''' + owner = cast(m.User, request.user) if not request.user.is_anonymous else None + if 'file' not in request.FILES: + return Response( + status=c.HTTP_400_BAD_REQUEST, + data={'file': msg.missingFile()} + ) + else: + data = utils.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME) + _prepare_rsform_data(data, request, owner) + serializer_rsform = s.RSFormTRSSerializer(data=data, context={'load_meta': True}) + serializer_rsform.is_valid(raise_exception=True) + schema = serializer_rsform.save() + result = s.LibraryItemSerializer(schema.item) + return Response( + status=c.HTTP_201_CREATED, + data=result.data + ) + + +def _prepare_rsform_data(data: dict, request: Request, owner: Union[m.User, None]): + data['owner'] = owner + if 'title' in request.data and request.data['title'] != '': + data['title'] = request.data['title'] + if data['title'] == '': + data['title'] = 'Без названия ' + request.FILES['file'].fileName + if 'alias' in request.data and request.data['alias'] != '': + data['alias'] = request.data['alias'] + if 'comment' in request.data and request.data['comment'] != '': + data['comment'] = request.data['comment'] + + visible = True + if 'visible' in request.data: + visible = request.data['visible'] == 'true' + data['visible'] = visible + + read_only = False + if 'read_only' in request.data: + read_only = request.data['read_only'] == 'true' + data['read_only'] = read_only + + data['access_policy'] = request.data.get('access_policy', m.AccessPolicy.PUBLIC) + data['location'] = request.data.get('location', m.LocationHead.USER) diff --git a/rsconcept/backend/apps/rsform/views/rslang.py b/rsconcept/backend/apps/rsform/views/rslang.py new file mode 100644 index 00000000..528416dc --- /dev/null +++ b/rsconcept/backend/apps/rsform/views/rslang.py @@ -0,0 +1,71 @@ +''' Endpoints pyconcept formal language parsing. ''' +import json + +import pyconcept +from drf_spectacular.utils import extend_schema +from rest_framework import status as c +from rest_framework.decorators import api_view +from rest_framework.request import Request +from rest_framework.response import Response + +from .. import serializers as s + + +@extend_schema( + summary='RS expression into Syntax Tree', + tags=['FormalLanguage'], + request=s.ExpressionSerializer, + responses={c.HTTP_200_OK: s.ExpressionParseSerializer}, + auth=None +) +@api_view(['POST']) +def parse_expression(request: Request): + ''' Endpoint: Parse RS expression. ''' + serializer = s.ExpressionSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + expression = serializer.validated_data['expression'] + result = pyconcept.parse_expression(expression) + return Response( + status=c.HTTP_200_OK, + data=json.loads(result) + ) + + +@extend_schema( + summary='Unicode syntax to ASCII TeX', + tags=['FormalLanguage'], + request=s.ExpressionSerializer, + responses={c.HTTP_200_OK: s.ResultTextResponse}, + auth=None +) +@api_view(['POST']) +def convert_to_ascii(request: Request): + ''' Endpoint: Convert expression to ASCII syntax. ''' + serializer = s.ExpressionSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + expression = serializer.validated_data['expression'] + result = pyconcept.convert_to_ascii(expression) + return Response( + status=c.HTTP_200_OK, + data={'result': result} + ) + + +@extend_schema( + summary='ASCII TeX syntax to Unicode symbols', + tags=['FormalLanguage'], + request=s.ExpressionSerializer, + responses={200: s.ResultTextResponse}, + auth=None +) +@api_view(['POST']) +def convert_to_math(request: Request): + ''' Endpoint: Convert expression to MATH syntax. ''' + serializer = s.ExpressionSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + expression = serializer.validated_data['expression'] + result = pyconcept.convert_to_math(expression) + return Response( + status=c.HTTP_200_OK, + data={'result': result} + ) diff --git a/rsconcept/backend/apps/rsform/views/versions.py b/rsconcept/backend/apps/rsform/views/versions.py new file mode 100644 index 00000000..a5e69c9a --- /dev/null +++ b/rsconcept/backend/apps/rsform/views/versions.py @@ -0,0 +1,141 @@ +''' Endpoints for versions. ''' +from typing import cast + +from django.http import HttpResponse +from drf_spectacular.utils import extend_schema, extend_schema_view +from rest_framework import generics +from rest_framework import status as c +from rest_framework import viewsets +from rest_framework.decorators import action, api_view, permission_classes +from rest_framework.request import Request +from rest_framework.response import Response + +from .. import models as m +from .. import permissions +from .. import serializers as s +from .. import utils + + +@extend_schema(tags=['Version']) +@extend_schema_view() +class VersionViewset( + viewsets.GenericViewSet, + generics.RetrieveUpdateDestroyAPIView, + permissions.EditorMixin +): + ''' Endpoint: Get / Update Constituenta. ''' + queryset = m.Version.objects.all() + serializer_class = s.VersionSerializer + + @extend_schema( + summary='restore version data into current item', + request=None, + responses={ + c.HTTP_200_OK: s.RSFormParseSerializer, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=True, methods=['patch'], url_path='restore') + def restore(self, request: Request, pk): + ''' Restore version data into current item. ''' + version = cast(m.Version, self.get_object()) + item = cast(m.LibraryItem, version.item) + s.RSFormSerializer(item).restore_from_version(version.data) + return Response( + status=c.HTTP_200_OK, + data=s.RSFormParseSerializer(item).data + ) + + +@extend_schema( + summary='save version for RSForm copying current content', + tags=['Version'], + request=s.VersionCreateSerializer, + responses={ + c.HTTP_201_CREATED: s.NewVersionResponse, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } +) +@api_view(['POST']) +@permission_classes([permissions.GlobalUser]) +def create_version(request: Request, pk_item: int): + ''' Endpoint: Create new version for RSForm copying current content. ''' + try: + item = m.LibraryItem.objects.get(pk=pk_item) + except m.LibraryItem.DoesNotExist: + return Response(status=c.HTTP_404_NOT_FOUND) + creator = request.user + if not creator.is_staff and creator != item.owner: + return Response(status=c.HTTP_403_FORBIDDEN) + + version_input = s.VersionCreateSerializer(data=request.data) + version_input.is_valid(raise_exception=True) + data = s.RSFormSerializer(item).to_versioned_data() + result = m.RSForm(item).create_version( + version=version_input.validated_data['version'], + description=version_input.validated_data['description'], + data=data + ) + return Response( + status=c.HTTP_201_CREATED, + data={ + 'version': result.pk, + 'schema': s.RSFormParseSerializer(item).data + } + ) + + +@extend_schema( + summary='retrieve versioned data for RSForm', + tags=['Version'], + request=None, + responses={ + c.HTTP_200_OK: s.RSFormParseSerializer, + c.HTTP_404_NOT_FOUND: None + } +) +@api_view(['GET']) +def retrieve_version(request: Request, pk_item: int, pk_version: int): + ''' Endpoint: Retrieve version for RSForm. ''' + try: + item = m.LibraryItem.objects.get(pk=pk_item) + except m.LibraryItem.DoesNotExist: + return Response(status=c.HTTP_404_NOT_FOUND) + try: + version = m.Version.objects.get(pk=pk_version) + except m.Version.DoesNotExist: + return Response(status=c.HTTP_404_NOT_FOUND) + if version.item != item: + return Response(status=c.HTTP_404_NOT_FOUND) + + data = s.RSFormParseSerializer(item).from_versioned_data(version.pk, version.data) + return Response( + status=c.HTTP_200_OK, + data=data + ) + + +@extend_schema( + summary='export versioned data as file', + tags=['Version'], + request=None, + responses={ + (c.HTTP_200_OK, 'application/zip'): bytes, + c.HTTP_404_NOT_FOUND: None + } +) +@api_view(['GET']) +def export_file(request: Request, pk: int): + ''' Endpoint: Download Exteor compatible file for versioned data. ''' + try: + version = m.Version.objects.get(pk=pk) + except m.Version.DoesNotExist: + return Response(status=c.HTTP_404_NOT_FOUND) + data = s.RSFormTRSSerializer(m.RSForm(version.item)).from_versioned_data(version.data) + file = utils.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME) + filename = utils.filename_for_schema(data['alias']) + response = HttpResponse(file, content_type='application/zip') + response['Content-Disposition'] = f'attachment; filename={filename}' + return response diff --git a/rsconcept/backend/apps/users/__init__.py b/rsconcept/backend/apps/users/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rsconcept/backend/apps/users/admin.py b/rsconcept/backend/apps/users/admin.py new file mode 100644 index 00000000..f5d7fdaa --- /dev/null +++ b/rsconcept/backend/apps/users/admin.py @@ -0,0 +1 @@ +''' Admin: User profile and Authorization. ''' diff --git a/rsconcept/backend/apps/users/apps.py b/rsconcept/backend/apps/users/apps.py new file mode 100644 index 00000000..3d6b9e51 --- /dev/null +++ b/rsconcept/backend/apps/users/apps.py @@ -0,0 +1,11 @@ +''' Application: User profile and Authorization. ''' +from django.apps import AppConfig + + +class UsersConfig(AppConfig): + ''' Application config. ''' + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.users' + + def ready(self): + import apps.users.signals # pylint: disable=unused-import,import-outside-toplevel diff --git a/rsconcept/backend/apps/users/messages.py b/rsconcept/backend/apps/users/messages.py new file mode 100644 index 00000000..f142f37a --- /dev/null +++ b/rsconcept/backend/apps/users/messages.py @@ -0,0 +1,14 @@ +''' Utility: Text messages. ''' +# pylint: skip-file + + +def passwordAuthFailed(): + return 'Неизвестное сочетание имени пользователя (email) и пароля' + + +def passwordsNotMatch(): + return 'Введенные пароли не совпадают' + + +def emailAlreadyTaken(): + return 'Пользователь с данным email уже существует' diff --git a/rsconcept/backend/apps/users/migrations/__init__.py b/rsconcept/backend/apps/users/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rsconcept/backend/apps/users/models.py b/rsconcept/backend/apps/users/models.py new file mode 100644 index 00000000..510b504b --- /dev/null +++ b/rsconcept/backend/apps/users/models.py @@ -0,0 +1,5 @@ +''' Models: User profile and Authorization. ''' + +# Note: using User import to isolate original +# pylint: disable=unused-import,ungrouped-imports +from django.contrib.auth.models import User diff --git a/rsconcept/backend/apps/users/serializers.py b/rsconcept/backend/apps/users/serializers.py new file mode 100644 index 00000000..d7ed3dd6 --- /dev/null +++ b/rsconcept/backend/apps/users/serializers.py @@ -0,0 +1,169 @@ +''' Serializers: User profile and Authorization. ''' +from django.contrib.auth import authenticate +from django.contrib.auth.password_validation import validate_password +from rest_framework import serializers + +from apps.rsform.models import Editor, Subscription + +from . import messages as msg +from . import models + + +class NonFieldErrorSerializer(serializers.Serializer): + ''' Serializer: list of non-field errors. ''' + non_field_errors = serializers.ListField( + child=serializers.CharField() + ) + + +class LoginSerializer(serializers.Serializer): + ''' Serializer: User authentication by login/password. ''' + username = serializers.CharField( + label='Имя пользователя', + write_only=True + ) + password = serializers.CharField( + label='Пароль', + style={'input_type': 'password'}, + trim_whitespace=False, + write_only=True + ) + + def validate(self, attrs): + username = attrs['username'] + if '@' in username: + user = models.User.objects.filter(email=username) + if not user.exists() or user.count() > 1: + raise serializers.ValidationError( + msg.passwordAuthFailed(), + code='authorization' + ) + username = user.first().username + password = attrs['password'] + authenticated = authenticate( + request=self.context.get('request'), + username=username, + password=password + ) + if not authenticated: + raise serializers.ValidationError( + msg.passwordAuthFailed(), + code='authorization' + ) + attrs['user'] = authenticated + return attrs + + +class AuthSerializer(serializers.Serializer): + ''' Serializer: Authorization data. ''' + id = serializers.IntegerField() + username = serializers.CharField() + is_staff = serializers.BooleanField() + subscriptions = serializers.ListField( + child=serializers.IntegerField() + ) + + def to_representation(self, instance: models.User) -> dict: + if instance.is_anonymous: + return { + 'id': None, + 'username': '', + 'is_staff': False, + 'subscriptions': [], + 'editor': [] + } + else: + return { + 'id': instance.pk, + 'username': instance.username, + 'is_staff': instance.is_staff, + 'subscriptions': [sub.item.pk for sub in Subscription.objects.filter(user=instance)], + 'editor': [edit.item.pk for edit in Editor.objects.filter(editor=instance)] + } + + +class UserInfoSerializer(serializers.ModelSerializer): + ''' Serializer: User data. ''' + class Meta: + ''' serializer metadata. ''' + model = models.User + fields = [ + 'id', + 'first_name', + 'last_name', + ] + + +class UserSerializer(serializers.ModelSerializer): + ''' Serializer: User data. ''' + id = serializers.IntegerField(read_only=True) + + class Meta: + ''' serializer metadata. ''' + model = models.User + fields = [ + 'id', + 'username', + 'email', + 'first_name', + 'last_name', + ] + + def validate(self, attrs): + attrs = super().validate(attrs) + if 'email' in attrs: + maybe_user = models.User.objects.filter(email=attrs['email']) + if maybe_user.exists(): + if maybe_user.count() > 1 or maybe_user.first().pk != self.context['request'].user.pk: + raise serializers.ValidationError({ + 'email': msg.emailAlreadyTaken() + }) + return attrs + + +class ChangePasswordSerializer(serializers.Serializer): + ''' Serializer: Change password. ''' + old_password = serializers.CharField(required=True) + new_password = serializers.CharField(required=True) + + +class SignupSerializer(serializers.ModelSerializer): + ''' Serializer: Create user profile. ''' + id = serializers.IntegerField(read_only=True) + password = serializers.CharField(write_only=True, required=True, validators=[validate_password]) + password2 = serializers.CharField(write_only=True, required=True) + + class Meta: + ''' serializer metadata. ''' + model = models.User + fields = [ + 'id', + 'username', + 'email', + 'first_name', + 'last_name', + 'password', + 'password2' + ] + + def validate(self, attrs): + if attrs['password'] != attrs['password2']: + raise serializers.ValidationError({ + 'password': msg.passwordsNotMatch() + }) + if models.User.objects.filter(email=attrs['email']).exists(): + raise serializers.ValidationError({ + 'email': msg.emailAlreadyTaken() + }) + return attrs + + def create(self, validated_data): + user = models.User.objects.create_user( + username=validated_data['username'], + email=validated_data['email'], + password=validated_data['password'] + ) + user.first_name = validated_data['first_name'] + user.last_name = validated_data['last_name'] + user.save() + return user diff --git a/rsconcept/backend/apps/users/signals.py b/rsconcept/backend/apps/users/signals.py new file mode 100644 index 00000000..522c6664 --- /dev/null +++ b/rsconcept/backend/apps/users/signals.py @@ -0,0 +1,36 @@ +''' Signals: user events. ''' +from django.core.mail import send_mail +from django.dispatch import receiver +from django.template.loader import render_to_string +from django.utils.html import strip_tags +from django_rest_passwordreset.signals import reset_password_token_created # type: ignore + +_EMAIL_NOREPLY = 'noreply.portal@yandex.ru' +_EMAIL_SUBJECT = 'Восстановление пароля КонцептПортал' +_EMAIL_TEMPLATE = 'password_reset_email.html' +_RECOVERY_URL = 'https://portal.acconcept.ru/password-change' + + +@receiver(reset_password_token_created) +def password_reset_token_created(sender, instance, reset_password_token, *args, **kwargs): + ''' + Handles password reset tokens + When a token is created, an e-mail needs to be sent to the user + ''' + context = { + 'current_user': reset_password_token.user, + 'username': reset_password_token.user.username, + 'first_name': reset_password_token.user.first_name, + 'email': reset_password_token.user.email, + 'reset_password_url': f'{_RECOVERY_URL}?token={reset_password_token.key}' + } + email_html_message = render_to_string(_EMAIL_TEMPLATE, context) + email_plaintext_message = strip_tags(email_html_message) + send_mail( + subject=_EMAIL_SUBJECT, + message=email_plaintext_message, + from_email=_EMAIL_NOREPLY, + recipient_list=[context['email']], + html_message=email_html_message, + fail_silently=False + ) diff --git a/rsconcept/backend/apps/users/templates/password_reset_email.html b/rsconcept/backend/apps/users/templates/password_reset_email.html new file mode 100644 index 00000000..b82ca7f3 --- /dev/null +++ b/rsconcept/backend/apps/users/templates/password_reset_email.html @@ -0,0 +1,26 @@ + + + + Восстановление пароля КонцептПортал + + +

Здравствуйте, {{first_name}}

+

+ Мы получили запрос на восстановление пароля для пользователя, привязанного + к данному email. +

+

+

Логин: {{username}}

+

+

+ Если Вы отправили указанный запрос, то перейдите по ссылке для задания + нового пароля: +

+

Сбросить пароль

+

+ Если Вы не отправляли запрос, то можете игнорировать данное сообщение. +

+

С уважением,

+

Команда КонцептПортал

+ + diff --git a/rsconcept/backend/apps/users/tests/__init__.py b/rsconcept/backend/apps/users/tests/__init__.py new file mode 100644 index 00000000..bdcf0305 --- /dev/null +++ b/rsconcept/backend/apps/users/tests/__init__.py @@ -0,0 +1,3 @@ +''' Tests. ''' +from .t_serializers import * +from .t_views import * diff --git a/rsconcept/backend/apps/users/tests/t_serializers.py b/rsconcept/backend/apps/users/tests/t_serializers.py new file mode 100644 index 00000000..fd12fad9 --- /dev/null +++ b/rsconcept/backend/apps/users/tests/t_serializers.py @@ -0,0 +1,37 @@ +''' Testing serializers ''' +from rest_framework.test import APIClient, APIRequestFactory, APITestCase + +from apps.users.models import User +from apps.users.serializers import LoginSerializer + + +class TestLoginSerializer(APITestCase): + def setUp(self): + self.user = User.objects.create_user(username='UserTest', password='123') + self.factory = APIRequestFactory() + self.client = APIClient() + + def test_validate(self): + data = {'username': 'UserTest', 'password': '123'} + request = self.factory.post('/users/api/login', data) + serializer = LoginSerializer(data=data, context={'request': request}) + self.assertTrue(serializer.is_valid(raise_exception=True)) + self.assertEqual(serializer.validated_data['user'], self.user) + + def test_validate_invalid_password(self): + data = {'username': 'UserTest', 'password': 'invalid'} + request = self.factory.post('/users/api/login', data) + serializer = LoginSerializer(data=data, context={'request': request}) + self.assertFalse(serializer.is_valid(raise_exception=False)) + + def test_validate_invalid_request(self): + data = {'username': 'UserTest', 'auth': 'invalid'} + request = self.factory.post('/users/api/login', data) + serializer = LoginSerializer(data=data, context={'request': request}) + self.assertFalse(serializer.is_valid(raise_exception=False)) + + def test_validate_empty_username(self): + data = {'username': '', 'auth': 'invalid'} + request = self.factory.post('/users/api/login', data) + serializer = LoginSerializer(data=data, context={'request': request}) + self.assertFalse(serializer.is_valid(raise_exception=False)) diff --git a/rsconcept/backend/apps/users/tests/t_views.py b/rsconcept/backend/apps/users/tests/t_views.py new file mode 100644 index 00000000..44551a5c --- /dev/null +++ b/rsconcept/backend/apps/users/tests/t_views.py @@ -0,0 +1,192 @@ +''' Testing API: users. ''' +from rest_framework.test import APIClient, APITestCase + +from apps.rsform.tests.EndpointTester import EndpointTester, decl_endpoint +from apps.users.models import User + + +class TestUserAPIViews(EndpointTester): + ''' Testing Authentication views. ''' + + def setUp(self): + super().setUp() + + + @decl_endpoint('/users/api/login', method='post') + def test_login(self): + self.logout() + data = {'username': self.user.username, 'password': 'invalid'} + self.executeBadData(data) + + data = {'username': self.user.username, 'password': 'password'} + self.executeAccepted(data) + self.executeAccepted(data) + + self.logout() + data = {'username': self.user.email, 'password': 'password'} + self.executeAccepted(data) + + + @decl_endpoint('/users/api/logout', method='post') + def test_logout(self): + self.logout() + self.executeForbidden() + + self.login() + self.executeNoContent() + self.executeNoContent() + + + @decl_endpoint('/users/api/auth', method='get') + def test_auth(self): + response = self.executeOK() + self.assertEqual(response.data['id'], self.user.pk) + self.assertEqual(response.data['username'], self.user.username) + self.assertEqual(response.data['is_staff'], self.user.is_staff) + self.assertEqual(response.data['subscriptions'], []) + self.assertEqual(response.data['editor'], []) + + self.logout() + response = self.executeOK() + self.assertEqual(response.data['id'], None) + self.assertEqual(response.data['username'], '') + self.assertEqual(response.data['is_staff'], False) + self.assertEqual(response.data['subscriptions'], []) + self.assertEqual(response.data['editor'], []) + + +class TestUserUserProfileAPIView(EndpointTester): + ''' Testing User profile views. ''' + + def setUp(self): + super().setUp() + self.user.first_name = 'John' + self.user.second_name = 'Smith' + self.user.save() + + + @decl_endpoint('/users/api/profile', method='get') + def test_read_profile(self): + response = self.executeOK() + self.assertEqual(response.data['username'], self.user.username) + self.assertEqual(response.data['email'], self.user.email) + self.assertEqual(response.data['first_name'], self.user.first_name) + self.assertEqual(response.data['last_name'], self.user.last_name) + + self.logout() + self.executeForbidden() + + + @decl_endpoint('/users/api/profile', method='patch') + def test_edit_profile(self): + data = { + 'email': '123@mail.ru', + 'first_name': 'firstName', + 'last_name': 'lastName', + } + response = self.executeOK(data) + self.user.refresh_from_db() + self.assertEqual(response.data['email'], '123@mail.ru') + self.assertEqual(self.user.email, '123@mail.ru') + self.assertEqual(response.data['first_name'], 'firstName') + self.assertEqual(self.user.first_name, 'firstName') + self.assertEqual(response.data['last_name'], 'lastName') + self.assertEqual(self.user.last_name, 'lastName') + + data = { + 'email': data['email'], + 'first_name': 'new', + 'last_name': 'new2', + } + self.executeOK(data) + + data = {'email': self.user2.email} + self.executeBadData(data) + + self.logout() + self.executeForbidden() + + + @decl_endpoint('/users/api/change-password', method='patch') + def test_change_password(self): + data = { + 'old_password': 'invalid', + 'new_password': 'password2' + } + self.executeBadData(data) + + data = { + 'old_password': 'password', + 'new_password': 'password2' + } + oldHash = self.user.password + response = self.executeNoContent(data) + self.user.refresh_from_db() + self.assertNotEqual(self.user.password, oldHash) + self.assertTrue(self.client.login(username=self.user.username, password='password2')) + self.assertFalse(self.client.login(username=self.user.username, password='password')) + + self.logout() + self.executeForbidden() + + + @decl_endpoint('/users/api/password-reset', method='post') + def test_password_reset_request(self): + self.executeBadData({'email': 'invalid@mail.ru'}) + self.executeOK({'email': self.user.email}) + # TODO: check if mail server actually sent email and if reset procedure works + + +class TestSignupAPIView(EndpointTester): + ''' Testing signup. ''' + + def setUp(self): + super().setUp() + + + @decl_endpoint('/users/api/signup', method='post') + def test_signup(self): + data = { + 'username': 'NewUser', + 'email': 'newMail@mail.ru', + 'password': 'Test@@123', + 'password2': 'Test@@123456', + 'first_name': 'firstName', + 'last_name': 'lastName' + } + self.executeBadData(data) + + data = { + 'username': 'NewUser', + 'email': 'newMail@mail.ru', + 'password': 'Test@@123', + 'password2': 'Test@@123', + 'first_name': 'firstName', + 'last_name': 'lastName' + } + response = self.executeCreated(data) + self.assertTrue('id' in response.data) + self.assertEqual(response.data['username'], data['username']) + self.assertEqual(response.data['email'], data['email']) + self.assertEqual(response.data['first_name'], data['first_name']) + self.assertEqual(response.data['last_name'], data['last_name']) + + data = { + 'username': 'NewUser', + 'email': 'newMail2@mail.ru', + 'password': 'Test@@123', + 'password2': 'Test@@123', + 'first_name': 'firstName', + 'last_name': 'lastName' + } + self.executeBadData(data) + + data = { + 'username': 'NewUser2', + 'email': self.user.email, + 'password': 'Test@@123', + 'password2': 'Test@@123', + 'first_name': 'firstName', + 'last_name': 'lastName' + } + self.executeBadData(data) diff --git a/rsconcept/backend/apps/users/urls.py b/rsconcept/backend/apps/users/urls.py new file mode 100644 index 00000000..4972e3c1 --- /dev/null +++ b/rsconcept/backend/apps/users/urls.py @@ -0,0 +1,21 @@ +''' Routing: User profile and Authorization. ''' +from django.urls import path +from django_rest_passwordreset.views import reset_password_confirm # type: ignore +from django_rest_passwordreset.views import reset_password_request_token # type: ignore +from django_rest_passwordreset.views import reset_password_validate_token # type: ignore + +from . import views + +urlpatterns = [ + path('api/auth', views.AuthAPIView.as_view()), + path('api/active-users', views.ActiveUsersView.as_view()), + path('api/profile', views.UserProfileAPIView.as_view()), + path('api/signup', views.SignupAPIView.as_view()), + path('api/login', views.LoginAPIView.as_view()), + path('api/logout', views.LogoutAPIView.as_view()), + path('api/change-password', views.UpdatePassword.as_view()), + # django_rest_passwordreset APIs see https://pypi.org/project/django-rest-passwordreset/ + path('api/password-reset', reset_password_request_token, name='reset-password-request'), + path('api/password-reset/validate', reset_password_validate_token, name='reset-password-validate'), + path('api/password-reset/confirm', reset_password_confirm, name='reset-password-confirm') +] diff --git a/rsconcept/backend/apps/users/views.py b/rsconcept/backend/apps/users/views.py new file mode 100644 index 00000000..ef67bb72 --- /dev/null +++ b/rsconcept/backend/apps/users/views.py @@ -0,0 +1,123 @@ +''' REST API: User profile and Authorization. ''' +from django.contrib.auth import login, logout +from drf_spectacular.utils import extend_schema, extend_schema_view +from rest_framework import generics, permissions +from rest_framework import status as c +from rest_framework import views +from rest_framework.response import Response + +from . import models as m +from . import serializers as s + + +class LoginAPIView(views.APIView): + ''' Endpoint: Login via username + password. ''' + permission_classes = (permissions.AllowAny,) + + @extend_schema( + summary='login user', + tags=['Auth'], + request=s.LoginSerializer, + responses={ + c.HTTP_202_ACCEPTED: None, + c.HTTP_400_BAD_REQUEST: s.NonFieldErrorSerializer + } + ) + def post(self, request): + serializer = s.LoginSerializer( + data=self.request.data, + context={'request': self.request} + ) + serializer.is_valid(raise_exception=True) + user = serializer.validated_data['user'] + login(request, user) + return Response(None, status=c.HTTP_202_ACCEPTED) + + +class LogoutAPIView(views.APIView): + ''' Endpoint: Logout. ''' + permission_classes = (permissions.IsAuthenticated,) + + @extend_schema( + summary='logout current user', + tags=['Auth'], + request=None, + responses={c.HTTP_204_NO_CONTENT: None} + ) + def post(self, request): + logout(request) + return Response(None, status=c.HTTP_204_NO_CONTENT) + + +@extend_schema(tags=['User']) +@extend_schema_view() +class SignupAPIView(generics.CreateAPIView): + ''' Endpoint: Register user. ''' + permission_classes = (permissions.AllowAny, ) + serializer_class = s.SignupSerializer + + +@extend_schema(tags=['Auth']) +@extend_schema_view() +class AuthAPIView(generics.RetrieveAPIView): + ''' Endpoint: Current user info. ''' + permission_classes = (permissions.AllowAny,) + serializer_class = s.AuthSerializer + + def get_object(self): + return self.request.user + + +@extend_schema(tags=['User']) +@extend_schema_view() +class ActiveUsersView(generics.ListAPIView): + ''' Endpoint: Get list of active users. ''' + permission_classes = (permissions.AllowAny,) + serializer_class = s.UserSerializer + + def get_queryset(self): + return m.User.objects.filter(is_active=True) + + +@extend_schema(tags=['User']) +@extend_schema_view() +class UserProfileAPIView(generics.RetrieveUpdateAPIView): + ''' Endpoint: User profile. ''' + permission_classes = (permissions.IsAuthenticated,) + serializer_class = s.UserSerializer + + def get_object(self): + return self.request.user + + +class UpdatePassword(views.APIView): + ''' Endpoint: Change password for current user. ''' + permission_classes = (permissions.IsAuthenticated, ) + + def get_object(self, queryset=None): + return self.request.user + + @extend_schema( + description='change current user password', + tags=['Auth'], + request=s.ChangePasswordSerializer, + responses={ + c.HTTP_204_NO_CONTENT: None, + c.HTTP_400_BAD_REQUEST: None + } + ) + def patch(self, request, *args, **kwargs): + self.object = self.get_object() + serializer = s.ChangePasswordSerializer(data=request.data) + if serializer.is_valid(): + old_password = serializer.data.get("old_password") + if not self.object.check_password(old_password): + return Response( + {"old_password": ["Wrong password."]}, + status=c.HTTP_400_BAD_REQUEST + ) + # Note: set_password also hashes the password that the user will get + self.object.set_password(serializer.data.get("new_password")) + self.object.save() + return Response(status=c.HTTP_204_NO_CONTENT) + return Response(serializer.errors, status=c.HTTP_400_BAD_REQUEST) diff --git a/rsconcept/backend/entrypoint.sh b/rsconcept/backend/entrypoint.sh new file mode 100644 index 00000000..e1a33d1f --- /dev/null +++ b/rsconcept/backend/entrypoint.sh @@ -0,0 +1,19 @@ +# Before doing anything wait for database to come online +if [ "$DB_ENGINE" = "django.db.backends.postgresql_psycopg2" ] +then + echo "Waiting DB..." + + while ! nc -z $DB_HOST $DB_PORT; + do + sleep 0.1 + done + + echo "Ready!" +fi + +cd $APP_HOME +python3.12 $APP_HOME/manage.py collectstatic --noinput --clear +python3.12 $APP_HOME/manage.py migrate + +# Execute given input command +exec "$@" \ No newline at end of file diff --git a/rsconcept/backend/fixtures/InitialData.json b/rsconcept/backend/fixtures/InitialData.json new file mode 100644 index 00000000..cfb7982c --- /dev/null +++ b/rsconcept/backend/fixtures/InitialData.json @@ -0,0 +1,4786 @@ +[ +{ + "model": "auth.user", + "pk": 1, + "fields": { + "password": "pbkdf2_sha256$720000$gFZkaBswurtL0naQKiUnW7$3b6SUN3fY2Xl1H7erAszVpQl2LpoKusan+yJP7Bp3JA=", + "last_login": "2024-06-03T20:57:02.522Z", + "is_superuser": true, + "username": "admin", + "first_name": "Администратор", + "last_name": "", + "email": "admin@mail.ru", + "is_staff": true, + "is_active": true, + "date_joined": "2024-05-27T18:31:48.913Z", + "groups": [], + "user_permissions": [] + } +}, +{ + "model": "auth.user", + "pk": 3, + "fields": { + "password": "pbkdf2_sha256$720000$UrGOcoUoKyC5A0nCXwNszp$UR3FdvkB9EkQ8PT+mJ7CtCyrwbmmz3tYZcZKTaxyzoM=", + "last_login": "2024-06-03T20:57:25.539Z", + "is_superuser": false, + "username": "User1", + "first_name": "User1", + "last_name": "", + "email": "User1@test.ru", + "is_staff": false, + "is_active": true, + "date_joined": "2024-06-03T20:52:55.670Z", + "groups": [], + "user_permissions": [] + } +}, +{ + "model": "auth.user", + "pk": 4, + "fields": { + "password": "pbkdf2_sha256$720000$2gYTKuX3Wiap5y3AokUpub$PExwc31nwYTLFOZDwHpg5loC8WOi//EZvSJ8V9TCmIs=", + "last_login": null, + "is_superuser": false, + "username": "User2", + "first_name": "User2", + "last_name": "", + "email": "User2@test.ru", + "is_staff": false, + "is_active": true, + "date_joined": "2024-06-03T20:53:20.356Z", + "groups": [], + "user_permissions": [] + } +}, +{ + "model": "auth.user", + "pk": 5, + "fields": { + "password": "pbkdf2_sha256$720000$uoCzGQ7zxoJOqZ3d8y99nl$d0frHrK9a4ydo0iNp4kGLAsq5eZ4QpQl1lU2bXR3elI=", + "last_login": "2024-06-03T20:53:51.231Z", + "is_superuser": false, + "username": "User3", + "first_name": "User3", + "last_name": "", + "email": "User3@test.ru", + "is_staff": false, + "is_active": true, + "date_joined": "2024-06-03T20:53:45.900Z", + "groups": [], + "user_permissions": [] + } +}, +{ + "model": "rsform.constituenta", + "pk": 589, + "fields": { + "schema": 34, + "order": 1, + "alias": "T1", + "cst_type": "theorem", + "convention": "", + "term_raw": "Управляющие конструкции", + "term_resolved": "Управляющие конструкции", + "term_forms": [], + "definition_formal": "", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 590, + "fields": { + "schema": 34, + "order": 2, + "alias": "F1", + "cst_type": "function", + "convention": "", + "term_raw": "схема ограниченного выделения", + "term_resolved": "схема ограниченного выделения", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1)] D{ ξ∈α | ξ=ξ }", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 591, + "fields": { + "schema": 34, + "order": 3, + "alias": "F2", + "cst_type": "function", + "convention": "", + "term_raw": "условный переход", + "term_resolved": "условный переход", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1)] debool(I{ α | 1=1} ∪ I{ α\\α | 1≠1})", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 592, + "fields": { + "schema": 34, + "order": 4, + "alias": "F3", + "cst_type": "function", + "convention": "", + "term_raw": "рекурсивное определение", + "term_resolved": "рекурсивное определение", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1)] R{ ξ:=α | ξ≠∅ | ξ\\ξ }", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 593, + "fields": { + "schema": 34, + "order": 5, + "alias": "T2", + "cst_type": "theorem", + "convention": "", + "term_raw": "Бинарные отношения двух множеств", + "term_resolved": "Бинарные отношения двух множеств", + "term_forms": [], + "definition_formal": "", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 594, + "fields": { + "schema": 34, + "order": 6, + "alias": "F4", + "cst_type": "function", + "convention": "", + "term_raw": "обратное отношение", + "term_resolved": "обратное отношение", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×R2)] Pr2,1(σ)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 595, + "fields": { + "schema": 34, + "order": 7, + "alias": "P1", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство всюдуопределенности", + "term_resolved": "свойство всюдуопределенности", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), σ∈ℬ(R1×R2)] Pr1(σ) = α", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 596, + "fields": { + "schema": 34, + "order": 8, + "alias": "P2", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство всюдузначности", + "term_resolved": "свойство всюдузначности", + "term_forms": [], + "definition_formal": "[α∈ℬ(R2), σ∈ℬ(R1×R2)] Pr2(σ) = α", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 597, + "fields": { + "schema": 34, + "order": 9, + "alias": "P3", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство прямой однозначности", + "term_resolved": "свойство прямой однозначности", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×R2)] card(Pr1(σ)) = card(σ)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 598, + "fields": { + "schema": 34, + "order": 10, + "alias": "P4", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство обратной однозначности", + "term_resolved": "свойство обратной однозначности", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×R2)] card(Pr2(σ)) = card(σ)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 599, + "fields": { + "schema": 34, + "order": 11, + "alias": "P5", + "cst_type": "predicate", + "convention": "", + "term_raw": "функция", + "term_resolved": "функция", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), σ∈ℬ(R1×R2)] card(Pr1(σ)) = card(σ) & Pr1(σ) = α", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 600, + "fields": { + "schema": 34, + "order": 12, + "alias": "F5", + "cst_type": "function", + "convention": "", + "term_raw": "образ элемента", + "term_resolved": "образ элемента", + "term_forms": [], + "definition_formal": "[α∈R1, σ∈ℬ(R1×R2)] debool(Pr2(Fi1[{α}](σ)))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 601, + "fields": { + "schema": 34, + "order": 13, + "alias": "F6", + "cst_type": "function", + "convention": "", + "term_raw": "сечение по элементу", + "term_resolved": "сечение по элементу", + "term_forms": [], + "definition_formal": "[α∈R1, σ∈ℬ(R1×R2)] Pr2(Fi1[{α}](σ))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 602, + "fields": { + "schema": 34, + "order": 14, + "alias": "F7", + "cst_type": "function", + "convention": "", + "term_raw": "прообраз значения", + "term_resolved": "прообраз значения", + "term_forms": [], + "definition_formal": "[α∈R2, σ∈ℬ(R1×R2)] debool(Pr1(Fi2[{α}](σ)))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 603, + "fields": { + "schema": 34, + "order": 15, + "alias": "F8", + "cst_type": "function", + "convention": "", + "term_raw": "сечение по значению", + "term_resolved": "сечение по значению", + "term_forms": [], + "definition_formal": "[α∈R2, σ∈ℬ(R1×R2)] Pr1(Fi2[{α}](σ))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 604, + "fields": { + "schema": 34, + "order": 16, + "alias": "F9", + "cst_type": "function", + "convention": "", + "term_raw": "образ множества", + "term_resolved": "образ множества", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), σ∈ℬ(R1×R2)] Pr2(Fi1[α](σ))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 605, + "fields": { + "schema": 34, + "order": 17, + "alias": "F10", + "cst_type": "function", + "convention": "", + "term_raw": "прообраз множества", + "term_resolved": "прообраз множества", + "term_forms": [], + "definition_formal": "[α∈ℬ(R2), σ∈ℬ(R1×R2)] Pr1(Fi2[α](σ))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 606, + "fields": { + "schema": 34, + "order": 18, + "alias": "F11", + "cst_type": "function", + "convention": "", + "term_raw": "композиция отношений", + "term_resolved": "композиция отношений", + "term_forms": [], + "definition_formal": "[σ1∈ℬ(R1×R2), σ2∈ℬ(R2×R3)] \nI{(ξ1, ξ3) | (ξ1,ξ2):∈σ1; ξ3:∈Pr2(Fi1[{ξ2}](σ2))}", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 607, + "fields": { + "schema": 34, + "order": 19, + "alias": "T3", + "cst_type": "theorem", + "convention": "", + "term_raw": "Бинарные отношения на множестве", + "term_resolved": "Бинарные отношения на множестве", + "term_forms": [], + "definition_formal": "T2", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 608, + "fields": { + "schema": 34, + "order": 20, + "alias": "F12", + "cst_type": "function", + "convention": "", + "term_raw": "участники отношения", + "term_resolved": "участники отношения", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×R1)] Pr1(σ)∪Pr2(σ)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 609, + "fields": { + "schema": 34, + "order": 21, + "alias": "P6", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство полноты (линейности)", + "term_resolved": "свойство полноты (линейности)", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), σ∈ℬ(R1×R1)] σ∪Pr2,1(σ)∪Pr1,1(σ)∪Pr2,2(σ) = α×α", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 610, + "fields": { + "schema": 34, + "order": 22, + "alias": "P7", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство симметричности", + "term_resolved": "свойство симметричности", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×R1)] Pr2,1(σ) = σ", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 611, + "fields": { + "schema": 34, + "order": 23, + "alias": "P8", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство антисимметричности", + "term_resolved": "свойство антисимметричности", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×R1)] (Pr2,1(σ)∩σ)\\Pr1,1(σ) = ∅", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 612, + "fields": { + "schema": 34, + "order": 24, + "alias": "P9", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство рефлексивности", + "term_resolved": "свойство рефлексивности", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×R1)] Pr1,1(σ) ⊆ σ", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 613, + "fields": { + "schema": 34, + "order": 25, + "alias": "P10", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство антирефлексивности", + "term_resolved": "свойство антирефлексивности", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×R1)] Pr1,1(σ)∩σ = ∅", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 614, + "fields": { + "schema": 34, + "order": 26, + "alias": "P11", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство транзитивности", + "term_resolved": "свойство транзитивности", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×R1)] ∀(α1,α2),(β1,β2)∈σ (α2=β1 ⇒ (α1,β2)∈σ)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 615, + "fields": { + "schema": 34, + "order": 27, + "alias": "P12", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство фундированности", + "term_resolved": "свойство фундированности", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), σ∈ℬ(R1×R1)] ∀γ∈ℬ(α) (γ≠∅ ⇒ card(F13[γ,σ])>0)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 616, + "fields": { + "schema": 34, + "order": 28, + "alias": "P13", + "cst_type": "predicate", + "convention": "", + "term_raw": "существование infinum для отношения на данном !подмножестве!", + "term_resolved": "существование infinum для отношения на данном !подмножестве!", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), σ∈ℬ(R1×R1)] ∀ξ1,ξ2∈α ∃ε∈α ((ε,ξ1)∈σ & (ε,ξ2)∈σ & ∀ξ3∈α\\{ε} ((ξ3,ξ1)∈σ & (ξ3,ξ2)∈σ ⇒ (ξ3,ε)∈σ))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 617, + "fields": { + "schema": 34, + "order": 29, + "alias": "P14", + "cst_type": "predicate", + "convention": "", + "term_raw": "существование supremum для отношения на данном !подмножестве!", + "term_resolved": "существование supremum для отношения на данном !подмножестве!", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), σ∈ℬ(R1×R1)] ∀ξ1,ξ2∈α ∃π∈α ((ξ1,π)∈σ & (ξ2,π)∈σ & ∀ξ3∈α\\{π} ((ξ1,ξ3)∈σ & (ξ2,ξ3)∈σ ⇒ (π,ξ3)∈σ))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 618, + "fields": { + "schema": 34, + "order": 30, + "alias": "P15", + "cst_type": "predicate", + "convention": "", + "term_raw": "частичный порядок", + "term_resolved": "частичный порядок", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×R1)]\nPr2,1(σ)∩σ\\Pr1,1(σ) = ∅ & \n(Pr1,1(σ)∩σ = ∅ ∨ Pr1,1(σ) ⊆ σ) & \n∀(α1,α2),(β1,β2)∈σ (α2=β1 ⇒ (α1,β2)∈σ)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 619, + "fields": { + "schema": 34, + "order": 31, + "alias": "P16", + "cst_type": "predicate", + "convention": "", + "term_raw": "линейный порядок", + "term_resolved": "линейный порядок", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), σ∈ℬ(R1×R1)]\nPr2,1(σ)∩σ\\Pr1,1(σ) = ∅ & \n(Pr1,1(σ)∩σ = ∅ ∨ Pr1,1(σ) ⊆ σ) & \n∀(α1,α2),(β1,β2)∈σ (α2=β1 ⇒ (α1,β2)∈σ) & \nσ∪Pr2,1(σ)∪Pr1,1(σ)∪Pr2,2(σ) = α×α", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 620, + "fields": { + "schema": 34, + "order": 32, + "alias": "F13", + "cst_type": "function", + "convention": "альтернативное определение:\n[α∈ℬ(R1), σ∈ℬ(R1×R1)] D{ξ∈α | ∀ω∈α\\{ξ} (ω, ξ)∉σ}", + "term_raw": "минимальные элементы !порядка! на данном !подмножестве!", + "term_resolved": "минимальные элементы !порядка! на данном !подмножестве!", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), σ∈ℬ(R1×R1)] α \\ Pr2(Fi1,2[α, α](σ))", + "definition_raw": "элементы, для которых отсутствуют большие их элементы", + "definition_resolved": "элементы, для которых отсутствуют большие их элементы" + } +}, +{ + "model": "rsform.constituenta", + "pk": 621, + "fields": { + "schema": 34, + "order": 33, + "alias": "F14", + "cst_type": "function", + "convention": "альтернативное определение:\n[α∈ℬ(R1), σ∈ℬ(R1×R1)] debool(D{ξ∈α | ∀ω∈α\\{ξ} (ξ, ω)∈σ})", + "term_raw": "наименьший элемент !порядка! на данном !подмножестве!", + "term_resolved": "наименьший элемент !порядка! на данном !подмножестве!", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), σ∈ℬ(R1×R1)] debool(I{ β | δ1:=Fi1,2[α, α](σ); δ2:=δ1\\Pr1,1(δ1); β:=Pr1(δ2)\\Pr2(δ2) })", + "definition_raw": "элементы, меньшие или равные всем остальным", + "definition_resolved": "элементы, меньшие или равные всем остальным" + } +}, +{ + "model": "rsform.constituenta", + "pk": 622, + "fields": { + "schema": 34, + "order": 34, + "alias": "P17", + "cst_type": "predicate", + "convention": "", + "term_raw": "эквивалентность", + "term_resolved": "эквивалентность", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×R1)] Pr1,1(σ) ⊆ σ & Pr2,1(σ) = σ & \n∀(α1,α2),(β1,β2)∈σ (α2=β1 ⇒ (α1,β2)∈σ)", + "definition_raw": "транзитивное рефлексивное симметричное бинарное отношение", + "definition_resolved": "транзитивное рефлексивное симметричное бинарное отношение" + } +}, +{ + "model": "rsform.constituenta", + "pk": 623, + "fields": { + "schema": 34, + "order": 35, + "alias": "F15", + "cst_type": "function", + "convention": "", + "term_raw": "класс эквивалентности !элемента!", + "term_resolved": "класс эквивалентности !элемента!", + "term_forms": [], + "definition_formal": "[α∈R1, σ∈ℬ(R1×R1)] Pr2(Fi1[{α}](σ))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 624, + "fields": { + "schema": 34, + "order": 36, + "alias": "P18", + "cst_type": "predicate", + "convention": "", + "term_raw": "толерантность", + "term_resolved": "толерантность", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×R1)] Pr1,1(σ) ⊆ σ & Pr2,1(σ) = σ", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 625, + "fields": { + "schema": 34, + "order": 37, + "alias": "T4", + "cst_type": "theorem", + "convention": "", + "term_raw": "Цепочки", + "term_resolved": "Цепочки", + "term_forms": [], + "definition_formal": "T2 & T3", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 626, + "fields": { + "schema": 34, + "order": 38, + "alias": "P19", + "cst_type": "predicate", + "convention": "", + "term_raw": "условие связности цепочки", + "term_resolved": "условие связности цепочки", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×R1)]\ncard(Pr1(σ)) = card(σ) & card(Pr2(σ)) = card(σ) &\ncard(Pr1(σ) \\ Pr2(σ)) = 1 & card(Pr2(σ) \\ Pr1(σ)) = 1", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 627, + "fields": { + "schema": 34, + "order": 39, + "alias": "F16", + "cst_type": "function", + "convention": "", + "term_raw": "начало цепочки", + "term_resolved": "начало цепочки", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×R1)] debool(Pr1(σ) \\ Pr2(σ))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 628, + "fields": { + "schema": 34, + "order": 40, + "alias": "F17", + "cst_type": "function", + "convention": "", + "term_raw": "конец цепочки", + "term_resolved": "конец цепочки", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×R1)] debool(Pr2(σ) \\ Pr1(σ))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 629, + "fields": { + "schema": 34, + "order": 41, + "alias": "F18", + "cst_type": "function", + "convention": "", + "term_raw": "следующий за данным !элемент! цепочки", + "term_resolved": "следующий за данным !элемент! цепочки", + "term_forms": [], + "definition_formal": "[α∈R1, σ∈ℬ(R1×R1)] debool(Pr2(Fi1[{α}](σ)))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 630, + "fields": { + "schema": 34, + "order": 42, + "alias": "F19", + "cst_type": "function", + "convention": "", + "term_raw": "предшествующий данному !элемент! цепочки", + "term_resolved": "предшествующий данному !элемент! цепочки", + "term_forms": [], + "definition_formal": "[α∈R1, σ∈ℬ(R1×R1)] debool(Pr1(Fi2[{α}](σ)))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 631, + "fields": { + "schema": 34, + "order": 43, + "alias": "T5", + "cst_type": "theorem", + "convention": "", + "term_raw": "Графы", + "term_resolved": "Графы", + "term_forms": [], + "definition_formal": "T2 & T3", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 632, + "fields": { + "schema": 34, + "order": 44, + "alias": "P20", + "cst_type": "predicate", + "convention": "", + "term_raw": "неориентированный граф", + "term_resolved": "неориентированный граф", + "term_forms": [], + "definition_formal": "[γ∈ℬ(R1)×ℬ(R1×R1)] pr2(γ) = Pr2,1(pr2(γ))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 633, + "fields": { + "schema": 34, + "order": 45, + "alias": "F20", + "cst_type": "function", + "convention": "", + "term_raw": "истоки", + "term_resolved": "истоки", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×R1)] Pr1(σ) \\ Pr2(σ)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 634, + "fields": { + "schema": 34, + "order": 46, + "alias": "F21", + "cst_type": "function", + "convention": "", + "term_raw": "стоки", + "term_resolved": "стоки", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×R1)] Pr2(σ) \\ Pr1(σ)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 635, + "fields": { + "schema": 34, + "order": 47, + "alias": "F22", + "cst_type": "function", + "convention": "", + "term_raw": "изолированные вершины", + "term_resolved": "изолированные вершины", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), σ∈ℬ(R1×R1)] α \\ (Pr2(σ) ∪ Pr1(σ))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 636, + "fields": { + "schema": 34, + "order": 48, + "alias": "F23", + "cst_type": "function", + "convention": "", + "term_raw": "потребители", + "term_resolved": "потребители", + "term_forms": [], + "definition_formal": "[α∈R1, σ∈ℬ(R1×R1)] Pr2(Fi1[{α}](σ))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 637, + "fields": { + "schema": 34, + "order": 49, + "alias": "F24", + "cst_type": "function", + "convention": "", + "term_raw": "поставщики", + "term_resolved": "поставщики", + "term_forms": [], + "definition_formal": "[α∈R1, σ∈ℬ(R1×R1)] Pr1(Fi2[{α}](σ))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 638, + "fields": { + "schema": 34, + "order": 50, + "alias": "F25", + "cst_type": "function", + "convention": "", + "term_raw": "зависимые вершины", + "term_resolved": "зависимые вершины", + "term_forms": [], + "definition_formal": "[α∈R1, σ∈ℬ(R1×R1)] R{ ξ:=Pr2(Fi1[{α}](σ)) | ξ ∪ Pr2(Fi1[ξ](σ)) }", + "definition_raw": "вершины, достижимые из данной", + "definition_resolved": "вершины, достижимые из данной" + } +}, +{ + "model": "rsform.constituenta", + "pk": 639, + "fields": { + "schema": 34, + "order": 51, + "alias": "F26", + "cst_type": "function", + "convention": "", + "term_raw": "зависимые вершины совокупности", + "term_resolved": "зависимые вершины совокупности", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), σ∈ℬ(R1×R1)] R{ ξ:=Pr2(Fi1[α](σ)) | ξ ∪ Pr2(Fi1[ξ](σ)) }", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 640, + "fields": { + "schema": 34, + "order": 52, + "alias": "F27", + "cst_type": "function", + "convention": "", + "term_raw": "влияющие вершины", + "term_resolved": "влияющие вершины", + "term_forms": [], + "definition_formal": "[α∈R1, σ∈ℬ(R1×R1)] R{ ξ:=Pr1(Fi2[{α}](σ)) | ξ ∪ Pr1(Fi2[ξ](σ)) }", + "definition_raw": "вершины, из которых данная вершина достижима", + "definition_resolved": "вершины, из которых данная вершина достижима" + } +}, +{ + "model": "rsform.constituenta", + "pk": 641, + "fields": { + "schema": 34, + "order": 53, + "alias": "F28", + "cst_type": "function", + "convention": "", + "term_raw": "влияющие вершины совокупности", + "term_resolved": "влияющие вершины совокупности", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), σ∈ℬ(R1×R1)] R{ ξ:=Pr1(Fi2[α](σ)) | ξ ∪ Pr1(Fi2[ξ](σ)) }", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 642, + "fields": { + "schema": 34, + "order": 54, + "alias": "P21", + "cst_type": "predicate", + "convention": "", + "term_raw": "цикл", + "term_resolved": "цикл", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×R1)] Pr1(σ) = Pr2(σ)", + "definition_raw": "совокупность связей, в которой из каждой вершины в каждую другую есть путь", + "definition_resolved": "совокупность связей, в которой из каждой вершины в каждую другую есть путь" + } +}, +{ + "model": "rsform.constituenta", + "pk": 643, + "fields": { + "schema": 34, + "order": 55, + "alias": "P22", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство ацикличности", + "term_resolved": "свойство ацикличности", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×R1)] ∀γ∈Pr1(σ) γ∉R{ ξ:=Pr2(Fi1[{γ}](σ)) | ξ ∪ Pr2(Fi1[ξ](σ)) }", + "definition_raw": "в ацикличном графе никакая вершина не является собственной зависимой вершиной", + "definition_resolved": "в ацикличном графе никакая вершина не является собственной зависимой вершиной" + } +}, +{ + "model": "rsform.constituenta", + "pk": 644, + "fields": { + "schema": 34, + "order": 56, + "alias": "P23", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство ацикличности (декларативная формулировка)", + "term_resolved": "свойство ацикличности (декларативная формулировка)", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×R1)] ∀γ∈ℬ(σ) Pr1(γ) ≠ Pr2(γ)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 645, + "fields": { + "schema": 34, + "order": 57, + "alias": "F29", + "cst_type": "function", + "convention": "", + "term_raw": "транзитивное замыкание", + "term_resolved": "транзитивное замыкание", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×R1)] I{(ξ1, ξ2) | ξ1:∈Pr1(σ); ξ2:∈F25[ξ1, σ]}", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 646, + "fields": { + "schema": 34, + "order": 58, + "alias": "F30", + "cst_type": "function", + "convention": "", + "term_raw": "транзитивная редукция", + "term_resolved": "транзитивная редукция", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×R1)] σ \\ I{(ξ1, ξ2) | ξ1:∈Pr1(σ); γ:=F23[ξ1, σ]; ξ2:∈γ∩F26[γ, σ]}", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 647, + "fields": { + "schema": 34, + "order": 59, + "alias": "P24", + "cst_type": "predicate", + "convention": "1", + "term_raw": "дерево", + "term_resolved": "дерево", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×R1)] card(Pr1(σ)\\Pr2(σ)) = 1 & card(Pr2(σ)) = card(σ)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 648, + "fields": { + "schema": 34, + "order": 60, + "alias": "T6", + "cst_type": "theorem", + "convention": "", + "term_raw": "Целые числа", + "term_resolved": "Целые числа", + "term_forms": [], + "definition_formal": "", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 649, + "fields": { + "schema": 34, + "order": 61, + "alias": "F31", + "cst_type": "function", + "convention": "", + "term_raw": "минимум набора чисел", + "term_resolved": "минимум набора чисел", + "term_forms": [], + "definition_formal": "[σ∈ℬ(Z)] debool(D{ξ∈σ | ∀α∈σ α ≤ ξ})", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 650, + "fields": { + "schema": 34, + "order": 62, + "alias": "F32", + "cst_type": "function", + "convention": "", + "term_raw": "максимум набора чисел", + "term_resolved": "максимум набора чисел", + "term_forms": [], + "definition_formal": "[σ∈ℬ(Z)] debool(D{ξ∈σ | ∀α∈σ α ≥ ξ})", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 651, + "fields": { + "schema": 34, + "order": 63, + "alias": "F33", + "cst_type": "function", + "convention": "", + "term_raw": "натуральные числа, меньшие или равные данному", + "term_resolved": "натуральные числа, меньшие или равные данному", + "term_forms": [], + "definition_formal": "[α∈Z] R{(ξ,σ):=(0, {0}) | ξ<α | (ξ+1, σ ∪ {ξ+1})}", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 652, + "fields": { + "schema": 34, + "order": 64, + "alias": "P25", + "cst_type": "predicate", + "convention": "", + "term_raw": "набор чисел, удовлетворяющий данному периоду", + "term_resolved": "набор чисел, удовлетворяющий данному периоду", + "term_forms": [], + "definition_formal": "[σ∈ℬ(Z), τ∈Z] ∀α∈σ (α+τ ≤ F32[σ] ⇒ α+τ∈σ)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 653, + "fields": { + "schema": 34, + "order": 65, + "alias": "F34", + "cst_type": "function", + "convention": "", + "term_raw": "периоды данного набора чисел", + "term_resolved": "периоды данного набора чисел", + "term_forms": [], + "definition_formal": "[σ∈ℬ(Z)] pr1(R{\n(γ, τ) := (∅, F31[σ]) | \nτ ≠ F32[σ] | \n(γ ∪ I{τ | P25[σ, τ]}, τ + 1)})", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 654, + "fields": { + "schema": 34, + "order": 66, + "alias": "T7", + "cst_type": "theorem", + "convention": "", + "term_raw": "Последовательности", + "term_resolved": "Последовательности", + "term_forms": [], + "definition_formal": "", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 655, + "fields": { + "schema": 34, + "order": 67, + "alias": "P26", + "cst_type": "predicate", + "convention": "", + "term_raw": "корректная последовательность (позиции нумеруются с нуля)", + "term_resolved": "корректная последовательность (позиции нумеруются с нуля)", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×Z)] card(σ)=card(Pr2(σ)) & ∀λ∈Pr2(σ) λ < card(σ)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 656, + "fields": { + "schema": 34, + "order": 68, + "alias": "P27", + "cst_type": "predicate", + "convention": "", + "term_raw": "корректная последовательность (позиции нумеруются с единицы)", + "term_resolved": "корректная последовательность (позиции нумеруются с единицы)", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×Z)] card(σ)=card(Pr2(σ)) & ∀λ∈Pr2(σ) (λ ≤ card(σ) & λ > 0)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 657, + "fields": { + "schema": 34, + "order": 69, + "alias": "F35", + "cst_type": "function", + "convention": "", + "term_raw": "начало последовательности", + "term_resolved": "начало последовательности", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×Z)] debool(Pr1(Fi2[{0}](σ)))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 658, + "fields": { + "schema": 34, + "order": 70, + "alias": "F36", + "cst_type": "function", + "convention": "", + "term_raw": "конец последовательности", + "term_resolved": "конец последовательности", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×Z)] debool(Pr1(Fi2[{card(σ) - 1}](σ)))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 659, + "fields": { + "schema": 34, + "order": 71, + "alias": "F37", + "cst_type": "function", + "convention": "", + "term_raw": "следующий за данным !элемент! последовательности", + "term_resolved": "следующий за данным !элемент! последовательности", + "term_forms": [], + "definition_formal": "[α∈R1×Z, σ∈ℬ(R1×Z)] debool(Fi2[{pr2(α) + 1}](σ))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 660, + "fields": { + "schema": 34, + "order": 72, + "alias": "F38", + "cst_type": "function", + "convention": "", + "term_raw": "предыдущий данному !элемент! последовательности", + "term_resolved": "предыдущий данному !элемент! последовательности", + "term_forms": [], + "definition_formal": "[α∈R1×Z, σ∈ℬ(R1×Z)] debool(Fi2[{pr2(α) - 1}](σ))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 661, + "fields": { + "schema": 34, + "order": 73, + "alias": "F39", + "cst_type": "function", + "convention": "", + "term_raw": "обращенная последовательность", + "term_resolved": "обращенная последовательность", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×Z)] I{(α, λ) | (α,μ):∈σ; λ := card(σ) - μ - 1}", + "definition_raw": "не определен для последнего элемента", + "definition_resolved": "не определен для последнего элемента" + } +}, +{ + "model": "rsform.constituenta", + "pk": 662, + "fields": { + "schema": 34, + "order": 74, + "alias": "P28", + "cst_type": "predicate", + "convention": "", + "term_raw": "подпоследовательность", + "term_resolved": "подпоследовательность", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×Z)] card(σ) = card(Pr2(σ))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 663, + "fields": { + "schema": 34, + "order": 75, + "alias": "F40", + "cst_type": "function", + "convention": "", + "term_raw": "последовательность, построенная из данной подпоследовательности", + "term_resolved": "последовательность, построенная из данной подпоследовательности", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×Z)] pr1(R{\n(γ, α):=(∅, σ) | α≠∅ | \ndebool(I{(γ2, α2) | τ := F31[Pr2(α)]; ξ := debool(F8[τ,σ]); \nγ2 := γ ∪ {(ξ, card(γ))}; α2 := α \\ {(ξ,τ)}})})", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 664, + "fields": { + "schema": 34, + "order": 76, + "alias": "T8", + "cst_type": "theorem", + "convention": "Условия для \"левых\" и \"правых\" свойств через конкатенацию", + "term_raw": "Групповые операции", + "term_resolved": "Групповые операции", + "term_forms": [], + "definition_formal": "T10", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 665, + "fields": { + "schema": 34, + "order": 77, + "alias": "F41", + "cst_type": "function", + "convention": "", + "term_raw": "результат операции для данных аргументов", + "term_resolved": "результат операции для данных аргументов", + "term_forms": [], + "definition_formal": "[α∈R1, β∈R1, σ∈ℬ((R1×R1)×R1)] debool(Pr2(Fi1[{(α,β)}](σ)))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 666, + "fields": { + "schema": 34, + "order": 78, + "alias": "F42", + "cst_type": "function", + "convention": "например ноль для операции умножения целых чисел", + "term_raw": "инвариантные элементы", + "term_resolved": "инвариантные элементы", + "term_forms": [], + "definition_formal": "[σ∈ℬ((R1×R1)×R1)] D{ξ∈Pr1(Pr1(σ)) | ∀α∈Pr1(Pr1(σ)) \n(((α,ξ),ξ)∈σ & ((ξ,α),ξ)∈σ)}", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 667, + "fields": { + "schema": 34, + "order": 79, + "alias": "F43", + "cst_type": "function", + "convention": "", + "term_raw": "нейтральные элементы", + "term_resolved": "нейтральные элементы", + "term_forms": [], + "definition_formal": "[σ∈ℬ((R1×R1)×R1)] D{ξ∈Pr1(Pr1(σ)) | ∀α∈Pr1(Pr1(σ)) \n(((α,ξ),α)∈σ & ((ξ,α),α)∈σ)}", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 668, + "fields": { + "schema": 34, + "order": 80, + "alias": "F44", + "cst_type": "function", + "convention": "", + "term_raw": "идемпотентные элементы", + "term_resolved": "идемпотентные элементы", + "term_forms": [], + "definition_formal": "[σ∈ℬ((R1×R1)×R1)] D{ξ∈Pr1(Pr1(σ)) | ((ξ,ξ),ξ)∈σ}", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 669, + "fields": { + "schema": 34, + "order": 81, + "alias": "F45", + "cst_type": "function", + "convention": "", + "term_raw": "обратные элементы для данного элемента", + "term_resolved": "обратные элементы для данного элемента", + "term_forms": [], + "definition_formal": "[α∈R1, σ∈ℬ((R1×R1)×R1)] D{ω∈Pr1(Pr1(σ)) | F41[α,ω,σ]∈F43[σ] & F41[ω,α,σ]∈F43[σ]}", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 670, + "fields": { + "schema": 34, + "order": 82, + "alias": "F46", + "cst_type": "function", + "convention": "", + "term_raw": "элемент, являющийся данной степенью данного элемента", + "term_resolved": "элемент, являющийся данной степенью данного элемента", + "term_forms": [], + "definition_formal": "[α∈R1, λ∈Z, σ∈ℬ((R1×R1)×R1)] pr1(R{(ξ, μ) := (α, 1) | μ<λ & λ>0 | (F41[ξ,α,σ], μ + 1)})", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 671, + "fields": { + "schema": 34, + "order": 83, + "alias": "P29", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство замкнутости и однозначности", + "term_resolved": "свойство замкнутости и однозначности", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), σ∈ℬ((R1×R1)×R1)] Pr1(σ) = α×α & card(σ) = card(Pr1(σ))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 672, + "fields": { + "schema": 34, + "order": 84, + "alias": "P30", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство ассоциативности", + "term_resolved": "свойство ассоциативности", + "term_forms": [], + "definition_formal": "[σ∈ℬ((R1×R1)×R1)] ∀α,β,γ∈Pr1(Pr1(σ)) F41[F41[α,β,σ],γ,σ] = F41[α,F41[β,γ,σ],σ]", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 673, + "fields": { + "schema": 34, + "order": 85, + "alias": "P31", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство коммутативности", + "term_resolved": "свойство коммутативности", + "term_forms": [], + "definition_formal": "[σ∈ℬ((R1×R1)×R1)] ∀((α1,α2),γ)∈σ ((α2,α1),γ)∈σ", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 674, + "fields": { + "schema": 34, + "order": 86, + "alias": "P32", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство идемпотентности", + "term_resolved": "свойство идемпотентности", + "term_forms": [], + "definition_formal": "[σ∈ℬ((R1×R1)×R1)] ∀α∈Pr1(Pr1(σ)) ((α,α),α)∈σ", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 675, + "fields": { + "schema": 34, + "order": 87, + "alias": "P33", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство цикличности", + "term_resolved": "свойство цикличности", + "term_forms": [], + "definition_formal": "[σ∈ℬ((R1×R1)×R1)] ∃α∈Pr1(Pr1(σ)) ∀β∈Pr1(Pr1(σ)) ∃μ∈Z F46[α,μ,σ] = β", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 676, + "fields": { + "schema": 34, + "order": 88, + "alias": "P34", + "cst_type": "predicate", + "convention": "", + "term_raw": "группоид", + "term_resolved": "группоид", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), σ∈ℬ((R1×R1)×R1)] P29[α, σ]", + "definition_raw": "всюдуопределенная замкнутая функция пары элементов некоторого множества", + "definition_resolved": "всюдуопределенная замкнутая функция пары элементов некоторого множества" + } +}, +{ + "model": "rsform.constituenta", + "pk": 677, + "fields": { + "schema": 34, + "order": 89, + "alias": "P35", + "cst_type": "predicate", + "convention": "", + "term_raw": "полугруппа", + "term_resolved": "полугруппа", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), σ∈ℬ((R1×R1)×R1)] P29[α, σ] & P30[σ]", + "definition_raw": "ассоциативный группоид", + "definition_resolved": "ассоциативный группоид" + } +}, +{ + "model": "rsform.constituenta", + "pk": 678, + "fields": { + "schema": 34, + "order": 90, + "alias": "P36", + "cst_type": "predicate", + "convention": "", + "term_raw": "моноид", + "term_resolved": "моноид", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), σ∈ℬ((R1×R1)×R1)] P29[α, σ] & P30[σ] & card(F43[σ]) > 0", + "definition_raw": "полугруппа, в которой существует нейтральный элемент", + "definition_resolved": "полугруппа, в которой существует нейтральный элемент" + } +}, +{ + "model": "rsform.constituenta", + "pk": 679, + "fields": { + "schema": 34, + "order": 91, + "alias": "P37", + "cst_type": "predicate", + "convention": "", + "term_raw": "группа", + "term_resolved": "группа", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), σ∈ℬ((R1×R1)×R1)] P29[α, σ] & P30[σ] & card(F43[σ]) > 0 & ∀ξ∈α card(F45[ξ, σ]) > 0", + "definition_raw": "моноид, в котором для каждого элемента существует обратный", + "definition_resolved": "моноид, в котором для каждого элемента существует обратный" + } +}, +{ + "model": "rsform.constituenta", + "pk": 680, + "fields": { + "schema": 34, + "order": 92, + "alias": "P38", + "cst_type": "predicate", + "convention": "", + "term_raw": "абелева группа", + "term_resolved": "абелева группа", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), σ∈ℬ((R1×R1)×R1)] P29[α, σ] & P30[σ] & card(F43[σ]) > 0 & ∀ξ∈α card(F45[ξ, σ]) > 0 & P31[σ]", + "definition_raw": "коммутативная группа", + "definition_resolved": "коммутативная группа" + } +}, +{ + "model": "rsform.constituenta", + "pk": 681, + "fields": { + "schema": 34, + "order": 93, + "alias": "T9", + "cst_type": "theorem", + "convention": "Условия для \"левых\" и \"правых\" свойств через конкатенацию\nОперации по умолчанию считаются группоидами", + "term_raw": "Кольца", + "term_resolved": "Кольца", + "term_forms": [], + "definition_formal": "T8", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 682, + "fields": { + "schema": 34, + "order": 94, + "alias": "P39", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство дистрибутивности φψ", + "term_resolved": "свойство дистрибутивности φψ", + "term_forms": [], + "definition_formal": "[φ∈ℬ((R1×R1)×R1), ψ∈ℬ((R1×R1)×R1)] ∀α,β,γ∈Pr1(Pr1(φ)) (\n F41[α, F41[β, γ, φ], ψ] = F41[F41[α, β, ψ], F41[α, γ, ψ], φ] &\n F41[F41[α, β, φ], γ, ψ] = F41[F41[α, γ, ψ], F41[β, γ, ψ], φ]\n)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 683, + "fields": { + "schema": 34, + "order": 95, + "alias": "P40", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство поглощения φψ", + "term_resolved": "свойство поглощения φψ", + "term_forms": [], + "definition_formal": "[φ∈ℬ((R1×R1)×R1), ψ∈ℬ((R1×R1)×R1)] ∀α,β∈Pr1(Pr1(φ)) (\n F41[α, F41[α, β, φ], ψ] = α & F41[F41[α, β, φ], α, ψ] = α &\n F41[α, F41[β, α, φ], ψ] = α & F41[F41[β, α, φ], α, ψ] = α\n)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 684, + "fields": { + "schema": 34, + "order": 96, + "alias": "P41", + "cst_type": "predicate", + "convention": "", + "term_raw": "полукольцо", + "term_resolved": "полукольцо", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), φ∈ℬ((R1×R1)×R1), ψ∈ℬ((R1×R1)×R1)]\nP36[α, φ] & P31[φ] & P35[α, ψ] & P39[φ, ψ]", + "definition_raw": "сложение – моноид + коммутативность; умножение – полугруппа; дистрибутивность сложения относительно умножения", + "definition_resolved": "сложение – моноид + коммутативность; умножение – полугруппа; дистрибутивность сложения относительно умножения" + } +}, +{ + "model": "rsform.constituenta", + "pk": 685, + "fields": { + "schema": 34, + "order": 97, + "alias": "P42", + "cst_type": "predicate", + "convention": "", + "term_raw": "кольцо", + "term_resolved": "кольцо", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), φ∈ℬ((R1×R1)×R1), ψ∈ℬ((R1×R1)×R1)]\nP38[α, φ] & P35[α, ψ] & P39[φ, ψ]", + "definition_raw": "полукольцо, в котором для умножения существуют обратные элементы", + "definition_resolved": "полукольцо, в котором для умножения существуют обратные элементы" + } +}, +{ + "model": "rsform.constituenta", + "pk": 686, + "fields": { + "schema": 34, + "order": 98, + "alias": "P43", + "cst_type": "predicate", + "convention": "", + "term_raw": "тело", + "term_resolved": "тело", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), φ∈ℬ((R1×R1)×R1), ψ∈ℬ((R1×R1)×R1)]\nP38[α, φ] & P35[α, ψ] & P39[φ, ψ] & \ncard(F43[ψ]) > 0 & ∀ξ∈α\\F43[φ] card(F45[ξ, ψ]) > 0", + "definition_raw": "кольцо, в котором умножение обладает нейтральным элементов и обратными элементами для всех элементов, кроме нейтральных элементов сложения", + "definition_resolved": "кольцо, в котором умножение обладает нейтральным элементов и обратными элементами для всех элементов, кроме нейтральных элементов сложения" + } +}, +{ + "model": "rsform.constituenta", + "pk": 687, + "fields": { + "schema": 34, + "order": 99, + "alias": "P44", + "cst_type": "predicate", + "convention": "", + "term_raw": "поле", + "term_resolved": "поле", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), φ∈ℬ((R1×R1)×R1), ψ∈ℬ((R1×R1)×R1)] \nP38[α, φ] & P35[α, ψ] & P31[ψ] & P39[φ, ψ] & \ncard(F43[ψ]) > 0 & ∀ξ∈α\\F43[φ] card(F45[ξ, ψ]) > 0", + "definition_raw": "тело, в котором умножение обладает коммутативностью", + "definition_resolved": "тело, в котором умножение обладает коммутативностью" + } +}, +{ + "model": "rsform.constituenta", + "pk": 688, + "fields": { + "schema": 34, + "order": 100, + "alias": "P45", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство покрытия", + "term_resolved": "свойство покрытия", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), σ∈ℬℬ(R1)] α⊆red(σ)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 689, + "fields": { + "schema": 34, + "order": 101, + "alias": "P46", + "cst_type": "predicate", + "convention": "", + "term_raw": "алгебраическая решетка", + "term_resolved": "алгебраическая решетка", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), φ∈ℬ((R1×R1)×R1), ψ∈ℬ((R1×R1)×R1)] \nP35[α, φ] & P31[φ] & P35[α, ψ] & P31[ψ] & P40[φ, ψ] & P40[ψ, φ]", + "definition_raw": "обе операции - коммутативные полугруппы с идемпотентностью и взаимным поглощением", + "definition_resolved": "обе операции - коммутативные полугруппы с идемпотентностью и взаимным поглощением" + } +}, +{ + "model": "rsform.constituenta", + "pk": 690, + "fields": { + "schema": 34, + "order": 102, + "alias": "T10", + "cst_type": "theorem", + "convention": "", + "term_raw": "Множества подмножеств", + "term_resolved": "Множества подмножеств", + "term_forms": [], + "definition_formal": "", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 691, + "fields": { + "schema": 34, + "order": 103, + "alias": "P47", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство попарного непересечения", + "term_resolved": "свойство попарного непересечения", + "term_forms": [], + "definition_formal": "[σ∈ℬℬ(R1)] ∀α,β∈σ (α≠β ⇒ α∩β=∅)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 692, + "fields": { + "schema": 34, + "order": 104, + "alias": "P48", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство замкнутости по объединению", + "term_resolved": "свойство замкнутости по объединению", + "term_forms": [], + "definition_formal": "[σ∈ℬℬ(R1)] ∀α,β∈σ α∪β∈σ", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 693, + "fields": { + "schema": 34, + "order": 105, + "alias": "P49", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство замкнутости по пересечению", + "term_resolved": "свойство замкнутости по пересечению", + "term_forms": [], + "definition_formal": "[σ∈ℬℬ(R1)] ∀α,β∈σ α∩β∈σ", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 694, + "fields": { + "schema": 34, + "order": 106, + "alias": "P50", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство замкнутости по надмножеству", + "term_resolved": "свойство замкнутости по надмножеству", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), σ∈ℬℬ(R1)] ∀ξ∈σ (ξ≠∅ ⇒ ∀δ∈α\\ξ ξ∪{δ}∈σ)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 695, + "fields": { + "schema": 34, + "order": 107, + "alias": "P51", + "cst_type": "predicate", + "convention": "", + "term_raw": "свойство полноты подмножеств", + "term_resolved": "свойство полноты подмножеств", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), σ∈ℬℬ(R1)] ∀ξ∈ℬ(α) ¬(ξ∈σ ⇔ α\\ξ∈σ)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 696, + "fields": { + "schema": 34, + "order": 108, + "alias": "P52", + "cst_type": "predicate", + "convention": "", + "term_raw": "разбиение", + "term_resolved": "разбиение", + "term_forms": [], + "definition_formal": "[α∈ℬ(R1), σ∈ℬℬ(R1)] α⊆red(σ) & ∀ξ1,ξ2∈σ (ξ1≠ξ2 ⇒ ξ1∩ξ2=∅)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 697, + "fields": { + "schema": 34, + "order": 109, + "alias": "F47", + "cst_type": "function", + "convention": "", + "term_raw": "множество-пересечение", + "term_resolved": "множество-пересечение", + "term_forms": [], + "definition_formal": "[σ∈ℬℬ(R1)] D{ξ∈red(σ) | ∀β∈σ (ξ∈β)}", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 698, + "fields": { + "schema": 34, + "order": 110, + "alias": "P53", + "cst_type": "predicate", + "convention": "", + "term_raw": "вложенные подмножества", + "term_resolved": "вложенные подмножества", + "term_forms": [], + "definition_formal": "[σ∈ℬℬ(R1)] ∀α,β∈σ (card(α)≤card(β) ⇒ α⊆β)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 699, + "fields": { + "schema": 34, + "order": 111, + "alias": "P54", + "cst_type": "predicate", + "convention": "", + "term_raw": "вложенные подмножества (декларативно)", + "term_resolved": "вложенные подмножества (декларативно)", + "term_forms": [], + "definition_formal": "[σ∈ℬℬ(R1)] ∀γ∈ℬ(σ) (σ≠∅ ⇒ red(σ)∈σ)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 700, + "fields": { + "schema": 35, + "order": 1, + "alias": "X1", + "cst_type": "basic", + "convention": "", + "term_raw": "элементы", + "term_resolved": "элементы", + "term_forms": [], + "definition_formal": "", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 701, + "fields": { + "schema": 35, + "order": 2, + "alias": "S1", + "cst_type": "structure", + "convention": "", + "term_raw": "операция", + "term_resolved": "операция", + "term_forms": [], + "definition_formal": "ℬ((X1×X1)×X1)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 702, + "fields": { + "schema": 35, + "order": 3, + "alias": "F1", + "cst_type": "function", + "convention": "", + "term_raw": "результат операции для данных аргументов", + "term_resolved": "результат операции для данных аргументов", + "term_forms": [], + "definition_formal": "[α∈X1, β∈X1] debool(Pr2(Fi1[{(α,β)}](S1)))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 703, + "fields": { + "schema": 35, + "order": 4, + "alias": "A1", + "cst_type": "axiom", + "convention": "", + "term_raw": "свойство замкнутости", + "term_resolved": "свойство замкнутости", + "term_forms": [], + "definition_formal": "Pr1(S1) = X1×X1 & card(S1) = card(Pr1(S1))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 704, + "fields": { + "schema": 35, + "order": 5, + "alias": "A2", + "cst_type": "axiom", + "convention": "", + "term_raw": "свойство ассоциативности", + "term_resolved": "свойство ассоциативности", + "term_forms": [], + "definition_formal": "∀α,β,γ∈X1 F1[F1[α,β],γ]=F1[α,F1[β,γ]]", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 705, + "fields": { + "schema": 35, + "order": 6, + "alias": "D1", + "cst_type": "term", + "convention": "", + "term_raw": "нейтральные элементы", + "term_resolved": "нейтральные элементы", + "term_forms": [], + "definition_formal": "D{ξ∈X1 | ∀α∈X1 F1[ξ,α]=α}", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 706, + "fields": { + "schema": 35, + "order": 7, + "alias": "A3", + "cst_type": "axiom", + "convention": "", + "term_raw": "существование нейтрального элемента", + "term_resolved": "существование нейтрального элемента", + "term_forms": [], + "definition_formal": "card(D1)>0", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 707, + "fields": { + "schema": 35, + "order": 8, + "alias": "F2", + "cst_type": "function", + "convention": "", + "term_raw": "обратный данному элементу элемент", + "term_resolved": "обратный данному элементу элемент", + "term_forms": [], + "definition_formal": "[αα∈X1] D{αω∈X1 | F1[αα,αω]∈D1 & F1[αω,αα]∈D1}", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 708, + "fields": { + "schema": 35, + "order": 9, + "alias": "D3", + "cst_type": "term", + "convention": "", + "term_raw": "инвариантные элементы", + "term_resolved": "инвариантные элементы", + "term_forms": [], + "definition_formal": "D{ξ∈X1 | ∀α∈X1 F1[ξ,α]=ξ}", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 709, + "fields": { + "schema": 35, + "order": 10, + "alias": "A4", + "cst_type": "axiom", + "convention": "", + "term_raw": "существования всех обратных элементов", + "term_resolved": "существования всех обратных элементов", + "term_forms": [], + "definition_formal": "∀α∈X1 card(F2[α])>0", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 710, + "fields": { + "schema": 35, + "order": 11, + "alias": "D2", + "cst_type": "term", + "convention": "", + "term_raw": "коммутативные совокупности аргументов", + "term_resolved": "коммутативные совокупности аргументов", + "term_forms": [], + "definition_formal": "I{{α, β} | α:∈X1; β:∈X1; α≠β; F1[α,β]=F1[β,α]}", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 711, + "fields": { + "schema": 35, + "order": 12, + "alias": "A5", + "cst_type": "axiom", + "convention": "", + "term_raw": "условие коммутативности", + "term_resolved": "условие коммутативности", + "term_forms": [], + "definition_formal": "∀α,β∈X1 F1[α,β]=F1[β,α]", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 712, + "fields": { + "schema": 35, + "order": 13, + "alias": "A6", + "cst_type": "axiom", + "convention": "", + "term_raw": "условие идемпотентности", + "term_resolved": "условие идемпотентности", + "term_forms": [], + "definition_formal": "∀α∈X1 F1[α,α]=α", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 713, + "fields": { + "schema": 36, + "order": 1, + "alias": "X1", + "cst_type": "basic", + "convention": "", + "term_raw": "элементы", + "term_resolved": "элементы", + "term_forms": [], + "definition_formal": "", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 714, + "fields": { + "schema": 36, + "order": 2, + "alias": "S1", + "cst_type": "structure", + "convention": "", + "term_raw": "операция И", + "term_resolved": "операция И", + "term_forms": [], + "definition_formal": "ℬ((X1×X1)×X1)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 715, + "fields": { + "schema": 36, + "order": 3, + "alias": "S2", + "cst_type": "structure", + "convention": "", + "term_raw": "операция ИЛИ", + "term_resolved": "операция ИЛИ", + "term_forms": [], + "definition_formal": "ℬ((X1×X1)×X1)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 716, + "fields": { + "schema": 36, + "order": 4, + "alias": "S3", + "cst_type": "structure", + "convention": "", + "term_raw": "операция НЕ", + "term_resolved": "операция НЕ", + "term_forms": [], + "definition_formal": "ℬ(X1×X1)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 717, + "fields": { + "schema": 36, + "order": 5, + "alias": "S4", + "cst_type": "structure", + "convention": "", + "term_raw": "значение ИСТИНА", + "term_resolved": "значение ИСТИНА", + "term_forms": [], + "definition_formal": "X1", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 718, + "fields": { + "schema": 36, + "order": 6, + "alias": "S5", + "cst_type": "structure", + "convention": "", + "term_raw": "значение ЛОЖЬ", + "term_resolved": "значение ЛОЖЬ", + "term_forms": [], + "definition_formal": "X1", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 719, + "fields": { + "schema": 36, + "order": 7, + "alias": "F2", + "cst_type": "function", + "convention": "", + "term_raw": "результат операции И для данных аргументов", + "term_resolved": "результат операции И для данных аргументов", + "term_forms": [], + "definition_formal": "[α∈X1, β∈X1] debool(Pr2(Fi1[{(α,β)}](S1)))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 720, + "fields": { + "schema": 36, + "order": 8, + "alias": "F3", + "cst_type": "function", + "convention": "", + "term_raw": "результат операции ИЛИ для данных аргументов", + "term_resolved": "результат операции ИЛИ для данных аргументов", + "term_forms": [], + "definition_formal": "[α∈X1, β∈X1] debool(Pr2(Fi1[{(α,β)}](S2)))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 721, + "fields": { + "schema": 36, + "order": 9, + "alias": "F4", + "cst_type": "function", + "convention": "", + "term_raw": "результат операции НЕ для данного аргумента", + "term_resolved": "результат операции НЕ для данного аргумента", + "term_forms": [], + "definition_formal": "[α∈X1] debool(Pr2(Fi1[{α}](S3)))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 722, + "fields": { + "schema": 36, + "order": 10, + "alias": "P1", + "cst_type": "predicate", + "convention": "", + "term_raw": "прямая однозначность", + "term_resolved": "прямая однозначность", + "term_forms": [], + "definition_formal": "[σ∈ℬ(R1×R2)] ∀α∈Pr1(σ) card(Pr2(Fi1[{α}](σ))) = 1", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 723, + "fields": { + "schema": 36, + "order": 11, + "alias": "P2", + "cst_type": "predicate", + "convention": "", + "term_raw": "идемпотентность", + "term_resolved": "идемпотентность", + "term_forms": [], + "definition_formal": "[σ∈ℬ((R1×R1)×R1)] ∀α∈Pr1(Pr1(σ)) ((α,α),α)∈σ", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 724, + "fields": { + "schema": 36, + "order": 12, + "alias": "P3", + "cst_type": "predicate", + "convention": "", + "term_raw": "коммутативность", + "term_resolved": "коммутативность", + "term_forms": [], + "definition_formal": "[σ∈ℬ((R1×R1)×R1)] ∀((α1,α2),γ)∈σ ((α2,α1),γ)∈σ", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 725, + "fields": { + "schema": 36, + "order": 13, + "alias": "P4", + "cst_type": "predicate", + "convention": "", + "term_raw": "ассоциативность", + "term_resolved": "ассоциативность", + "term_forms": [], + "definition_formal": "[σ∈ℬ((R1×R1)×R1)] \n∀α1,α2,α3∈Pr1(Pr1(σ)) F1[(F1[(α1, α2),σ],α3),σ] = F1[(α1,F1[(α2, α3), σ]),σ]", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 726, + "fields": { + "schema": 36, + "order": 14, + "alias": "P5", + "cst_type": "predicate", + "convention": "", + "term_raw": "поглощение бинарных операций", + "term_resolved": "поглощение бинарных операций", + "term_forms": [], + "definition_formal": "[σ1∈ℬ((R1×R1)×R1), σ2∈ℬ((R1×R1)×R1)] \n∀(α1,α2)∈Pr1(σ1) F1[(F1[(α1, α2),σ1],α1),σ2] = α1", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 727, + "fields": { + "schema": 36, + "order": 15, + "alias": "A1", + "cst_type": "axiom", + "convention": "", + "term_raw": "@{P1|nomn,sing} @{S1|gent,sing}", + "term_resolved": "прямая однозначность операции И", + "term_forms": [], + "definition_formal": "P1[S1]", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 728, + "fields": { + "schema": 36, + "order": 16, + "alias": "A2", + "cst_type": "axiom", + "convention": "", + "term_raw": "@{P1|nomn,sing} @{S2|gent,sing}", + "term_resolved": "прямая однозначность операции ИЛИ", + "term_forms": [], + "definition_formal": "P1[S2]", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 729, + "fields": { + "schema": 36, + "order": 17, + "alias": "A3", + "cst_type": "axiom", + "convention": "", + "term_raw": "@{P2|nomn,sing} @{S1|gent,sing}", + "term_resolved": "идемпотентность операции И", + "term_forms": [], + "definition_formal": "P2[S1]", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 730, + "fields": { + "schema": 36, + "order": 18, + "alias": "A4", + "cst_type": "axiom", + "convention": "", + "term_raw": "@{P2|nomn,sing} @{S2|gent,sing}", + "term_resolved": "идемпотентность операции ИЛИ", + "term_forms": [], + "definition_formal": "P2[S2]", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 731, + "fields": { + "schema": 36, + "order": 19, + "alias": "A5", + "cst_type": "axiom", + "convention": "", + "term_raw": "@{P3|nomn,sing} @{S1|gent,sing}", + "term_resolved": "коммутативность операции И", + "term_forms": [], + "definition_formal": "P3[S1]", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 732, + "fields": { + "schema": 36, + "order": 20, + "alias": "A6", + "cst_type": "axiom", + "convention": "", + "term_raw": "@{P3|nomn,sing} @{S2|gent,sing}", + "term_resolved": "коммутативность операции ИЛИ", + "term_forms": [], + "definition_formal": "P3[S2]", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 733, + "fields": { + "schema": 36, + "order": 21, + "alias": "A7", + "cst_type": "axiom", + "convention": "", + "term_raw": "@{P4|nomn,sing} @{S1|gent,sing}", + "term_resolved": "ассоциативность операции И", + "term_forms": [], + "definition_formal": "P4[S1]", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 734, + "fields": { + "schema": 36, + "order": 22, + "alias": "A8", + "cst_type": "axiom", + "convention": "", + "term_raw": "@{P4|nomn,sing} @{S2|gent,sing}", + "term_resolved": "ассоциативность операции ИЛИ", + "term_forms": [], + "definition_formal": "P4[S2]", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 735, + "fields": { + "schema": 36, + "order": 23, + "alias": "A9", + "cst_type": "axiom", + "convention": "", + "term_raw": "поглощение @{S1|gent,sing} @{S2|ablt,sing}", + "term_resolved": "поглощение операции И операцией ИЛИ", + "term_forms": [], + "definition_formal": "P5[S1,S2]", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 736, + "fields": { + "schema": 36, + "order": 24, + "alias": "A10", + "cst_type": "axiom", + "convention": "", + "term_raw": "поглощение @{S2|gent,sing} @{S1|ablt,sing}", + "term_resolved": "поглощение операции ИЛИ операцией И", + "term_forms": [], + "definition_formal": "P5[S2,S1]", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 737, + "fields": { + "schema": 37, + "order": 1, + "alias": "X1", + "cst_type": "basic", + "convention": "Различимые в предметной области люди", + "term_raw": "индивиды", + "term_resolved": "индивиды", + "term_forms": [], + "definition_formal": "", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 738, + "fields": { + "schema": 37, + "order": 2, + "alias": "S1", + "cst_type": "structure", + "convention": "Пол человека устанавливается в соответствии с документами", + "term_raw": "мужчины", + "term_resolved": "мужчины", + "term_forms": [], + "definition_formal": "ℬ(X1)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 739, + "fields": { + "schema": 37, + "order": 3, + "alias": "S2", + "cst_type": "structure", + "convention": "Пол человека устанавливается в соответствии с документами", + "term_raw": "женщины", + "term_resolved": "женщины", + "term_forms": [], + "definition_formal": "ℬ(X1)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 740, + "fields": { + "schema": 37, + "order": 4, + "alias": "A1", + "cst_type": "axiom", + "convention": "", + "term_raw": "условие разбиения индивидов на @{S1|gent,plur} и @{S2|gent,plur}", + "term_resolved": "условие разбиения индивидов на мужчин и женщин", + "term_forms": [], + "definition_formal": "S1∪S2=X1 & S1∩S2=∅", + "definition_raw": "каждый индивид является либо мужчиной либо женщиной", + "definition_resolved": "каждый индивид является либо мужчиной либо женщиной" + } +}, +{ + "model": "rsform.constituenta", + "pk": 741, + "fields": { + "schema": 37, + "order": 5, + "alias": "S3", + "cst_type": "structure", + "convention": "устанавливается на основе документов, подтверждающих факт рождения или паспорта родителя", + "term_raw": "отношение @{D2|nomn,sing} - @{D1|nomn,sing}", + "term_resolved": "отношение родитель - ребёнок", + "term_forms": [ + { + "tags": "sing,ablt", + "text": "отношением родитель - ребёнок" + }, + { + "tags": "sing,accs", + "text": "отношение родитель - ребёнок" + }, + { + "tags": "sing,datv", + "text": "отношению родитель - ребёнок" + }, + { + "tags": "sing,gent", + "text": "отношения родитель - ребёнок" + }, + { + "tags": "sing,nomn", + "text": "отношение родитель - ребёнок" + }, + { + "tags": "sing,loct", + "text": "отношение родителе - ребёнок" + }, + { + "tags": "plur,nomn", + "text": "отношение родители - ребёнок" + }, + { + "tags": "plur,gent", + "text": "отношение родителей - ребёнок" + }, + { + "tags": "plur,datv", + "text": "отношение родителям - ребёнок" + }, + { + "tags": "plur,accs", + "text": "отношение родителей - ребёнок" + }, + { + "tags": "plur,ablt", + "text": "отношение родителями - ребёнок" + }, + { + "tags": "plur,loct", + "text": "отношение родителях - ребёнок" + } + ], + "definition_formal": "ℬ(X1×X1)", + "definition_raw": "множество пар: @{D2|nomn,sing} и его @{D1|nomn,sing}", + "definition_resolved": "множество пар: родитель и его ребёнок" + } +}, +{ + "model": "rsform.constituenta", + "pk": 742, + "fields": { + "schema": 37, + "order": 6, + "alias": "D1", + "cst_type": "term", + "convention": "", + "term_raw": "дети", + "term_resolved": "дети", + "term_forms": [], + "definition_formal": "Pr2(S3)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 743, + "fields": { + "schema": 37, + "order": 7, + "alias": "D2", + "cst_type": "term", + "convention": "", + "term_raw": "родители", + "term_resolved": "родители", + "term_forms": [], + "definition_formal": "Pr1(S3)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 744, + "fields": { + "schema": 37, + "order": 8, + "alias": "F1", + "cst_type": "function", + "convention": "", + "term_raw": "@{D2|nomn,plur} @{X1|sing,gent}", + "term_resolved": "родители индивида", + "term_forms": [], + "definition_formal": "[α∈X1] Pr1(Fi2[{α}](S3))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 745, + "fields": { + "schema": 37, + "order": 9, + "alias": "F2", + "cst_type": "function", + "convention": "", + "term_raw": "@{D2|nomn,plur} группы @{X1|gent,plur}", + "term_resolved": "родители группы индивидов", + "term_forms": [], + "definition_formal": "[α∈ℬ(X1)] Pr1(Fi2[α](S3))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 746, + "fields": { + "schema": 37, + "order": 10, + "alias": "F3", + "cst_type": "function", + "convention": "", + "term_raw": "@{D1|nomn,plur} @{X1|gent,sing}", + "term_resolved": "дети индивида", + "term_forms": [], + "definition_formal": "[α∈X1] Pr2(Fi1[{α}](S3))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 747, + "fields": { + "schema": 37, + "order": 11, + "alias": "F4", + "cst_type": "function", + "convention": "", + "term_raw": "@{D1|nomn,plur} группы @{X1|gent,plur}", + "term_resolved": "дети группы индивидов", + "term_forms": [], + "definition_formal": "[α∈ℬ(X1)] Pr2(Fi1[α](S3))", + "definition_raw": "совокупность @{D1|plur,gent}, одним из родителей которых является индивиди из данной группы индивидов", + "definition_resolved": "совокупность детей, одним из родителей которых является индивиди из данной группы индивидов" + } +}, +{ + "model": "rsform.constituenta", + "pk": 748, + "fields": { + "schema": 37, + "order": 12, + "alias": "F5", + "cst_type": "function", + "convention": "", + "term_raw": "сиблинги", + "term_resolved": "сиблинги", + "term_forms": [ + { + "tags": "plur,gent", + "text": "сиблингов" + } + ], + "definition_formal": "[α∈X1] Pr2(Fi1[F1[α]](S3))\\{α}", + "definition_raw": "@{D1|plur,nomn} @{D2|plur,gent} данного @{X1|sing,gent} за исключением его самого", + "definition_resolved": "дети родителей данного индивида за исключением его самого" + } +}, +{ + "model": "rsform.constituenta", + "pk": 749, + "fields": { + "schema": 37, + "order": 13, + "alias": "F6", + "cst_type": "function", + "convention": "", + "term_raw": "@{F5|nomn,plur} группы @{X1|gent,plur}", + "term_resolved": "сиблинги группы индивидов", + "term_forms": [], + "definition_formal": "[α∈ℬ(X1)] Pr2(Fi1[F2[α]](S3))\\α", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 750, + "fields": { + "schema": 37, + "order": 14, + "alias": "F7", + "cst_type": "function", + "convention": "", + "term_raw": "прадети", + "term_resolved": "прадети", + "term_forms": [], + "definition_formal": "[α∈X1] F4[F3[α]]", + "definition_raw": "@{D1|nomn,plur} @{F3|gent,plur}", + "definition_resolved": "дети детей индивида" + } +}, +{ + "model": "rsform.constituenta", + "pk": 751, + "fields": { + "schema": 37, + "order": 15, + "alias": "F8", + "cst_type": "function", + "convention": "", + "term_raw": "прародители", + "term_resolved": "прародители", + "term_forms": [], + "definition_formal": "[α∈X1] F2[F1[α]]", + "definition_raw": "@{D2|nomn,plur} @{D2|gent,plur} данного @{X1|sing,gent}", + "definition_resolved": "родители родителей данного индивида" + } +}, +{ + "model": "rsform.constituenta", + "pk": 752, + "fields": { + "schema": 37, + "order": 16, + "alias": "S4", + "cst_type": "structure", + "convention": "действующие браки", + "term_raw": "браки", + "term_resolved": "браки", + "term_forms": [], + "definition_formal": "ℬ(S1×S2)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 753, + "fields": { + "schema": 37, + "order": 17, + "alias": "D3", + "cst_type": "term", + "convention": "", + "term_raw": "мужья", + "term_resolved": "мужья", + "term_forms": [], + "definition_formal": "Pr1(S4)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 754, + "fields": { + "schema": 37, + "order": 18, + "alias": "D4", + "cst_type": "term", + "convention": "", + "term_raw": "жены", + "term_resolved": "жены", + "term_forms": [], + "definition_formal": "Pr2(S4)", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 755, + "fields": { + "schema": 37, + "order": 19, + "alias": "D5", + "cst_type": "term", + "convention": "", + "term_raw": "супруги", + "term_resolved": "супруги", + "term_forms": [], + "definition_formal": "Pr1(S4)∪Pr2(S4)", + "definition_raw": "множество множеств @{X1|gent,plur}, состоящих в @{S4|loct,sing}", + "definition_resolved": "множество множеств индивидов, состоящих в браке" + } +}, +{ + "model": "rsform.constituenta", + "pk": 756, + "fields": { + "schema": 37, + "order": 20, + "alias": "A2", + "cst_type": "axiom", + "convention": "", + "term_raw": "единственность @{S4|gent,plur}", + "term_resolved": "единственность браков", + "term_forms": [], + "definition_formal": "card(S4)=card(D3) & card(S4)=card(D4)", + "definition_raw": "каждый @{X1|nomn,sing} может состоять только в одном @{S4|loct,sing}", + "definition_resolved": "каждый индивид может состоять только в одном браке" + } +}, +{ + "model": "rsform.constituenta", + "pk": 757, + "fields": { + "schema": 37, + "order": 21, + "alias": "F9", + "cst_type": "function", + "convention": "", + "term_raw": "@{D3|nomn,sing} @{D4|gent,sing}", + "term_resolved": "муж жены", + "term_forms": [], + "definition_formal": "[α∈D4] debool(F11[{α}])", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 758, + "fields": { + "schema": 37, + "order": 22, + "alias": "F10", + "cst_type": "function", + "convention": "", + "term_raw": "@{D4|nomn,sing} @{D3|gent,sing}", + "term_resolved": "жена мужа", + "term_forms": [], + "definition_formal": "[α∈D3] debool(F12[{α}])", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 759, + "fields": { + "schema": 37, + "order": 23, + "alias": "F11", + "cst_type": "function", + "convention": "", + "term_raw": "@{D3|nomn,plur} группы @{X1|gent,plur}", + "term_resolved": "мужья группы индивидов", + "term_forms": [], + "definition_formal": "[α∈ℬ(X1)] Pr1(Fi2[α](S4))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 760, + "fields": { + "schema": 37, + "order": 24, + "alias": "F12", + "cst_type": "function", + "convention": "", + "term_raw": "@{D4|nomn,plur} группы @{X1|gent,plur}", + "term_resolved": "жены группы индивидов", + "term_forms": [], + "definition_formal": "[α∈ℬ(X1)] Pr2(Fi1[α](S4))", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 761, + "fields": { + "schema": 37, + "order": 25, + "alias": "F13", + "cst_type": "function", + "convention": "", + "term_raw": "@{D5|nomn,plur} группы @{X1|gent,plur}", + "term_resolved": "супруги группы индивидов", + "term_forms": [], + "definition_formal": "[α∈ℬ(X1)] F11[α]∪F12[α]", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 762, + "fields": { + "schema": 37, + "order": 26, + "alias": "F14", + "cst_type": "function", + "convention": "", + "term_raw": "потомки", + "term_resolved": "потомки", + "term_forms": [], + "definition_formal": "[α∈X1] R{ξ:=F3[α] | ξ∪F4[ξ]}", + "definition_raw": "@{D1|nomn,plur} данного @{X1|sing,gent} или @{D1|nomn,plur} его @{F14|gent,plur}", + "definition_resolved": "дети данного индивида или дети его потомков" + } +}, +{ + "model": "rsform.constituenta", + "pk": 763, + "fields": { + "schema": 37, + "order": 27, + "alias": "F15", + "cst_type": "function", + "convention": "", + "term_raw": "@{F14|nomn,plur} данной группы @{X1|gent,plur}", + "term_resolved": "потомки данной группы индивидов", + "term_forms": [], + "definition_formal": "[α∈ℬ(X1)] R{ξ:=F4[α] | ξ∪F4[ξ]}", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 764, + "fields": { + "schema": 37, + "order": 28, + "alias": "A3", + "cst_type": "axiom", + "convention": "", + "term_raw": "ацикличность @{S3|sing,gent}", + "term_resolved": "ацикличность отношение родителя - ребёнок", + "term_forms": [], + "definition_formal": "∀ξ∈X1 ξ∉F14[ξ]", + "definition_raw": "@{X1|sing,nomn} не может быть своим @{F14|sing,ablt}", + "definition_resolved": "индивид не может быть своим потомком" + } +}, +{ + "model": "rsform.constituenta", + "pk": 765, + "fields": { + "schema": 37, + "order": 29, + "alias": "D6", + "cst_type": "term", + "convention": "", + "term_raw": "отцы", + "term_resolved": "отцы", + "term_forms": [], + "definition_formal": "D2∩S1", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 766, + "fields": { + "schema": 37, + "order": 30, + "alias": "D7", + "cst_type": "term", + "convention": "", + "term_raw": "матери", + "term_resolved": "матери", + "term_forms": [ + { + "tags": "sing,nomn", + "text": "мать" + } + ], + "definition_formal": "D2∩S2", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 767, + "fields": { + "schema": 37, + "order": 31, + "alias": "F16", + "cst_type": "function", + "convention": "", + "term_raw": "@{D6|nomn,plur} группы @{X1|gent,plur}", + "term_resolved": "отцы группы индивидов", + "term_forms": [], + "definition_formal": "[α∈ℬ(X1)] F2[α]∩S1", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 768, + "fields": { + "schema": 37, + "order": 32, + "alias": "F17", + "cst_type": "function", + "convention": "", + "term_raw": "@{D6|nomn,plur} @{X1|sing,gent}", + "term_resolved": "отцы индивида", + "term_forms": [], + "definition_formal": "[α∈X1] F1[α]∩S1", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 769, + "fields": { + "schema": 37, + "order": 33, + "alias": "F18", + "cst_type": "function", + "convention": "", + "term_raw": "@{D7|nomn,plur} @{X1|gent,sing}", + "term_resolved": "матери индивида", + "term_forms": [], + "definition_formal": "[α∈X1] F1[α]∩S2", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 770, + "fields": { + "schema": 37, + "order": 34, + "alias": "F19", + "cst_type": "function", + "convention": "", + "term_raw": "@{D7|nomn,plur} группы @{X1|gent,plur}", + "term_resolved": "матери группы индивидов", + "term_forms": [], + "definition_formal": "[α∈ℬ(X1)] F2[α]∩S2", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 771, + "fields": { + "schema": 37, + "order": 35, + "alias": "D8", + "cst_type": "term", + "convention": "", + "term_raw": "сироты", + "term_resolved": "сироты", + "term_forms": [], + "definition_formal": "X1\\D1", + "definition_raw": "@{X1|plur,nomn}, не имеющие являющиеся @{D1|plur,ablt}", + "definition_resolved": "индивиды, не имеющие являющиеся детьми" + } +}, +{ + "model": "rsform.constituenta", + "pk": 772, + "fields": { + "schema": 37, + "order": 36, + "alias": "A4", + "cst_type": "axiom", + "convention": "", + "term_raw": "ограничение @{D2|plur,gent}", + "term_resolved": "ограничение родителей", + "term_forms": [], + "definition_formal": "∀α∈X1 (card(F17[α])≤1 & card(F18[α])≤1)", + "definition_raw": "у @{D1|sing,gent} может быть максимум один @{D6|sing,nomn} и одна @{D7|sing,nomn}", + "definition_resolved": "у ребёнка может быть максимум один отец и одна мать" + } +}, +{ + "model": "rsform.constituenta", + "pk": 773, + "fields": { + "schema": 37, + "order": 37, + "alias": "F20", + "cst_type": "function", + "convention": "", + "term_raw": "сыновья", + "term_resolved": "сыновья", + "term_forms": [ + { + "tags": "plur,gent", + "text": "сыновей" + } + ], + "definition_formal": "[α∈X1] F3[α]∩S1", + "definition_raw": "@{D1|plur,nomn} данного @{X1|sing,gent}, являющиеся @{S1|plur,ablt}", + "definition_resolved": "дети данного индивида, являющиеся мужчинами" + } +}, +{ + "model": "rsform.constituenta", + "pk": 774, + "fields": { + "schema": 37, + "order": 38, + "alias": "F21", + "cst_type": "function", + "convention": "", + "term_raw": "@{F20|nomn,plur} группы @{X1|gent,plur}", + "term_resolved": "сыновья группы индивидов", + "term_forms": [], + "definition_formal": "[α∈ℬ(X1)] F4[α]∩S1", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 775, + "fields": { + "schema": 37, + "order": 39, + "alias": "F22", + "cst_type": "function", + "convention": "", + "term_raw": "дочери", + "term_resolved": "дочери", + "term_forms": [], + "definition_formal": "[α∈X1] F3[α]∩S2", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 776, + "fields": { + "schema": 37, + "order": 40, + "alias": "F23", + "cst_type": "function", + "convention": "", + "term_raw": "@{F22|nomn,plur} группы @{X1|gent,plur}", + "term_resolved": "дочери группы индивидов", + "term_forms": [], + "definition_formal": "[α∈ℬ(X1)] F4[α]∩S2", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 777, + "fields": { + "schema": 37, + "order": 41, + "alias": "F24", + "cst_type": "function", + "convention": "", + "term_raw": "внуки", + "term_resolved": "внуки", + "term_forms": [], + "definition_formal": "[α∈X1] F21[F3[α]]", + "definition_raw": "@{F20|nomn,plur} @{D1|gent,plur}", + "definition_resolved": "сыновья детей" + } +}, +{ + "model": "rsform.constituenta", + "pk": 778, + "fields": { + "schema": 37, + "order": 42, + "alias": "F25", + "cst_type": "function", + "convention": "", + "term_raw": "внучки", + "term_resolved": "внучки", + "term_forms": [], + "definition_formal": "[α∈X1] F23[F3[α]]", + "definition_raw": "@{F22|nomn,plur} @{D1|gent,plur}", + "definition_resolved": "дочери детей" + } +}, +{ + "model": "rsform.constituenta", + "pk": 779, + "fields": { + "schema": 37, + "order": 43, + "alias": "F26", + "cst_type": "function", + "convention": "", + "term_raw": "дедушки", + "term_resolved": "дедушки", + "term_forms": [], + "definition_formal": "[α∈X1] F16[F1[α]]", + "definition_raw": "@{D6|nomn,plur} @{D2|gent,plur}", + "definition_resolved": "отцы родителей" + } +}, +{ + "model": "rsform.constituenta", + "pk": 780, + "fields": { + "schema": 37, + "order": 44, + "alias": "F27", + "cst_type": "function", + "convention": "", + "term_raw": "бабушки", + "term_resolved": "бабушки", + "term_forms": [], + "definition_formal": "[α∈X1] F19[F1[α]]", + "definition_raw": "@{D7|nomn,plur} @{D2|gent,plur}", + "definition_resolved": "матери родителей" + } +}, +{ + "model": "rsform.constituenta", + "pk": 781, + "fields": { + "schema": 37, + "order": 45, + "alias": "F28", + "cst_type": "function", + "convention": "", + "term_raw": "братья", + "term_resolved": "братья", + "term_forms": [ + { + "tags": "plur,gent", + "text": "братьев" + } + ], + "definition_formal": "[α∈X1] F5[α]∩S1", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 782, + "fields": { + "schema": 37, + "order": 46, + "alias": "F29", + "cst_type": "function", + "convention": "", + "term_raw": "@{F28|nomn,plur} группы @{X1|gent,plur}", + "term_resolved": "братья группы индивидов", + "term_forms": [], + "definition_formal": "[α∈ℬ(X1)] F6[α]∩S1", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 783, + "fields": { + "schema": 37, + "order": 47, + "alias": "F30", + "cst_type": "function", + "convention": "", + "term_raw": "сёстры", + "term_resolved": "сёстры", + "term_forms": [], + "definition_formal": "[α∈X1] F5[α]∩S2", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 784, + "fields": { + "schema": 37, + "order": 48, + "alias": "F31", + "cst_type": "function", + "convention": "", + "term_raw": "@{F30|nomn,plur} группы @{X1|gent,plur}", + "term_resolved": "сёстры группы индивидов", + "term_forms": [], + "definition_formal": "[α∈ℬ(X1)] F6[α]∩S2", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 785, + "fields": { + "schema": 37, + "order": 49, + "alias": "F32", + "cst_type": "function", + "convention": "", + "term_raw": "полнородные @{F5|nomn,plur}", + "term_resolved": "полнородные сиблинги", + "term_forms": [], + "definition_formal": "[α∈X1] D{ξ∈F5[α] | F1[α]=F1[ξ] }", + "definition_raw": "@{F5|plur,nomn} данного @{X1|sing,gent}, @{D2|plur,nomn} которых полностью совпадают с его @{D2|plur,gent}", + "definition_resolved": "сиблинги данного индивида, родители которых полностью совпадают с его родителей" + } +}, +{ + "model": "rsform.constituenta", + "pk": 786, + "fields": { + "schema": 37, + "order": 50, + "alias": "F33", + "cst_type": "function", + "convention": "", + "term_raw": "полнородные @{F28|nomn,plur} @{X1|gent,sing}", + "term_resolved": "полнородные братья индивида", + "term_forms": [], + "definition_formal": "[α∈X1] F32[α]∩S1", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 787, + "fields": { + "schema": 37, + "order": 51, + "alias": "F34", + "cst_type": "function", + "convention": "", + "term_raw": "полнородные @{F30|nomn,plur} @{X1|gent,sing}", + "term_resolved": "полнородные сёстры индивида", + "term_forms": [], + "definition_formal": "[α∈X1] F32[α]∩S2", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 788, + "fields": { + "schema": 37, + "order": 52, + "alias": "F35", + "cst_type": "function", + "convention": "", + "term_raw": "единокровные @{F5|nomn,plur}", + "term_resolved": "единокровные сиблинги", + "term_forms": [], + "definition_formal": "[α∈X1] D{ξ∈F5[α] | F17[α]=F17[ξ] & F18[α]≠F18[ξ] }", + "definition_raw": "@{F5|nomn,plur}, у которых @{1|общий} @{D6|nomn,sing}, но разные @{D7|nomn,plur}", + "definition_resolved": "сиблинги, у которых общий отец, но разные матери" + } +}, +{ + "model": "rsform.constituenta", + "pk": 789, + "fields": { + "schema": 37, + "order": 53, + "alias": "F36", + "cst_type": "function", + "convention": "", + "term_raw": "единокровные @{F28|nomn,plur}", + "term_resolved": "единокровные братья", + "term_forms": [], + "definition_formal": "[α∈X1] F35[α]∩S1", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 790, + "fields": { + "schema": 37, + "order": 54, + "alias": "F37", + "cst_type": "function", + "convention": "", + "term_raw": "единокровные @{F30|nomn,plur}", + "term_resolved": "единокровные сёстры", + "term_forms": [], + "definition_formal": "[α∈X1] F35[α]∩S2", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 791, + "fields": { + "schema": 37, + "order": 55, + "alias": "F38", + "cst_type": "function", + "convention": "", + "term_raw": "единоутробные @{F5|nomn,plur}", + "term_resolved": "единоутробные сиблинги", + "term_forms": [], + "definition_formal": "[α∈X1] D{ξ∈F5[α] | F17[α]≠F17[ξ] & F18[α]=F18[ξ] }", + "definition_raw": "@{F5|nomn,plur}, у которых @{1|общий} @{D7|nomn,sing}, но разные @{D6|nomn,plur}", + "definition_resolved": "сиблинги, у которых общая мать, но разные отцы" + } +}, +{ + "model": "rsform.constituenta", + "pk": 792, + "fields": { + "schema": 37, + "order": 56, + "alias": "F39", + "cst_type": "function", + "convention": "", + "term_raw": "единоутробные @{F28|nomn,plur}", + "term_resolved": "единоутробные братья", + "term_forms": [], + "definition_formal": "[α∈X1] F38[α]∩F28[α]", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 793, + "fields": { + "schema": 37, + "order": 57, + "alias": "F40", + "cst_type": "function", + "convention": "", + "term_raw": "единоутробные @{F30|nomn,plur}", + "term_resolved": "единоутробные сёстры", + "term_forms": [], + "definition_formal": "[α∈X1] F38[α]∩F30[α]", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 794, + "fields": { + "schema": 37, + "order": 58, + "alias": "F41", + "cst_type": "function", + "convention": "", + "term_raw": "двоюродные @{F5|nomn,plur}", + "term_resolved": "двоюродные сиблинги", + "term_forms": [ + { + "tags": "plur,gent", + "text": "двоюродных сиблингов" + } + ], + "definition_formal": "[α∈X1] F15[F6[F1[α]]]", + "definition_raw": "@{D1|nomn,plur} @{F5|gent,plur} @{D2|gent,plur}", + "definition_resolved": "дети сиблингов родителей" + } +}, +{ + "model": "rsform.constituenta", + "pk": 795, + "fields": { + "schema": 37, + "order": 59, + "alias": "F42", + "cst_type": "function", + "convention": "", + "term_raw": "@{F41|nomn,plur} группы @{X1|gent,plur}", + "term_resolved": "двоюродные сиблинги группы индивидов", + "term_forms": [], + "definition_formal": "[α∈ℬ(X1)] F15[F6[F2[α]]]", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 796, + "fields": { + "schema": 37, + "order": 60, + "alias": "F43", + "cst_type": "function", + "convention": "", + "term_raw": "@{F41|nomn,plur} по @{D6|datv,sing}", + "term_resolved": "двоюродные сиблинги по отцу", + "term_forms": [], + "definition_formal": "[α∈X1] F15[F6[F17[α]]]", + "definition_raw": "@{D1|nomn,plur} @{F5|gent,plur} @{D6|gent,sing}", + "definition_resolved": "дети сиблингов отца" + } +}, +{ + "model": "rsform.constituenta", + "pk": 797, + "fields": { + "schema": 37, + "order": 61, + "alias": "F44", + "cst_type": "function", + "convention": "", + "term_raw": "@{F41|nomn,plur} по @{D7|datv,sing}", + "term_resolved": "двоюродные сиблинги по матери", + "term_forms": [], + "definition_formal": "[α∈X1] F15[F6[F18[α]]]", + "definition_raw": "@{D1|nomn,plur} @{F5|gent,plur} @{D7|gent,sing}", + "definition_resolved": "дети сиблингов матери" + } +}, +{ + "model": "rsform.constituenta", + "pk": 798, + "fields": { + "schema": 37, + "order": 62, + "alias": "F45", + "cst_type": "function", + "convention": "", + "term_raw": "двоюродные @{F28|nomn,plur}", + "term_resolved": "двоюродные братья", + "term_forms": [], + "definition_formal": "[α∈X1] F21[F6[F1[α]]]", + "definition_raw": "@{F20|nomn,plur} @{F5|gent,plur} @{D2|gent,plur}", + "definition_resolved": "сыновья сиблингов родителей" + } +}, +{ + "model": "rsform.constituenta", + "pk": 799, + "fields": { + "schema": 37, + "order": 63, + "alias": "F46", + "cst_type": "function", + "convention": "", + "term_raw": "@{F45|nomn,plur} группы @{X1|gent,plur}", + "term_resolved": "двоюродные братья группы индивидов", + "term_forms": [], + "definition_formal": "[α∈ℬ(X1)] F21[F6[F2[α]]]", + "definition_raw": "", + "definition_resolved": "" + } +}, +{ + "model": "rsform.constituenta", + "pk": 800, + "fields": { + "schema": 37, + "order": 64, + "alias": "F47", + "cst_type": "function", + "convention": "", + "term_raw": "двоюродные @{F30|nomn,plur}", + "term_resolved": "двоюродные сёстры", + "term_forms": [], + "definition_formal": "[α∈X1] F23[F6[F1[α]]]", + "definition_raw": "@{F41|nomn,plur}, являющиеся @{S2|ablt,plur}", + "definition_resolved": "двоюродные сиблинги, являющиеся женщинами" + } +}, +{ + "model": "rsform.constituenta", + "pk": 801, + "fields": { + "schema": 37, + "order": 65, + "alias": "F48", + "cst_type": "function", + "convention": "", + "term_raw": "@{F47|nomn,plur} группы @{X1|gent,plur}", + "term_resolved": "двоюродные сёстры группы индивидов", + "term_forms": [], + "definition_formal": "[α∈ℬ(X1)] F23[F6[F2[α]]]", + "definition_raw": "@{F22|nomn,plur} @{F5|gent,plur} @{D2|gent,plur}", + "definition_resolved": "дочери сиблингов родителей" + } +}, +{ + "model": "rsform.constituenta", + "pk": 802, + "fields": { + "schema": 37, + "order": 66, + "alias": "F49", + "cst_type": "function", + "convention": "", + "term_raw": "стрыйчичи", + "term_resolved": "стрыйчичи", + "term_forms": [], + "definition_formal": "[α∈X1] F45[α]∩F43[α]", + "definition_raw": "@{F45|nomn,plur} по @{D6|datv,sing}", + "definition_resolved": "двоюродные братья по отцу" + } +}, +{ + "model": "rsform.constituenta", + "pk": 803, + "fields": { + "schema": 37, + "order": 67, + "alias": "F50", + "cst_type": "function", + "convention": "", + "term_raw": "стрыечки", + "term_resolved": "стрыечки", + "term_forms": [], + "definition_formal": "[α∈X1] F47[α]∩F43[α]", + "definition_raw": "@{F47|nomn,plur} по @{D6|datv,sing}", + "definition_resolved": "двоюродные сёстры по отцу" + } +}, +{ + "model": "rsform.constituenta", + "pk": 804, + "fields": { + "schema": 37, + "order": 68, + "alias": "F51", + "cst_type": "function", + "convention": "", + "term_raw": "уйчичи", + "term_resolved": "уйчичи", + "term_forms": [], + "definition_formal": "[α∈X1] F45[α]∩F44[α]", + "definition_raw": "@{F45|nomn,plur} по @{D7|datv,sing}", + "definition_resolved": "двоюродные братья по матери" + } +}, +{ + "model": "rsform.constituenta", + "pk": 805, + "fields": { + "schema": 37, + "order": 69, + "alias": "F52", + "cst_type": "function", + "convention": "", + "term_raw": "дяди", + "term_resolved": "дяди", + "term_forms": [], + "definition_formal": "[α∈X1] F29[F1[α]]", + "definition_raw": "@{F28|nomn,plur} @{D2|gent,plur}", + "definition_resolved": "братья родителей" + } +}, +{ + "model": "rsform.constituenta", + "pk": 806, + "fields": { + "schema": 37, + "order": 70, + "alias": "F53", + "cst_type": "function", + "convention": "", + "term_raw": "стрыи", + "term_resolved": "стрыи", + "term_forms": [], + "definition_formal": "[α∈D1] F29[F17[α]]", + "definition_raw": "@{F28|nomn,plur} @{D6|gent,sing}", + "definition_resolved": "братья отца" + } +}, +{ + "model": "rsform.constituenta", + "pk": 807, + "fields": { + "schema": 37, + "order": 71, + "alias": "F54", + "cst_type": "function", + "convention": "", + "term_raw": "вуи", + "term_resolved": "вуи", + "term_forms": [ + { + "tags": "sing,nomn", + "text": "вуй" + }, + { + "tags": "sing,gent", + "text": "вуя" + }, + { + "tags": "sing,datv", + "text": "вую" + }, + { + "tags": "plur,gent", + "text": "вуёв" + } + ], + "definition_formal": "[α∈D1] F29[F18[α]]", + "definition_raw": "@{F28|nomn,plur} @{D7|gent,sing}", + "definition_resolved": "братья матери" + } +}, +{ + "model": "rsform.constituenta", + "pk": 808, + "fields": { + "schema": 37, + "order": 72, + "alias": "F55", + "cst_type": "function", + "convention": "", + "term_raw": "тёти", + "term_resolved": "тёти", + "term_forms": [], + "definition_formal": "[α∈X1] F31[F1[α]]", + "definition_raw": "@{F30|nomn,plur} @{D2|gent,plur}", + "definition_resolved": "сёстры родителей" + } +}, +{ + "model": "rsform.constituenta", + "pk": 809, + "fields": { + "schema": 37, + "order": 73, + "alias": "F56", + "cst_type": "function", + "convention": "", + "term_raw": "стрыйны", + "term_resolved": "стрыйны", + "term_forms": [], + "definition_formal": "[α∈D1] F31[F17[α]]", + "definition_raw": "@{F30|nomn,plur} @{D6|gent,sing}", + "definition_resolved": "сёстры отца" + } +}, +{ + "model": "rsform.constituenta", + "pk": 810, + "fields": { + "schema": 37, + "order": 74, + "alias": "F57", + "cst_type": "function", + "convention": "", + "term_raw": "вуйны", + "term_resolved": "вуйны", + "term_forms": [], + "definition_formal": "[α∈D1] F31[F18[α]]", + "definition_raw": "@{F30|nomn,plur} @{D7|gent,sing}", + "definition_resolved": "сёстры матери" + } +}, +{ + "model": "rsform.constituenta", + "pk": 811, + "fields": { + "schema": 37, + "order": 75, + "alias": "F58", + "cst_type": "function", + "convention": "", + "term_raw": "племянники", + "term_resolved": "племянники", + "term_forms": [], + "definition_formal": "[α∈X1] F21[F5[α]]", + "definition_raw": "@{F20|nomn,plur} @{F5|gent,plur}", + "definition_resolved": "сыновья сиблингов" + } +}, +{ + "model": "rsform.constituenta", + "pk": 812, + "fields": { + "schema": 37, + "order": 76, + "alias": "F59", + "cst_type": "function", + "convention": "", + "term_raw": "братычи", + "term_resolved": "братычи", + "term_forms": [], + "definition_formal": "[α∈X1] F21[F28[α]]", + "definition_raw": "@{F20|nomn,plur} @{F28|gent,plur}", + "definition_resolved": "сыновья братьев" + } +}, +{ + "model": "rsform.constituenta", + "pk": 813, + "fields": { + "schema": 37, + "order": 77, + "alias": "F60", + "cst_type": "function", + "convention": "", + "term_raw": "сестричи", + "term_resolved": "сестричи", + "term_forms": [], + "definition_formal": "[α∈X1] F21[F30[α]]", + "definition_raw": "@{F20|nomn,plur} @{F30|gent,plur}", + "definition_resolved": "сыновья сестёр" + } +}, +{ + "model": "rsform.constituenta", + "pk": 814, + "fields": { + "schema": 37, + "order": 78, + "alias": "F61", + "cst_type": "function", + "convention": "", + "term_raw": "племянницы", + "term_resolved": "племянницы", + "term_forms": [], + "definition_formal": "[α∈X1] F23[F5[α]]", + "definition_raw": "@{F22|gent,sing} @{F5|gent,plur}", + "definition_resolved": "дочери сиблингов" + } +}, +{ + "model": "rsform.constituenta", + "pk": 815, + "fields": { + "schema": 37, + "order": 79, + "alias": "F62", + "cst_type": "function", + "convention": "", + "term_raw": "братанины", + "term_resolved": "братанины", + "term_forms": [], + "definition_formal": "[α∈X1] F23[F28[α]]", + "definition_raw": "@{F22|gent,sing} @{F28|gent,plur}", + "definition_resolved": "дочери братьев" + } +}, +{ + "model": "rsform.constituenta", + "pk": 816, + "fields": { + "schema": 37, + "order": 80, + "alias": "F63", + "cst_type": "function", + "convention": "", + "term_raw": "сестрины", + "term_resolved": "сестрины", + "term_forms": [], + "definition_formal": "[α∈X1] F23[F30[α]]", + "definition_raw": "@{F22|gent,sing} @{F30|gent,plur}", + "definition_resolved": "дочери сестёр" + } +}, +{ + "model": "rsform.constituenta", + "pk": 817, + "fields": { + "schema": 37, + "order": 81, + "alias": "F64", + "cst_type": "function", + "convention": "", + "term_raw": "двоюродные @{F52|nomn,plur}", + "term_resolved": "двоюродные дяди", + "term_forms": [], + "definition_formal": "[α∈X1] F46[F1[α]]", + "definition_raw": "@{F45|nomn,plur} @{D2|gent,plur}", + "definition_resolved": "двоюродные братья родителей" + } +}, +{ + "model": "rsform.constituenta", + "pk": 818, + "fields": { + "schema": 37, + "order": 82, + "alias": "F65", + "cst_type": "function", + "convention": "", + "term_raw": "двоюродные @{F55|nomn,plur}", + "term_resolved": "двоюродные тёти", + "term_forms": [], + "definition_formal": "[α∈X1] F48[F1[α]]", + "definition_raw": "@{F47|nomn,plur} @{D2|gent,plur}", + "definition_resolved": "двоюродные сёстры родителей" + } +}, +{ + "model": "rsform.constituenta", + "pk": 819, + "fields": { + "schema": 37, + "order": 83, + "alias": "F66", + "cst_type": "function", + "convention": "", + "term_raw": "двоюродные @{F58|nomn,plur}", + "term_resolved": "двоюродные племянники", + "term_forms": [], + "definition_formal": "[α∈X1] F21[F41[α]]", + "definition_raw": "@{F20|nomn,plur} @{F41|gent,plur}", + "definition_resolved": "сыновья двоюродных сиблингов" + } +}, +{ + "model": "rsform.constituenta", + "pk": 820, + "fields": { + "schema": 37, + "order": 84, + "alias": "F67", + "cst_type": "function", + "convention": "", + "term_raw": "дщеричи", + "term_resolved": "дщеричи", + "term_forms": [], + "definition_formal": "[α∈X1] F21[F47[α]]", + "definition_raw": "@{F58|nomn,plur} по @{F55|datv,sing}", + "definition_resolved": "племянники по тёте" + } +}, +{ + "model": "rsform.constituenta", + "pk": 821, + "fields": { + "schema": 37, + "order": 85, + "alias": "F68", + "cst_type": "function", + "convention": "", + "term_raw": "двоюродные @{F61|nomn,plur}", + "term_resolved": "двоюродные племянницы", + "term_forms": [], + "definition_formal": "[α∈X1] F23[F41[α]]", + "definition_raw": "@{F22|nomn,plur} @{F41|gent,plur}", + "definition_resolved": "дочери двоюродных сиблингов" + } +}, +{ + "model": "rsform.constituenta", + "pk": 822, + "fields": { + "schema": 37, + "order": 86, + "alias": "F69", + "cst_type": "function", + "convention": "", + "term_raw": "дщерши", + "term_resolved": "дщерши", + "term_forms": [], + "definition_formal": "[α∈X1] F23[F47[α]]", + "definition_raw": "@{F22|nomn,plur} @{F47|gent,plur}", + "definition_resolved": "дочери двоюродных сестёр" + } +}, +{ + "model": "rsform.constituenta", + "pk": 823, + "fields": { + "schema": 37, + "order": 87, + "alias": "F70", + "cst_type": "function", + "convention": "альтернативное определение: великий дядя - дядя родителя", + "term_raw": "двоюродные @{F26|nomn,plur}", + "term_resolved": "двоюродные дедушки", + "term_forms": [], + "definition_formal": "[α∈X1] F29[F8[α]]", + "definition_raw": "@{F28|nomn,plur} @{F8|gent,plur}", + "definition_resolved": "братья прародителей" + } +}, +{ + "model": "rsform.constituenta", + "pk": 824, + "fields": { + "schema": 37, + "order": 88, + "alias": "F71", + "cst_type": "function", + "convention": "", + "term_raw": "двоюродные @{F27|nomn,plur}", + "term_resolved": "двоюродные бабушки", + "term_forms": [], + "definition_formal": "[α∈X1] F31[F8[α]]", + "definition_raw": "@{F30|nomn,plur} @{F8|gent,plur}", + "definition_resolved": "сёстры прародителей" + } +}, +{ + "model": "rsform.constituenta", + "pk": 825, + "fields": { + "schema": 37, + "order": 89, + "alias": "F72", + "cst_type": "function", + "convention": "альтернативное определение: внук родного брата или сестры", + "term_raw": "внучатые @{F58|nomn,plur}", + "term_resolved": "внучатые племянники", + "term_forms": [], + "definition_formal": "[α∈X1] F21[F58[α]∪F61[α]]", + "definition_raw": "@{F20|nomn,plur} @{F58|gent,plur} и @{F61|gent,plur}", + "definition_resolved": "сыновья племянников и племянниц" + } +}, +{ + "model": "rsform.constituenta", + "pk": 826, + "fields": { + "schema": 37, + "order": 90, + "alias": "F73", + "cst_type": "function", + "convention": "", + "term_raw": "внучатые @{F61|nomn,plur}", + "term_resolved": "внучатые племянницы", + "term_forms": [], + "definition_formal": "[α∈X1] F23[F58[α]∪F61[α]]", + "definition_raw": "@{F22|nomn,plur} @{F58|gent,plur} и @{F61|gent,plur}", + "definition_resolved": "дочери племянников и племянниц" + } +}, +{ + "model": "rsform.constituenta", + "pk": 827, + "fields": { + "schema": 37, + "order": 91, + "alias": "F74", + "cst_type": "function", + "convention": "", + "term_raw": "свёкр", + "term_resolved": "свёкр", + "term_forms": [], + "definition_formal": "[α∈D4] F17[F9[α]]", + "definition_raw": "@{D6|nomn,sing} @{D3|gent,sing}", + "definition_resolved": "отец мужа" + } +}, +{ + "model": "rsform.constituenta", + "pk": 828, + "fields": { + "schema": 37, + "order": 92, + "alias": "F75", + "cst_type": "function", + "convention": "", + "term_raw": "свекровь", + "term_resolved": "свекровь", + "term_forms": [], + "definition_formal": "[α∈D4] F18[F9[α]]", + "definition_raw": "@{D7|nomn,sing} @{D3|gent,sing}", + "definition_resolved": "мать мужа" + } +}, +{ + "model": "rsform.constituenta", + "pk": 829, + "fields": { + "schema": 37, + "order": 93, + "alias": "F76", + "cst_type": "function", + "convention": "", + "term_raw": "тесть", + "term_resolved": "тесть", + "term_forms": [], + "definition_formal": "[α∈D3] F17[F10[α]]", + "definition_raw": "@{D6|nomn,sing} @{D4|gent,sing}", + "definition_resolved": "отец жены" + } +}, +{ + "model": "rsform.constituenta", + "pk": 830, + "fields": { + "schema": 37, + "order": 94, + "alias": "F77", + "cst_type": "function", + "convention": "", + "term_raw": "тёща", + "term_resolved": "тёща", + "term_forms": [], + "definition_formal": "[α∈D3] F18[F10[α]]", + "definition_raw": "@{D7|nomn,sing} @{D4|gent,sing}", + "definition_resolved": "мать жены" + } +}, +{ + "model": "rsform.constituenta", + "pk": 831, + "fields": { + "schema": 37, + "order": 95, + "alias": "F78", + "cst_type": "function", + "convention": "", + "term_raw": "сваты", + "term_resolved": "сваты", + "term_forms": [], + "definition_formal": "[α∈X1] F16[F13[F3[α]]]", + "definition_raw": "@{D6|nomn,plur} @{D5|gent,plur} @{D1|gent,plur}", + "definition_resolved": "отцы супругов детей" + } +}, +{ + "model": "rsform.constituenta", + "pk": 832, + "fields": { + "schema": 37, + "order": 96, + "alias": "F79", + "cst_type": "function", + "convention": "", + "term_raw": "сватьи", + "term_resolved": "сватьи", + "term_forms": [], + "definition_formal": "[α∈X1] F19[F13[F3[α]]]", + "definition_raw": "@{D7|gent,sing} @{D5|gent,plur} @{D1|gent,plur}", + "definition_resolved": "матери супругов детей" + } +}, +{ + "model": "rsform.constituenta", + "pk": 833, + "fields": { + "schema": 37, + "order": 97, + "alias": "F80", + "cst_type": "function", + "convention": "", + "term_raw": "девери", + "term_resolved": "девери", + "term_forms": [], + "definition_formal": "[α∈D4] F28[F9[α]]", + "definition_raw": "@{F28|nomn,plur} @{D3|gent,sing}", + "definition_resolved": "братья мужа" + } +}, +{ + "model": "rsform.constituenta", + "pk": 834, + "fields": { + "schema": 37, + "order": 98, + "alias": "F81", + "cst_type": "function", + "convention": "", + "term_raw": "золовки", + "term_resolved": "золовки", + "term_forms": [], + "definition_formal": "[α∈D4] F30[F9[α]]", + "definition_raw": "@{F30|nomn,plur} @{D3|gent,sing}", + "definition_resolved": "сёстры мужа" + } +}, +{ + "model": "rsform.constituenta", + "pk": 835, + "fields": { + "schema": 37, + "order": 99, + "alias": "F82", + "cst_type": "function", + "convention": "", + "term_raw": "шурины", + "term_resolved": "шурины", + "term_forms": [], + "definition_formal": "[α∈D3] F28[F10[α]]", + "definition_raw": "@{F28|nomn,plur} @{D4|gent,sing}", + "definition_resolved": "братья жены" + } +}, +{ + "model": "rsform.constituenta", + "pk": 836, + "fields": { + "schema": 37, + "order": 100, + "alias": "F83", + "cst_type": "function", + "convention": "", + "term_raw": "свояченицы", + "term_resolved": "свояченицы", + "term_forms": [], + "definition_formal": "[α∈D3] F30[F10[α]]", + "definition_raw": "@{F30|nomn,plur} @{D4|gent,sing}", + "definition_resolved": "сёстры жены" + } +}, +{ + "model": "rsform.constituenta", + "pk": 837, + "fields": { + "schema": 37, + "order": 101, + "alias": "F84", + "cst_type": "function", + "convention": "", + "term_raw": "шуричи", + "term_resolved": "шуричи", + "term_forms": [], + "definition_formal": "[α∈D3] F21[F82[α]]", + "definition_raw": "@{F20|nomn,plur} @{F82|gent,plur}", + "definition_resolved": "сыновья шуринов" + } +}, +{ + "model": "rsform.constituenta", + "pk": 838, + "fields": { + "schema": 37, + "order": 102, + "alias": "F85", + "cst_type": "function", + "convention": "", + "term_raw": "снохи", + "term_resolved": "снохи", + "term_forms": [], + "definition_formal": "[α∈X1] F12[F20[α]]", + "definition_raw": "@{D4|nomn,plur} @{F20|gent,plur}", + "definition_resolved": "жены сыновей" + } +}, +{ + "model": "rsform.constituenta", + "pk": 839, + "fields": { + "schema": 37, + "order": 103, + "alias": "F86", + "cst_type": "function", + "convention": "", + "term_raw": "свояки", + "term_resolved": "свояки", + "term_forms": [], + "definition_formal": "[α∈D3] F11[F83[α]]", + "definition_raw": "@{D3|nomn,plur} @{F83|gent,plur}", + "definition_resolved": "мужья своячениц" + } +}, +{ + "model": "rsform.constituenta", + "pk": 840, + "fields": { + "schema": 37, + "order": 104, + "alias": "F87", + "cst_type": "function", + "convention": "", + "term_raw": "ятровки", + "term_resolved": "ятровки", + "term_forms": [], + "definition_formal": "[α∈D4] F12[F80[α]]", + "definition_raw": "@{D4|nomn,plur} @{F80|gent,plur}", + "definition_resolved": "жены деверей" + } +}, +{ + "model": "rsform.constituenta", + "pk": 841, + "fields": { + "schema": 37, + "order": 105, + "alias": "F88", + "cst_type": "function", + "convention": "", + "term_raw": "зятья", + "term_resolved": "зятья", + "term_forms": [], + "definition_formal": "[α∈X1] F11[F22[α]∪F30[α]∪F81[α]∪F61[α]]", + "definition_raw": "@{D3|nomn,plur} @{F22|gent,plur}, @{F30|gent,plur}, @{F81|gent,plur} или @{F61|gent,plur}", + "definition_resolved": "мужья дочерей, сестёр, золовок или племянниц" + } +}, +{ + "model": "rsform.constituenta", + "pk": 842, + "fields": { + "schema": 37, + "order": 106, + "alias": "F89", + "cst_type": "function", + "convention": "", + "term_raw": "невестки", + "term_resolved": "невестки", + "term_forms": [], + "definition_formal": "[α∈X1] F12[F20[α]∪F28[α]∪F80[α]∪F58[α]]", + "definition_raw": "@{D4|nomn,plur} @{F20|gent,plur}, @{F28|gent,plur}, @{F80|gent,plur} или @{F58|gent,plur}", + "definition_resolved": "жены сыновей, братьев, деверей или племянников" + } +}, +{ + "model": "rsform.constituenta", + "pk": 843, + "fields": { + "schema": 37, + "order": 107, + "alias": "F90", + "cst_type": "function", + "convention": "", + "term_raw": "вуйки", + "term_resolved": "вуйки", + "term_forms": [], + "definition_formal": "[α∈X1] F12[F54[α]]", + "definition_raw": "@{D4|nomn,plur} @{F54|gent,plur}", + "definition_resolved": "жены вуи" + } +}, +{ + "model": "rsform.constituenta", + "pk": 844, + "fields": { + "schema": 37, + "order": 108, + "alias": "F91", + "cst_type": "function", + "convention": "", + "term_raw": "братанихи", + "term_resolved": "братанихи", + "term_forms": [], + "definition_formal": "[α∈X1] F12[F45[α]]", + "definition_raw": "@{D4|nomn,plur} @{F45|gent,plur}", + "definition_resolved": "жены двоюродных братьев" + } +}, +{ + "model": "rsform.constituenta", + "pk": 845, + "fields": { + "schema": 37, + "order": 109, + "alias": "D9", + "cst_type": "term", + "convention": "", + "term_raw": "законные @{D1|nomn,plur}", + "term_resolved": "законные дети", + "term_forms": [], + "definition_formal": "D{ξ∈D1 | F13[F1[ξ]]=F1[ξ]}", + "definition_raw": "@{D1|nomn,plur}, @{D2|nomn,plur} которых состоят в @{S4|loct,sing}", + "definition_resolved": "дети, родители которых состоят в браке" + } +}, +{ + "model": "rsform.constituenta", + "pk": 846, + "fields": { + "schema": 37, + "order": 110, + "alias": "D10", + "cst_type": "term", + "convention": "", + "term_raw": "бастарды", + "term_resolved": "бастарды", + "term_forms": [], + "definition_formal": "D1\\D9", + "definition_raw": "@{D1|nomn,plur}, @{D2|nomn,plur} которых не состоят в @{S4|loct,sing}", + "definition_resolved": "дети, родители которых не состоят в браке" + } +}, +{ + "model": "rsform.constituenta", + "pk": 847, + "fields": { + "schema": 37, + "order": 111, + "alias": "F92", + "cst_type": "function", + "convention": "", + "term_raw": "сводные @{F5|nomn,plur}", + "term_resolved": "сводные сиблинги", + "term_forms": [], + "definition_formal": "[α∈X1] F4[F13[F1[α]]] \\ F5[α] \\ {α}", + "definition_raw": "@{D1|nomn,plur} @{D5|gent,plur} @{D2|gent,plur}, не являющиеся @{F5|ablt,plur}", + "definition_resolved": "дети супругов родителей, не являющиеся сиблингами" + } +}, +{ + "model": "rsform.constituenta", + "pk": 848, + "fields": { + "schema": 37, + "order": 112, + "alias": "F93", + "cst_type": "function", + "convention": "", + "term_raw": "сводные @{F28|nomn,plur}", + "term_resolved": "сводные братья", + "term_forms": [], + "definition_formal": "[α∈X1] F21[F13[F1[α]]] \\ F28[α] \\ {α}", + "definition_raw": "@{F20|nomn,plur} @{D5|gent,plur} @{D2|gent,plur}, не являющиеся @{F28|ablt,plur}", + "definition_resolved": "сыновья супругов родителей, не являющиеся братьями" + } +}, +{ + "model": "rsform.constituenta", + "pk": 849, + "fields": { + "schema": 37, + "order": 113, + "alias": "F94", + "cst_type": "function", + "convention": "", + "term_raw": "сводные @{F30|nomn,plur}", + "term_resolved": "сводные сёстры", + "term_forms": [], + "definition_formal": "[α∈X1] F23[F13[F1[α]]] \\ F30[α] \\ {α}", + "definition_raw": "@{F22|nomn,plur} @{D5|gent,plur} @{D2|gent,plur}, не являющиеся @{F30|ablt,plur}", + "definition_resolved": "дочери супругов родителей, не являющиеся сёстрами" + } +}, +{ + "model": "rsform.constituenta", + "pk": 850, + "fields": { + "schema": 37, + "order": 114, + "alias": "F95", + "cst_type": "function", + "convention": "", + "term_raw": "пасынки", + "term_resolved": "пасынки", + "term_forms": [], + "definition_formal": "[α∈X1] F21[F13[{α}]] \\ F20[α]", + "definition_raw": "@{F20|nomn,plur} @{D5|gent,sing}, не являющиеся @{D1|ablt,plur}", + "definition_resolved": "сыновья супруга, не являющиеся детьми" + } +}, +{ + "model": "rsform.constituenta", + "pk": 851, + "fields": { + "schema": 37, + "order": 115, + "alias": "F96", + "cst_type": "function", + "convention": "", + "term_raw": "падчерицы", + "term_resolved": "падчерицы", + "term_forms": [], + "definition_formal": "[α∈X1] F23[F13[{α}]] \\ F22[α]", + "definition_raw": "@{F22|nomn,plur} @{D5|gent,sing}, не являющиеся @{D1|ablt,plur}", + "definition_resolved": "дочери супруга, не являющиеся детьми" + } +}, +{ + "model": "rsform.constituenta", + "pk": 852, + "fields": { + "schema": 37, + "order": 116, + "alias": "F97", + "cst_type": "function", + "convention": "", + "term_raw": "отчимы", + "term_resolved": "отчимы", + "term_forms": [], + "definition_formal": "[α∈X1] F11[F19[{α}]] \\ F16[{α}]", + "definition_raw": "@{D3|nomn,sing} @{D7|gent,sing}, не являющийся @{D6|ablt,sing}", + "definition_resolved": "муж матери, не являющийся отцом" + } +}, +{ + "model": "rsform.constituenta", + "pk": 853, + "fields": { + "schema": 37, + "order": 117, + "alias": "F98", + "cst_type": "function", + "convention": "", + "term_raw": "мачехи", + "term_resolved": "мачехи", + "term_forms": [], + "definition_formal": "[α∈X1] F12[F16[{α}]] \\ F19[{α}]", + "definition_raw": "@{D4|nomn,sing} @{D6|gent,sing}, не являющаяся @{D7|ablt,sing}", + "definition_resolved": "жена отца, не являющаяся матерью" + } +}, +{ + "model": "rsform.editor", + "pk": 2, + "fields": { + "item": 35, + "editor": 3, + "time_create": "2024-06-03T20:55:45.359Z" + } +}, +{ + "model": "rsform.subscription", + "pk": 11, + "fields": { + "user": 1, + "item": 34 + } +}, +{ + "model": "rsform.subscription", + "pk": 12, + "fields": { + "user": 5, + "item": 35 + } +}, +{ + "model": "rsform.subscription", + "pk": 13, + "fields": { + "user": 3, + "item": 36 + } +}, +{ + "model": "rsform.subscription", + "pk": 14, + "fields": { + "user": 3, + "item": 37 + } +}, +{ + "model": "rsform.libraryitem", + "pk": 34, + "fields": { + "item_type": "rsform", + "owner": 1, + "title": "Банк выражений", + "alias": "БВ", + "comment": "Банк шаблонов для генерации выражений", + "visible": true, + "read_only": false, + "access_policy": "public", + "location": "/L", + "time_create": "2024-06-03T20:52:06.322Z", + "time_update": "2024-06-03T20:52:06.325Z" + } +}, +{ + "model": "rsform.libraryitem", + "pk": 35, + "fields": { + "item_type": "rsform", + "owner": 5, + "title": "Групповая операция", + "alias": "БК09", + "comment": "", + "visible": true, + "read_only": false, + "access_policy": "public", + "location": "/S", + "time_create": "2024-06-03T20:55:36.489Z", + "time_update": "2024-06-03T20:55:36.492Z" + } +}, +{ + "model": "rsform.libraryitem", + "pk": 36, + "fields": { + "item_type": "rsform", + "owner": 3, + "title": "Булева алгебра", + "alias": "БК12", + "comment": "", + "visible": true, + "read_only": false, + "access_policy": "public", + "location": "/S", + "time_create": "2024-06-03T20:58:30.529Z", + "time_update": "2024-06-03T20:58:32.512Z" + } +}, +{ + "model": "rsform.libraryitem", + "pk": 37, + "fields": { + "item_type": "rsform", + "owner": 3, + "title": "Генеалогия", + "alias": "D0001", + "comment": "построено на основе понятия \"родство\" из Википедии", + "visible": true, + "read_only": false, + "access_policy": "public", + "location": "/U/test", + "time_create": "2024-06-03T20:59:40.845Z", + "time_update": "2024-06-03T21:00:01.809Z" + } +}, +{ + "model": "rsform.librarytemplate", + "pk": 1, + "fields": { + "lib_source": 34 + } +} +] diff --git a/rsconcept/backend/manage.py b/rsconcept/backend/manage.py new file mode 100644 index 00000000..2c49f3ac --- /dev/null +++ b/rsconcept/backend/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/rsconcept/backend/mypy.ini b/rsconcept/backend/mypy.ini new file mode 100644 index 00000000..519f4292 --- /dev/null +++ b/rsconcept/backend/mypy.ini @@ -0,0 +1,20 @@ +# Global options: + +[mypy] +warn_return_any = True +warn_unused_configs = True + +plugins = mypy_django_plugin.main + +# Per-module options: +[mypy.plugins.django-stubs] +django_settings_module = "project.settings" + +[mypy-django_filters.*] +ignore_missing_imports = True + +[mypy-pyconcept.*] +ignore_missing_imports = True + +[mypy-cctext.*] +ignore_missing_imports = True diff --git a/rsconcept/backend/project/__init__.py b/rsconcept/backend/project/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rsconcept/backend/project/settings.py b/rsconcept/backend/project/settings.py new file mode 100644 index 00000000..2d2f0de2 --- /dev/null +++ b/rsconcept/backend/project/settings.py @@ -0,0 +1,249 @@ +''' +Django settings for project. + +Generated by 'django-admin startproject' using Django 4.1.7. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.1/ref/settings/ +''' + +import logging +import os +import sys +from pathlib import Path + + +def _get_secret(key: str, default): + value = os.environ.get(key, default) + if os.path.isfile(value): + with open(value, mode='r', encoding='utf-8') as f: + return f.read() + return value + + +_TRUE_VARIANTS = [True, 'True', '1'] + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = _get_secret('SECRET_KEY', 'not-a-secret') + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = os.environ.get('DEBUG', True) in _TRUE_VARIANTS + +ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '*').split(';') +INTERNAL_IPS = ['127.0.0.1'] if DEBUG else [] + +# MAIL SETUP +EMAIL_HOST = _get_secret('EMAIL_HOST', '') +EMAIL_PORT = int(os.environ.get('EMAIL_PORT', '1025')) +EMAIL_USE_SSL = os.environ.get('EMAIL_SSL', False) in _TRUE_VARIANTS +EMAIL_USE_TLS = os.environ.get('EMAIL_TLS', False) in _TRUE_VARIANTS +EMAIL_HOST_USER = _get_secret('EMAIL_HOST_USER', '') +EMAIL_HOST_PASSWORD = _get_secret('EMAIL_HOST_PASSWORD', '') + +DEFAULT_FROM_EMAIL = EMAIL_HOST_USER +SERVER_EMAIL = EMAIL_HOST_USER +EMAIL_ADMIN = EMAIL_HOST_USER +EMAIL_BACKEND = \ + 'django.core.mail.backends.smtp.EmailBackend' \ + if EMAIL_HOST != '' else \ + 'django.core.mail.backends.console.EmailBackend' + + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + + 'django_filters', + 'rest_framework', + 'django_rest_passwordreset', + 'corsheaders', + + 'apps.users', + 'apps.rsform', + + 'drf_spectacular', + 'drf_spectacular_sidecar', +] + + +REST_FRAMEWORK = { + 'TEST_REQUEST_DEFAULT_FORMAT': 'json', + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.SessionAuthentication', + ], + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.AllowAny' + ], + 'DEFAULT_FILTER_BACKENDS': [ + 'django_filters.rest_framework.DjangoFilterBackend' + ], +} + + +CORS_ALLOW_CREDENTIALS = True +CORS_ALLOWED_ORIGINS = os.environ.get('CORS_ALLOWED_ORIGINS', 'http://localhost:3000').split(';') +CSRF_TRUSTED_ORIGINS = os.environ.get('CSRF_TRUSTED_ORIGINS', 'http://localhost:3000').split(';') +CSRF_COOKIE_AGE = 365 * 24 * 60 * 60 +SESSION_COOKIE_AGE = 365 * 24 * 60 * 60 * 2 + +_domain = os.environ.get('CSRF_COOKIE_DOMAIN', '') +if _domain != '': + CSRF_COOKIE_DOMAIN = _domain + SESSION_COOKIE_DOMAIN = _domain + + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'project.urls' +LOGIN_URL = '/admin/login' +LOGIN_REDIRECT_URL = '/' +LOGOUT_REDIRECT_URL = '/' +APPEND_SLASH = False + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.1/howto/static-files/ + +STATIC_ROOT = os.environ.get('STATIC_ROOT', os.path.join(BASE_DIR, 'static')) +STATIC_URL = 'static/' +MEDIA_ROOT = os.environ.get('MEDIA_ROOT', os.path.join(BASE_DIR, 'media')) +MEDIA_URL = 'media/' + + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [str(BASE_DIR) + '/templates/'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'project.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.1/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': os.environ.get('DB_ENGINE', 'django.db.backends.sqlite3'), + 'NAME': os.environ.get('DB_NAME', BASE_DIR / 'db.sqlite3'), + 'USER': os.environ.get('DB_USER'), + 'PASSWORD': _get_secret('DB_PASSWORD', ''), + 'HOST': os.environ.get('DB_HOST'), + 'DB_PORT': os.environ.get('DB_PORT'), + } +} + +# drf-spectacular settings. API docs generator +# https://drf-spectacular.readthedocs.io/en/latest/settings.html +SPECTACULAR_SETTINGS = { + 'TITLE': 'ConceptPortal API', + 'DESCRIPTION': 'Портал для работы с экспликациями концептуальных схем', + 'VERSION': '0.1.2', + 'SERVE_INCLUDE_SCHEMA': False, + + 'COMPONENT_SPLIT_PATCH': True, + 'COMPONENT_SPLIT_REQUEST': True, + + 'SERVE_PERMISSIONS': ['rest_framework.permissions.AllowAny'], + 'SERVE_AUTHENTICATION': None, + + 'DISABLE_ERRORS_AND_WARNINGS': False, + + 'SWAGGER_UI_SETTINGS': { + 'deepLinking': True, + 'persistAuthorization': True, + 'tryItOutEnabled': False, + 'supportedSubmitMethods': [''], + }, + 'SWAGGER_UI_DIST': 'SIDECAR', + 'SWAGGER_UI_FAVICON_HREF': 'SIDECAR', + 'REDOC_DIST': 'SIDECAR' +} + + +# Password validation +# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS: list[str] = [ + # NOTE: Password validators disabled + # { + # 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + # }, + # { + # 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + # }, + # { + # 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + # }, + # { + # 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + # }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.1/topics/i18n/ + +LANGUAGE_CODE = 'ru' +TIME_ZONE = 'Europe/Moscow' +USE_I18N = True +USE_TZ = True + + +# Default primary key field type +# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + }, + }, + 'root': { + 'handlers': ['console'], + 'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO') + }, +} + + +if len(sys.argv) > 1 and sys.argv[1] == 'test': + logging.disable(logging.CRITICAL) diff --git a/rsconcept/backend/project/urls.py b/rsconcept/backend/project/urls.py new file mode 100644 index 00000000..550874ba --- /dev/null +++ b/rsconcept/backend/project/urls.py @@ -0,0 +1,20 @@ +''' Main URL router ''' +from django.conf import settings +from django.conf.urls.static import static +from django.contrib import admin +from django.urls import include, path +from django.views.generic.base import TemplateView +from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView + +urlpatterns = [ + path('admin', admin.site.urls), + path('api/', include('apps.rsform.urls')), + path('users/', include('apps.users.urls')), + path('schema', SpectacularAPIView.as_view(), name='schema'), + path('redoc', SpectacularRedocView.as_view()), + path( + 'robots.txt', + TemplateView.as_view(template_name='robots.txt', content_type='text/plain'), + ), + path('', SpectacularSwaggerView.as_view(), name='home'), +] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/rsconcept/backend/project/wsgi.py b/rsconcept/backend/project/wsgi.py new file mode 100644 index 00000000..ebe89c39 --- /dev/null +++ b/rsconcept/backend/project/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for rsconcept project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') + +application = get_wsgi_application() diff --git a/rsconcept/backend/requirements-dev.txt b/rsconcept/backend/requirements-dev.txt new file mode 100644 index 00000000..e3b3cb24 --- /dev/null +++ b/rsconcept/backend/requirements-dev.txt @@ -0,0 +1,19 @@ +tzdata +django +djangorestframework +django-cors-headers +django-filter +drf-spectacular +drf-spectacular[sidecar] +coreapi +cctext +pyconcept + +mypy +pylint +coverage +djangorestframework-stubs[compatible-mypy] +django-rest-passwordreset + +psycopg2-binary +gunicorn \ No newline at end of file diff --git a/rsconcept/backend/requirements.txt b/rsconcept/backend/requirements.txt new file mode 100644 index 00000000..4ad504c2 --- /dev/null +++ b/rsconcept/backend/requirements.txt @@ -0,0 +1,14 @@ +tzdata==2024.1 +Django==5.0.6 +djangorestframework==3.15.1 +django-cors-headers==4.3.1 +django-filter==24.2 +drf-spectacular==0.27.2 +drf-spectacular-sidecar==2024.5.1 +coreapi==2.3.3 +django-rest-passwordreset==1.4.1 +cctext==0.1.3 +pyconcept==0.1.5 + +psycopg2-binary==2.9.9 +gunicorn==22.0.0 \ No newline at end of file diff --git a/rsconcept/backend/templates/robots.txt b/rsconcept/backend/templates/robots.txt new file mode 100644 index 00000000..77470cb3 --- /dev/null +++ b/rsconcept/backend/templates/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file diff --git a/rsconcept/frontend/.dockerignore b/rsconcept/frontend/.dockerignore new file mode 100644 index 00000000..0441a02d --- /dev/null +++ b/rsconcept/frontend/.dockerignore @@ -0,0 +1,5 @@ +# Dev specific +.gitignore +node_modules +.env.local +.next \ No newline at end of file diff --git a/rsconcept/frontend/.env.local b/rsconcept/frontend/.env.local new file mode 100644 index 00000000..e406b9c7 --- /dev/null +++ b/rsconcept/frontend/.env.local @@ -0,0 +1,5 @@ +# Local build config + +VITE_PORTAL_BACKEND=http://localhost:8000 +VITE_PORTAL_FRONT_PORT=3000 +VITE_PORTAL_FRONT_HTTPS=false diff --git a/rsconcept/frontend/.eslintignore b/rsconcept/frontend/.eslintignore new file mode 100644 index 00000000..9a7e5cf6 --- /dev/null +++ b/rsconcept/frontend/.eslintignore @@ -0,0 +1,2 @@ +**/parser.ts +**/node_modules/** \ No newline at end of file diff --git a/rsconcept/frontend/.eslintrc.json b/rsconcept/frontend/.eslintrc.json new file mode 100644 index 00000000..8339fce5 --- /dev/null +++ b/rsconcept/frontend/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended-type-checked", + "plugin:react-hooks/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module", + "project": ["tsconfig.json", "tsconfig.node.json"] + }, + "plugins": ["react-refresh", "simple-import-sort", "eslint-plugin-tsdoc"], + "rules": { + "no-console": "off", + "require-jsdoc": "off", + "react-refresh/only-export-components": ["off", { "allowConstantExport": true }], + "simple-import-sort/imports": "warn", + "tsdoc/syntax": "warn" + } +} diff --git a/rsconcept/frontend/.prettierignore b/rsconcept/frontend/.prettierignore new file mode 100644 index 00000000..cbe6c044 --- /dev/null +++ b/rsconcept/frontend/.prettierignore @@ -0,0 +1,6 @@ +build/ +node_modules/ +package-lock.json +yarn.lock +package.json +coverage \ No newline at end of file diff --git a/rsconcept/frontend/.prettierrc.json b/rsconcept/frontend/.prettierrc.json new file mode 100644 index 00000000..d4dd284f --- /dev/null +++ b/rsconcept/frontend/.prettierrc.json @@ -0,0 +1,13 @@ +{ + "semi": true, + "useTabs": false, + "printWidth": 120, + "tabWidth": 2, + "trailingComma": "none", + "arrowParens": "avoid", + "singleQuote": true, + "jsxSingleQuote": true, + "quoteProps": "consistent", + "bracketSameLine": false, + "bracketSpacing": true +} \ No newline at end of file diff --git a/rsconcept/frontend/Dockerfile b/rsconcept/frontend/Dockerfile new file mode 100644 index 00000000..fdff3fce --- /dev/null +++ b/rsconcept/frontend/Dockerfile @@ -0,0 +1,37 @@ +# ======== Multi-stage base ========== +FROM node:bullseye-slim as node-base +RUN apt-get update -qq && \ + apt-get upgrade -y && \ + rm -rf /var/lib/apt/lists/* + +# ======= Build ======= +ARG BUILD_TYPE=production +FROM node-base as builder + +WORKDIR /result + +COPY ./ ./ +COPY ./env/.env.$BUILD_TYPE ./ +RUN rm -rf ./env +RUN npm ci +ENV NODE_ENV production +RUN npm run build + +# ========= Server ======= +FROM node-base as product-server + +ENV NODE_ENV production + +# Install serve util +RUN npm install -g serve + +# Setup USER +RUN adduser --system --group app +USER node + +# Bring up deployment files +WORKDIR /home/node +COPY --chown=node:node --from=builder /result/dist ./ + +# Start server through docker-compose +# serve -s /home/node -l 3000 \ No newline at end of file diff --git a/rsconcept/frontend/Dockerfile.dev b/rsconcept/frontend/Dockerfile.dev new file mode 100644 index 00000000..be4e8c9f --- /dev/null +++ b/rsconcept/frontend/Dockerfile.dev @@ -0,0 +1,17 @@ +# ======== Multi-stage base ========== +FROM node:bullseye-slim as node-base +RUN apt-get update -qq && \ + apt-get upgrade -y && \ + rm -rf /var/lib/apt/lists/* + +# ========= Server ======= +FROM node-base as product-server +ARG BUILD_TYPE=production + +WORKDIR /home + +COPY ./ ./ +COPY ./env/.env.$BUILD_TYPE ./ +RUN rm -rf ./env + +RUN npm install diff --git a/rsconcept/frontend/README.md b/rsconcept/frontend/README.md new file mode 100644 index 00000000..681d51b4 --- /dev/null +++ b/rsconcept/frontend/README.md @@ -0,0 +1,22 @@ +# Frontend Developer guidelines + +Styling conventions + +- static > conditional static > props. All dynamic styling should go in styles props +- dimensions = rectangle + outer layout + +
+clsx className grouping and order +
+  - layer: z-position
+  - outer layout: fixed bottom-1/2 left-0 -translate-x-1/2
+  - rectangle: mt-3 min-w-fit min-w-10 flex-grow shrink-0
+  - inner layout: px-3 py-2 flex flex-col gap-3 justify-between items-center
+  - overflow behavior: overflow-scroll overscroll-contain
+  - border: borer-2 outline-none shadow-md
+  - colors: clr-controls
+  - text: text-start text-sm font-semibold whitespace-nowrap
+  - behavior modifiers: select-none disabled:cursor-not-allowed
+  - transitions: 
+  
+
diff --git a/rsconcept/frontend/env/.env.development b/rsconcept/frontend/env/.env.development new file mode 100644 index 00000000..b662d21b --- /dev/null +++ b/rsconcept/frontend/env/.env.development @@ -0,0 +1,5 @@ +# Frontend public settings: Production Local + +VITE_PORTAL_BACKEND=http://localhost:8002 +VITE_PORTAL_FRONT_PORT=3002 +VITE_PORTAL_FRONT_HTTPS=false diff --git a/rsconcept/frontend/env/.env.production b/rsconcept/frontend/env/.env.production new file mode 100644 index 00000000..7d450ce8 --- /dev/null +++ b/rsconcept/frontend/env/.env.production @@ -0,0 +1,5 @@ +# Frontend public settings: Production + +VITE_PORTAL_BACKEND=https://api.portal.acconcept.ru +VITE_PORTAL_FRONT_PORT=443 +VITE_PORTAL_FRONT_HTTPS=true diff --git a/rsconcept/frontend/env/.env.production.local b/rsconcept/frontend/env/.env.production.local new file mode 100644 index 00000000..afc1a660 --- /dev/null +++ b/rsconcept/frontend/env/.env.production.local @@ -0,0 +1,6 @@ +# Frontend public settings: Production Local + +VITE_PORTAL_BACKEND=https://localhost:8001 +VITE_PORTAL_FRONT_PORT=3001 +VITE_PORTAL_FRONT_HTTPS=true + diff --git a/rsconcept/frontend/index.html b/rsconcept/frontend/index.html new file mode 100644 index 00000000..1933410f --- /dev/null +++ b/rsconcept/frontend/index.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + Концепт Портал + + + + +
+ + + diff --git a/rsconcept/frontend/package-lock.json b/rsconcept/frontend/package-lock.json new file mode 100644 index 00000000..3ea66d90 --- /dev/null +++ b/rsconcept/frontend/package-lock.json @@ -0,0 +1,11357 @@ +{ + "name": "frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "1.0.0", + "dependencies": { + "@lezer/lr": "^1.4.1", + "@tanstack/react-table": "^8.17.3", + "@uiw/codemirror-themes": "^4.22.2", + "@uiw/react-codemirror": "^4.22.2", + "axios": "^1.7.2", + "clsx": "^2.1.1", + "framer-motion": "^10.18.0", + "js-file-download": "^0.4.12", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-error-boundary": "^4.0.13", + "react-icons": "^4.12.0", + "react-intl": "^6.6.8", + "react-loader-spinner": "^5.4.5", + "react-pdf": "^9.0.0", + "react-router-dom": "^6.23.1", + "react-select": "^5.8.0", + "react-tabs": "^6.0.2", + "react-toastify": "^9.1.3", + "react-tooltip": "^5.26.4", + "reagraph": "^4.19.1", + "use-debounce": "^10.0.1" + }, + "devDependencies": { + "@lezer/generator": "^1.7.0", + "@types/jest": "^29.5.12", + "@types/node": "^20.14.2", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "@vitejs/plugin-react": "^4.3.0", + "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", + "eslint-plugin-simple-import-sort": "^10.0.0", + "eslint-plugin-tsdoc": "^0.2.17", + "jest": "^29.7.0", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.4", + "ts-jest": "^29.1.4", + "typescript": "^5.4.5", + "vite": "^4.5.3" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", + "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", + "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.16.2.tgz", + "integrity": "sha512-MjfDrHy0gHKlPWsvSsikhO1+BOh+eBHNgfH1OXs1+DAf30IonQldgMM3kxLDTG9ktE7kDLaA1j/l7KMPA4KNfw==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + }, + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.0.tgz", + "integrity": "sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.2.tgz", + "integrity": "sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.0.tgz", + "integrity": "sha512-lsFofvaw0lnPRJlQylNsC4IRt/1lI4OD/yYslrSGVndOJfStc58v+8p9dgGiD90ktOfL7OhBWns1ZETYgz0EJA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.6", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz", + "integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", + "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==", + "license": "MIT" + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz", + "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.27.0.tgz", + "integrity": "sha512-8kqX1sHbVW1lVzWwrjAbh4dR7eKhV8eIQ952JKaBXOoXE04WncoqCy4DMU701LSrPZ3N2Q4zsTawz7GQ+2mrUw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.4.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache/node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emotion/memoize": "0.7.4" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "license": "MIT", + "optional": true + }, + "node_modules/@emotion/react": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz", + "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", + "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/serialize/node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" + }, + "node_modules/@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==", + "license": "MIT" + }, + "node_modules/@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==", + "license": "MIT" + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", + "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz", + "integrity": "sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz", + "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz", + "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==", + "license": "MIT" + }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.0.0.tgz", + "integrity": "sha512-rRqXOqdFmk7RYvj4khklyqzcfQl9vEL/usogncBHRZfZBDOwMGuSRNFl02fu5KGHXdbinju+YXyuR+Nk8xlr/g==", + "license": "MIT", + "dependencies": { + "@formatjs/intl-localematcher": "0.5.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz", + "integrity": "sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.7.8", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.7.8.tgz", + "integrity": "sha512-nBZJYmhpcSX0WeJ5SDYUkZ42AgR3xiyhNCsQweFx3cz/ULJjym8bHAzWKvG5e2+1XO98dBYC0fWeeAECAVSwLA==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.0.0", + "@formatjs/icu-skeleton-parser": "1.8.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.2.tgz", + "integrity": "sha512-k4ERKgw7aKGWJZgTarIcNEmvyTVD9FYh0mTrrBMHZ1b8hUu6iOJ4SzsZlo3UNAvHYa+PnvntIwRPt1/vy4nA9Q==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.0.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/intl": { + "version": "2.10.4", + "resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.10.4.tgz", + "integrity": "sha512-56483O+HVcL0c7VucAS2tyH020mt9XTozZO67cwtGg0a7KWDukS/FzW3OnvaHmTHDuYsoPIzO+ZHVfU6fT/bJw==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.0.0", + "@formatjs/fast-memoize": "2.2.0", + "@formatjs/icu-messageformat-parser": "2.7.8", + "@formatjs/intl-displaynames": "6.6.8", + "@formatjs/intl-listformat": "7.5.7", + "intl-messageformat": "10.5.14", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "typescript": "^4.7 || 5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@formatjs/intl-displaynames": { + "version": "6.6.8", + "resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-6.6.8.tgz", + "integrity": "sha512-Lgx6n5KxN16B3Pb05z3NLEBQkGoXnGjkTBNCZI+Cn17YjHJ3fhCeEJJUqRlIZmJdmaXQhjcQVDp6WIiNeRYT5g==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.0.0", + "@formatjs/intl-localematcher": "0.5.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/intl-listformat": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.5.7.tgz", + "integrity": "sha512-MG2TSChQJQT9f7Rlv+eXwUFiG24mKSzmF144PLb8m8OixyXqn4+YWU+5wZracZGCgVTVmx8viCf7IH3QXoiB2g==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.0.0", + "@formatjs/intl-localematcher": "0.5.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz", + "integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/console/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/console/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/console/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/core/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/core/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/reporters/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/reporters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/transform/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/transform/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/types/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/types/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lezer/common": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz", + "integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==", + "license": "MIT" + }, + "node_modules/@lezer/generator": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@lezer/generator/-/generator-1.7.0.tgz", + "integrity": "sha512-IJ16tx3biLKlCXUzcK4v8S10AVa2BSM2rB12rtAL6f1hL2TS/HQQlGCoWRvanlL2J4mCYEEIv9uG7n4kVMkVDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.1.0", + "@lezer/lr": "^1.3.0" + }, + "bin": { + "lezer-generator": "src/lezer-generator.cjs" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.0.tgz", + "integrity": "sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.1.tgz", + "integrity": "sha512-CHsKq8DMKBf9b3yXPDIU4DbH+ZJd/sJdYOW2llbW/HudP5u0VS6Bfq1hLYfgU7uAYGFIyGGQIsSOXGPEErZiJw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@mediapipe/tasks-vision": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.8.tgz", + "integrity": "sha512-Rp7ll8BHrKB3wXaRFKhrltwZl1CiXGdibPxuWXvqGnKTnv8fqa/nvftYNuSbf+pbJWKYCXdBtYTITdAUTGGh0Q==", + "license": "Apache-2.0" + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@microsoft/tsdoc-config": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz", + "integrity": "sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "ajv": "~6.12.6", + "jju": "~1.4.0", + "resolve": "~1.19.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@react-spring/animated": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.6.1.tgz", + "integrity": "sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==", + "license": "MIT", + "dependencies": { + "@react-spring/shared": "~9.6.1", + "@react-spring/types": "~9.6.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.6.1.tgz", + "integrity": "sha512-3HAAinAyCPessyQNNXe5W0OHzRfa8Yo5P748paPcmMowZ/4sMfaZ2ZB6e5x5khQI8NusOHj8nquoutd6FRY5WQ==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.6.1", + "@react-spring/rafz": "~9.6.1", + "@react-spring/shared": "~9.6.1", + "@react-spring/types": "~9.6.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/rafz": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.6.1.tgz", + "integrity": "sha512-v6qbgNRpztJFFfSE3e2W1Uz+g8KnIBs6SmzCzcVVF61GdGfGOuBrbjIcp+nUz301awVmREKi4eMQb2Ab2gGgyQ==", + "license": "MIT" + }, + "node_modules/@react-spring/shared": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.6.1.tgz", + "integrity": "sha512-PBFBXabxFEuF8enNLkVqMC9h5uLRBo6GQhRMQT/nRTnemVENimgRd+0ZT4yFnAQ0AxWNiJfX3qux+bW2LbG6Bw==", + "license": "MIT", + "dependencies": { + "@react-spring/rafz": "~9.6.1", + "@react-spring/types": "~9.6.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/three": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.6.1.tgz", + "integrity": "sha512-Tyw2YhZPKJAX3t2FcqvpLRb71CyTe1GvT3V+i+xJzfALgpk10uPGdGaQQ5Xrzmok1340DAeg2pR/MCfaW7b8AA==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.6.1", + "@react-spring/core": "~9.6.1", + "@react-spring/shared": "~9.6.1", + "@react-spring/types": "~9.6.1" + }, + "peerDependencies": { + "@react-three/fiber": ">=6.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "three": ">=0.126" + } + }, + "node_modules/@react-spring/types": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.6.1.tgz", + "integrity": "sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q==", + "license": "MIT" + }, + "node_modules/@react-three/fiber": { + "version": "8.13.5", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.13.5.tgz", + "integrity": "sha512-x9QdsaB/Wm/6NGvRXQahPPWfn2dQce7Fg3C2r00NNzyDdqRKw32YavL+WEqjZOOd0nvFpzv7FtaKc+VCOTR59w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.8", + "@types/react-reconciler": "^0.26.7", + "its-fine": "^1.0.6", + "react-reconciler": "^0.27.0", + "react-use-measure": "^2.1.1", + "scheduler": "^0.21.0", + "suspend-react": "^0.1.3", + "zustand": "^3.7.1" + }, + "peerDependencies": { + "expo": ">=43.0", + "expo-asset": ">=8.4", + "expo-gl": ">=11.0", + "react": ">=18.0", + "react-dom": ">=18.0", + "react-native": ">=0.64", + "three": ">=0.133" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "expo-asset": { + "optional": true + }, + "expo-gl": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@react-three/fiber/node_modules/scheduler": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", + "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/@react-three/fiber/node_modules/zustand": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", + "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==", + "license": "MIT", + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, + "node_modules/@remix-run/router": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", + "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tanstack/react-table": { + "version": "8.17.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.17.3.tgz", + "integrity": "sha512-5gwg5SvPD3lNAXPuJJz1fOCEZYk9/GeBFH3w/hCgnfyszOIzwkwgp5I7Q4MJtn0WECp84b5STQUDdmvGi8m3nA==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.17.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.17.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.17.3.tgz", + "integrity": "sha512-mPBodDGVL+fl6d90wUREepHa/7lhsghg2A3vFpakEhrhtbIlgNAZiMr7ccTgak5qbHqF14Fwy+W1yFWQt+WmYQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.2", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.2.tgz", + "integrity": "sha512-kMCNaZCJugWI86xiEHaY338CU5JpD0B97p1j1IKNn/Zto8PgACjQx0UxbHjmOcLl/dDOBnItwD07KmCs75pxtQ==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/draco3d": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", + "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==", + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "license": "MIT", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.14.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", + "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-reconciler": { + "version": "0.26.7", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.26.7.tgz", + "integrity": "sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stats.js": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.3.tgz", + "integrity": "sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==", + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.165.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.165.0.tgz", + "integrity": "sha512-AJK8JZAFNBF0kBXiAIl5pggYlzAGGA8geVYQXAcPCEDRbyA+oEjkpUBcJJrtNz6IiALwzGexFJGZG2yV3WsYBw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@tweenjs/tween.js": "~23.1.1", + "@types/stats.js": "*", + "@types/webxr": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.18.1" + } + }, + "node_modules/@types/webxr": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.16.tgz", + "integrity": "sha512-0E0Cl84FECtzrB4qG19TNTqpunw0F1YF0QZZnFMF6pDw1kNKJtrlTKlVB34stGIsHbZsYQ7H0tNjPfZftkHHoA==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.22.2.tgz", + "integrity": "sha512-zcHGkldLFN3cGoI5XdOGAkeW24yaAgrDEYoyPyWHODmPiNwybQQoZGnH3qUdzZwUaXtAcLWoAeOPzfNRW2yGww==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/codemirror-themes": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.22.2.tgz", + "integrity": "sha512-gsLHn6SUuV5iboBvGrM7YimzLFHQmsNlkGIYs3UaVUJTo/A/ZrKoSJNyPziShLRjBXA2UwKdBTIU6VhHyyaChw==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/language": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/react-codemirror": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.22.2.tgz", + "integrity": "sha512-okCSl+WJG63gRx8Fdz7v0C6RakBQnbb3pHhuzIgDB+fwhipgFodSnu2n9oOsQesJ5YQ7mSOcKMgX0JEsu4nnfQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.22.2", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@use-gesture/core": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", + "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==", + "license": "MIT" + }, + "node_modules/@use-gesture/react": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz", + "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==", + "license": "MIT", + "dependencies": { + "@use-gesture/core": "10.3.1" + }, + "peerDependencies": { + "react": ">= 16.8.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.0.tgz", + "integrity": "sha512-KcEbMsn4Dpk+LIbHMj7gDPRKaTMStxxWRkRmxsg/jVdFdJCZWt1SchZcf0M4t8lIKdwwMsEyzhrcOXRrDPtOBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.5", + "@babel/plugin-transform-react-jsx-self": "^7.24.5", + "@babel/plugin-transform-react-jsx-source": "^7.24.1", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0" + } + }, + "node_modules/@yomguithereal/helpers": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@yomguithereal/helpers/-/helpers-1.1.1.tgz", + "integrity": "sha512-UYvAq/XCA7xoh1juWDYsq3W0WywOB+pz8cgVnE1b45ZfdMhBvHDrgmSFG3jXeZSr2tMTYLGHFHON+ekG05Jebg==", + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001599", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-jest/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/babel-jest/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-plugin-styled-components": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz", + "integrity": "sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.22.5", + "lodash": "^4.17.21", + "picomatch": "^2.3.1" + }, + "peerDependencies": { + "styled-components": ">= 2" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camera-controls": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-2.8.4.tgz", + "integrity": "sha512-pzVKpeZCRXIx2VOMB+E4OPjOhErHqhxrHYxcRLofOVgBeCeKSb8QAC2toc1onMllrxldRWXR8bl4K50hkrtwsg==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.126.1" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001629", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001629.tgz", + "integrity": "sha512-c3dl911slnQhmxUIT4HhYzT7wnBK/XYpGnYLOj4nJBaRiw52Ibe7YxlDaAeRECvA786zCuExhxIUJ2K7nHMrBw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/create-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/create-jest/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/create-jest/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/create-jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/ctrl-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ctrl-keys/-/ctrl-keys-1.0.2.tgz", + "integrity": "sha512-MBVjzR+RlUqbgmDRbnrGsXdFo0xB5/OxO4o54tyhBvPtbZ/f5hPl/De4i5yEGPyKPKyyEbLCxR5qmY+NEbXnCA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-binarytree": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz", + "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==", + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force-3d": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.5.tgz", + "integrity": "sha512-tdwhAhoTYZY/a6eo9nR7HP3xSW/C6XvJTbeRpR92nlPzH6OiE+4MliN9feuSFd0tPtEUo+191qOhCTWx3NYifg==", + "license": "MIT", + "dependencies": { + "d3-binarytree": "1", + "d3-dispatch": "1 - 3", + "d3-octree": "1", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-octree": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.0.2.tgz", + "integrity": "sha512-Qxg4oirJrNXauiuC94uKMbgxwnhdda9xRLl9ihq45srlJ4Ga3CSgqGcAL8iW7N5CIv4Oz8x3E734ulxyvHPvwA==", + "license": "MIT" + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-gpu": { + "version": "5.0.38", + "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.38.tgz", + "integrity": "sha512-36QeGHSXYcJ/RfrnPEScR8GDprbXFG4ZhXsfVNVHztZr38+fRxgHnJl3CjYXXjbeRUhu3ZZBJh6Lg0A9v0Qd8A==", + "license": "MIT", + "dependencies": { + "webgl-constants": "^1.1.1" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/draco3d": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", + "license": "Apache-2.0" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.4.794", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.794.tgz", + "integrity": "sha512-6FApLtsYhDCY0Vglq3AptsdxQ+PJLc6AxlAM0HjEihUAiOPPbkASEsq9gtxUeZY9o0sJIEa3WnF0vVH4VT4iug==", + "license": "ISC" + }, + "node_modules/ellipsize": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/ellipsize/-/ellipsize-0.5.1.tgz", + "integrity": "sha512-0jEAyuIRU6U8MN0S5yUqIrkK/AQWkChh642N3zQuGV57s9bsUWYLc0jJOoDIUkZ2sbEL3ySq8xfq71BvG4q3hw==", + "license": "MIT" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.7.tgz", + "integrity": "sha512-yrj+KInFmwuQS2UQcg1SF83ha1tuHC1jMQbRNyuWtlEzzKRDgAl7L4Yp4NlDUZTZNlWvHEzOtJhMi40R7JxcSw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-plugin-simple-import-sort": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-10.0.0.tgz", + "integrity": "sha512-AeTvO9UCMSNzIHRkg8S6c3RPy5YEwKWSQPx3DYghLedo2ZQxowPFLGDN1AZ2evfg6r6mjBSZSLxLFsWSu3acsw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=5.0.0" + } + }, + "node_modules/eslint-plugin-tsdoc": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.2.17.tgz", + "integrity": "sha512-xRmVi7Zx44lOBuYqG8vzTXuL6IdGOeF9nHX17bjJ8+VE6fsxpdGem0/SBTmAwgYMKYB1WBkqRJVQ+n8GK041pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "0.16.2" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.18.0.tgz", + "integrity": "sha512-oGlDh1Q1XqYPksuTD/usb0I70hq95OUzmL9+6Zd+Hs4XV0oaISBa/UUMSjYiq6m8EUF32132mOJ8xVZS+I0S6w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + }, + "optionalDependencies": { + "@emotion/is-prop-valid": "^0.8.2" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "devOptional": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glodrei": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/glodrei/-/glodrei-0.0.1.tgz", + "integrity": "sha512-DMx6ElCSwh1pR4IyDS3LvyFwZHSCCKCqdqo8P1G7klQtqH6PcOjleduCDsHehDtyYQ1E4dzVeoEzHIL1DIxjag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@mediapipe/tasks-vision": "0.10.8", + "@react-spring/three": "~9.6.1", + "@use-gesture/react": "^10.2.24", + "camera-controls": "^2.4.2", + "cross-env": "^7.0.3", + "detect-gpu": "^5.0.28", + "glsl-noise": "^0.0.0", + "maath": "^0.10.7", + "meshline": "^3.1.6", + "react-composer": "^5.0.3", + "react-merge-refs": "^1.1.0", + "stats-gl": "^2.0.0", + "stats.js": "^0.17.0", + "suspend-react": "^0.1.3", + "three-mesh-bvh": "^0.7.0", + "three-stdlib": "^2.29.4", + "troika-three-text": "^0.47.2", + "tunnel-rat": "^0.1.2", + "utility-types": "^3.10.0", + "uuid": "^9.0.1", + "zustand": "^3.7.1" + }, + "peerDependencies": { + "@react-three/fiber": ">=8.0", + "react": ">=18.0", + "react-dom": ">=18.0", + "three": ">=0.137" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/glodrei/node_modules/zustand": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", + "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==", + "license": "MIT", + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, + "node_modules/glsl-noise": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz", + "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==", + "license": "MIT" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/graphology": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/graphology/-/graphology-0.25.4.tgz", + "integrity": "sha512-33g0Ol9nkWdD6ulw687viS8YJQBxqG5LWII6FI6nul0pq6iM2t5EKquOTFDbyTblRB3O9I+7KX4xI8u5ffekAQ==", + "license": "MIT", + "dependencies": { + "events": "^3.3.0", + "obliterator": "^2.0.2" + }, + "peerDependencies": { + "graphology-types": ">=0.24.0" + } + }, + "node_modules/graphology-indices": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/graphology-indices/-/graphology-indices-0.17.0.tgz", + "integrity": "sha512-A7RXuKQvdqSWOpn7ZVQo4S33O0vCfPBnUSf7FwE0zNCasqwZVUaCXePuWo5HBpWw68KJcwObZDHpFk6HKH6MYQ==", + "license": "MIT", + "dependencies": { + "graphology-utils": "^2.4.2", + "mnemonist": "^0.39.0" + }, + "peerDependencies": { + "graphology-types": ">=0.20.0" + } + }, + "node_modules/graphology-layout": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/graphology-layout/-/graphology-layout-0.6.1.tgz", + "integrity": "sha512-m9aMvbd0uDPffUCFPng5ibRkb2pmfNvdKjQWeZrf71RS1aOoat5874+DcyNfMeCT4aQguKC7Lj9eCbqZj/h8Ag==", + "license": "MIT", + "dependencies": { + "graphology-utils": "^2.3.0", + "pandemonium": "^2.4.0" + }, + "peerDependencies": { + "graphology-types": ">=0.19.0" + } + }, + "node_modules/graphology-layout-forceatlas2": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/graphology-layout-forceatlas2/-/graphology-layout-forceatlas2-0.10.1.tgz", + "integrity": "sha512-ogzBeF1FvWzjkikrIFwxhlZXvD2+wlY54lqhsrWprcdPjopM2J9HoMweUmIgwaTvY4bUYVimpSsOdvDv1gPRFQ==", + "license": "MIT", + "dependencies": { + "graphology-utils": "^2.1.0" + }, + "peerDependencies": { + "graphology-types": ">=0.19.0" + } + }, + "node_modules/graphology-layout-noverlap": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/graphology-layout-noverlap/-/graphology-layout-noverlap-0.4.2.tgz", + "integrity": "sha512-13WwZSx96zim6l1dfZONcqLh3oqyRcjIBsqz2c2iJ3ohgs3605IDWjldH41Gnhh462xGB1j6VGmuGhZ2FKISXA==", + "license": "MIT", + "dependencies": { + "graphology-utils": "^2.3.0" + }, + "peerDependencies": { + "graphology-types": ">=0.19.0" + } + }, + "node_modules/graphology-metrics": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/graphology-metrics/-/graphology-metrics-2.2.0.tgz", + "integrity": "sha512-eZZFRLGGyyI+iD+XwQvc+lLM3EKCoqUvVjvF/14Htgy4grB2m95OytToYq3saWuHfuf22VVnj9GBHv/pTzKuTw==", + "license": "MIT", + "dependencies": { + "graphology-shortest-path": "^2.0.0", + "graphology-utils": "^2.4.4", + "mnemonist": "^0.39.0" + }, + "peerDependencies": { + "graphology-types": ">=0.20.0" + } + }, + "node_modules/graphology-shortest-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/graphology-shortest-path/-/graphology-shortest-path-2.1.0.tgz", + "integrity": "sha512-KbT9CTkP/u72vGEJzyRr24xFC7usI9Es3LMmCPHGwQ1KTsoZjxwA9lMKxfU0syvT/w+7fZUdB/Hu2wWYcJBm6Q==", + "license": "MIT", + "dependencies": { + "@yomguithereal/helpers": "^1.1.1", + "graphology-indices": "^0.17.0", + "graphology-utils": "^2.4.3", + "mnemonist": "^0.39.0" + }, + "peerDependencies": { + "graphology-types": ">=0.20.0" + } + }, + "node_modules/graphology-types": { + "version": "0.24.7", + "resolved": "https://registry.npmjs.org/graphology-types/-/graphology-types-0.24.7.tgz", + "integrity": "sha512-tdcqOOpwArNjEr0gNQKCXwaNCWnQJrog14nJNQPeemcLnXQUUGrsCWpWkVKt46zLjcS6/KGoayeJfHHyPDlvwA==", + "license": "MIT", + "peer": true + }, + "node_modules/graphology-utils": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/graphology-utils/-/graphology-utils-2.5.2.tgz", + "integrity": "sha512-ckHg8MXrXJkOARk56ZaSCM1g1Wihe2d6iTmz1enGOz4W/l831MBCKSayeFQfowgF8wd+PQ4rlch/56Vs/VZLDQ==", + "license": "MIT", + "peerDependencies": { + "graphology-types": ">=0.23.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/hold-event": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/hold-event/-/hold-event-0.2.0.tgz", + "integrity": "sha512-rko5P1XgHzy4B0NR0xVHEpWPgj0i23f8Mf8qsOugd1CHvfLR0PyIyy+8TAQQA9v8qAa1OZ4XuCKk04rxmPGHNQ==", + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "devOptional": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/intl-messageformat": { + "version": "10.5.14", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.14.tgz", + "integrity": "sha512-IjC6sI0X7YRjjyVH9aUgdftcmZK7WXdHeil4KwbjDnRWjnVitKpAx3rr6t6di1joFp5188VqKcobOPA6mCLG/w==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.0.0", + "@formatjs/fast-memoize": "2.2.0", + "@formatjs/icu-messageformat-parser": "2.7.8", + "tslib": "^2.4.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", + "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/its-fine": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz", + "integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==", + "license": "MIT", + "dependencies": { + "@types/react-reconciler": "^0.28.0" + }, + "peerDependencies": { + "react": ">=18.0" + } + }, + "node_modules/its-fine/node_modules/@types/react-reconciler": { + "version": "0.28.8", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.8.tgz", + "integrity": "sha512-SN9c4kxXZonFhbX4hJrZy37yw9e7EIxcpHCxQv5JUS18wDE5ovkQKlqQEkufdJCCMfuI9BnjUJvhYeJ9x5Ra7g==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/jackspeak": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz", + "integrity": "sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-circus/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-circus/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-circus/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-cli/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-cli/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-config/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-config/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-diff/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-diff/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-each/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-each/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-each/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-matcher-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-matcher-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-message-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-message-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-resolve/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-resolve/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-resolve/node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/jest-resolve/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-runner/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-runner/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-runtime/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-runtime/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-snapshot/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-validate/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-validate/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-watcher/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-watcher/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.3", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.3.tgz", + "integrity": "sha512-uy2bNX5zQ+tESe+TiC7ilGRz8AtRGmnJH55NC5S0nSUjvvvM2hJHmefHErugGXN4pNv4Qx7vLsnNw9qJ9mtIsw==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-file-download": { + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/js-file-download/-/js-file-download-0.4.12.tgz", + "integrity": "sha512-rML+NkoD08p5Dllpjo0ffy4jRHeY6Zsapvr/W86N7E0yuzAO6qa5X9+xog6zQNlH102J7IXljNY2FtS6Lj3ucg==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/maath": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.7.tgz", + "integrity": "sha512-zQ2xd7dNOIVTjAS+hj22fyj1EFYmOJX6tzKjZ92r6WDoq8hyFxjuGA2q950tmR4iC/EKXoMQdSipkaJVuUHDTg==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.144.0", + "three": ">=0.144.0" + } + }, + "node_modules/make-cancellable-promise": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-1.3.2.tgz", + "integrity": "sha512-GCXh3bq/WuMbS+Ky4JBPW1hYTOU+znU+Q5m9Pu+pI8EoUqIHk9+tviOKC6/qhHh8C4/As3tzJ69IF32kdz85ww==", + "license": "MIT", + "funding": { + "url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/make-event-props": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-1.6.2.tgz", + "integrity": "sha512-iDwf7mA03WPiR8QxvcVHmVWEPfMY1RZXerDVNCRYW7dUr2ppH3J58Rwb39/WG39yTZdRSxr3x+2v22tvI0VEvA==", + "license": "MIT", + "funding": { + "url": "https://github.com/wojtekmaj/make-event-props?sponsor=1" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/merge-refs": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.3.0.tgz", + "integrity": "sha512-nqXPXbso+1dcKDpPCXvwZyJILz+vSLqGGOnDrYHQYE+B8n9JTCekVLC65AfCpR4ggVyA/45Y0iR9LDyS2iI+zA==", + "license": "MIT", + "funding": { + "url": "https://github.com/wojtekmaj/merge-refs?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/meshline": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz", + "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.137" + } + }, + "node_modules/meshoptimizer": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", + "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mnemonist": { + "version": "0.39.8", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz", + "integrity": "sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==", + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.1" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nan": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", + "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==", + "license": "MIT", + "optional": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "license": "MIT" + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/obliterator": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz", + "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==", + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pandemonium": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/pandemonium/-/pandemonium-2.4.1.tgz", + "integrity": "sha512-wRqjisUyiUfXowgm7MFH2rwJzKIr20rca5FsHXCMNm1W5YPP1hCtrZfgmQ62kP7OZ7Xt+cR858aB28lu5NX55g==", + "license": "MIT", + "dependencies": { + "mnemonist": "^0.39.2" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path2d": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/path2d/-/path2d-0.2.0.tgz", + "integrity": "sha512-KdPAykQX6kmLSOO6Jpu2KNcCED7CKjmaBNGGNuctOsG0hgYO1OdYQaan6cYXJiG0WmXOwZZPILPBimu5QAIw3A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pdfjs-dist": { + "version": "4.3.136", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.3.136.tgz", + "integrity": "sha512-gzfnt1qc4yA+U46golPGYtU4WM2ssqP2MvFjKga8GEKOrEnzRPrA/9jogLLPYHiA3sGBPJ+p7BdAq+ytmw3jEg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "canvas": "^2.11.2", + "path2d": "^0.2.0" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.3.tgz", + "integrity": "sha512-sntgmxj8o7DE7g/Qi60cqpLBA3HG3STcDA0kO+WfB05jEKhZMbY7umNm2rBpQvsmZ16/lPXCJGW2672dgOUkrg==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", + "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", + "license": "ISC" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-composer": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/react-composer/-/react-composer-5.0.3.tgz", + "integrity": "sha512-1uWd07EME6XZvMfapwZmc7NgCZqDemcvicRi3wMJzXsQLvZ3L7fTHVyPy1bZdnWXM4iPjYuNE+uJ41MLKeTtnA==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.6.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-error-boundary": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz", + "integrity": "sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, + "node_modules/react-icons": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz", + "integrity": "sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-intl": { + "version": "6.6.8", + "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.6.8.tgz", + "integrity": "sha512-M0pkhzcgV31h++2901BiRXWl69hp2zPyLxRrSwRjd1ErXbNoubz/f4M6DrRTd4OiSUrT4ajRQzrmtS5plG4FtA==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.0.0", + "@formatjs/icu-messageformat-parser": "2.7.8", + "@formatjs/intl": "2.10.4", + "@formatjs/intl-displaynames": "6.6.8", + "@formatjs/intl-listformat": "7.5.7", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/react": "16 || 17 || 18", + "hoist-non-react-statics": "^3.3.2", + "intl-messageformat": "10.5.14", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "react": "^16.6.0 || 17 || 18", + "typescript": "^4.7 || 5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-loader-spinner": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-5.4.5.tgz", + "integrity": "sha512-32f+sb/v2tnNfyvnCCOS4fpyVHsGXjSyNo6QLniHcaj1XjKLxx14L2z0h6szRugOL8IEJ+53GPwNAdbkDqmy4g==", + "license": "MIT", + "dependencies": { + "react-is": "^18.2.0", + "styled-components": "^5.3.5", + "styled-tools": "^1.7.2" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-merge-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/react-merge-refs/-/react-merge-refs-1.1.0.tgz", + "integrity": "sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/react-pdf": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-9.0.0.tgz", + "integrity": "sha512-J+pza8R2p9oNEOJOHIQJI4o5rFK7ji7bBl2IvsHvz1OOyphvuzVDo5tOJwWAFAbxYauCH3Kt8jOvcMJUOpxYZQ==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "dequal": "^2.0.3", + "make-cancellable-promise": "^1.3.1", + "make-event-props": "^1.6.0", + "merge-refs": "^1.3.0", + "pdfjs-dist": "4.3.136", + "tiny-invariant": "^1.0.0", + "warning": "^4.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-reconciler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz", + "integrity": "sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.21.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/react-reconciler/node_modules/scheduler": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", + "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", + "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.16.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", + "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.16.1", + "react-router": "6.23.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-select": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.0.tgz", + "integrity": "sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.1.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-tabs": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-6.0.2.tgz", + "integrity": "sha512-aQXTKolnM28k3KguGDBSAbJvcowOQr23A+CUJdzJtOSDOtTwzEaJA+1U4KwhNL9+Obe+jFS7geuvA7ICQPXOnQ==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "prop-types": "^15.5.0" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/react-toastify": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz", + "integrity": "sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==", + "license": "MIT", + "dependencies": { + "clsx": "^1.1.1" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/react-toastify/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react-tooltip": { + "version": "5.26.4", + "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.26.4.tgz", + "integrity": "sha512-5WyDrsfw1+6qNVSr3IjqElqJ+cCwE8+44b+HpJ8qRLv7v0a3mcKf8wvv+NfgALFS6QpksGFqTLV2JQ60c+okZQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.6.1", + "classnames": "^2.3.0" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/react-use-gesture": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/react-use-gesture/-/react-use-gesture-9.1.3.tgz", + "integrity": "sha512-CdqA2SmS/fj3kkS2W8ZU8wjTbVBAIwDWaRprX7OKaj7HlGwBasGEFggmk5qNklknqk9zK/h8D355bEJFTpqEMg==", + "deprecated": "This package is no longer maintained. Please use @use-gesture/react instead", + "license": "MIT", + "peerDependencies": { + "react": ">= 16.8.0" + } + }, + "node_modules/react-use-measure": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.1.tgz", + "integrity": "sha512-nocZhN26cproIiIduswYpV5y5lQpSQS1y/4KuvUCjSKmw7ZWIS/+g3aFnX3WdBkyuGUtTLif3UTqnLLhbDoQig==", + "license": "MIT", + "dependencies": { + "debounce": "^1.2.1" + }, + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reagraph": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/reagraph/-/reagraph-4.19.1.tgz", + "integrity": "sha512-BMSfZ2CoLSWsxe+vowHbCZSqeJJlH2UcbQA6hM1JHI0PmZ/3UHIXCQUHXrsROtYIIH5AnFEj9pUAZh2FGnfGhw==", + "license": "Apache-2.0", + "dependencies": { + "@react-spring/three": "9.6.1", + "@react-three/fiber": "8.13.5", + "camera-controls": "^2.8.3", + "classnames": "^2.5.1", + "d3-array": "^3.2.4", + "d3-force-3d": "^3.0.3", + "d3-hierarchy": "^3.1.2", + "d3-scale": "^4.0.2", + "ellipsize": "^0.5.1", + "glodrei": "^0.0.1", + "graphology": "^0.25.4", + "graphology-layout": "^0.6.1", + "graphology-layout-forceatlas2": "^0.10.1", + "graphology-layout-noverlap": "^0.4.2", + "graphology-metrics": "^2.1.0", + "graphology-shortest-path": "^2.0.2", + "hold-event": "^0.2.0", + "react-use-gesture": "^9.1.3", + "reakeys": "^2.0.1", + "three": "^0.154.0", + "three-stdlib": "^2.23.13", + "zustand": "4.3.9" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/reakeys": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/reakeys/-/reakeys-2.0.2.tgz", + "integrity": "sha512-xY6OvVxuSC3iH55wvfPDyZ5xxMXIzEp6tNdipNXkaP6eznC9NdH+S3OqX3gktY731oIX31zH7u0QBV4lRXo1iQ==", + "license": "Apache-2.0", + "dependencies": { + "ctrl-keys": "^1.0.2" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "devOptional": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", + "optional": true + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stats-gl": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.2.8.tgz", + "integrity": "sha512-94G5nZvduDmzxBS7K0lYnynYwreZpkknD8g5dZmU6mpwIhy3caCrjAm11Qm1cbyx7mqix7Fp00RkbsonzKWnoQ==", + "license": "MIT", + "dependencies": { + "@types/three": "^0.163.0" + } + }, + "node_modules/stats-gl/node_modules/@types/three": { + "version": "0.163.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.163.0.tgz", + "integrity": "sha512-uIdDhsXRpQiBUkflBS/i1l3JX14fW6Ot9csed60nfbZNXHDTRsnV2xnTVwXcgbvTiboAR4IW+t+lTL5f1rqIqA==", + "license": "MIT", + "dependencies": { + "@tweenjs/tween.js": "~23.1.1", + "@types/stats.js": "*", + "@types/webxr": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.18.1" + } + }, + "node_modules/stats.js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "license": "MIT" + }, + "node_modules/styled-components": { + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz", + "integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/traverse": "^7.4.5", + "@emotion/is-prop-valid": "^1.1.0", + "@emotion/stylis": "^0.8.4", + "@emotion/unitless": "^0.7.4", + "babel-plugin-styled-components": ">= 1.12.0", + "css-to-react-native": "^3.0.0", + "hoist-non-react-statics": "^3.0.0", + "shallowequal": "^1.1.0", + "supports-color": "^5.5.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0", + "react-is": ">= 16.8.0" + } + }, + "node_modules/styled-components/node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/styled-components/node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" + }, + "node_modules/styled-components/node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, + "node_modules/styled-tools": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/styled-tools/-/styled-tools-1.7.2.tgz", + "integrity": "sha512-IjLxzM20RMwAsx8M1QoRlCG/Kmq8lKzCGyospjtSXt/BTIIcvgTonaxQAsKnBrsZNwhpHzO9ADx5te0h76ILVg==", + "license": "MIT" + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", + "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=17.0" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", + "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "optional": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/three": { + "version": "0.154.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.154.0.tgz", + "integrity": "sha512-Uzz8C/5GesJzv8i+Y2prEMYUwodwZySPcNhuJUdsVMH2Yn4Nm8qlbQe6qRN5fOhg55XB0WiLfTPBxVHxpE60ug==", + "license": "MIT" + }, + "node_modules/three-mesh-bvh": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.7.5.tgz", + "integrity": "sha512-WDd77RklE52pZSKZx8sDXzrd2JCF/gL/hugFvsIBylpMRlJxxwesKn2rW7TcQZ809NocDVkQx1UJo9pJtVAPYg==", + "license": "MIT", + "peerDependencies": { + "three": ">= 0.151.0" + } + }, + "node_modules/three-stdlib": { + "version": "2.30.3", + "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.30.3.tgz", + "integrity": "sha512-rYr8PqMljMza+Ct8kQk90Y7y+YcWoPu1thfYv5YGCp0hytNRbxSQWXY4GpdTGymCj3bDggEBpxso53C3pPwhIw==", + "license": "MIT", + "dependencies": { + "@types/draco3d": "^1.4.0", + "@types/offscreencanvas": "^2019.6.4", + "@types/webxr": "^0.5.2", + "draco3d": "^1.4.1", + "fflate": "^0.6.9", + "potpack": "^1.0.1" + }, + "peerDependencies": { + "three": ">=0.128.0" + } + }, + "node_modules/three-stdlib/node_modules/fflate": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", + "license": "MIT" + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT", + "optional": true + }, + "node_modules/troika-three-text": { + "version": "0.47.2", + "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.47.2.tgz", + "integrity": "sha512-qylT0F+U7xGs+/PEf3ujBdJMYWbn0Qci0kLqI5BJG2kW1wdg4T1XSxneypnF05DxFqJhEzuaOR9S2SjiyknMng==", + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.2", + "troika-three-utils": "^0.47.2", + "troika-worker-utils": "^0.47.2", + "webgl-sdf-generator": "1.1.1" + }, + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-three-utils": { + "version": "0.47.2", + "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.47.2.tgz", + "integrity": "sha512-/28plhCxfKtH7MSxEGx8e3b/OXU5A0xlwl+Sbdp0H8FXUHKZDoksduEKmjQayXYtxAyuUiCRunYIv/8Vi7aiyg==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-worker-utils": { + "version": "0.47.2", + "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.47.2.tgz", + "integrity": "sha512-mzss4MeyzUkYBppn4x5cdAqrhBHFEuVmMMgLMTyFV23x6GvQMyo+/R5E5Lsbrt7WSt5RfvewjcwD1DChRTA9lA==", + "license": "MIT" + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/ts-jest": { + "version": "29.1.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.4.tgz", + "integrity": "sha512-YiHwDhSvCiItoAgsKtoLFCuakDzDsJ1DLDnSouTaTmdOcOwIkSzbLXduaQ6M5DRVhuZC/NYaaZ/mtHbWMv/S6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "license": "0BSD" + }, + "node_modules/tunnel-rat": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", + "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==", + "license": "MIT", + "dependencies": { + "zustand": "^4.3.2" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-debounce": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.1.tgz", + "integrity": "sha512-0uUXjOfm44e6z4LZ/woZvkM8FwV1wiuoB6xnrrOmeAEjRDDzTLQNRFtYHvqUsJdrz1X37j0rVGIVp144GLHGKg==", + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vite": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/webgl-constants": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", + "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==" + }, + "node_modules/webgl-sdf-generator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", + "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==", + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zustand": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.9.tgz", + "integrity": "sha512-Tat5r8jOMG1Vcsj8uldMyqYKC5IZvQif8zetmLHs9WoZlntTHmIoNM8TpLRY31ExncuUvUOXehd0kvahkuHjDw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "immer": ">=9.0", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + } +} diff --git a/rsconcept/frontend/package.json b/rsconcept/frontend/package.json new file mode 100644 index 00000000..26bfd922 --- /dev/null +++ b/rsconcept/frontend/package.json @@ -0,0 +1,73 @@ +{ + "name": "frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "prepare": "lezer-generator src/components/RSInput/rslang/rslangFull.grammar -o src/components/RSInput/rslang/parser.ts && lezer-generator src/components/RefsInput/parse/refsText.grammar -o src/components/RefsInput/parse/parser.ts", + "test": "jest", + "dev": "vite --host", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "@lezer/lr": "^1.4.1", + "@tanstack/react-table": "^8.17.3", + "@uiw/codemirror-themes": "^4.22.2", + "@uiw/react-codemirror": "^4.22.2", + "axios": "^1.7.2", + "clsx": "^2.1.1", + "framer-motion": "^10.18.0", + "js-file-download": "^0.4.12", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-error-boundary": "^4.0.13", + "react-icons": "^4.12.0", + "react-intl": "^6.6.8", + "react-loader-spinner": "^5.4.5", + "react-pdf": "^9.0.0", + "react-router-dom": "^6.23.1", + "react-select": "^5.8.0", + "react-tabs": "^6.0.2", + "react-toastify": "^9.1.3", + "react-tooltip": "^5.26.4", + "reagraph": "^4.19.1", + "use-debounce": "^10.0.1" + }, + "devDependencies": { + "@lezer/generator": "^1.7.0", + "@types/jest": "^29.5.12", + "@types/node": "^20.14.2", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "@vitejs/plugin-react": "^4.3.0", + "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", + "eslint-plugin-simple-import-sort": "^10.0.0", + "eslint-plugin-tsdoc": "^0.2.17", + "jest": "^29.7.0", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.4", + "ts-jest": "^29.1.4", + "typescript": "^5.4.5", + "vite": "^4.5.3" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "node", + "transform": { + "node_modules/variables/.+\\.(j|t)sx?$": "ts-jest" + }, + "transformIgnorePatterns": [ + "node_modules/(?!variables/.*)" + ], + "moduleNameMapper": { + "^@/(.*)$": "/src/$1" + } + } +} diff --git a/rsconcept/frontend/postcss.config.js b/rsconcept/frontend/postcss.config.js new file mode 100644 index 00000000..09c32be6 --- /dev/null +++ b/rsconcept/frontend/postcss.config.js @@ -0,0 +1,8 @@ +export default { + plugins: { + 'postcss-import': {}, + 'tailwindcss/nesting': {}, + 'tailwindcss': {}, + 'autoprefixer': {} + } +}; diff --git a/rsconcept/frontend/public/DejaVu.ttf b/rsconcept/frontend/public/DejaVu.ttf new file mode 100644 index 00000000..c1180728 Binary files /dev/null and b/rsconcept/frontend/public/DejaVu.ttf differ diff --git a/rsconcept/frontend/public/favicon.svg b/rsconcept/frontend/public/favicon.svg new file mode 100644 index 00000000..82a15b4b --- /dev/null +++ b/rsconcept/frontend/public/favicon.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + diff --git a/rsconcept/frontend/public/logo_full.svg b/rsconcept/frontend/public/logo_full.svg new file mode 100644 index 00000000..030bbc96 --- /dev/null +++ b/rsconcept/frontend/public/logo_full.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rsconcept/frontend/public/logo_full_dark.svg b/rsconcept/frontend/public/logo_full_dark.svg new file mode 100644 index 00000000..253a2530 --- /dev/null +++ b/rsconcept/frontend/public/logo_full_dark.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rsconcept/frontend/public/logo_sign.svg b/rsconcept/frontend/public/logo_sign.svg new file mode 100644 index 00000000..0a3dde38 --- /dev/null +++ b/rsconcept/frontend/public/logo_sign.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/rsconcept/frontend/public/robots.txt b/rsconcept/frontend/public/robots.txt new file mode 100644 index 00000000..087e56dc --- /dev/null +++ b/rsconcept/frontend/public/robots.txt @@ -0,0 +1,3 @@ +User-agent: * +Allow: / +Disallow: /library \ No newline at end of file diff --git a/rsconcept/frontend/src/app/ApplicationLayout.tsx b/rsconcept/frontend/src/app/ApplicationLayout.tsx new file mode 100644 index 00000000..84b95b75 --- /dev/null +++ b/rsconcept/frontend/src/app/ApplicationLayout.tsx @@ -0,0 +1,44 @@ +import { Outlet } from 'react-router-dom'; + +import ConceptToaster from '@/app/ConceptToaster'; +import Footer from '@/app/Footer'; +import Navigation from '@/app/Navigation'; +import { NavigationState } from '@/context/NavigationContext'; +import { useConceptOptions } from '@/context/OptionsContext'; +import { globals } from '@/utils/constants'; + +function ApplicationLayout() { + const { viewportHeight, mainHeight, showScroll } = useConceptOptions(); + return ( + +
+ + + + +
+
+ +
+
+
+
+
+ ); +} + +export default ApplicationLayout; diff --git a/rsconcept/frontend/src/app/ConceptToaster.tsx b/rsconcept/frontend/src/app/ConceptToaster.tsx new file mode 100644 index 00000000..09e361f2 --- /dev/null +++ b/rsconcept/frontend/src/app/ConceptToaster.tsx @@ -0,0 +1,13 @@ +import { ToastContainer, type ToastContainerProps } from 'react-toastify'; + +import { useConceptOptions } from '@/context/OptionsContext'; + +interface ToasterThemedProps extends Omit {} + +function ToasterThemed(props: ToasterThemedProps) { + const { darkMode } = useConceptOptions(); + + return ; +} + +export default ToasterThemed; diff --git a/rsconcept/frontend/src/app/ErrorFallback.tsx b/rsconcept/frontend/src/app/ErrorFallback.tsx new file mode 100644 index 00000000..4447a277 --- /dev/null +++ b/rsconcept/frontend/src/app/ErrorFallback.tsx @@ -0,0 +1,16 @@ +import { type FallbackProps } from 'react-error-boundary'; + +import InfoError from '../components/info/InfoError'; +import Button from '../components/ui/Button'; + +function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) { + return ( +
+

Что-то пошло не так!

+
+ ); +} + +export default ErrorFallback; diff --git a/rsconcept/frontend/src/app/Footer.tsx b/rsconcept/frontend/src/app/Footer.tsx new file mode 100644 index 00000000..bae26d81 --- /dev/null +++ b/rsconcept/frontend/src/app/Footer.tsx @@ -0,0 +1,35 @@ +import clsx from 'clsx'; + +import { useConceptOptions } from '@/context/OptionsContext'; +import { external_urls } from '@/utils/constants'; + +import TextURL from '../components/ui/TextURL'; + +function Footer() { + const { noNavigation, noFooter } = useConceptOptions(); + if (noNavigation || noFooter) { + return null; + } + return ( +
+
+ + + + +
+
+

© 2024 ЦИВТ КОНЦЕПТ

+
+
+ ); +} + +export default Footer; diff --git a/rsconcept/frontend/src/app/GlobalProviders.tsx b/rsconcept/frontend/src/app/GlobalProviders.tsx new file mode 100644 index 00000000..d62c99d5 --- /dev/null +++ b/rsconcept/frontend/src/app/GlobalProviders.tsx @@ -0,0 +1,51 @@ +'use client'; + +import { ErrorBoundary } from 'react-error-boundary'; +import { IntlProvider } from 'react-intl'; +import { pdfjs } from 'react-pdf'; + +import { AuthState } from '@/context/AuthContext'; +import { LibraryState } from '@/context/LibraryContext'; +import { OptionsState } from '@/context/OptionsContext'; +import { UsersState } from '@/context/UsersContext'; + +import ErrorFallback from './ErrorFallback'; + +pdfjs.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); + +const resetState = () => { + console.log('Resetting state after error fallback'); +}; + +const logError = (error: Error, info: { componentStack?: string | null | undefined }) => { + console.log('Error fallback: ' + error.message); + if (info.componentStack) { + console.log('Component stack: ' + info.componentStack); + } +}; + +// prettier-ignore +function GlobalProviders({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + {children} + + + + + + + ); +} + +export default GlobalProviders; diff --git a/rsconcept/frontend/src/app/Navigation/Logo.tsx b/rsconcept/frontend/src/app/Navigation/Logo.tsx new file mode 100644 index 00000000..a4d16662 --- /dev/null +++ b/rsconcept/frontend/src/app/Navigation/Logo.tsx @@ -0,0 +1,18 @@ +import clsx from 'clsx'; + +import { useConceptOptions } from '@/context/OptionsContext'; +import useWindowSize from '@/hooks/useWindowSize'; + +function Logo() { + const { darkMode } = useConceptOptions(); + const size = useWindowSize(); + + return ( + + ); +} +export default Logo; diff --git a/rsconcept/frontend/src/app/Navigation/Navigation.tsx b/rsconcept/frontend/src/app/Navigation/Navigation.tsx new file mode 100644 index 00000000..e7f82256 --- /dev/null +++ b/rsconcept/frontend/src/app/Navigation/Navigation.tsx @@ -0,0 +1,71 @@ +import clsx from 'clsx'; +import { motion } from 'framer-motion'; + +import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/Icons'; +import { CProps } from '@/components/props'; +import { useConceptNavigation } from '@/context/NavigationContext'; +import { useConceptOptions } from '@/context/OptionsContext'; +import { animateNavigation } from '@/styling/animations'; + +import { urls } from '../urls'; +import Logo from './Logo'; +import NavigationButton from './NavigationButton'; +import ToggleNavigationButton from './ToggleNavigationButton'; +import UserMenu from './UserMenu'; + +function Navigation() { + const router = useConceptNavigation(); + const { noNavigationAnimation } = useConceptOptions(); + + const navigateHome = (event: CProps.EventMouse) => router.push(urls.home, event.ctrlKey || event.metaKey); + const navigateLibrary = (event: CProps.EventMouse) => router.push(urls.library, event.ctrlKey || event.metaKey); + const navigateHelp = (event: CProps.EventMouse) => router.push(urls.manuals, event.ctrlKey || event.metaKey); + const navigateCreateNew = (event: CProps.EventMouse) => + router.push(urls.create_schema, event.ctrlKey || event.metaKey); + + return ( + + ); +} + +export default Navigation; diff --git a/rsconcept/frontend/src/app/Navigation/NavigationButton.tsx b/rsconcept/frontend/src/app/Navigation/NavigationButton.tsx new file mode 100644 index 00000000..18cf36f7 --- /dev/null +++ b/rsconcept/frontend/src/app/Navigation/NavigationButton.tsx @@ -0,0 +1,39 @@ +import clsx from 'clsx'; + +import { CProps } from '@/components/props'; +import { globals } from '@/utils/constants'; + +interface NavigationButtonProps extends CProps.Titled { + text?: string; + icon: React.ReactNode; + onClick?: (event: CProps.EventMouse) => void; +} + +function NavigationButton({ icon, title, titleHtml, hideTitle, onClick, text }: NavigationButtonProps) { + return ( + + ); +} + +export default NavigationButton; diff --git a/rsconcept/frontend/src/app/Navigation/ToggleNavigationButton.tsx b/rsconcept/frontend/src/app/Navigation/ToggleNavigationButton.tsx new file mode 100644 index 00000000..43166e66 --- /dev/null +++ b/rsconcept/frontend/src/app/Navigation/ToggleNavigationButton.tsx @@ -0,0 +1,33 @@ +import clsx from 'clsx'; +import { motion } from 'framer-motion'; + +import { IconPin, IconUnpin } from '@/components/Icons'; +import { useConceptOptions } from '@/context/OptionsContext'; +import { animateNavigationToggle } from '@/styling/animations'; +import { globals } from '@/utils/constants'; + +function ToggleNavigationButton() { + const { noNavigationAnimation, toggleNoNavigation } = useConceptOptions(); + return ( + + {!noNavigationAnimation ? : null} + {noNavigationAnimation ? : null} + + ); +} + +export default ToggleNavigationButton; diff --git a/rsconcept/frontend/src/app/Navigation/UserDropdown.tsx b/rsconcept/frontend/src/app/Navigation/UserDropdown.tsx new file mode 100644 index 00000000..a05af23e --- /dev/null +++ b/rsconcept/frontend/src/app/Navigation/UserDropdown.tsx @@ -0,0 +1,92 @@ +import { + IconAdmin, + IconAdminOff, + IconDarkTheme, + IconDatabase, + IconHelp, + IconHelpOff, + IconLightTheme, + IconLogout, + IconUser +} from '@/components/Icons'; +import { CProps } from '@/components/props'; +import Dropdown from '@/components/ui/Dropdown'; +import DropdownButton from '@/components/ui/DropdownButton'; +import { useAuth } from '@/context/AuthContext'; +import { useConceptNavigation } from '@/context/NavigationContext'; +import { useConceptOptions } from '@/context/OptionsContext'; + +import { urls } from '../urls'; + +interface UserDropdownProps { + isOpen: boolean; + hideDropdown: () => void; +} + +function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) { + const { darkMode, adminMode, toggleAdminMode, toggleDarkMode, showHelp, toggleShowHelp } = useConceptOptions(); + const router = useConceptNavigation(); + const { user, logout } = useAuth(); + + function navigateProfile(event: CProps.EventMouse) { + hideDropdown(); + router.push(urls.profile, event.ctrlKey || event.metaKey); + } + + function logoutAndRedirect() { + hideDropdown(); + logout(() => router.push(urls.login)); + } + + function gotoAdmin() { + hideDropdown(); + logout(() => router.push(urls.admin, true)); + } + + function handleToggleDarkMode() { + hideDropdown(); + toggleDarkMode(); + } + + return ( + + } + onClick={navigateProfile} + /> + : } + title='Переключение темы оформления' + onClick={handleToggleDarkMode} + /> + : } + title='Отображение иконок подсказок' + onClick={toggleShowHelp} + /> + {user?.is_staff ? ( + : } + title='Работа в режиме администратора' + onClick={toggleAdminMode} + /> + ) : null} + {user?.is_staff ? ( + } onClick={gotoAdmin} /> + ) : null} + } + onClick={logoutAndRedirect} + /> + + ); +} + +export default UserDropdown; diff --git a/rsconcept/frontend/src/app/Navigation/UserMenu.tsx b/rsconcept/frontend/src/app/Navigation/UserMenu.tsx new file mode 100644 index 00000000..e6a6284a --- /dev/null +++ b/rsconcept/frontend/src/app/Navigation/UserMenu.tsx @@ -0,0 +1,53 @@ +import { AnimatePresence } from 'framer-motion'; + +import { IconLogin, IconUser2 } from '@/components/Icons'; +import Loader from '@/components/ui/Loader'; +import AnimateFade from '@/components/wrap/AnimateFade'; +import { useAuth } from '@/context/AuthContext'; +import { useConceptNavigation } from '@/context/NavigationContext'; +import { useConceptOptions } from '@/context/OptionsContext'; +import useDropdown from '@/hooks/useDropdown'; + +import { urls } from '../urls'; +import NavigationButton from './NavigationButton'; +import UserDropdown from './UserDropdown'; + +function UserMenu() { + const router = useConceptNavigation(); + const { user, loading } = useAuth(); + const { adminMode } = useConceptOptions(); + const menu = useDropdown(); + + const navigateLogin = () => router.push(urls.login); + return ( +
+ + {loading ? ( + + + + ) : null} + {!user && !loading ? ( + + } + onClick={navigateLogin} + /> + + ) : null} + {user ? ( + + } + onClick={menu.toggle} + /> + + ) : null} + + menu.hide()} /> +
+ ); +} + +export default UserMenu; diff --git a/rsconcept/frontend/src/app/Navigation/index.tsx b/rsconcept/frontend/src/app/Navigation/index.tsx new file mode 100644 index 00000000..7bc6dc0e --- /dev/null +++ b/rsconcept/frontend/src/app/Navigation/index.tsx @@ -0,0 +1 @@ +export { default } from './Navigation'; diff --git a/rsconcept/frontend/src/app/Router.tsx b/rsconcept/frontend/src/app/Router.tsx new file mode 100644 index 00000000..621bc3ee --- /dev/null +++ b/rsconcept/frontend/src/app/Router.tsx @@ -0,0 +1,76 @@ +import { createBrowserRouter } from 'react-router-dom'; + +import CreateItemPage from '@/pages/CreateItemPage'; +import HomePage from '@/pages/HomePage'; +import IconsPage from '@/pages/IconsPage'; +import LibraryPage from '@/pages/LibraryPage'; +import LoginPage from '@/pages/LoginPage'; +import ManualsPage from '@/pages/ManualsPage'; +import NotFoundPage from '@/pages/NotFoundPage'; +import OssPage from '@/pages/OssPage'; +import PasswordChangePage from '@/pages/PasswordChangePage'; +import RegisterPage from '@/pages/RegisterPage'; +import RestorePasswordPage from '@/pages/RestorePasswordPage'; +import RSFormPage from '@/pages/RSFormPage'; +import UserProfilePage from '@/pages/UserProfilePage'; + +import ApplicationLayout from './ApplicationLayout'; +import { routes } from './urls'; + +export const Router = createBrowserRouter([ + { + path: '/', + element: , + errorElement: , + children: [ + { + path: '', + element: + }, + { + path: routes.login, + element: + }, + { + path: routes.signup, + element: + }, + { + path: routes.profile, + element: + }, + { + path: routes.restore_password, + element: + }, + { + path: routes.password_change, + element: + }, + { + path: routes.library, + element: + }, + { + path: routes.create_schema, + element: + }, + { + path: `${routes.rsforms}/:id`, + element: + }, + { + path: `${routes.oss}/:id`, + element: + }, + { + path: `${routes.icons}`, + element: + }, + { + path: routes.manuals, + element: + } + ] + } +]); diff --git a/rsconcept/frontend/src/app/backendAPI.ts b/rsconcept/frontend/src/app/backendAPI.ts new file mode 100644 index 00000000..77163451 --- /dev/null +++ b/rsconcept/frontend/src/app/backendAPI.ts @@ -0,0 +1,578 @@ +/** + * Module: API for backend communications. + */ + +import axios, { AxiosError, AxiosRequestConfig } from 'axios'; +import { toast } from 'react-toastify'; + +import { type ErrorData } from '@/components/info/InfoError'; +import { ILexemeData, IResolutionData, ITextRequest, ITextResult, IWordFormPlain } from '@/models/language'; +import { + AccessPolicy, + ILibraryItem, + ILibraryUpdateData, + ITargetAccessPolicy, + ITargetLocation, + IVersionData, + LibraryItemType +} from '@/models/library'; +import { ILibraryCreateData } from '@/models/library'; +import { IOperationSchemaData } from '@/models/oss'; +import { + IConstituentaList, + IConstituentaMeta, + ICstCreateData, + ICstCreatedResponse, + ICstMovetoData, + ICstRenameData, + ICstSubstituteData, + ICstUpdateData, + IInlineSynthesisData, + IProduceStructureResponse, + IRSFormCloneData, + IRSFormData, + IRSFormUploadData, + ITargetCst, + IVersionCreatedResponse +} from '@/models/rsform'; +import { IExpressionParse, IRSExpression } from '@/models/rslang'; +import { + ICurrentUser, + IPasswordTokenData, + IRequestPasswordData, + IResetPasswordData, + ITargetUser, + ITargetUsers, + IUserInfo, + IUserLoginData, + IUserProfile, + IUserSignupData, + IUserUpdateData, + IUserUpdatePassword +} from '@/models/user'; +import { buildConstants } from '@/utils/buildConstants'; + +const defaultOptions = { + xsrfCookieName: 'csrftoken', + xsrfHeaderName: 'x-csrftoken', + baseURL: `${buildConstants.backend}`, + withCredentials: true +}; + +const axiosInstance = axios.create(defaultOptions); +axiosInstance.interceptors.request.use(config => { + const token = document.cookie + .split('; ') + .find(row => row.startsWith('csrftoken=')) + ?.split('=')[1]; + if (token) { + config.headers['x-csrftoken'] = token; + } + return config; +}); + +// ================ Data transfer types ================ +export type DataCallback = (data: ResponseData) => void; + +interface IFrontRequest { + data?: RequestData; + onSuccess?: DataCallback; + onError?: (error: ErrorData) => void; + setLoading?: (loading: boolean) => void; + showError?: boolean; +} + +export interface FrontPush extends IFrontRequest { + data: DataType; +} +export interface FrontPull extends IFrontRequest { + onSuccess: DataCallback; +} + +export interface FrontExchange extends IFrontRequest { + data: RequestData; + onSuccess: DataCallback; +} + +export interface FrontAction extends IFrontRequest {} + +interface IAxiosRequest { + endpoint: string; + request: IFrontRequest; + options?: AxiosRequestConfig; +} + +// ==================== API ==================== +export function getAuth(request: FrontPull) { + AxiosGet({ + endpoint: `/users/api/auth`, + request: request + }); +} + +export function postLogin(request: FrontPush) { + AxiosPost({ + endpoint: '/users/api/login', + request: request + }); +} + +export function postLogout(request: FrontAction) { + AxiosPost({ + endpoint: '/users/api/logout', + request: request + }); +} + +export function postSignup(request: FrontExchange) { + AxiosPost({ + endpoint: '/users/api/signup', + request: request + }); +} + +export function getProfile(request: FrontPull) { + AxiosGet({ + endpoint: '/users/api/profile', + request: request + }); +} + +export function patchProfile(request: FrontExchange) { + AxiosPatch({ + endpoint: '/users/api/profile', + request: request + }); +} + +export function patchPassword(request: FrontPush) { + AxiosPatch({ + endpoint: '/users/api/change-password', + request: request + }); +} + +export function postRequestPasswordReset(request: FrontPush) { + // title: 'Request password reset', + AxiosPost({ + endpoint: '/users/api/password-reset', + request: request + }); +} + +export function postValidatePasswordToken(request: FrontPush) { + // title: 'Validate password token', + AxiosPost({ + endpoint: '/users/api/password-reset/validate', + request: request + }); +} + +export function postResetPassword(request: FrontPush) { + // title: 'Reset password', + AxiosPost({ + endpoint: '/users/api/password-reset/confirm', + request: request + }); +} + +export function getActiveUsers(request: FrontPull) { + // title: 'Active users list', + AxiosGet({ + endpoint: '/users/api/active-users', + request: request + }); +} + +export function getLibrary(request: FrontPull) { + // title: 'Available LibraryItems list', + AxiosGet({ + endpoint: '/api/library/active', + request: request + }); +} + +export function getAdminLibrary(request: FrontPull) { + // title: 'All LibraryItems list', + AxiosGet({ + endpoint: '/api/library/all', + request: request + }); +} + +export function getTemplates(request: FrontPull) { + AxiosGet({ + endpoint: '/api/library/templates', + request: request + }); +} + +export function postRSFormFromFile(request: FrontExchange) { + AxiosPost({ + endpoint: '/api/rsforms/create-detailed', + request: request, + options: { + headers: { + 'Content-Type': 'multipart/form-data' + } + } + }); +} + +export function postCreateLibraryItem(request: FrontExchange) { + AxiosPost({ + endpoint: '/api/library', + request: request + }); +} + +export function postCloneLibraryItem(target: string, request: FrontExchange) { + AxiosPost({ + endpoint: `/api/library/${target}/clone`, + request: request + }); +} + +export function getOssDetails(target: string, request: FrontPull) { + request.onSuccess({ + id: Number(target), + comment: '123', + alias: 'oss1', + access_policy: AccessPolicy.PUBLIC, + editors: [], + owner: 1, + item_type: LibraryItemType.OSS, + location: '/U', + read_only: false, + subscribers: [], + time_create: '0', + time_update: '0', + title: 'TestOss', + visible: false + }); + // AxiosGet({ + // endpoint: `/api/oss/${target}`, // TODO: endpoint to access OSS + // request: request + // }); +} + +export function getRSFormDetails(target: string, version: string, request: FrontPull) { + if (!version) { + AxiosGet({ + endpoint: `/api/rsforms/${target}/details`, + request: request + }); + } else { + AxiosGet({ + endpoint: `/api/rsforms/${target}/versions/${version}`, + request: request + }); + } +} + +export function patchLibraryItem(target: string, request: FrontExchange) { + AxiosPatch({ + endpoint: `/api/library/${target}`, + request: request + }); +} + +export function deleteLibraryItem(target: string, request: FrontAction) { + AxiosDelete({ + endpoint: `/api/library/${target}`, + request: request + }); +} + +export function patchSetOwner(target: string, request: FrontPush) { + AxiosPatch({ + endpoint: `/api/library/${target}/set-owner`, + request: request + }); +} + +export function patchSetAccessPolicy(target: string, request: FrontPush) { + AxiosPatch({ + endpoint: `/api/library/${target}/set-access-policy`, + request: request + }); +} + +export function patchSetLocation(target: string, request: FrontPush) { + AxiosPatch({ + endpoint: `/api/library/${target}/set-location`, + request: request + }); +} + +export function patchEditorsAdd(target: string, request: FrontPush) { + AxiosPatch({ + endpoint: `/api/library/${target}/editors-add`, + request: request + }); +} + +export function patchEditorsRemove(target: string, request: FrontPush) { + AxiosPatch({ + endpoint: `/api/library/${target}/editors-remove`, + request: request + }); +} + +export function patchEditorsSet(target: string, request: FrontPush) { + AxiosPatch({ + endpoint: `/api/library/${target}/editors-set`, + request: request + }); +} + +export function postSubscribe(target: string, request: FrontAction) { + AxiosPost({ + endpoint: `/api/library/${target}/subscribe`, + request: request + }); +} + +export function deleteUnsubscribe(target: string, request: FrontAction) { + AxiosDelete({ + endpoint: `/api/library/${target}/unsubscribe`, + request: request + }); +} + +export function getTRSFile(target: string, version: string, request: FrontPull) { + if (!version) { + AxiosGet({ + endpoint: `/api/rsforms/${target}/export-trs`, + request: request, + options: { responseType: 'blob' } + }); + } else { + AxiosGet({ + endpoint: `/api/versions/${version}/export-file`, + request: request, + options: { responseType: 'blob' } + }); + } +} + +export function postNewConstituenta(schema: string, request: FrontExchange) { + AxiosPost({ + endpoint: `/api/rsforms/${schema}/cst-create`, + request: request + }); +} + +export function patchDeleteConstituenta(schema: string, request: FrontExchange) { + AxiosPatch({ + endpoint: `/api/rsforms/${schema}/cst-delete-multiple`, + request: request + }); +} + +export function patchConstituenta(target: string, request: FrontExchange) { + AxiosPatch({ + endpoint: `/api/constituents/${target}`, + request: request + }); +} + +export function patchRenameConstituenta(schema: string, request: FrontExchange) { + AxiosPatch({ + endpoint: `/api/rsforms/${schema}/cst-rename`, + request: request + }); +} + +export function patchProduceStructure(schema: string, request: FrontExchange) { + AxiosPatch({ + endpoint: `/api/rsforms/${schema}/cst-produce-structure`, + request: request + }); +} + +export function patchSubstituteConstituents(schema: string, request: FrontExchange) { + AxiosPatch({ + endpoint: `/api/rsforms/${schema}/cst-substitute`, + request: request + }); +} + +export function patchMoveConstituenta(schema: string, request: FrontExchange) { + AxiosPatch({ + endpoint: `/api/rsforms/${schema}/cst-moveto`, + request: request + }); +} + +export function postCheckExpression(schema: string, request: FrontExchange) { + AxiosPost({ + endpoint: `/api/rsforms/${schema}/check`, + request: request + }); +} + +export function patchResetAliases(target: string, request: FrontPull) { + AxiosPatch({ + endpoint: `/api/rsforms/${target}/reset-aliases`, + request: request + }); +} + +export function patchRestoreOrder(target: string, request: FrontPull) { + AxiosPatch({ + endpoint: `/api/rsforms/${target}/restore-order`, + request: request + }); +} + +export function patchUploadTRS(target: string, request: FrontExchange) { + AxiosPatch({ + endpoint: `/api/rsforms/${target}/load-trs`, + request: request, + options: { + headers: { + 'Content-Type': 'multipart/form-data' + } + } + }); +} +export function patchInlineSynthesis(request: FrontExchange) { + AxiosPatch({ + endpoint: `/api/operations/inline-synthesis`, + request: request + }); +} + +export function postResolveText(schema: string, request: FrontExchange) { + AxiosPost({ + endpoint: `/api/rsforms/${schema}/resolve`, + request: request + }); +} + +export function postInflectText(request: FrontExchange) { + AxiosPost({ + endpoint: `/api/cctext/inflect`, + request: request + }); +} + +export function postParseText(request: FrontExchange) { + AxiosPost({ + endpoint: `/api/cctext/parse`, + request: request + }); +} + +export function postGenerateLexeme(request: FrontExchange) { + // title: `Parse text ${request.data.text}`, + AxiosPost({ + endpoint: `/api/cctext/generate-lexeme`, + request: request + }); +} + +export function postCreateVersion(target: string, request: FrontExchange) { + // title: `Create version for RSForm id=${target}`, + AxiosPost({ + endpoint: `/api/rsforms/${target}/versions/create`, + request: request + }); +} + +export function patchVersion(target: string, request: FrontPush) { + // title: `Version id=${target}`, + AxiosPatch({ + endpoint: `/api/versions/${target}`, + request: request + }); +} + +export function patchRestoreVersion(target: string, request: FrontPull) { + AxiosPatch({ + endpoint: `/api/versions/${target}/restore`, + request: request + }); +} + +export function deleteVersion(target: string, request: FrontAction) { + AxiosDelete({ + endpoint: `/api/versions/${target}`, + request: request + }); +} + +// ============ Helper functions ============= +function AxiosGet({ endpoint, request, options }: IAxiosRequest) { + if (request.setLoading) request.setLoading(true); + axiosInstance + .get(endpoint, options) + .then(response => { + if (request.setLoading) request.setLoading(false); + if (request.onSuccess) request.onSuccess(response.data); + }) + .catch((error: Error | AxiosError) => { + if (request.setLoading) request.setLoading(false); + if (request.showError) toast.error(error.message); + if (request.onError) request.onError(error); + }); +} + +function AxiosPost({ + endpoint, + request, + options +}: IAxiosRequest) { + if (request.setLoading) request.setLoading(true); + axiosInstance + .post(endpoint, request.data, options) + .then(response => { + if (request.setLoading) request.setLoading(false); + if (request.onSuccess) request.onSuccess(response.data); + }) + .catch((error: Error | AxiosError) => { + if (request.setLoading) request.setLoading(false); + if (request.showError) toast.error(error.message); + if (request.onError) request.onError(error); + }); +} + +function AxiosDelete({ + endpoint, + request, + options +}: IAxiosRequest) { + if (request.setLoading) request.setLoading(true); + axiosInstance + .delete(endpoint, options) + .then(response => { + if (request.setLoading) request.setLoading(false); + if (request.onSuccess) request.onSuccess(response.data); + }) + .catch((error: Error | AxiosError) => { + if (request.setLoading) request.setLoading(false); + if (request.showError) toast.error(error.message); + if (request.onError) request.onError(error); + }); +} + +function AxiosPatch({ + endpoint, + request, + options +}: IAxiosRequest) { + if (request.setLoading) request.setLoading(true); + axiosInstance + .patch(endpoint, request.data, options) + .then(response => { + if (request.setLoading) request.setLoading(false); + if (request.onSuccess) request.onSuccess(response.data); + return response.data; + }) + .catch((error: Error | AxiosError) => { + if (request.setLoading) request.setLoading(false); + if (request.showError) toast.error(error.message); + if (request.onError) request.onError(error); + }); +} diff --git a/rsconcept/frontend/src/app/index.tsx b/rsconcept/frontend/src/app/index.tsx new file mode 100644 index 00000000..e8f95bdf --- /dev/null +++ b/rsconcept/frontend/src/app/index.tsx @@ -0,0 +1,9 @@ +import { RouterProvider } from 'react-router-dom'; + +import { Router } from './Router'; + +function App() { + return ; +} + +export default App; diff --git a/rsconcept/frontend/src/app/urls.ts b/rsconcept/frontend/src/app/urls.ts new file mode 100644 index 00000000..d7ff7432 --- /dev/null +++ b/rsconcept/frontend/src/app/urls.ts @@ -0,0 +1,63 @@ +/** + * Module: Internal navigation constants. + */ + +import { buildConstants } from '@/utils/buildConstants'; + +/** + * Routes. + */ +export const routes = { + login: 'login', + signup: 'signup', + profile: 'profile', + restore_password: 'restore-password', + password_change: 'password-change', + library: 'library', + create_schema: 'library/create', + manuals: 'manuals', + help: 'manuals', + rsforms: 'rsforms', + oss: 'oss', + icons: 'icons' +}; + +interface SchemaProps { + id: number | string; + tab: number; + version?: number | string; + active?: number | string; +} + +interface OssProps { + id: number | string; + tab: number; +} + +/** + * Internal navigation URLs. + */ +export const urls = { + admin: `${buildConstants.backend}/admin`, + home: '/', + login: `/${routes.login}`, + login_hint: (userName: string) => `/login?username=${userName}`, + profile: `/${routes.profile}`, + signup: `/${routes.signup}`, + library: `/${routes.library}`, + library_filter: (strategy: string) => `/library?filter=${strategy}`, + create_schema: `/${routes.create_schema}`, + manuals: `/${routes.manuals}`, + help_topic: (topic: string) => `/manuals?topic=${topic}`, + schema: (id: number | string, version?: number | string) => + `/rsforms/${id}` + (version !== undefined ? `?v=${version}` : ''), + oss: (id: number | string) => `/oss/${id}`, + schema_props: ({ id, tab, version, active }: SchemaProps) => { + const versionStr = version !== undefined ? `v=${version}&` : ''; + const activeStr = active !== undefined ? `&active=${active}` : ''; + return `/rsforms/${id}?${versionStr}tab=${tab}${activeStr}`; + }, + oss_props: ({ id, tab }: OssProps) => { + return `/oss/${id}?tab=${tab}`; + } +}; diff --git a/rsconcept/frontend/src/components/DomainIcons.tsx b/rsconcept/frontend/src/components/DomainIcons.tsx new file mode 100644 index 00000000..cf93b85d --- /dev/null +++ b/rsconcept/frontend/src/components/DomainIcons.tsx @@ -0,0 +1,135 @@ +import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library'; +import { CstMatchMode, DependencyMode } from '@/models/miscellaneous'; +import { ExpressionStatus } from '@/models/rsform'; + +import { + IconAlias, + IconBusiness, + IconFilter, + IconFollow, + IconFollowOff, + IconFormula, + IconGraphCollapse, + IconGraphExpand, + IconGraphInputs, + IconGraphOutputs, + IconHide, + IconOSS, + IconPrivate, + IconProps, + IconProtected, + IconPublic, + IconRSForm, + IconSettings, + IconShow, + IconStatusError, + IconStatusIncalculable, + IconStatusOK, + IconStatusUnknown, + IconTemplates, + IconTerm, + IconText, + IconUser +} from './Icons'; + +export interface DomIconProps extends IconProps { + value: RequestData; +} + +export function ItemTypeIcon({ value, size = '1.25rem', className }: DomIconProps) { + switch (value) { + case LibraryItemType.RSFORM: + return ; + case LibraryItemType.OSS: + return ; + } +} + +export function PolicyIcon({ value, size = '1.25rem', className }: DomIconProps) { + switch (value) { + case AccessPolicy.PRIVATE: + return ; + case AccessPolicy.PROTECTED: + return ; + case AccessPolicy.PUBLIC: + return ; + } +} + +export function VisibilityIcon({ value, size = '1.25rem', className }: DomIconProps) { + if (value) { + return ; + } else { + return ; + } +} + +export function SubscribeIcon({ value, size = '1.25rem', className }: DomIconProps) { + if (value) { + return ; + } else { + return ; + } +} + +export function LocationIcon({ value, size = '1.25rem', className }: DomIconProps) { + switch (value.substring(0, 2) as LocationHead) { + case LocationHead.COMMON: + return ; + case LocationHead.LIBRARY: + return ; + case LocationHead.PROJECTS: + return ; + case LocationHead.USER: + return ; + } +} + +export function DependencyIcon({ value, size = '1.25rem', className }: DomIconProps) { + switch (value) { + case DependencyMode.ALL: + return ; + case DependencyMode.EXPRESSION: + return ; + case DependencyMode.OUTPUTS: + return ; + case DependencyMode.INPUTS: + return ; + case DependencyMode.EXPAND_OUTPUTS: + return ; + case DependencyMode.EXPAND_INPUTS: + return ; + } +} + +export function MatchModeIcon({ value, size = '1.25rem', className }: DomIconProps) { + switch (value) { + case CstMatchMode.ALL: + return ; + case CstMatchMode.TEXT: + return ; + case CstMatchMode.EXPR: + return ; + case CstMatchMode.TERM: + return ; + case CstMatchMode.NAME: + return ; + } +} + +export function StatusIcon({ value, size = '1.25rem', className }: DomIconProps) { + switch (value) { + case ExpressionStatus.VERIFIED: + case ExpressionStatus.PROPERTY: + return ; + + case ExpressionStatus.UNKNOWN: + return ; + case ExpressionStatus.INCALCULABLE: + return ; + + case ExpressionStatus.INCORRECT: + case ExpressionStatus.UNDEFINED: + return ; + } +} diff --git a/rsconcept/frontend/src/components/Icons.tsx b/rsconcept/frontend/src/components/Icons.tsx new file mode 100644 index 00000000..425f95bf --- /dev/null +++ b/rsconcept/frontend/src/components/Icons.tsx @@ -0,0 +1,176 @@ +// Search new icons at https://reactsvgicons.com/ +// Note: save this file using Ctrl + K, Ctrl + Shift + S to disable autoformat + +// ==== General actions ======= +export { BiMenu as IconMenu } from 'react-icons/bi'; +export { LuLogOut as IconLogout } from 'react-icons/lu'; +export { FiSave as IconSave } from 'react-icons/fi'; +export { BiCheck as IconAccept } from 'react-icons/bi'; +export { BiX as IconRemove } from 'react-icons/bi'; +export { BiTrash as IconDestroy } from 'react-icons/bi'; +export { BiReset as IconReset } from 'react-icons/bi'; +export { LiaEdit as IconEdit } from 'react-icons/lia'; +export { FiEdit as IconEdit2 } from 'react-icons/fi'; +export { BiSearchAlt2 as IconSearch } from 'react-icons/bi'; +export { BiDownload as IconDownload } from 'react-icons/bi'; +export { BiUpload as IconUpload } from 'react-icons/bi'; +export { BiCog as IconSettings } from 'react-icons/bi'; +export { TbEye as IconShow } from 'react-icons/tb'; +export { TbEyeX as IconHide } from 'react-icons/tb'; +export { BiShareAlt as IconShare } from 'react-icons/bi'; +export { BiFilterAlt as IconFilter } from 'react-icons/bi'; +export {BiDownArrowCircle as IconOpenList } from 'react-icons/bi'; +export { LuAlertTriangle as IconAlert } from 'react-icons/lu'; + +// ===== UI elements ======= +export { BiX as IconClose } from 'react-icons/bi'; +export { LuChevronDown as IconDropArrow } from 'react-icons/lu'; +export { LuChevronUp as IconDropArrowUp } from 'react-icons/lu'; +export { RiMenuFoldFill as IconMenuFold } from 'react-icons/ri'; +export { RiMenuUnfoldFill as IconMenuUnfold } from 'react-icons/ri'; +export { LuMoon as IconDarkTheme } from 'react-icons/lu'; +export { LuSun as IconLightTheme } from 'react-icons/lu'; +export { FaRegFolder as IconFolder } from 'react-icons/fa'; +export { LuLightbulb as IconHelp } from 'react-icons/lu'; +export { LuLightbulbOff as IconHelpOff } from 'react-icons/lu'; +export { RiPushpinFill as IconPin } from 'react-icons/ri'; +export { RiUnpinLine as IconUnpin } from 'react-icons/ri'; +export { BiCaretDown as IconSortDesc } from 'react-icons/bi'; +export { BiCaretUp as IconSortAsc } from 'react-icons/bi'; +export { BiChevronLeft as IconPageLeft } from 'react-icons/bi'; +export { BiChevronRight as IconPageRight } from 'react-icons/bi'; +export { BiFirstPage as IconPageFirst } from 'react-icons/bi'; +export { BiLastPage as IconPageLast } from 'react-icons/bi'; + +// ==== User status ======= +export { LuUserCircle2 as IconUser } from 'react-icons/lu'; +export { FaCircleUser as IconUser2 } from 'react-icons/fa6'; +export { TbUserEdit as IconEditor } from 'react-icons/tb'; +export { LuCrown as IconOwner } from 'react-icons/lu'; +export { TbMeteor as IconAdmin } from 'react-icons/tb'; +export { TbMeteorOff as IconAdminOff } from 'react-icons/tb'; +export { LuGlasses as IconReader } from 'react-icons/lu'; + +// ===== Domain entities ======= +export { TbBriefcase as IconBusiness } from 'react-icons/tb'; +export { VscLibrary as IconLibrary } from 'react-icons/vsc'; +export { IoLibrary as IconLibrary2 } from 'react-icons/io5'; +export { BiDiamond as IconTemplates } from 'react-icons/bi'; +export { FaRegObjectGroup as IconOSS } from 'react-icons/fa'; +export { RiHexagonLine as IconRSForm } from 'react-icons/ri'; +export { LuArchive as IconArchive } from 'react-icons/lu'; +export { LuDatabase as IconDatabase } from 'react-icons/lu'; +export { LuImage as IconImage } from 'react-icons/lu'; +export { TbColumns as IconList } from 'react-icons/tb'; +export { ImStack as IconVersions } from 'react-icons/im'; +export { TbColumnsOff as IconListOff } from 'react-icons/tb'; +export { LuAtSign as IconTerm } from 'react-icons/lu'; +export { LuSubscript as IconAlias } from 'react-icons/lu'; +export { TbMathFunction as IconFormula } from 'react-icons/tb'; +export { BiFontFamily as IconText } from 'react-icons/bi'; +export { BiFont as IconTextOff } from 'react-icons/bi'; +export { RiTreeLine as IconTree } from 'react-icons/ri'; +export { FaRegKeyboard as IconControls } from 'react-icons/fa6'; +export { RiLockLine as IconImmutable } from 'react-icons/ri'; +export { RiLockUnlockLine as IconMutable } from 'react-icons/ri'; +export { RiOpenSourceLine as IconPublic } from 'react-icons/ri'; +export { RiShieldLine as IconProtected } from 'react-icons/ri'; +export { RiShieldKeyholeLine as IconPrivate } from 'react-icons/ri'; +export { BiBug as IconStatusError } from 'react-icons/bi'; +export { BiCheckCircle as IconStatusOK } from 'react-icons/bi'; +export { BiHelpCircle as IconStatusUnknown } from 'react-icons/bi'; +export { BiPauseCircle as IconStatusIncalculable } from 'react-icons/bi'; +export { LuPower as IconKeepAliasOn } from 'react-icons/lu'; +export { LuPowerOff as IconKeepAliasOff } from 'react-icons/lu'; +export { LuFlag as IconKeepTermOn } from 'react-icons/lu'; +export { LuFlagOff as IconKeepTermOff } from 'react-icons/lu'; + +// ===== Domain actions ===== +export { BiUpvote as IconMoveUp } from 'react-icons/bi'; +export { BiDownvote as IconMoveDown } from 'react-icons/bi'; +export { BiRightArrow as IconMoveRight } from 'react-icons/bi'; +export { BiLeftArrow as IconMoveLeft } from 'react-icons/bi'; +export { FiBell as IconFollow } from 'react-icons/fi'; +export { FiBellOff as IconFollowOff } from 'react-icons/fi'; +export { BiPlusCircle as IconNewItem } from 'react-icons/bi'; +export { FaSquarePlus as IconNewItem2 } from 'react-icons/fa6'; +export { BiDuplicate as IconClone } from 'react-icons/bi'; +export { LuReplace as IconReplace } from 'react-icons/lu'; +export { FaSortAmountDownAlt as IconSortList } from 'react-icons/fa'; +export { LuNetwork as IconGenerateStructure } from 'react-icons/lu'; +export { LuBookCopy as IconInlineSynthesis } from 'react-icons/lu'; +export { LuWand2 as IconGenerateNames } from 'react-icons/lu'; + +// ======== Graph UI ======= +export { BiCollapse as IconGraphCollapse } from 'react-icons/bi'; +export { BiExpand as IconGraphExpand } from 'react-icons/bi'; +export { LuMaximize as IconGraphMaximize } from 'react-icons/lu'; +export { BiGitBranch as IconGraphInputs } from 'react-icons/bi'; +export { BiGitMerge as IconGraphOutputs } from 'react-icons/bi'; +export { LuAtom as IconGraphCore } from 'react-icons/lu'; +export { LuRotate3D as IconRotate3D } from 'react-icons/lu'; +export { MdOutlineFitScreen as IconFitImage } from 'react-icons/md'; +export { LuSparkles as IconClustering } from 'react-icons/lu'; +export { LuSparkle as IconClusteringOff } from 'react-icons/lu'; + +// ===== Custom elements ====== +interface IconSVGProps { + viewBox: string; + size?: string; + className?: string; + props?: React.SVGProps; + children: React.ReactNode; +} + +export interface IconProps { + size?: string; + className?: string; +} + +function MetaIconSVG({ viewBox, size = '1.5rem', className, props, children }: IconSVGProps) { + return ( + + {children} + + ); +} + +export function IconManuals(props: IconProps) { + return ( + + + + ); +} + +export function IconLogin(props: IconProps) { + return ( + + + + + ); +} + +export function CheckboxChecked() { + return ( + + + + ); +} + +export function CheckboxNull() { + return ( + + + + ); +} diff --git a/rsconcept/frontend/src/components/RSInput/RSInput.tsx b/rsconcept/frontend/src/components/RSInput/RSInput.tsx new file mode 100644 index 00000000..d20f5d8d --- /dev/null +++ b/rsconcept/frontend/src/components/RSInput/RSInput.tsx @@ -0,0 +1,195 @@ +'use client'; + +import { Extension } from '@codemirror/state'; +import { tags } from '@lezer/highlight'; +import { createTheme } from '@uiw/codemirror-themes'; +import CodeMirror, { BasicSetupOptions, ReactCodeMirrorProps, ReactCodeMirrorRef } from '@uiw/react-codemirror'; +import clsx from 'clsx'; +import { EditorView } from 'codemirror'; +import { forwardRef, useCallback, useMemo, useRef } from 'react'; + +import Label from '@/components/ui/Label'; +import { useConceptOptions } from '@/context/OptionsContext'; +import { useRSForm } from '@/context/RSFormContext'; +import { getFontClassName } from '@/models/miscellaneousAPI'; +import { generateAlias, getCstTypePrefix, guessCstType } from '@/models/rsformAPI'; +import { extractGlobals } from '@/models/rslangAPI'; + +import { ccBracketMatching } from './bracketMatching'; +import { RSLanguage } from './rslang'; +import { getSymbolSubstitute, RSTextWrapper } from './textEditing'; +import { rsHoverTooltip } from './tooltip'; + +interface RSInputProps + extends Pick< + ReactCodeMirrorProps, + | 'id' // prettier: split-lines + | 'height' + | 'minHeight' + | 'maxHeight' + | 'value' + | 'onFocus' + | 'onBlur' + | 'placeholder' + | 'style' + | 'className' + > { + label?: string; + disabled?: boolean; + noTooltip?: boolean; + onChange?: (newValue: string) => void; + onAnalyze?: () => void; +} + +const RSInput = forwardRef( + ( + { + id, // prettier: split lines + label, + disabled, + noTooltip, + + className, + style, + + onChange, + onAnalyze, + ...restProps + }, + ref + ) => { + const { darkMode, colors, mathFont } = useConceptOptions(); + const { schema } = useRSForm(); + + const internalRef = useRef(null); + const thisRef = useMemo(() => (!ref || typeof ref === 'function' ? internalRef : ref), [internalRef, ref]); + + const cursor = useMemo(() => (!disabled ? 'cursor-text' : 'cursor-default'), [disabled]); + const customTheme: Extension = useMemo( + () => + createTheme({ + theme: darkMode ? 'dark' : 'light', + settings: { + fontFamily: 'inherit', + background: !disabled ? colors.bgInput : colors.bgDefault, + foreground: colors.fgDefault, + selection: colors.bgHover, + caret: colors.fgDefault + }, + styles: [ + { tag: tags.name, color: colors.fgPurple, cursor: 'default' }, // GlobalID + { tag: tags.variableName, color: colors.fgGreen }, // LocalID + { tag: tags.propertyName, color: colors.fgTeal }, // Radical + { tag: tags.keyword, color: colors.fgBlue }, // keywords + { tag: tags.literal, color: colors.fgBlue }, // literals + { tag: tags.controlKeyword, fontWeight: '400' }, // R | I | D + { tag: tags.unit, fontSize: '0.75rem' }, // indices + { tag: tags.brace, color: colors.fgPurple, fontWeight: '600' } // braces (curly brackets) + ] + }), + [disabled, colors, darkMode] + ); + + const editorExtensions = useMemo( + () => [ + EditorView.lineWrapping, + RSLanguage, + ccBracketMatching(darkMode), + ...(noTooltip || !schema ? [] : [rsHoverTooltip(schema)]) + ], + [darkMode, schema, noTooltip] + ); + + const handleInput = useCallback( + (event: React.KeyboardEvent) => { + if (!thisRef.current) { + return; + } + const text = new RSTextWrapper(thisRef.current as Required); + if ((event.ctrlKey || event.metaKey) && event.code === 'Space') { + const selection = text.getSelection(); + if (!selection.empty || !schema) { + return; + } + const hint = text.getText(selection.from - 1, selection.from); + const type = guessCstType(hint); + if (hint === getCstTypePrefix(type)) { + text.setSelection(selection.from - 1, selection.from); + } + const takenAliases = [...extractGlobals(thisRef.current.view?.state.doc.toString() ?? '')]; + const newAlias = generateAlias(type, schema, takenAliases); + text.replaceWith(newAlias); + event.preventDefault(); + event.stopPropagation(); + } else if (event.altKey) { + if (text.processAltKey(event.code, event.shiftKey)) { + event.preventDefault(); + event.stopPropagation(); + } + } else if (!(event.ctrlKey || event.metaKey)) { + const newSymbol = getSymbolSubstitute(event.code, event.shiftKey); + if (newSymbol) { + text.replaceWith(newSymbol); + event.preventDefault(); + event.stopPropagation(); + } + } else if (event.code === 'KeyQ' && onAnalyze) { + onAnalyze(); + event.preventDefault(); + event.stopPropagation(); + } + }, + [thisRef, onAnalyze, schema] + ); + + return ( +
+
+ ); + } +); + +export default RSInput; + +// ======= Internal ========== +const editorSetup: BasicSetupOptions = { + highlightSpecialChars: false, + history: true, + drawSelection: true, + syntaxHighlighting: false, + defaultKeymap: true, + historyKeymap: true, + + lineNumbers: false, + highlightActiveLineGutter: false, + foldGutter: false, + dropCursor: true, + allowMultipleSelections: false, + indentOnInput: false, + bracketMatching: false, + closeBrackets: false, + autocompletion: false, + rectangularSelection: false, + crosshairCursor: false, + highlightActiveLine: false, + highlightSelectionMatches: false, + closeBracketsKeymap: false, + searchKeymap: false, + foldKeymap: false, + completionKeymap: false, + lintKeymap: false +}; diff --git a/rsconcept/frontend/src/components/RSInput/bracketMatching.ts b/rsconcept/frontend/src/components/RSInput/bracketMatching.ts new file mode 100644 index 00000000..a1955f40 --- /dev/null +++ b/rsconcept/frontend/src/components/RSInput/bracketMatching.ts @@ -0,0 +1,31 @@ +import { bracketMatching, MatchResult } from '@codemirror/language'; +import { Decoration, EditorView } from '@codemirror/view'; + +import { bracketsDarkT, bracketsLightT } from '@/styling/color'; + +const matchingMark = Decoration.mark({ class: 'cc-matchingBracket' }); +const nonMatchingMark = Decoration.mark({ class: 'cc-nonmatchingBracket' }); + +function bracketRender(match: MatchResult) { + const decorations = []; + const mark = match.matched ? matchingMark : nonMatchingMark; + decorations.push(mark.range(match.start.from, match.start.to)); + if (match.end) { + decorations.push(mark.range(match.end.from, match.end.to)); + } + return decorations; +} + +const darkTheme = EditorView.baseTheme(bracketsDarkT); + +const lightTheme = EditorView.baseTheme(bracketsLightT); + +export function ccBracketMatching(darkMode: boolean) { + return [ + bracketMatching({ + renderMatch: bracketRender, + brackets: '{}[]()' + }), + darkMode ? darkTheme : lightTheme + ]; +} diff --git a/rsconcept/frontend/src/components/RSInput/index.tsx b/rsconcept/frontend/src/components/RSInput/index.tsx new file mode 100644 index 00000000..1a46b244 --- /dev/null +++ b/rsconcept/frontend/src/components/RSInput/index.tsx @@ -0,0 +1 @@ +export { default } from './RSInput'; diff --git a/rsconcept/frontend/src/components/RSInput/rslang/highlight.ts b/rsconcept/frontend/src/components/RSInput/rslang/highlight.ts new file mode 100644 index 00000000..98e6919c --- /dev/null +++ b/rsconcept/frontend/src/components/RSInput/rslang/highlight.ts @@ -0,0 +1,23 @@ +import { styleTags, tags } from '@lezer/highlight'; + +export const highlighting = styleTags({ + 'Index': tags.unit, + 'ComplexIndex': tags.unit, + 'Literal': tags.literal, + + 'Radical': tags.propertyName, + 'Function': tags.name, + 'Predicate': tags.name, + 'Global': tags.name, + 'Local': tags.variableName, + + 'TextFunction': tags.keyword, + 'Filter': tags.keyword, + 'PrefixR': tags.controlKeyword, + 'PrefixI': tags.controlKeyword, + 'PrefixD': tags.controlKeyword, + '{': tags.brace, + '}': tags.brace, + '|': tags.brace, + ';': tags.brace +}); diff --git a/rsconcept/frontend/src/components/RSInput/rslang/index.ts b/rsconcept/frontend/src/components/RSInput/rslang/index.ts new file mode 100644 index 00000000..de4efece --- /dev/null +++ b/rsconcept/frontend/src/components/RSInput/rslang/index.ts @@ -0,0 +1,11 @@ +import { LRLanguage } from '@codemirror/language'; + +import { parser } from './parser'; +import { Function, Global, Predicate } from './parser.terms'; + +export const GlobalTokens: number[] = [Global, Function, Predicate]; + +export const RSLanguage = LRLanguage.define({ + parser: parser, + languageData: {} +}); diff --git a/rsconcept/frontend/src/components/RSInput/rslang/parser.terms.ts b/rsconcept/frontend/src/components/RSInput/rslang/parser.terms.ts new file mode 100644 index 00000000..b9021547 --- /dev/null +++ b/rsconcept/frontend/src/components/RSInput/rslang/parser.terms.ts @@ -0,0 +1,16 @@ +// This file was generated by lezer-generator. You probably shouldn't edit it. +export const + Expression = 1, + Local = 2, + Index = 3, + Literal = 5, + Global = 6, + Radical = 7, + Filter = 20, + ComplexIndex = 21, + PrefixD = 26, + PrefixI = 27, + PrefixR = 29, + Function = 31, + TextFunction = 32, + Predicate = 45 diff --git a/rsconcept/frontend/src/components/RSInput/rslang/parser.test.ts b/rsconcept/frontend/src/components/RSInput/rslang/parser.test.ts new file mode 100644 index 00000000..c49808c6 --- /dev/null +++ b/rsconcept/frontend/src/components/RSInput/rslang/parser.test.ts @@ -0,0 +1,55 @@ +import { printTree } from '@/utils/codemirror'; + +import { parser } from './parser'; + +const testData = [ + ['a1', '[Expression[Local[Index]]]'], + ['A1', '[Expression[Global]]'], + ['∅', '[Expression[Literal]]'], + ['Z', '[Expression[Literal]]'], + ['1', '[Expression[Literal]]'], + ['12<41', '[Expression[Literal][<][Literal]]'], + ['12=41', '[Expression[Literal][=][Literal]]'], + ['ξ=ξ', '[Expression[Local][=][Local]]'], + ['0-5', '[Expression[Literal][-][Literal]]'], + ['¬2=2', '[Expression[¬][Literal][=][Literal]]'], + ['12+41', '[Expression[Literal][+][Literal]]'], + ['1+2*5', '[Expression[Literal][+][Literal][*][Literal]]'], + ['a1∪Z', '[Expression[Local[Index]][∪][Literal]]'], + ['Pr1(X1)', '[Expression[TextFunction[ComplexIndex]][(][Global][)]]'], + ['Pr11(X1)', '[Expression[TextFunction[ComplexIndex]][(][Global][)]]'], + ['Pr11,21(X1)', '[Expression[TextFunction[ComplexIndex]][(][Global][)]]'], + ['pr1(S1)', '[Expression[TextFunction[ComplexIndex]][(][Global][)]]'], + ['Pr1,2(X1)', '[Expression[TextFunction[ComplexIndex]][(][Global][)]]'], + ['card(X1)', '[Expression[TextFunction][(][Global][)]]'], + ['red(X1)', '[Expression[TextFunction][(][Global][)]]'], + ['bool(X1)', '[Expression[TextFunction][(][Global][)]]'], + ['debool(X1)', '[Expression[TextFunction][(][Global][)]]'], + ['Fi1,2[ξ, ξ](ξ)', '[Expression[Filter][ComplexIndex][[][Local][Local][]][(][Local][)]]'], + ['ℬℬ(X1)', '[Expression[ℬ][ℬ][(][Global][)]]'], + ['P2[S1]', '[Expression[Predicate][[][Global][]]]'], + ['[σ∈R1×R1] F6[σ]', '[Expression[[][Local][∈][Radical][×][Radical][]][Function][[][Local][]]]'], + ['D{ξ∈red(S1) | ξ=ξ}', '[Expression[PrefixD][{][Local][∈][TextFunction][(][Global][)][|][Local][=][Local][}]]'], + [ + 'I{(σ, γ) | σ:∈X1; γ:=F1[σ]; P1[σ, γ]}', + '[Expression[PrefixI][{][(][Local][Local][)][|][Local][:∈][Global][;][Local][:=][Function][[][Local][]][;][Predicate][[][Local][Local][]][}]]' + ], + [ + 'R{ξ:=D1 | F1[ξ]≠∅ | ξ∪F1[ξ]}', + '[Expression[PrefixR][{][Local][:=][Global][|][Function][[][Local][]][≠][Literal][|][Local][∪][Function][[][Local][]][}]]' + ], + ['∀ξ∈∅ 1=1', '[Expression[∀][Local][∈][Literal][Literal][=][Literal]]'], + [ + '∀ξ1∈β (ξ1≠∅ & ∀ξ2∈β ξ1∩ξ2=∅)', + '[Expression[∀][Local[Index]][∈][Local][(][Local[Index]][≠][Literal][&][∀][Local[Index]][∈][Local][Local[Index]][∩][Local[Index]][=][Literal][)]]' + ] +]; + +describe('Testing RSParser', () => { + it.each(testData)('Parse %p', (input: string, expectedTree: string) => { + // NOTE: use strict parser to determine exact error position + // const tree = parser.configure({strict: true}).parse(input); + const tree = parser.parse(input); + expect(printTree(tree)).toBe(expectedTree); + }); +}); diff --git a/rsconcept/frontend/src/components/RSInput/rslang/parser.ts b/rsconcept/frontend/src/components/RSInput/rslang/parser.ts new file mode 100644 index 00000000..1f3daaf5 --- /dev/null +++ b/rsconcept/frontend/src/components/RSInput/rslang/parser.ts @@ -0,0 +1,18 @@ +// This file was generated by lezer-generator. You probably shouldn't edit it. +import {LRParser} from "@lezer/lr" +import {highlighting} from "./highlight" +export const parser = LRParser.deserialize({ + version: 14, + states: "5lO!sQPOOO!zQPO'#C^OOQO'#Ca'#CaO%uQPO'#DqOOQO'#Dq'#DqO(SQPO'#DeOVQPO'#DrO(ZQPO'#DtO)kQPO'#DuO)sQQO'#DvO)xQPO'#DwO)}QPO'#DxO*SQPO'#DzOOQO'#Ds'#DsO*XQPO'#DsO+uQQO'#C|OOQO'#C|'#C|OOQO'#Dn'#DnO+zQPO'#DnO,PQPO'#DnO,UQPO'#DgOVQPO'#ESO,^QPO'#ESO,cQPO'#ESO,kQPO'#DeOOQO'#Df'#DfO,fQPO'#EVQOQPOOOOQO'#C_'#C_OOQO,58x,58xO(ZQPO,5:^O(ZQPO,5:^O(ZQPO,5:^O(ZQPO,5:^O(ZQPO,5:^O(ZQPO,5:^O(ZQPO,5:^O(ZQPO,5:^O(ZQPO,5:RO,|QPO'#DmO-TQPO,5:^O.hQPO,5:VO.pQPO'#DlO.uQPO,5:QO/WQPO,5:cO/yQPO'#DmO(ZQPO'#DrO0WQPO,5:`OOQO'#Dm'#DmO(ZQPO,5:aOOQO,5:a,5:aO0`QPO,5:bO,cQPO,5:cO(ZQPO,5:dO,cQPO,5:fOOQO,59h,59hO(ZQPO,5:YO(ZQPO,5:YO&uQPO'#DgOOQO,5:n,5:nO(ZQPO,5:nO(ZQPO'#DkOOQO'#Dh'#DhOOQO'#ET'#ETO0eQPO,5:nOVQPO,5:pOVQPO,5:pOVQPO,5:pOVQPO,5:pO0mQPO'#EXOOQO'#EW'#EWO0rQPO,5:qOOQO1G/x1G/xO2wQPO1G/xO3OQPO1G/xO5SQPO1G/xO7WQPO1G/xO7_QPO1G/xO7fQPO1G/xO8OQPO1G/xO9qQPO1G/mOOQO1G/q1G/qO(ZQPO,5:WOOQO1G/l1G/lO(ZQPO1G/}OOQO1G/z1G/zO:bQPO1G/{O(ZQPO1G/|O:iQPO1G/}O:nQPO1G0OO:uQPO1G0QO:zQPO1G/tO;SQPO1G/tO;ZQPO1G0YO,cQPO,5:oO;cQPO1G0YOOQO1G0[1G0[O=_QPO1G0[O=fQPO1G0[O=mQPO1G0[O(ZQPO,5:sO,fQPO,5:rOVQPO1G0]O>[QPO1G/rO>lQPO7+%iOOQO7+%g7+%gO>sQPO7+%hO(ZQPO7+%iOVQPO7+%jO(ZQPO7+%lOOQO7+%`7+%`OOQO7+%t7+%tOOQO1G0Z1G0ZOOQO'#C^'#C^O>{QPO7+%tO?iQPO1G0_OOQO1G0^1G0^OOQO7+%w7+%wOVQPO<oO(ZQPOAN>nOVQPOAN>oOVQPO,5:eOOQOAN>pAN>pOVQPOAN>rOOQOG24ZG24ZOAtQPOG24YOA{QPOG24ZOB^QPO1G0POBrQPOG24^OByQPOG24^OOQOLD)tLD)tOOQOLD)uLD)uOOQOLD)xLD)xO(ZQPOLD)xOC[QPO!$'MdOOQO!)9CO!)9COO;cQPO,5:^O;cQPO,5:^O;cQPO,5:^O;cQPO,5:^O;cQPO,5:^O;cQPO,5:^O;cQPO,5:^O;cQPO,5:^OCcQPO1G/xOE[QPO1G/xOIWQPO1G/xOKbQPO1G/xOKiQPO1G/xOKpQPO1G/xOKwQPO1G/x", + stateData: "NR~O!WOS~OSUOUSOVSOaVOcWOdXOjYOkZOm[OobO|eO}fO!OgO!PgO!]PO!^QO!cQO!dQO!o_O!p_O!q`O!r`O!s`O!t`O~OfjO~PVO!^lOWQXXQXYQXZQX[QX]QX^QX_QXhQXnQXqQXrQXsQXtQXuQXvQXwQXxQXyQXzQX{QX!UQX!uQXbQX!QQX!RQX!SQX!TQX`QXiQXgQXlQX~OW!eXX!eXY!eXZ!eX[!eX]!eX^!eX_!eX!u!eXb!eX~Oh!eXn![Xq![Xr!eXs!eXt!eXu!eXv!eXw!eXx!eXy!eXz!eX{!eX!U!eX~P%TOWnOXoOYpOZqO[rO]sO^tO_uOhvOrvOsvOtvOuvOvvOwvOxvOyvOzvO{vO~O!U!XX~P&uOS!OOUSOVSOaVOcWOdXOjYOkZOm[OobO!]PO!^QO!cQO!dQO!o_O!p_O!q`O!r`O!s`O!t`O~OS!ROcWO~Oe!TO~Oa!UO~Oa!VO~Oa!WO~OW!gXX!gXY!gXZ!gX[!gX]!gX^!gX_!gXh!gXn![Xq![Xr!gXs!gXt!gXu!gXv!gXw!gXx!gXy!gXz!gX{!gX!U!gX!u!gXb!gX~Oe!XO~Of!YO~OS!ZO~OnvOqvO~Of!^O~OS!_O!]PO~O!Q!cO!R!dO!S!eO!T!fO!U!XX~O!u!aX~P&uO`!jOW!bXX!bXY!bXZ!bX[!bX]!bX^!bX_!bXh!bXr!bXs!bXt!bXu!bXv!bXw!bXx!bXy!bXz!bX{!bX!u!bX~O`!sO!u!aX~O!u!tO~O`!uO!Q!cO!R!dO!S!eO!T!fO~Oh!vO~P%TOWnOXoOYpOZqO[rO]sO^tO_uO~Ob!aX!u!aXg!aX~P/_Ob!wO!u!tO~Of!yO~Oh#RO!u#QO~Oh#WO~Og#YO!u#XO~OWnOY!fiZ!fi[!fi]!fi^!fi_!fih!fir!fis!fit!fiu!fiv!fiw!fix!fiy!fiz!fi{!fi!U!fi`!fi!u!fib!fi!Q!fi!R!fi!S!fi!T!fii!fig!fil!fi~OX!fi~P0zOXoO~P0zOWnOXoOYpO_uO[!fi]!fi^!fih!fir!fis!fit!fiu!fiv!fiw!fix!fiy!fiz!fi{!fi!U!fi`!fi!u!fib!fi!Q!fi!R!fi!S!fi!T!fii!fig!fil!fi~OZ!fi~P3VOWnOXoOYpOZqO^tO_uO]!fih!fir!fis!fit!fiu!fiv!fiw!fix!fiy!fiz!fi{!fi!U!fi`!fi!u!fib!fi!Q!fi!R!fi!S!fi!T!fii!fig!fil!fi~O[!fi~P5ZO[rO~P5ZOZqO~P3VOZ!fi[!fi]!fi^!fi_!fi~OWnOXoOYpOh!fir!fis!fit!fiu!fiv!fiw!fix!fiy!fiz!fi{!fi!U!fi`!fi!u!fib!fi!Q!fi!R!fi!S!fi!T!fii!fig!fil!fi~P7mO!Q!Zi!R!Zi!S!Zi!T!Zi!U!Zi`!Zib!Zil!Zii!Zi~P/_O`#]O~P/_Oh#_O~Oi#`O~P/_On#aO~Og#bO!u!tO~O`#bO~P/_Og#cO!u!tO~OS!OOUSOVSOaVOcWOdXOjYOkZOm[OobO!]#eO!^QO!cQO!dQO!o_O!p_O!q`O!r`O!s`O!t`O~O!Q!cO!S!xi!T!xi!U!xi`!xib!xil!xii!xi~O!R!xi~P ≥ < ≤ ≠ = ¬ Predicate ∀ ∃ ⇔ ⇒ ∨ &", + maxTerm: 89, + propSources: [highlighting], + skippedNodes: [0], + repeatNodeCount: 0, + tokenData: "4i~R!jX^%spq%svw&hxy&myz&rz{&w{|&||}'R}!O'W!Q!R']!R!['e![!](T!]!^(h!^!_(m!_!`(r!`!a(w!c!d(|!e!f(|!f!g)[!h!i)d!k!l)z!r!s*P!t!u*g!u!v(|!v!w(|!z!{(|!|!}*w!}#O*|#O#P+R#P#Q+W#R#S+]#T#U+]#U#V+h#V#W,z#W#X.Z#X#d+]#d#e0a#e#f+]#f#g1P#g#o+]#o#p2Q#p#q2V#q#r2[#y#z%s$f$g%s$r$s2a%o%p2f5i6S+]#BY#BZ%s$IS$I_%s$I|$JO%s$JT$JU%s$KV$KW%s% l% m2k%%Y%%Z2p%%[%%]2u%&Y%&Z2z%&]%&^3P%&_%&`3U%&`%&a3Z%&b%&c3`%&c%&d3e%'S%'T3j%'T%'U3o%'U%'V3t%(^%(_3y%(b%(c4O%(c%(d4T%)Q%)R4Y%)S%)T4_%)U%)V4d&FU&FV%s~%xY!W~X^%spq%s#y#z%s$f$g%s#BY#BZ%s$IS$I_%s$I|$JO%s$JT$JU%s$KV$KW%s&FU&FV%s~&mO!T~~&rOS~~&wO`~~&|OY~~'ROW~~'WO!u~~']OX~P'bP!^P!Q![']R'lQeQ!^P|}'r!Q!['eQ'uP!R!['xQ'}QeQ|}'r!Q!['x~(WQ!_!`(^%&b%&c(c~(cOn~~(hOq~~(mOl~~(rOx~~(wO{~~(|Ov~~)PP!Q![)S~)XPU~!Q![)S~)aPj~!Q![)S~)gQ!Q![)m#]#^)u~)rPo~!Q![)m~)zOd~~*POk~~*SQ!Q![*Y#f#g*b~*_P}~!Q![*Y~*gO!o~~*lPm~!Q![*o~*tPV~!Q![*o~*|O!d~~+ROf~~+WO[~~+]Og~~+bQ!]~#T#o+]5i6S+]~+mS!]~#T#c+]#c#d+y#d#o+]5i6S+]~,OS!]~#T#c+]#c#d,[#d#o+]5i6S+]~,aS!]~#T#`+]#`#a,m#a#o+]5i6S+]~,tQ!r~!]~#T#o+]5i6S+]~-PR!]~#T#U-Y#U#o+]5i6S+]~-_S!]~#T#f+]#f#g-k#g#o+]5i6S+]~-pS!]~#T#W+]#W#X-|#X#o+]5i6S+]~.TQ!q~!]~#T#o+]5i6S+]~.`S!]~#T#X+]#X#Y.l#Y#o+]5i6S+]~.qS!]~#T#U+]#U#V.}#V#o+]5i6S+]~/SS!]~#T#c+]#c#d/`#d#o+]5i6S+]~/eS!]~#T#c+]#c#d/q#d#o+]5i6S+]~/vS!]~#T#`+]#`#a0S#a#o+]5i6S+]~0ZQ!s~!]~#T#o+]5i6S+]~0fS!]~#T#f+]#f#g0r#g#o+]5i6S+]~0yQ!p~!]~#T#o+]5i6S+]~1US!]~#T#X+]#X#Y1b#Y#o+]5i6S+]~1gS!]~#T#W+]#W#X1s#X#o+]5i6S+]~1zQ!t~!]~#T#o+]5i6S+]~2VOa~~2[Oi~~2aOb~~2fO|~~2kO_~~2pOc~~2uO!R~~2zO!Q~~3PO!O~~3UO!P~~3ZO!c~~3`O]~~3eOh~~3jOr~~3oO!S~~3tO^~~3yOZ~~4OOz~~4TOy~~4YOw~~4_Ou~~4dOt~~4iOs~", + tokenizers: [0, 1], + topRules: {"Expression":[0,1]}, + tokenPrec: 2056 +}) diff --git a/rsconcept/frontend/src/components/RSInput/rslang/rslangFull.grammar b/rsconcept/frontend/src/components/RSInput/rslang/rslangFull.grammar new file mode 100644 index 00000000..1a1972eb --- /dev/null +++ b/rsconcept/frontend/src/components/RSInput/rslang/rslangFull.grammar @@ -0,0 +1,253 @@ +/////////////////////////////////////////////////////////// +// ------------- Generator Definitions -------------------- +/////////////////////////////////////////////////////////// +@detectDelim +@external propSource highlighting from "./highlight" + +/////////////////////////////////////////////////////////// +// ------------- Precedence Definitions -------------------- +/////////////////////////////////////////////////////////// +@precedence { + plus @left minus @left, + times @left, + not @right, + log_equiv @left, + log_impl @left, + log_or @left, + log_and @left, + set_decart @left set_union @left set_intersect @left set_minus @left set_symminus @left, + set_bool @right, + quant @right, + p1, p2 +} + +/////////////////////////////////////////////////////////// +// ------------- Terminal Tokens -------------------------- +/////////////////////////////////////////////////////////// +@tokens { + space { @whitespace+ } + ComplexIndex { $[1-9]$[0-9]*(","$[1-9]$[0-9]*)* } + integer { $[0-9]+ } + emptySet { "∅" } + integerSet { "Z" } + + bigPr { "Pr" } + smallPr { "pr" } + Filter { "Fi" } + card { "card" } + bool { "bool" } + debool { "debool" } + red { "red" } + + Global { $[XCSDAT]$[0-9]+ } + Function { "F"$[0-9]+ } + Predicate { "P"$[0-9]+ } + Radical { "R"$[0-9]+ } + local { $[_a-zα-ω]($[a-zα-ω])* } + PrefixR { "R" } + PrefixI { "I" } + PrefixD { "D" } + + "¬" + "∀" "∃" "⇔" "⇒" "∨" "&" + "ℬ" + "+" "-" "*" "∪" "\\" "∆" "∩" "×" + "∈" "∉" "⊆" "⊄" "⊂" ">" "≥" "≤" "<" "≠" "=" + ":∈" ":=" + + ";" "|" + "[" "]" + "{" "}" + "(" ")" + + @precedence { + Filter + bigPr + Predicate + Function + Global + Radical + PrefixR + PrefixI + PrefixD + } + @precedence { + card + bool + debool + red + smallPr + local + } +} + +@skip { space } + + +/////////////////////////////////////////////////////////////////////////////// +// ------------------------- Grammar Rules ------------------------------------ +/////////////////////////////////////////////////////////////////////////////// + +// ------------------------- Language Expression ------------------------------ +@top Expression { + logic_or_setexpr | + function_decl +} +logic_or_setexpr { + logic | + setexpr +} +function_decl { + "[" arguments "]" logic_or_setexpr +} + + +// ------------------------- Variables and arguments ------------------------- +arguments { + declaration | + arguments "," declaration +} +declaration { + Local "∈" setexpr +} +Local { + !p1 local | + !p2 local Index +} +Index { + integer +} +variable { + Local | + tuple +} +variable_pack { + variable | + variable_pack "," variable +} + + +// ------------------------- Logic Expressions -------------------------------- +logic { + logic_predicates | + logic_unary | + logic_binary | + "(" logic ")" +} + +logic_predicates { + variable ":∈" setexpr | + variable ":=" setexpr | + setexpr "∈" setexpr | + setexpr "∉" setexpr | + setexpr "⊆" setexpr | + setexpr "⊄" setexpr | + setexpr "⊂" setexpr | + setexpr ">" setexpr | + setexpr "≥" setexpr | + setexpr "<" setexpr | + setexpr "≤" setexpr | + setexpr "≠" setexpr | + setexpr "=" setexpr +} + +logic_unary { + !not "¬" logic | + Predicate "[" setexpr_enum "]" | + "∀" variable_pack "∈" setexpr !quant logic | + "∃" variable_pack "∈" setexpr !quant logic +} + +logic_binary { + logic !log_equiv "⇔" logic | + logic !log_impl "⇒" logic | + logic !log_or "∨" logic | + logic !log_and "&" logic +} + + +// ------------------------- Set Expressions ---------------------------------- +setexpr { + Literal | + identifier | + setexpr_binary | + setexpr_generators | + Function "[" setexpr_enum "]" | + TextFunction "(" setexpr ")" +} +TextFunction { + bigPr ComplexIndex | + smallPr ComplexIndex | + card | + bool | + debool | + red +} +setexpr_enum { + setexpr | + setexpr_enum_min2 +} +setexpr_enum_min2 { + setexpr_enum "," setexpr +} + +Literal { + integer | + emptySet | + integerSet +} +identifier { + Local | + Global | + Radical +} + +setexpr_binary { + setexpr !plus "+" setexpr | + setexpr !minus "-" setexpr | + setexpr !times "*" setexpr | + setexpr !set_union "∪" setexpr | + setexpr !set_minus "\\" setexpr | + setexpr !set_symminus "∆" setexpr | + setexpr !set_intersect "∩" setexpr | + setexpr !set_decart "×" setexpr | + "(" setexpr_binary ")" +} + +setexpr_generators { + enumeration | + tuple | + boolean | + filter_expression | + declarative | + imperative | + recursion +} +enumeration { + "{" setexpr_enum "}" +} +tuple { + "(" setexpr_enum_min2 ")" +} +boolean { + !set_bool "ℬ" "(" setexpr ")" | + !set_bool "ℬ" boolean +} +filter_expression { + Filter ComplexIndex "[" setexpr_enum "]" "(" setexpr ")" +} + +declarative { + "{" Local "∈" setexpr "|" logic "}" | + PrefixD "{" variable "∈" setexpr "|" logic "}" +} +recursion { + PrefixR "{" variable ":=" setexpr ("|" logic)? "|" setexpr "}" +} +imperative { + PrefixI "{" setexpr "|" imp_blocks "}" +} +imp_blocks { + logic | + imp_blocks ";" logic +} diff --git a/rsconcept/frontend/src/components/RSInput/rslang/rslangLex.grammar b/rsconcept/frontend/src/components/RSInput/rslang/rslangLex.grammar new file mode 100644 index 00000000..197ac74a --- /dev/null +++ b/rsconcept/frontend/src/components/RSInput/rslang/rslangLex.grammar @@ -0,0 +1,74 @@ +@detectDelim +@external propSource highlighting from "./highlight" + +@top Expression { token* } + +@skip { space } + +@tokens { + space { @whitespace+ } + Index { $[0-9]+ } + ComplexIndex { $[1-9](","$[1-9])* } + Integer { space$[0-9]+space? } + + bigPr { "Pr" } + smallPr { "pr" } + filter { "Fi" } + card { "card" } + bool { "bool" } + debool { "debool" } + red { "red" } + + ConstructPrefix { "D" | "R" | "I" } + + Global { $[XCSDAPTF]$[0-9]+ } + Radical { "R"$[0-9]+ } + local { $[_a-zα-ω]$[a-zα-ω]* } + + "(" ")" + "[" "]" + "{" "}" + + @precedence { + filter + bigPr + Global + Radical + ConstructPrefix + } + @precedence { + card + bool + debool + red + smallPr + local + } + @precedence { + Integer + space + } +} + +TextFunction { + bigPr ComplexIndex | + smallPr ComplexIndex | + filter ComplexIndex | + card | + bool | + debool | + red +} + +Local { + local Index? +} + +token { + TextFunction | + ConstructPrefix | + Integer | + Global | + Radical | + Local +} \ No newline at end of file diff --git a/rsconcept/frontend/src/components/RSInput/textEditing.ts b/rsconcept/frontend/src/components/RSInput/textEditing.ts new file mode 100644 index 00000000..e972e674 --- /dev/null +++ b/rsconcept/frontend/src/components/RSInput/textEditing.ts @@ -0,0 +1,287 @@ +// Formatted text editing helpers + +import { ReactCodeMirrorRef } from '@uiw/react-codemirror'; + +import { TokenID } from '@/models/rslang'; +import { CodeMirrorWrapper } from '@/utils/codemirror'; + +export function getSymbolSubstitute(keyCode: string, shiftPressed: boolean): string | undefined { + // prettier-ignore + if (shiftPressed) { + switch (keyCode) { + case 'Backquote': return '∃'; + case 'Backslash': return '|'; + case 'BracketLeft': return '{'; + case 'BracketRight': return '}'; + case 'Comma': return '<'; + case 'Period': return '>'; + + case 'Digit8': return '×'; + case 'KeyB': return 'ℬ'; + case 'KeyZ': return 'Z'; + case 'KeyR': return 'R'; + case 'KeyF': return 'F'; + case 'KeyP': return 'P'; + case 'KeyX': return 'X'; + case 'KeyS': return 'S'; + case 'KeyD': return 'D'; + case 'KeyC': return 'C'; + } + } else { + switch (keyCode) { + case 'Backquote': return '∀'; + + case 'KeyQ': return 'μ'; + case 'KeyW': return 'ω'; + case 'KeyE': return 'ε'; + case 'KeyR': return 'ρ'; + case 'KeyT': return 'τ'; + case 'KeyY': return 'π'; + + case 'KeyA': return 'α'; + case 'KeyS': return 'σ'; + case 'KeyD': return 'δ'; + case 'KeyF': return 'φ'; + case 'KeyG': return 'γ'; + case 'KeyH': return 'λ'; + + case 'KeyZ': return 'ζ'; + case 'KeyX': return 'ξ'; + case 'KeyC': return 'ψ'; + case 'KeyV': return 'θ'; + case 'KeyB': return 'β'; + case 'KeyN': return 'η'; + + case 'BracketLeft': return '['; + case 'BracketRight': return ']'; + case 'Comma': return ','; + } + } + return undefined; +} + +/** + * Wrapper class for RSLang editor. + */ +export class RSTextWrapper extends CodeMirrorWrapper { + constructor(object: Required) { + super(object); + } + + insertToken(tokenID: TokenID): boolean { + const selection = this.getSelection(); + const hasSelection = selection.from !== selection.to; + switch (tokenID) { + case TokenID.NT_DECLARATIVE_EXPR: { + if (hasSelection) { + this.envelopeWith('D{ξ∈X1 | ', '}'); + } else { + this.envelopeWith('D{ξ∈X1 | P1[ξ]', '}'); + } + this.ref.view.dispatch({ + selection: { + anchor: selection.from + 2 + } + }); + return true; + } + case TokenID.NT_IMPERATIVE_EXPR: { + if (hasSelection) { + this.envelopeWith('I{(σ, γ) | σ:∈X1; γ:=F1[σ]; ', '}'); + } else { + this.envelopeWith('I{(σ, γ) | σ:∈X1; γ:=F1[σ]; P1[σ, γ]', '}'); + } + return true; + } + case TokenID.NT_RECURSIVE_FULL: { + if (hasSelection) { + this.envelopeWith('R{ξ:=D1 | F1[ξ]≠∅ | ', '}'); + } else { + this.envelopeWith('R{ξ:=D1 | F1[ξ]≠∅ | ξ∪F1[ξ]', '}'); + } + return true; + } + case TokenID.BIGPR: + this.envelopeWith('Pr1(', ')'); + return true; + case TokenID.SMALLPR: + this.envelopeWith('pr1(', ')'); + return true; + case TokenID.FILTER: + this.envelopeWith('Fi1[α](', ')'); + return true; + case TokenID.REDUCE: + this.envelopeWith('red(', ')'); + return true; + case TokenID.CARD: + this.envelopeWith('card(', ')'); + return true; + case TokenID.BOOL: + this.envelopeWith('bool(', ')'); + return true; + case TokenID.DEBOOL: + this.envelopeWith('debool(', ')'); + return true; + + case TokenID.PUNCTUATION_PL: { + this.envelopeWith('(', ')'); + this.ref.view.dispatch({ + selection: { + anchor: hasSelection ? selection.to : selection.from + 1 + } + }); + return true; + } + case TokenID.PUNCTUATION_SL: { + this.envelopeWith('[', ']'); + if (hasSelection) { + this.ref.view.dispatch({ + selection: { + anchor: hasSelection ? selection.to : selection.from + 1 + } + }); + } + return true; + } + case TokenID.BOOLEAN: { + const selStart = selection.from; + if (hasSelection && this.ref.view.state.sliceDoc(selStart, selStart + 1) === 'ℬ') { + this.envelopeWith('ℬ', ''); + } else { + this.envelopeWith('ℬ(', ')'); + } + return true; + } + + case TokenID.DECART: + this.replaceWith('×'); + return true; + case TokenID.QUANTOR_UNIVERSAL: + this.replaceWith('∀'); + return true; + case TokenID.QUANTOR_EXISTS: + this.replaceWith('∃'); + return true; + case TokenID.SET_IN: + this.replaceWith('∈'); + return true; + case TokenID.SET_NOT_IN: + this.replaceWith('∉'); + return true; + case TokenID.LOGIC_OR: + this.replaceWith('∨'); + return true; + case TokenID.LOGIC_AND: + this.replaceWith('&'); + return true; + case TokenID.SUBSET_OR_EQ: + this.replaceWith('⊆'); + return true; + case TokenID.LOGIC_IMPLICATION: + this.replaceWith('⇒'); + return true; + case TokenID.SET_INTERSECTION: + this.replaceWith('∩'); + return true; + case TokenID.SET_UNION: + this.replaceWith('∪'); + return true; + case TokenID.SET_MINUS: + this.replaceWith('\\'); + return true; + case TokenID.SET_SYMMETRIC_MINUS: + this.replaceWith('∆'); + return true; + case TokenID.LIT_EMPTYSET: + this.replaceWith('∅'); + return true; + case TokenID.LIT_WHOLE_NUMBERS: + this.replaceWith('Z'); + return true; + case TokenID.SUBSET: + this.replaceWith('⊂'); + return true; + case TokenID.NOT_SUBSET: + this.replaceWith('⊄'); + return true; + case TokenID.EQUAL: + this.replaceWith('='); + return true; + case TokenID.NOTEQUAL: + this.replaceWith('≠'); + return true; + case TokenID.LOGIC_NOT: + this.replaceWith('¬'); + return true; + case TokenID.LOGIC_EQUIVALENT: + this.replaceWith('⇔'); + return true; + case TokenID.GREATER_OR_EQ: + this.replaceWith('≥'); + return true; + case TokenID.LESSER_OR_EQ: + this.replaceWith('≤'); + return true; + case TokenID.ASSIGN: + this.replaceWith(':='); + return true; + case TokenID.ITERATE: + this.replaceWith(':∈'); + return true; + case TokenID.MULTIPLY: + this.replaceWith('*'); + return true; + } + return false; + } + + processAltKey(keyCode: string, shiftPressed: boolean): boolean { + // prettier-ignore + if (shiftPressed) { + switch (keyCode) { + case 'KeyE': return this.insertToken(TokenID.DECART); + + case 'Backquote': return this.insertToken(TokenID.NOTEQUAL); + case 'Digit1': return this.insertToken(TokenID.SET_NOT_IN); // ! + case 'Digit2': return this.insertToken(TokenID.NOT_SUBSET); // @ + case 'Digit3': return this.insertToken(TokenID.LOGIC_OR); // # + case 'Digit4': return this.insertToken(TokenID.LOGIC_EQUIVALENT); // $ + case 'Digit5': return this.insertToken(TokenID.SET_SYMMETRIC_MINUS); // % + case 'Digit6': return this.insertToken(TokenID.ASSIGN); // ^ + case 'Digit7': return this.insertToken(TokenID.GREATER_OR_EQ); // & + case 'Digit8': return this.insertToken(TokenID.LESSER_OR_EQ); // * + case 'Digit9': return this.insertToken(TokenID.PUNCTUATION_PL); // ( + } + } else { + switch (keyCode) { + case 'KeyQ': return this.insertToken(TokenID.BIGPR); + case 'KeyW': return this.insertToken(TokenID.SMALLPR); + case 'KeyE': return this.insertToken(TokenID.BOOLEAN); + case 'KeyR': return this.insertToken(TokenID.REDUCE); + case 'KeyT': return this.insertToken(TokenID.NT_RECURSIVE_FULL); + case 'KeyA': return this.insertToken(TokenID.SET_INTERSECTION); + case 'KeyS': return this.insertToken(TokenID.SET_UNION); + case 'KeyD': return this.insertToken(TokenID.NT_DECLARATIVE_EXPR); + case 'KeyF': return this.insertToken(TokenID.FILTER); + case 'KeyG': return this.insertToken(TokenID.NT_IMPERATIVE_EXPR); + case 'KeyZ': return this.insertToken(TokenID.LIT_WHOLE_NUMBERS); + case 'KeyX': return this.insertToken(TokenID.LIT_EMPTYSET); + case 'KeyC': return this.insertToken(TokenID.CARD); + case 'KeyV': return this.insertToken(TokenID.DEBOOL); + case 'KeyB': return this.insertToken(TokenID.BOOL); + + case 'Backquote': return this.insertToken(TokenID.LOGIC_NOT); + case 'Digit1': return this.insertToken(TokenID.SET_IN); + case 'Digit2': return this.insertToken(TokenID.SUBSET_OR_EQ); + case 'Digit3': return this.insertToken(TokenID.LOGIC_AND); + case 'Digit4': return this.insertToken(TokenID.LOGIC_IMPLICATION); + case 'Digit5': return this.insertToken(TokenID.SET_MINUS); + case 'Digit6': return this.insertToken(TokenID.ITERATE); + case 'Digit7': return this.insertToken(TokenID.SUBSET); + case 'Digit8': return this.insertToken(TokenID.MULTIPLY); + case 'BracketLeft': return this.insertToken(TokenID.PUNCTUATION_SL); + } + } + return false; + } +} diff --git a/rsconcept/frontend/src/components/RSInput/tooltip.ts b/rsconcept/frontend/src/components/RSInput/tooltip.ts new file mode 100644 index 00000000..341a6942 --- /dev/null +++ b/rsconcept/frontend/src/components/RSInput/tooltip.ts @@ -0,0 +1,46 @@ +import { syntaxTree } from '@codemirror/language'; +import { Extension } from '@codemirror/state'; +import { hoverTooltip } from '@codemirror/view'; +import { EditorState } from '@uiw/react-codemirror'; + +import { IRSForm } from '@/models/rsform'; +import { findEnvelopingNodes } from '@/utils/codemirror'; +import { domTooltipConstituenta } from '@/utils/codemirror'; + +import { GlobalTokens } from './rslang'; + +function findAliasAt(pos: number, state: EditorState) { + const { from: lineStart, to: lineEnd, text } = state.doc.lineAt(pos); + const nodes = findEnvelopingNodes(pos, pos, syntaxTree(state), GlobalTokens); + let alias = ''; + let start = 0; + let end = 0; + nodes.forEach(node => { + if (node.to <= lineEnd && node.from >= lineStart) { + alias = text.slice(node.from - lineStart, node.to - lineStart); + start = node.from; + end = node.to; + } + }); + return { alias, start, end }; +} + +const globalsHoverTooltip = (schema: IRSForm) => { + return hoverTooltip((view, pos) => { + const { alias, start, end } = findAliasAt(pos, view.state); + if (!alias) { + return null; + } + const cst = schema.cstByAlias.get(alias); + return { + pos: start, + end: end, + above: false, + create: () => domTooltipConstituenta(cst) + }; + }); +}; + +export function rsHoverTooltip(schema: IRSForm): Extension { + return [globalsHoverTooltip(schema)]; +} diff --git a/rsconcept/frontend/src/components/RefsInput/RefsInput.tsx b/rsconcept/frontend/src/components/RefsInput/RefsInput.tsx new file mode 100644 index 00000000..6a684a8e --- /dev/null +++ b/rsconcept/frontend/src/components/RefsInput/RefsInput.tsx @@ -0,0 +1,228 @@ +'use client'; + +import { Extension } from '@codemirror/state'; +import { tags } from '@lezer/highlight'; +import { createTheme } from '@uiw/codemirror-themes'; +import CodeMirror, { BasicSetupOptions, ReactCodeMirrorProps, ReactCodeMirrorRef } from '@uiw/react-codemirror'; +import clsx from 'clsx'; +import { EditorView } from 'codemirror'; +import { AnimatePresence } from 'framer-motion'; +import { forwardRef, useCallback, useMemo, useRef, useState } from 'react'; + +import Label from '@/components/ui/Label'; +import { useConceptOptions } from '@/context/OptionsContext'; +import DlgEditReference from '@/dialogs/DlgEditReference'; +import { ReferenceType } from '@/models/language'; +import { IRSForm } from '@/models/rsform'; +import { CodeMirrorWrapper } from '@/utils/codemirror'; +import { PARAMETER } from '@/utils/constants'; + +import { NaturalLanguage, ReferenceTokens } from './parse'; +import { RefEntity } from './parse/parser.terms'; +import { refsHoverTooltip } from './tooltip'; + +const editorSetup: BasicSetupOptions = { + highlightSpecialChars: false, + history: true, + drawSelection: true, + syntaxHighlighting: false, + defaultKeymap: true, + historyKeymap: true, + + lineNumbers: false, + highlightActiveLineGutter: false, + foldGutter: false, + dropCursor: true, + allowMultipleSelections: false, + indentOnInput: false, + bracketMatching: false, + closeBrackets: false, + autocompletion: false, + rectangularSelection: false, + crosshairCursor: false, + highlightActiveLine: false, + highlightSelectionMatches: false, + closeBracketsKeymap: false, + searchKeymap: false, + foldKeymap: false, + completionKeymap: false, + lintKeymap: false +}; + +interface RefsInputInputProps + extends Pick< + ReactCodeMirrorProps, + | 'id' // prettier: split-lines + | 'height' + | 'minHeight' + | 'maxHeight' + | 'value' + | 'className' + | 'onFocus' + | 'onBlur' + | 'placeholder' + > { + label?: string; + onChange?: (newValue: string) => void; + schema?: IRSForm; + disabled?: boolean; + + initialValue?: string; + value?: string; + resolved?: string; +} + +const RefsInput = forwardRef( + ({ id, label, disabled, schema, initialValue, value, resolved, onFocus, onBlur, onChange, ...restProps }, ref) => { + const { darkMode, colors } = useConceptOptions(); + + const [isFocused, setIsFocused] = useState(false); + + const [showEditor, setShowEditor] = useState(false); + const [currentType, setCurrentType] = useState(ReferenceType.ENTITY); + const [refText, setRefText] = useState(''); + const [hintText, setHintText] = useState(''); + const [basePosition, setBasePosition] = useState(0); + const [mainRefs, setMainRefs] = useState([]); + + const internalRef = useRef(null); + const thisRef = useMemo(() => (!ref || typeof ref === 'function' ? internalRef : ref), [internalRef, ref]); + + const cursor = useMemo(() => (!disabled ? 'cursor-text' : 'cursor-default'), [disabled]); + const customTheme: Extension = useMemo( + () => + createTheme({ + theme: darkMode ? 'dark' : 'light', + settings: { + fontFamily: 'inherit', + background: !disabled ? colors.bgInput : colors.bgDefault, + foreground: colors.fgDefault, + selection: colors.bgHover, + caret: colors.fgDefault + }, + styles: [ + { tag: tags.name, color: colors.fgPurple, cursor: 'default' }, // EntityReference + { tag: tags.literal, color: colors.fgTeal, cursor: 'default' }, // SyntacticReference + { tag: tags.comment, color: colors.fgRed } // Error + ] + }), + [disabled, colors, darkMode] + ); + + const editorExtensions = useMemo( + () => [ + EditorView.lineWrapping, + EditorView.contentAttributes.of({ spellcheck: 'true' }), + NaturalLanguage, + ...(schema ? [refsHoverTooltip(schema, colors)] : []) + ], + [schema, colors] + ); + + function handleChange(newValue: string) { + if (onChange) onChange(newValue); + } + + function handleFocusIn(event: React.FocusEvent) { + setIsFocused(true); + if (onFocus) onFocus(event); + } + + function handleFocusOut(event: React.FocusEvent) { + setIsFocused(false); + if (onBlur) onBlur(event); + } + + const handleInput = useCallback( + (event: React.KeyboardEvent) => { + if (!thisRef.current?.view) { + event.preventDefault(); + event.stopPropagation(); + return; + } + if ((event.ctrlKey || event.metaKey) && event.code === 'Space') { + event.preventDefault(); + event.stopPropagation(); + + const wrap = new CodeMirrorWrapper(thisRef.current as Required); + wrap.fixSelection(ReferenceTokens); + const nodes = wrap.getEnvelopingNodes(ReferenceTokens); + if (nodes.length !== 1) { + setCurrentType(ReferenceType.ENTITY); + setRefText(''); + setHintText(wrap.getSelectionText()); + } else { + setCurrentType(nodes[0].type.id === RefEntity ? ReferenceType.ENTITY : ReferenceType.SYNTACTIC); + setRefText(wrap.getSelectionText()); + } + + const selection = wrap.getSelection(); + const mainNodes = wrap + .getAllNodes([RefEntity]) + .filter(node => node.from >= selection.to || node.to <= selection.from); + setMainRefs(mainNodes.map(node => wrap.getText(node.from, node.to))); + setBasePosition(mainNodes.filter(node => node.to <= selection.from).length); + + setShowEditor(true); + } + }, + [thisRef] + ); + + const handleInputReference = useCallback( + (referenceText: string) => { + if (!thisRef.current?.view) { + return; + } + thisRef.current.view.focus(); + const wrap = new CodeMirrorWrapper(thisRef.current as Required); + wrap.replaceWith(referenceText); + }, + [thisRef] + ); + + const hideEditReference = useCallback(() => { + setShowEditor(false); + setTimeout(() => thisRef.current?.view?.focus(), PARAMETER.refreshTimeout); + }, [thisRef]); + + return ( +
+ + {showEditor && schema ? ( + + ) : null} + +
+ ); + } +); + +export default RefsInput; diff --git a/rsconcept/frontend/src/components/RefsInput/index.tsx b/rsconcept/frontend/src/components/RefsInput/index.tsx new file mode 100644 index 00000000..a34b3f76 --- /dev/null +++ b/rsconcept/frontend/src/components/RefsInput/index.tsx @@ -0,0 +1 @@ +export { default } from './RefsInput'; diff --git a/rsconcept/frontend/src/components/RefsInput/parse/highlight.ts b/rsconcept/frontend/src/components/RefsInput/parse/highlight.ts new file mode 100644 index 00000000..72364e9b --- /dev/null +++ b/rsconcept/frontend/src/components/RefsInput/parse/highlight.ts @@ -0,0 +1,13 @@ +import { styleTags, tags } from '@lezer/highlight'; + +export const highlighting = styleTags({ + RefEntity: tags.name, + Global: tags.name, + Grams: tags.name, + + RefSyntactic: tags.literal, + Offset: tags.literal, + Nominal: tags.literal, + + Error: tags.comment +}); diff --git a/rsconcept/frontend/src/components/RefsInput/parse/index.ts b/rsconcept/frontend/src/components/RefsInput/parse/index.ts new file mode 100644 index 00000000..4cc556f7 --- /dev/null +++ b/rsconcept/frontend/src/components/RefsInput/parse/index.ts @@ -0,0 +1,11 @@ +import { LRLanguage } from '@codemirror/language'; + +import { parser } from './parser'; +import { RefEntity, RefSyntactic } from './parser.terms'; + +export const ReferenceTokens: number[] = [RefSyntactic, RefEntity]; + +export const NaturalLanguage = LRLanguage.define({ + parser: parser, + languageData: {} +}); diff --git a/rsconcept/frontend/src/components/RefsInput/parse/parser.terms.ts b/rsconcept/frontend/src/components/RefsInput/parse/parser.terms.ts new file mode 100644 index 00000000..fa3d74ce --- /dev/null +++ b/rsconcept/frontend/src/components/RefsInput/parse/parser.terms.ts @@ -0,0 +1,11 @@ +// This file was generated by lezer-generator. You probably shouldn't edit it. +export const + Text = 1, + RefEntity = 2, + Global = 3, + Grams = 4, + RefSyntactic = 5, + Offset = 6, + Nominal = 7, + Error = 8, + Filler = 9 diff --git a/rsconcept/frontend/src/components/RefsInput/parse/parser.test.ts b/rsconcept/frontend/src/components/RefsInput/parse/parser.test.ts new file mode 100644 index 00000000..5ed7aa0d --- /dev/null +++ b/rsconcept/frontend/src/components/RefsInput/parse/parser.test.ts @@ -0,0 +1,26 @@ +import { printTree } from '@/utils/codemirror'; + +import { parser } from './parser'; + +const testData = [ + ['', '[Text]'], + ['тест русский', '[Text[Filler]]'], + ['test english', '[Text[Filler]]'], + ['test greek σσσ', '[Text[Filler]]'], + ['X1 раз два X2', '[Text[Filler]]'], + + ['@{1| черный }', '[Text[RefSyntactic[Offset][Nominal]]]'], + ['@{-1| черный }', '[Text[RefSyntactic[Offset][Nominal]]]'], + ['@{-100| черный слон }', '[Text[RefSyntactic[Offset][Nominal]]]'], + ['@{X1|VERB,past,sing}', '[Text[RefEntity[Global][Grams]]]'], + ['@{X12|VERB,past,sing}', '[Text[RefEntity[Global][Grams]]]'] +]; + +describe('Testing NaturalParser', () => { + it.each(testData)('Parse %p', (input: string, expectedTree: string) => { + // NOTE: use strict parser to determine exact error position + // const tree = parser.configure({strict: true}).parse(input); + const tree = parser.parse(input); + expect(printTree(tree)).toBe(expectedTree); + }); +}); diff --git a/rsconcept/frontend/src/components/RefsInput/parse/parser.ts b/rsconcept/frontend/src/components/RefsInput/parse/parser.ts new file mode 100644 index 00000000..e7d332b3 --- /dev/null +++ b/rsconcept/frontend/src/components/RefsInput/parse/parser.ts @@ -0,0 +1,18 @@ +// This file was generated by lezer-generator. You probably shouldn't edit it. +import {LRParser} from "@lezer/lr" +import {highlighting} from "./highlight" +export const parser = LRParser.deserialize({ + version: 14, + states: "$nQVQPOOObQQO'#C^OOQO'#Cl'#ClOjQPO'#CuOxQPO'#CdOOQO'#Ce'#CeOOQO'#Ck'#CkOOQO'#Cf'#CfQVQPOOO}QPO,58xO!SQPO,58{OOQO,59a,59aO!XQPO,59OOOQO-E6d-E6dO!^QSO1G.dO!cQPO1G.gOOQO1G.j1G.jO!hQQO'#CpOOQO'#C`'#C`O!pQPO7+$OOOQO'#Cg'#CgO!uQPO'#CcO!}QPO7+$RO!^QSO,59[OOQO< { + return hoverTooltip((view, pos) => { + const nodes = findEnvelopingNodes(pos, pos, syntaxTree(view.state), ReferenceTokens); + if (nodes.length !== 1) { + return null; + } + const start = nodes[0].from; + const end = nodes[0].to; + const text = view.state.doc.sliceString(start, end); + if (nodes[0].type.id === RefEntity) { + const ref = parseEntityReference(text); + const cst = schema.cstByAlias.get(ref.entity); + return { + pos: start, + end: end, + above: false, + create: () => domTooltipEntityReference(ref, cst, colors) + }; + } else if (nodes[0].type.id === RefSyntactic) { + const ref = parseSyntacticReference(text); + let masterText: string | undefined = undefined; + if (ref.offset > 0) { + const entities = findContainedNodes(end, view.state.doc.length, syntaxTree(view.state), [RefEntity]); + if (ref.offset <= entities.length) { + const master = entities[ref.offset - 1]; + masterText = view.state.doc.sliceString(master.from, master.to); + } + } else { + const entities = findContainedNodes(0, start, syntaxTree(view.state), [RefEntity]); + if (-ref.offset <= entities.length) { + const master = entities[-ref.offset - 1]; + masterText = view.state.doc.sliceString(master.from, master.to); + } + } + return { + pos: start, + end: end, + above: false, + create: () => domTooltipSyntacticReference(ref, masterText) + }; + } else { + return null; + } + }); +}; + +export function refsHoverTooltip(schema: IRSForm, colors: IColorTheme): Extension { + return [globalsHoverTooltip(schema, colors)]; +} diff --git a/rsconcept/frontend/src/components/info/BadgeConstituenta.tsx b/rsconcept/frontend/src/components/info/BadgeConstituenta.tsx new file mode 100644 index 00000000..06427b59 --- /dev/null +++ b/rsconcept/frontend/src/components/info/BadgeConstituenta.tsx @@ -0,0 +1,36 @@ +import clsx from 'clsx'; + +import ConstituentaTooltip from '@/components/info/ConstituentaTooltip'; +import { IConstituenta } from '@/models/rsform'; +import { isMockCst } from '@/models/rsformAPI'; +import { colorFgCstStatus, IColorTheme } from '@/styling/color'; + +interface BadgeConstituentaProps { + prefixID?: string; + value: IConstituenta; + theme: IColorTheme; +} + +function BadgeConstituenta({ value, prefixID, theme }: BadgeConstituentaProps) { + return ( +
+ {value.alias} + +
+ ); +} + +export default BadgeConstituenta; diff --git a/rsconcept/frontend/src/components/info/BadgeGrammeme.tsx b/rsconcept/frontend/src/components/info/BadgeGrammeme.tsx new file mode 100644 index 00000000..cf431f1a --- /dev/null +++ b/rsconcept/frontend/src/components/info/BadgeGrammeme.tsx @@ -0,0 +1,33 @@ +import clsx from 'clsx'; + +import { useConceptOptions } from '@/context/OptionsContext'; +import { GramData } from '@/models/language'; +import { colorFgGrammeme } from '@/styling/color'; +import { labelGrammeme } from '@/utils/labels'; + +interface BadgeGrammemeProps { + grammeme: GramData; +} + +function BadgeGrammeme({ grammeme }: BadgeGrammemeProps) { + const { colors } = useConceptOptions(); + return ( +
+ {labelGrammeme(grammeme)} +
+ ); +} + +export default BadgeGrammeme; diff --git a/rsconcept/frontend/src/components/info/BadgeHelp.tsx b/rsconcept/frontend/src/components/info/BadgeHelp.tsx new file mode 100644 index 00000000..77a68cde --- /dev/null +++ b/rsconcept/frontend/src/components/info/BadgeHelp.tsx @@ -0,0 +1,40 @@ +import clsx from 'clsx'; + +import TextURL from '@/components/ui/TextURL'; +import Tooltip, { PlacesType } from '@/components/ui/Tooltip'; +import { useConceptOptions } from '@/context/OptionsContext'; +import { HelpTopic } from '@/models/miscellaneous'; + +import TopicPage from '../../pages/ManualsPage/TopicPage'; +import { IconHelp } from '../Icons'; +import { CProps } from '../props'; + +interface BadgeHelpProps extends CProps.Styling { + topic: HelpTopic; + offset?: number; + padding?: string; + place?: PlacesType; +} + +function BadgeHelp({ topic, padding, ...restProps }: BadgeHelpProps) { + const { showHelp } = useConceptOptions(); + + if (!showHelp) { + return null; + } + return ( +
+ + +
event.stopPropagation()}> +
+ +
+
+ +
+
+ ); +} + +export default BadgeHelp; diff --git a/rsconcept/frontend/src/components/info/BadgeLocation.tsx b/rsconcept/frontend/src/components/info/BadgeLocation.tsx new file mode 100644 index 00000000..47811d93 --- /dev/null +++ b/rsconcept/frontend/src/components/info/BadgeLocation.tsx @@ -0,0 +1,17 @@ +import { globals } from '@/utils/constants'; + +import { LocationIcon } from '../DomainIcons'; + +interface BadgeLocationProps { + location: string; +} + +function BadgeLocation({ location }: BadgeLocationProps) { + return ( +
+ +
+ ); +} + +export default BadgeLocation; diff --git a/rsconcept/frontend/src/components/info/BadgeWordForm.tsx b/rsconcept/frontend/src/components/info/BadgeWordForm.tsx new file mode 100644 index 00000000..546eeb06 --- /dev/null +++ b/rsconcept/frontend/src/components/info/BadgeWordForm.tsx @@ -0,0 +1,20 @@ +import { IWordForm } from '@/models/language'; + +import BadgeGrammeme from './BadgeGrammeme'; + +interface BadgeWordFormProps { + keyPrefix?: string; + form: IWordForm; +} + +function BadgeWordForm({ keyPrefix, form }: BadgeWordFormProps) { + return ( +
+ {form.grams.map(gram => ( + + ))} +
+ ); +} + +export default BadgeWordForm; diff --git a/rsconcept/frontend/src/components/info/ConstituentaTooltip.tsx b/rsconcept/frontend/src/components/info/ConstituentaTooltip.tsx new file mode 100644 index 00000000..fc4f2601 --- /dev/null +++ b/rsconcept/frontend/src/components/info/ConstituentaTooltip.tsx @@ -0,0 +1,18 @@ +import InfoConstituenta from '@/components/info/InfoConstituenta'; +import Tooltip from '@/components/ui/Tooltip'; +import { IConstituenta } from '@/models/rsform'; + +interface ConstituentaTooltipProps { + data: IConstituenta; + anchor: string; +} + +function ConstituentaTooltip({ data, anchor }: ConstituentaTooltipProps) { + return ( + + event.stopPropagation()} /> + + ); +} + +export default ConstituentaTooltip; diff --git a/rsconcept/frontend/src/components/info/InfoConstituenta.tsx b/rsconcept/frontend/src/components/info/InfoConstituenta.tsx new file mode 100644 index 00000000..e59362ee --- /dev/null +++ b/rsconcept/frontend/src/components/info/InfoConstituenta.tsx @@ -0,0 +1,61 @@ +import clsx from 'clsx'; + +import { IConstituenta } from '@/models/rsform'; +import { isBasicConcept } from '@/models/rsformAPI'; +import { labelCstTypification } from '@/utils/labels'; + +import { CProps } from '../props'; + +interface InfoConstituentaProps extends CProps.Div { + data: IConstituenta; +} + +function InfoConstituenta({ data, className, ...restProps }: InfoConstituentaProps) { + return ( +
+

Конституента {data.alias}

+ {data.term_resolved ? ( +

+ Термин: + {data.term_resolved || data.term_raw} +

+ ) : null} +

+ Типизация: + {labelCstTypification(data)} +

+ {data.definition_formal ? ( +

+ Выражение: + {data.definition_formal} +

+ ) : null} + {data.definition_resolved ? ( +

+ Определение: + {data.definition_resolved} +

+ ) : null} + {data.parent_alias ? ( +

+ Основание: + {data.parent_alias} +

+ ) : null} + {data.children_alias.length > 0 ? ( +

+ Порождает: + {data.children_alias.join(', ')} +

+ ) : null} + {data.convention ? ( +

+ {isBasicConcept(data.cst_type) ? 'Конвенция' : 'Комментарий'}: + {data.convention} +

+ ) : null} +
+ ); +} + +export default InfoConstituenta; diff --git a/rsconcept/frontend/src/components/info/InfoCstClass.tsx b/rsconcept/frontend/src/components/info/InfoCstClass.tsx new file mode 100644 index 00000000..6580bc6e --- /dev/null +++ b/rsconcept/frontend/src/components/info/InfoCstClass.tsx @@ -0,0 +1,37 @@ +import clsx from 'clsx'; + +import { useConceptOptions } from '@/context/OptionsContext'; +import { CstClass } from '@/models/rsform'; +import { colorBgCstClass } from '@/styling/color'; +import { prefixes } from '@/utils/constants'; +import { describeCstClass, labelCstClass } from '@/utils/labels'; + +interface InfoCstClassProps { + header?: string; +} + +function InfoCstClass({ header }: InfoCstClassProps) { + const { colors } = useConceptOptions(); + + return ( +
+ {header ?

{header}

: null} + {Object.values(CstClass).map((cstClass, index) => { + return ( +

+ + {labelCstClass(cstClass)} + + - + {describeCstClass(cstClass)} +

+ ); + })} +
+ ); +} + +export default InfoCstClass; diff --git a/rsconcept/frontend/src/components/info/InfoCstStatus.tsx b/rsconcept/frontend/src/components/info/InfoCstStatus.tsx new file mode 100644 index 00000000..f3c8002c --- /dev/null +++ b/rsconcept/frontend/src/components/info/InfoCstStatus.tsx @@ -0,0 +1,43 @@ +import clsx from 'clsx'; + +import { useConceptOptions } from '@/context/OptionsContext'; +import { ExpressionStatus } from '@/models/rsform'; +import { colorBgCstStatus } from '@/styling/color'; +import { prefixes } from '@/utils/constants'; +import { describeExpressionStatus, labelExpressionStatus } from '@/utils/labels'; + +interface InfoCstStatusProps { + title?: string; +} + +function InfoCstStatus({ title }: InfoCstStatusProps) { + const { colors } = useConceptOptions(); + + return ( +
+ {title ?

{title}

: null} + {Object.values(ExpressionStatus) + .filter(status => status !== ExpressionStatus.UNDEFINED) + .map((status, index) => ( +

+ + {labelExpressionStatus(status)} + + - + {describeExpressionStatus(status)} +

+ ))} +
+ ); +} + +export default InfoCstStatus; diff --git a/rsconcept/frontend/src/components/info/InfoError.tsx b/rsconcept/frontend/src/components/info/InfoError.tsx new file mode 100644 index 00000000..4fc55d9c --- /dev/null +++ b/rsconcept/frontend/src/components/info/InfoError.tsx @@ -0,0 +1,82 @@ +import axios, { type AxiosError } from 'axios'; +import clsx from 'clsx'; + +import { isResponseHtml } from '@/utils/utils'; + +import PrettyJson from '../ui/PrettyJSON'; +import AnimateFade from '../wrap/AnimateFade'; + +export type ErrorData = string | Error | AxiosError | undefined; + +interface InfoErrorProps { + error: ErrorData; +} + +function DescribeError({ error }: { error: ErrorData }) { + if (!error) { + return

Ошибки отсутствуют

; + } else if (typeof error === 'string') { + return

{error}

; + } else if (!axios.isAxiosError(error)) { + return ; + } + if (!error?.response) { + return

Нет ответа от сервера

; + } + if (error.response.status === 404) { + return ( +
+

{'Обращение к несуществующему API'}

+ +
+ ); + } + if (error.response.status === 403 && error.message.includes('CSRF')) { + return ( +
+

{'Соединение с сервером потеряно. Перезагрузите страницу'}

+ +
+ ); + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const isHtml = isResponseHtml(error.response); + return ( +
+

Ошибка

+

{error.message}

+ {error.response.data && ( + <> +

Описание

+ {isHtml ?
: null} + {!isHtml ? : null} + + )} +
+ ); +} + +function InfoError({ error }: InfoErrorProps) { + return ( + +
+

Пожалуйста сделайте скриншот и отправьте вместе с описанием ситуации на почту portal@acconcept.ru

+
+

Для продолжения работы перезагрузите страницу

+
+ + +
+ ); +} + +export default InfoError; diff --git a/rsconcept/frontend/src/components/info/InfoUsers.tsx b/rsconcept/frontend/src/components/info/InfoUsers.tsx new file mode 100644 index 00000000..eeea4a84 --- /dev/null +++ b/rsconcept/frontend/src/components/info/InfoUsers.tsx @@ -0,0 +1,25 @@ +import clsx from 'clsx'; + +import { useUsers } from '@/context/UsersContext'; +import { UserID } from '@/models/user'; + +import { CProps } from '../props'; + +interface InfoUsersProps extends CProps.Styling { + items: UserID[]; + prefix: string; +} + +function InfoUsers({ items, className, prefix, ...restProps }: InfoUsersProps) { + const { getUserLabel } = useUsers(); + + return ( +
+ {items.map((user, index) => ( +
{getUserLabel(user)}
+ ))} +
+ ); +} + +export default InfoUsers; diff --git a/rsconcept/frontend/src/components/info/SelectedCounter.tsx b/rsconcept/frontend/src/components/info/SelectedCounter.tsx new file mode 100644 index 00000000..a5c29fce --- /dev/null +++ b/rsconcept/frontend/src/components/info/SelectedCounter.tsx @@ -0,0 +1,21 @@ +import Overlay from '@/components/ui/Overlay'; + +interface SelectedCounterProps { + totalCount: number; + selectedCount: number; + position?: string; + hideZero?: boolean; +} + +function SelectedCounter({ totalCount, selectedCount, hideZero, position = 'top-0 left-0' }: SelectedCounterProps) { + if (selectedCount === 0 && hideZero) { + return null; + } + return ( + + Выбор {selectedCount} из {totalCount} + + ); +} + +export default SelectedCounter; diff --git a/rsconcept/frontend/src/components/props.d.ts b/rsconcept/frontend/src/components/props.d.ts new file mode 100644 index 00000000..35b0bf7a --- /dev/null +++ b/rsconcept/frontend/src/components/props.d.ts @@ -0,0 +1,48 @@ +// =========== Module contains interfaces for common UI elements. ========== +import { HTMLMotionProps } from 'framer-motion'; + +export namespace CProps { + export type Titled = { + title?: string; + titleHtml?: string; + hideTitle?: boolean; + }; + + export type Control = Titled & { + disabled?: boolean; + noBorder?: boolean; + noOutline?: boolean; + }; + + export type Styling = { + style?: React.CSSProperties; + className?: string; + }; + + export type Editor = Control & { + label?: string; + }; + + export type Colors = { + colors?: string; + }; + + export type Div = React.DetailedHTMLProps, HTMLDivElement>; + export type Button = Titled & + Omit< + React.DetailedHTMLProps, HTMLButtonElement>, + 'children' | 'type' + >; + export type Label = Omit< + React.DetailedHTMLProps, HTMLLabelElement>, + 'children' + >; + export type TextArea = Titled & + React.DetailedHTMLProps, HTMLTextAreaElement>; + export type Input = Titled & React.DetailedHTMLProps, HTMLInputElement>; + + export type AnimatedButton = Titled & Omit, 'type'>; + export type AnimatedDiv = HTMLMotionProps<'div'>; + + export type EventMouse = React.MouseEvent; +} diff --git a/rsconcept/frontend/src/components/select/GraphSelectionToolbar.tsx b/rsconcept/frontend/src/components/select/GraphSelectionToolbar.tsx new file mode 100644 index 00000000..ff8a399a --- /dev/null +++ b/rsconcept/frontend/src/components/select/GraphSelectionToolbar.tsx @@ -0,0 +1,79 @@ +import clsx from 'clsx'; + +import { Graph } from '@/models/Graph'; + +import { + IconGraphCollapse, + IconGraphCore, + IconGraphExpand, + IconGraphInputs, + IconGraphMaximize, + IconGraphOutputs, + IconReset +} from '../Icons'; +import { CProps } from '../props'; +import MiniButton from '../ui/MiniButton'; + +interface GraphSelectionToolbarProps extends CProps.Styling { + graph: Graph; + core: number[]; + setSelected: React.Dispatch>; + emptySelection?: boolean; +} + +function GraphSelectionToolbar({ + className, + graph, + core, + setSelected, + emptySelection, + ...restProps +}: GraphSelectionToolbarProps) { + return ( +
+ } + onClick={() => setSelected([])} + disabled={emptySelection} + /> + } + onClick={() => setSelected(prev => [...prev, ...graph.expandAllInputs(prev)])} + disabled={emptySelection} + /> + } + onClick={() => setSelected(prev => [...prev, ...graph.expandAllOutputs(prev)])} + disabled={emptySelection} + /> + } + onClick={() => setSelected(prev => graph.maximizePart(prev))} + disabled={emptySelection} + /> + } + onClick={() => setSelected(prev => [...prev, ...graph.expandInputs(prev)])} + disabled={emptySelection} + /> + } + onClick={() => setSelected(prev => [...prev, ...graph.expandOutputs(prev)])} + disabled={emptySelection} + /> + } + onClick={() => setSelected([...core, ...graph.expandInputs(core)])} + /> +
+ ); +} + +export default GraphSelectionToolbar; diff --git a/rsconcept/frontend/src/components/select/PickConstituenta.tsx b/rsconcept/frontend/src/components/select/PickConstituenta.tsx new file mode 100644 index 00000000..fce1d5be --- /dev/null +++ b/rsconcept/frontend/src/components/select/PickConstituenta.tsx @@ -0,0 +1,120 @@ +'use client'; + +import { useEffect, useMemo, useState } from 'react'; + +import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/ui/DataTable'; +import SearchBar from '@/components/ui/SearchBar'; +import { useConceptOptions } from '@/context/OptionsContext'; +import { CstMatchMode } from '@/models/miscellaneous'; +import { IConstituenta } from '@/models/rsform'; +import { matchConstituenta } from '@/models/rsformAPI'; +import { prefixes } from '@/utils/constants'; +import { describeConstituenta } from '@/utils/labels'; + +import BadgeConstituenta from '../info/BadgeConstituenta'; +import FlexColumn from '../ui/FlexColumn'; + +interface PickConstituentaProps { + id?: string; + prefixID: string; + data?: IConstituenta[]; + rows?: number; + + initialFilter?: string; + onBeginFilter?: (cst: IConstituenta) => boolean; + describeFunc?: (cst: IConstituenta) => string; + matchFunc?: (cst: IConstituenta, filter: string) => boolean; + + value?: IConstituenta; + onSelectValue: (newValue: IConstituenta) => void; +} + +const columnHelper = createColumnHelper(); + +function PickConstituenta({ + id, + data, + value, + initialFilter = '', + rows = 4, + prefixID = prefixes.cst_list, + describeFunc = describeConstituenta, + matchFunc = (cst, filter) => matchConstituenta(cst, filter, CstMatchMode.ALL), + onBeginFilter, + onSelectValue +}: PickConstituentaProps) { + const { colors } = useConceptOptions(); + const [filteredData, setFilteredData] = useState([]); + const [filterText, setFilterText] = useState(initialFilter); + + useEffect(() => { + if (!data) { + setFilteredData([]); + } else { + const newData = onBeginFilter ? data.filter(onBeginFilter) : data; + if (filterText) { + setFilteredData(newData.filter(cst => matchFunc(cst, filterText))); + } else { + setFilteredData(newData); + } + } + }, [data, filterText, matchFunc, onBeginFilter]); + + const columns = useMemo( + () => [ + columnHelper.accessor('alias', { + id: 'alias', + size: 65, + minSize: 65, + maxSize: 65, + cell: props => + }), + columnHelper.accessor(cst => describeFunc(cst), { + id: 'description' + }) + ], + [colors, prefixID, describeFunc] + ); + + const conditionalRowStyles = useMemo( + (): IConditionalStyle[] => [ + { + when: (cst: IConstituenta) => cst.id === value?.id, + style: { backgroundColor: colors.bgSelected } + } + ], + [value, colors] + ); + + return ( +
+ setFilterText(newValue)} + /> + +

Список конституент пуст

+

Измените параметры фильтра

+ + } + onRowClicked={onSelectValue} + /> +
+ ); +} + +export default PickConstituenta; diff --git a/rsconcept/frontend/src/components/select/PickMultiConstituenta.tsx b/rsconcept/frontend/src/components/select/PickMultiConstituenta.tsx new file mode 100644 index 00000000..e9f679df --- /dev/null +++ b/rsconcept/frontend/src/components/select/PickMultiConstituenta.tsx @@ -0,0 +1,115 @@ +'use client'; + +import clsx from 'clsx'; +import { useLayoutEffect, useMemo, useState } from 'react'; + +import DataTable, { createColumnHelper, RowSelectionState } from '@/components/ui/DataTable'; +import { useConceptOptions } from '@/context/OptionsContext'; +import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform'; +import { isBasicConcept } from '@/models/rsformAPI'; +import { describeConstituenta } from '@/utils/labels'; + +import BadgeConstituenta from '../info/BadgeConstituenta'; +import FlexColumn from '../ui/FlexColumn'; +import GraphSelectionToolbar from './GraphSelectionToolbar'; + +interface PickMultiConstituentaProps { + id?: string; + schema?: IRSForm; + prefixID: string; + rows?: number; + + selected: ConstituentaID[]; + setSelected: React.Dispatch>; +} + +const columnHelper = createColumnHelper(); + +function PickMultiConstituenta({ id, schema, prefixID, rows, selected, setSelected }: PickMultiConstituentaProps) { + const { colors } = useConceptOptions(); + const [rowSelection, setRowSelection] = useState({}); + + useLayoutEffect(() => { + if (!schema || selected.length === 0) { + setRowSelection({}); + } else { + const newRowSelection: RowSelectionState = {}; + schema.items.forEach((cst, index) => { + newRowSelection[String(index)] = selected.includes(cst.id); + }); + setRowSelection(newRowSelection); + } + }, [selected, schema]); + + function handleRowSelection(updater: React.SetStateAction) { + if (!schema) { + setSelected([]); + } else { + const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater; + const newSelection: ConstituentaID[] = []; + schema.items.forEach((cst, index) => { + if (newRowSelection[String(index)] === true) { + newSelection.push(cst.id); + } + }); + setSelected(newSelection); + } + } + + const columns = useMemo( + () => [ + columnHelper.accessor('alias', { + id: 'alias', + header: () => Имя, + size: 65, + cell: props => + }), + columnHelper.accessor(cst => describeConstituenta(cst), { + id: 'description', + size: 1000, + header: 'Описание' + }) + ], + [colors, prefixID] + ); + + return ( +
+
+ + Выбраны {selected.length} из {schema?.items.length ?? 0} + + {schema ? ( + isBasicConcept(cst.cst_type)).map(cst => cst.id)} + setSelected={setSelected} + emptySelection={selected.length === 0} + className='w-full ml-8' + /> + ) : null} +
+ +

Список пуст

+ + } + /> +
+ ); +} + +export default PickMultiConstituenta; diff --git a/rsconcept/frontend/src/components/select/PickSchema.tsx b/rsconcept/frontend/src/components/select/PickSchema.tsx new file mode 100644 index 00000000..0d1cb27a --- /dev/null +++ b/rsconcept/frontend/src/components/select/PickSchema.tsx @@ -0,0 +1,117 @@ +import { useLayoutEffect, useMemo, useState } from 'react'; +import { useIntl } from 'react-intl'; + +import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/ui/DataTable'; +import SearchBar from '@/components/ui/SearchBar'; +import { useLibrary } from '@/context/LibraryContext'; +import { useConceptOptions } from '@/context/OptionsContext'; +import { ILibraryItem, LibraryItemID } from '@/models/library'; +import { ILibraryFilter } from '@/models/miscellaneous'; + +import FlexColumn from '../ui/FlexColumn'; + +interface PickSchemaProps { + id?: string; + initialFilter?: string; + rows?: number; + + value?: LibraryItemID; + onSelectValue: (newValue: LibraryItemID) => void; +} + +const columnHelper = createColumnHelper(); + +function PickSchema({ id, initialFilter = '', rows = 4, value, onSelectValue }: PickSchemaProps) { + const intl = useIntl(); + const { colors } = useConceptOptions(); + + const library = useLibrary(); + const [filterText, setFilterText] = useState(initialFilter); + const [filter, setFilter] = useState({}); + const [items, setItems] = useState([]); + + useLayoutEffect(() => { + setFilter({ + query: filterText + }); + }, [filterText]); + + useLayoutEffect(() => { + setItems(library.applyFilter(filter)); + }, [library, filter, filter.query]); + + const columns = useMemo( + () => [ + columnHelper.accessor('alias', { + id: 'alias', + header: 'Шифр', + size: 150, + minSize: 80, + maxSize: 150 + }), + columnHelper.accessor('title', { + id: 'title', + header: 'Название', + size: 1200, + minSize: 200, + maxSize: 1200, + cell: props =>
{props.getValue()}
+ }), + columnHelper.accessor('time_update', { + id: 'time_update', + header: 'Дата', + cell: props => ( +
+ {new Date(props.getValue()).toLocaleString(intl.locale, { + year: '2-digit', + month: '2-digit', + day: '2-digit' + })} +
+ ) + }) + ], + [intl] + ); + + const conditionalRowStyles = useMemo( + (): IConditionalStyle[] => [ + { + when: (item: ILibraryItem) => item.id === value, + style: { backgroundColor: colors.bgSelected } + } + ], + [value, colors] + ); + + return ( +
+ setFilterText(newValue)} + /> + +

Список схем пуст

+

Измените параметры фильтра

+ + } + onRowClicked={rowData => onSelectValue(rowData.id)} + /> +
+ ); +} + +export default PickSchema; diff --git a/rsconcept/frontend/src/components/select/PickSubstitutions.tsx b/rsconcept/frontend/src/components/select/PickSubstitutions.tsx new file mode 100644 index 00000000..c6fdcba1 --- /dev/null +++ b/rsconcept/frontend/src/components/select/PickSubstitutions.tsx @@ -0,0 +1,272 @@ +'use client'; + +import { useCallback, useMemo, useState } from 'react'; + +import BadgeConstituenta from '@/components/info/BadgeConstituenta'; +import SelectConstituenta from '@/components/select/SelectConstituenta'; +import DataTable, { createColumnHelper } from '@/components/ui/DataTable'; +import Label from '@/components/ui/Label'; +import MiniButton from '@/components/ui/MiniButton'; +import { useConceptOptions } from '@/context/OptionsContext'; +import { IConstituenta, IRSForm, ISubstitution } from '@/models/rsform'; +import { describeConstituenta } from '@/utils/labels'; + +import { + IconKeepAliasOff, + IconKeepAliasOn, + IconKeepTermOff, + IconKeepTermOn, + IconPageFirst, + IconPageLast, + IconPageLeft, + IconPageRight, + IconRemove, + IconReplace +} from '../Icons'; + +interface PickSubstitutionsProps { + prefixID: string; + rows?: number; + + schema1?: IRSForm; + schema2?: IRSForm; + filter1?: (cst: IConstituenta) => boolean; + filter2?: (cst: IConstituenta) => boolean; + + items: ISubstitution[]; + setItems: React.Dispatch>; +} + +function SubstitutionIcon({ item }: { item: ISubstitution }) { + if (item.deleteRight) { + if (item.takeLeftTerm) { + return ; + } else { + return ; + } + } else { + if (item.takeLeftTerm) { + return ; + } else { + return ; + } + } +} + +const columnHelper = createColumnHelper(); + +function PickSubstitutions({ + items, + schema1, + schema2, + filter1, + filter2, + rows, + setItems, + prefixID +}: PickSubstitutionsProps) { + const { colors } = useConceptOptions(); + + const [leftCst, setLeftCst] = useState(undefined); + const [rightCst, setRightCst] = useState(undefined); + const [deleteRight, setDeleteRight] = useState(true); + const [takeLeftTerm, setTakeLeftTerm] = useState(true); + + const toggleDelete = () => setDeleteRight(prev => !prev); + const toggleTerm = () => setTakeLeftTerm(prev => !prev); + + function addSubstitution() { + if (!leftCst || !rightCst) { + return; + } + const newSubstitution: ISubstitution = { + leftCst: leftCst, + rightCst: rightCst, + deleteRight: deleteRight, + takeLeftTerm: takeLeftTerm + }; + setItems([ + newSubstitution, + ...items.filter( + item => + (!item.deleteRight && item.leftCst.id !== leftCst.id) || + (item.deleteRight && item.rightCst.id !== rightCst.id) + ) + ]); + } + + const handleDeleteRow = useCallback( + (row: number) => { + setItems(prev => { + const newItems: ISubstitution[] = []; + prev.forEach((item, index) => { + if (index !== row) { + newItems.push(item); + } + }); + return newItems; + }); + }, + [setItems] + ); + + const columns = useMemo( + () => [ + columnHelper.accessor(item => describeConstituenta(item.leftCst), { + id: 'left_text', + header: 'Описание', + size: 1000, + cell: props =>
{props.getValue()}
+ }), + columnHelper.accessor(item => item.leftCst.alias, { + id: 'left_alias', + header: () => Имя, + size: 65, + cell: props => ( + + ) + }), + columnHelper.display({ + id: 'status', + header: '', + size: 40, + cell: props => + }), + columnHelper.accessor(item => item.rightCst.alias, { + id: 'right_alias', + header: () => Имя, + size: 65, + cell: props => ( + + ) + }), + columnHelper.accessor(item => describeConstituenta(item.rightCst), { + id: 'right_text', + header: 'Описание', + minSize: 1000, + cell: props =>
{props.getValue()}
+ }), + columnHelper.display({ + id: 'actions', + cell: props => ( + } + onClick={() => handleDeleteRow(props.row.index)} + /> + ) + }) + ], + [handleDeleteRow, colors, prefixID] + ); + + return ( +
+
+
+
+
+ !filter1 || filter1(cst))} + value={leftCst} + onSelectValue={setLeftCst} + /> +
+ + } + disabled={!leftCst || !rightCst || leftCst === rightCst} + onClick={addSubstitution} + /> + +
+
+
+ !filter2 || filter2(cst))} + value={rightCst} + onSelectValue={setRightCst} + /> +
+
+ + +

Список пуст

+

Добавьте отождествление

+ + } + /> +
+ ); +} + +export default PickSubstitutions; diff --git a/rsconcept/frontend/src/components/select/SelectAccessPolicy.tsx b/rsconcept/frontend/src/components/select/SelectAccessPolicy.tsx new file mode 100644 index 00000000..635cc4f6 --- /dev/null +++ b/rsconcept/frontend/src/components/select/SelectAccessPolicy.tsx @@ -0,0 +1,60 @@ +'use client'; + +import { useCallback } from 'react'; + +import Dropdown from '@/components/ui/Dropdown'; +import useDropdown from '@/hooks/useDropdown'; +import { AccessPolicy } from '@/models/library'; +import { prefixes } from '@/utils/constants'; +import { describeAccessPolicy, labelAccessPolicy } from '@/utils/labels'; + +import { PolicyIcon } from '../DomainIcons'; +import DropdownButton from '../ui/DropdownButton'; +import MiniButton from '../ui/MiniButton'; + +interface SelectAccessPolicyProps { + value: AccessPolicy; + onChange: (value: AccessPolicy) => void; + disabled?: boolean; + stretchLeft?: boolean; +} + +function SelectAccessPolicy({ value, disabled, stretchLeft, onChange }: SelectAccessPolicyProps) { + const menu = useDropdown(); + + const handleChange = useCallback( + (newValue: AccessPolicy) => { + menu.hide(); + if (newValue !== value) { + onChange(newValue); + } + }, + [menu, value, onChange] + ); + + return ( +
+ } + onClick={menu.toggle} + disabled={disabled} + /> + + {Object.values(AccessPolicy).map((item, index) => ( + } + onClick={() => handleChange(item)} + /> + ))} + +
+ ); +} + +export default SelectAccessPolicy; diff --git a/rsconcept/frontend/src/components/select/SelectConstituenta.tsx b/rsconcept/frontend/src/components/select/SelectConstituenta.tsx new file mode 100644 index 00000000..a4bdc8cb --- /dev/null +++ b/rsconcept/frontend/src/components/select/SelectConstituenta.tsx @@ -0,0 +1,60 @@ +'use client'; + +import clsx from 'clsx'; +import { useCallback, useMemo } from 'react'; + +import { CstMatchMode } from '@/models/miscellaneous'; +import { ConstituentaID, IConstituenta } from '@/models/rsform'; +import { matchConstituenta } from '@/models/rsformAPI'; +import { describeConstituenta, describeConstituentaTerm } from '@/utils/labels'; + +import { CProps } from '../props'; +import SelectSingle from '../ui/SelectSingle'; + +interface SelectConstituentaProps extends CProps.Styling { + items?: IConstituenta[]; + value?: IConstituenta; + onSelectValue: (newValue?: IConstituenta) => void; + placeholder?: string; +} + +function SelectConstituenta({ + className, + items, + value, + onSelectValue, + placeholder = 'Выберите конституенту', + ...restProps +}: SelectConstituentaProps) { + const options = useMemo(() => { + return ( + items?.map(cst => ({ + value: cst.id, + label: `${cst.alias}: ${describeConstituenta(cst)}` + })) ?? [] + ); + }, [items]); + + const filter = useCallback( + (option: { value: ConstituentaID | undefined; label: string }, inputValue: string) => { + const cst = items?.find(item => item.id === option.value); + return !cst ? false : matchConstituenta(cst, inputValue, CstMatchMode.ALL); + }, + [items] + ); + + return ( + onSelectValue(items?.find(cst => cst.id === data?.value))} + // @ts-expect-error: TODO: use type definitions from react-select in filter object + filterOption={filter} + placeholder={placeholder} + {...restProps} + /> + ); +} + +export default SelectConstituenta; diff --git a/rsconcept/frontend/src/components/select/SelectGraphFilter.tsx b/rsconcept/frontend/src/components/select/SelectGraphFilter.tsx new file mode 100644 index 00000000..d5eacd17 --- /dev/null +++ b/rsconcept/frontend/src/components/select/SelectGraphFilter.tsx @@ -0,0 +1,73 @@ +'use client'; + +import { useCallback } from 'react'; + +import Dropdown from '@/components/ui/Dropdown'; +import SelectorButton from '@/components/ui/SelectorButton'; +import useDropdown from '@/hooks/useDropdown'; +import useWindowSize from '@/hooks/useWindowSize'; +import { DependencyMode } from '@/models/miscellaneous'; +import { prefixes } from '@/utils/constants'; +import { describeCstSource, labelCstSource } from '@/utils/labels'; + +import { DependencyIcon } from '../DomainIcons'; +import DropdownButton from '../ui/DropdownButton'; + +interface SelectGraphFilterProps { + value: DependencyMode; + dense?: boolean; + onChange: (value: DependencyMode) => void; +} + +function SelectGraphFilter({ value, dense, onChange }: SelectGraphFilterProps) { + const menu = useDropdown(); + const size = useWindowSize(); + + const handleChange = useCallback( + (newValue: DependencyMode) => { + menu.hide(); + onChange(newValue); + }, + [menu, onChange] + ); + + return ( +
+ } + text={dense || size.isSmall ? undefined : labelCstSource(value)} + onClick={menu.toggle} + /> + + {Object.values(DependencyMode) + .filter(value => !isNaN(Number(value))) + .map((value, index) => { + const source = value as DependencyMode; + return ( + handleChange(source)} + > +
+ {} + {!dense ? ( + + {labelCstSource(source)}: {describeCstSource(source)} + + ) : null} +
+
+ ); + })} +
+
+ ); +} + +export default SelectGraphFilter; diff --git a/rsconcept/frontend/src/components/select/SelectItemType.tsx b/rsconcept/frontend/src/components/select/SelectItemType.tsx new file mode 100644 index 00000000..35d4f466 --- /dev/null +++ b/rsconcept/frontend/src/components/select/SelectItemType.tsx @@ -0,0 +1,62 @@ +'use client'; + +import { useCallback } from 'react'; + +import Dropdown from '@/components/ui/Dropdown'; +import useDropdown from '@/hooks/useDropdown'; +import { LibraryItemType } from '@/models/library'; +import { prefixes } from '@/utils/constants'; +import { describeLibraryItemType, labelLibraryItemType } from '@/utils/labels'; + +import { ItemTypeIcon } from '../DomainIcons'; +import DropdownButton from '../ui/DropdownButton'; +import SelectorButton from '../ui/SelectorButton'; + +interface SelectItemTypeProps { + value: LibraryItemType; + onChange: (value: LibraryItemType) => void; + disabled?: boolean; + stretchLeft?: boolean; +} + +function SelectItemType({ value, disabled, stretchLeft, onChange }: SelectItemTypeProps) { + const menu = useDropdown(); + + const handleChange = useCallback( + (newValue: LibraryItemType) => { + menu.hide(); + if (newValue !== value) { + onChange(newValue); + } + }, + [menu, value, onChange] + ); + + return ( +
+ } + text={labelLibraryItemType(value)} + onClick={menu.toggle} + disabled={disabled} + /> + + {Object.values(LibraryItemType).map((item, index) => ( + } + onClick={() => handleChange(item)} + /> + ))} + +
+ ); +} + +export default SelectItemType; diff --git a/rsconcept/frontend/src/components/select/SelectLocationHead.tsx b/rsconcept/frontend/src/components/select/SelectLocationHead.tsx new file mode 100644 index 00000000..bc6038c4 --- /dev/null +++ b/rsconcept/frontend/src/components/select/SelectLocationHead.tsx @@ -0,0 +1,68 @@ +'use client'; + +import { useCallback } from 'react'; + +import Dropdown from '@/components/ui/Dropdown'; +import SelectorButton from '@/components/ui/SelectorButton'; +import useDropdown from '@/hooks/useDropdown'; +import { LocationHead } from '@/models/library'; +import { prefixes } from '@/utils/constants'; +import { describeLocationHead, labelLocationHead } from '@/utils/labels'; + +import { LocationIcon } from '../DomainIcons'; +import DropdownButton from '../ui/DropdownButton'; + +interface SelectLocationHeadProps { + value: LocationHead; + onChange: (value: LocationHead) => void; + excluded?: LocationHead[]; +} + +function SelectLocationHead({ value, excluded = [], onChange }: SelectLocationHeadProps) { + const menu = useDropdown(); + + const handleChange = useCallback( + (newValue: LocationHead) => { + menu.hide(); + onChange(newValue); + }, + [menu, onChange] + ); + + return ( +
+ } + text={labelLocationHead(value)} + onClick={menu.toggle} + /> + + + {Object.values(LocationHead) + .filter(head => !excluded.includes(head)) + .map((head, index) => { + return ( + handleChange(head)} + title={describeLocationHead(head)} + > +
+ + {labelLocationHead(head)} +
+
+ ); + })} +
+
+ ); +} + +export default SelectLocationHead; diff --git a/rsconcept/frontend/src/components/select/SelectMatchMode.tsx b/rsconcept/frontend/src/components/select/SelectMatchMode.tsx new file mode 100644 index 00000000..5cb87d66 --- /dev/null +++ b/rsconcept/frontend/src/components/select/SelectMatchMode.tsx @@ -0,0 +1,72 @@ +'use client'; + +import { useCallback } from 'react'; + +import Dropdown from '@/components/ui/Dropdown'; +import SelectorButton from '@/components/ui/SelectorButton'; +import useDropdown from '@/hooks/useDropdown'; +import useWindowSize from '@/hooks/useWindowSize'; +import { CstMatchMode } from '@/models/miscellaneous'; +import { prefixes } from '@/utils/constants'; +import { describeCstMatchMode, labelCstMatchMode } from '@/utils/labels'; + +import { MatchModeIcon } from '../DomainIcons'; +import DropdownButton from '../ui/DropdownButton'; + +interface SelectMatchModeProps { + value: CstMatchMode; + dense?: boolean; + onChange: (value: CstMatchMode) => void; +} + +function SelectMatchMode({ value, dense, onChange }: SelectMatchModeProps) { + const menu = useDropdown(); + const size = useWindowSize(); + + const handleChange = useCallback( + (newValue: CstMatchMode) => { + menu.hide(); + onChange(newValue); + }, + [menu, onChange] + ); + + return ( +
+ } + text={dense || size.isSmall ? undefined : labelCstMatchMode(value)} + onClick={menu.toggle} + /> + + {Object.values(CstMatchMode) + .filter(value => !isNaN(Number(value))) + .map((value, index) => { + const matchMode = value as CstMatchMode; + return ( + handleChange(matchMode)} + > +
+ {} + {!dense ? ( + + {labelCstMatchMode(matchMode)}: {describeCstMatchMode(matchMode)} + + ) : null} +
+
+ ); + })} +
+
+ ); +} + +export default SelectMatchMode; diff --git a/rsconcept/frontend/src/components/select/SelectMultiGrammeme.tsx b/rsconcept/frontend/src/components/select/SelectMultiGrammeme.tsx new file mode 100644 index 00000000..fe8f89ca --- /dev/null +++ b/rsconcept/frontend/src/components/select/SelectMultiGrammeme.tsx @@ -0,0 +1,38 @@ +import { useEffect, useState } from 'react'; + +import SelectMulti, { SelectMultiProps } from '@/components/ui/SelectMulti'; +import { Grammeme } from '@/models/language'; +import { getCompatibleGrams } from '@/models/languageAPI'; +import { compareGrammemeOptions, IGrammemeOption, SelectorGrammemes } from '@/utils/selectors'; + +import { CProps } from '../props'; + +interface SelectMultiGrammemeProps + extends Omit, 'value' | 'onChange'>, + CProps.Styling { + value: IGrammemeOption[]; + setValue: React.Dispatch>; + placeholder?: string; +} + +function SelectMultiGrammeme({ value, setValue, ...restProps }: SelectMultiGrammemeProps) { + const [options, setOptions] = useState([]); + + useEffect(() => { + const compatible = getCompatibleGrams( + value.filter(data => Object.values(Grammeme).includes(data.value as Grammeme)).map(data => data.value as Grammeme) + ); + setOptions(SelectorGrammemes.filter(({ value }) => compatible.includes(value as Grammeme))); + }, [value]); + + return ( + setValue([...newValue].sort(compareGrammemeOptions))} + {...restProps} + /> + ); +} + +export default SelectMultiGrammeme; diff --git a/rsconcept/frontend/src/components/select/SelectUser.tsx b/rsconcept/frontend/src/components/select/SelectUser.tsx new file mode 100644 index 00000000..3cd0c7d2 --- /dev/null +++ b/rsconcept/frontend/src/components/select/SelectUser.tsx @@ -0,0 +1,62 @@ +'use client'; + +import clsx from 'clsx'; +import { useCallback, useMemo } from 'react'; + +import { useUsers } from '@/context/UsersContext'; +import { IUserInfo, UserID } from '@/models/user'; +import { matchUser } from '@/models/userAPI'; + +import { CProps } from '../props'; +import SelectSingle from '../ui/SelectSingle'; + +interface SelectUserProps extends CProps.Styling { + items?: IUserInfo[]; + value?: UserID; + placeholder?: string; + onSelectValue: (newValue: UserID) => void; +} + +function SelectUser({ + className, + items, + value, + onSelectValue, + placeholder = 'Выберите пользователя', + ...restProps +}: SelectUserProps) { + const { getUserLabel } = useUsers(); + const options = useMemo(() => { + return ( + items?.map(user => ({ + value: user.id, + label: getUserLabel(user.id) + })) ?? [] + ); + }, [items, getUserLabel]); + + const filter = useCallback( + (option: { value: UserID | undefined; label: string }, inputValue: string) => { + const user = items?.find(item => item.id === option.value); + return !user ? false : matchUser(user, inputValue); + }, + [items] + ); + + return ( + { + if (data !== null && data.value !== undefined) onSelectValue(data.value); + }} + // @ts-expect-error: TODO: use type definitions from react-select in filter object + filterOption={filter} + placeholder={placeholder} + {...restProps} + /> + ); +} + +export default SelectUser; diff --git a/rsconcept/frontend/src/components/select/SelectVersion.tsx b/rsconcept/frontend/src/components/select/SelectVersion.tsx new file mode 100644 index 00000000..e4cb349e --- /dev/null +++ b/rsconcept/frontend/src/components/select/SelectVersion.tsx @@ -0,0 +1,49 @@ +'use client'; + +import clsx from 'clsx'; +import { useMemo } from 'react'; + +import { IVersionInfo, VersionID } from '@/models/library'; +import { labelVersion } from '@/utils/labels'; + +import { CProps } from '../props'; +import SelectSingle from '../ui/SelectSingle'; + +interface SelectVersionProps extends CProps.Styling { + id?: string; + items?: IVersionInfo[]; + value?: VersionID; + onSelectValue: (newValue?: VersionID) => void; +} + +function SelectVersion({ id, className, items, value, onSelectValue, ...restProps }: SelectVersionProps) { + const options = useMemo(() => { + return [ + { + value: undefined, + label: labelVersion(undefined) + }, + ...(items?.map(version => ({ + value: version.id, + label: version.version + })) ?? []) + ]; + }, [items]); + const valueLabel = useMemo(() => { + const version = items?.find(ver => ver.id === value); + return version ? version.version : labelVersion(undefined); + }, [items, value]); + + return ( + onSelectValue(data?.value)} + {...restProps} + /> + ); +} + +export default SelectVersion; diff --git a/rsconcept/frontend/src/components/ui/Button.tsx b/rsconcept/frontend/src/components/ui/Button.tsx new file mode 100644 index 00000000..7d2dbb80 --- /dev/null +++ b/rsconcept/frontend/src/components/ui/Button.tsx @@ -0,0 +1,61 @@ +import clsx from 'clsx'; + +import { globals } from '@/utils/constants'; + +import { CProps } from '../props'; + +interface ButtonProps extends CProps.Control, CProps.Colors, CProps.Button { + text?: string; + icon?: React.ReactNode; + + dense?: boolean; + loading?: boolean; +} + +function Button({ + text, + icon, + title, + titleHtml, + hideTitle, + loading, + dense, + disabled, + noBorder, + noOutline, + colors = 'clr-btn-default', + className, + ...restProps +}: ButtonProps) { + return ( + + ); +} + +export default Button; diff --git a/rsconcept/frontend/src/components/ui/Checkbox.tsx b/rsconcept/frontend/src/components/ui/Checkbox.tsx new file mode 100644 index 00000000..dc4b8d3a --- /dev/null +++ b/rsconcept/frontend/src/components/ui/Checkbox.tsx @@ -0,0 +1,86 @@ +import clsx from 'clsx'; +import { useMemo } from 'react'; + +import { globals } from '@/utils/constants'; + +import { CheckboxChecked } from '../Icons'; +import { CProps } from '../props'; + +export interface CheckboxProps extends Omit { + label?: string; + disabled?: boolean; + + value: boolean; + setValue?: (newValue: boolean) => void; +} + +function Checkbox({ + disabled, + label, + title, + titleHtml, + hideTitle, + className, + value, + setValue, + ...restProps +}: CheckboxProps) { + const cursor = useMemo(() => { + if (disabled) { + return 'cursor-not-allowed'; + } else if (setValue) { + return 'cursor-pointer'; + } else { + return ''; + } + }, [disabled, setValue]); + + function handleClick(event: CProps.EventMouse): void { + event.preventDefault(); + event.stopPropagation(); + if (disabled || !setValue) { + return; + } + setValue(!value); + } + + return ( + + ); +} + +export default Checkbox; diff --git a/rsconcept/frontend/src/components/ui/CheckboxTristate.tsx b/rsconcept/frontend/src/components/ui/CheckboxTristate.tsx new file mode 100644 index 00000000..e19a195c --- /dev/null +++ b/rsconcept/frontend/src/components/ui/CheckboxTristate.tsx @@ -0,0 +1,95 @@ +import clsx from 'clsx'; +import { useMemo } from 'react'; + +import { globals } from '@/utils/constants'; + +import { CheckboxChecked, CheckboxNull } from '../Icons'; +import { CProps } from '../props'; +import { CheckboxProps } from './Checkbox'; + +export interface CheckboxTristateProps extends Omit { + value: boolean | null; + setValue?: (newValue: boolean | null) => void; +} + +function CheckboxTristate({ + disabled, + label, + title, + titleHtml, + hideTitle, + className, + value, + setValue, + ...restProps +}: CheckboxTristateProps) { + const cursor = useMemo(() => { + if (disabled) { + return 'cursor-not-allowed'; + } else if (setValue) { + return 'cursor-pointer'; + } else { + return ''; + } + }, [disabled, setValue]); + + function handleClick(event: CProps.EventMouse): void { + event.preventDefault(); + event.stopPropagation(); + if (disabled || !setValue) { + return; + } + if (value === false) { + setValue(null); + } else if (value === null) { + setValue(true); + } else { + setValue(false); + } + } + + return ( + + ); +} + +export default CheckboxTristate; diff --git a/rsconcept/frontend/src/components/ui/DataTable/DataTable.tsx b/rsconcept/frontend/src/components/ui/DataTable/DataTable.tsx new file mode 100644 index 00000000..8978ae49 --- /dev/null +++ b/rsconcept/frontend/src/components/ui/DataTable/DataTable.tsx @@ -0,0 +1,183 @@ +'use client'; + +import { + ColumnSort, + createColumnHelper, + getCoreRowModel, + getPaginationRowModel, + getSortedRowModel, + PaginationState, + RowData, + type RowSelectionState, + SortingState, + TableOptions, + useReactTable, + type VisibilityState +} from '@tanstack/react-table'; +import { useMemo, useState } from 'react'; + +import { CProps } from '../../props'; +import DefaultNoData from './DefaultNoData'; +import PaginationTools from './PaginationTools'; +import TableBody from './TableBody'; +import TableFooter from './TableFooter'; +import TableHeader from './TableHeader'; + +export { createColumnHelper, type ColumnSort, type RowSelectionState, type VisibilityState }; + +export interface IConditionalStyle { + when: (rowData: TData) => boolean; + style: React.CSSProperties; +} + +export interface DataTableProps + extends CProps.Styling, + Pick, 'data' | 'columns' | 'onRowSelectionChange' | 'onColumnVisibilityChange'> { + id?: string; + dense?: boolean; + rows?: number; + contentHeight?: string; + headPosition?: string; + noHeader?: boolean; + noFooter?: boolean; + + conditionalRowStyles?: IConditionalStyle[]; + noDataComponent?: React.ReactNode; + + onRowClicked?: (rowData: TData, event: CProps.EventMouse) => void; + onRowDoubleClicked?: (rowData: TData, event: CProps.EventMouse) => void; + + enableRowSelection?: boolean; + rowSelection?: RowSelectionState; + + enableHiding?: boolean; + columnVisibility?: VisibilityState; + + enablePagination?: boolean; + paginationPerPage?: number; + paginationOptions?: number[]; + onChangePaginationOption?: (newValue: number) => void; + + enableSorting?: boolean; + initialSorting?: ColumnSort; +} + +/** + * UI element: data representation as a table. + * + * @param headPosition - Top position of sticky header (0 if no other sticky elements are present). + * No sticky header if omitted + */ +function DataTable({ + id, + style, + className, + dense, + rows, + contentHeight = '1.1875rem', + headPosition, + conditionalRowStyles, + noFooter, + noHeader, + onRowClicked, + onRowDoubleClicked, + noDataComponent, + + enableRowSelection, + rowSelection, + + enableHiding, + columnVisibility, + + enableSorting, + initialSorting, + + enablePagination, + paginationPerPage = 10, + paginationOptions = [10, 20, 30, 40, 50], + onChangePaginationOption, + + ...restProps +}: DataTableProps) { + const [sorting, setSorting] = useState(initialSorting ? [initialSorting] : []); + const [lastSelected, setLastSelected] = useState(undefined); + + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: paginationPerPage + }); + + const tableImpl = useReactTable({ + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: enableSorting ? getSortedRowModel() : undefined, + getPaginationRowModel: enablePagination ? getPaginationRowModel() : undefined, + + state: { + pagination: pagination, + sorting: sorting, + rowSelection: rowSelection ?? {}, + columnVisibility: columnVisibility ?? {} + }, + enableHiding: enableHiding, + onPaginationChange: enablePagination ? setPagination : undefined, + onSortingChange: enableSorting ? setSorting : undefined, + enableMultiRowSelection: enableRowSelection, + ...restProps + }); + + const isEmpty = tableImpl.getRowModel().rows.length === 0; + + // TODO: refactor formula for different font sizes and pagination tools + const fixedSize = useMemo(() => { + if (!rows) { + return undefined; + } + if (dense) { + return `calc(2px + (2px + ${contentHeight} + 0.5rem)*${rows} + ${noHeader ? '0px' : '(2px + 2.1875rem)'})`; + } else { + return `calc(2px + (2px + ${contentHeight} + 1rem)*${rows + (noHeader ? 0 : 1)})`; + } + }, [rows, dense, noHeader, contentHeight]); + + return ( +
+ + {!noHeader ? ( + + ) : null} + + + + {!noFooter ? : null} +
+ + {enablePagination && !isEmpty ? ( + + ) : null} + {isEmpty ? noDataComponent ?? : null} +
+ ); +} + +export default DataTable; diff --git a/rsconcept/frontend/src/components/ui/DataTable/DefaultNoData.tsx b/rsconcept/frontend/src/components/ui/DataTable/DefaultNoData.tsx new file mode 100644 index 00000000..70ff4f98 --- /dev/null +++ b/rsconcept/frontend/src/components/ui/DataTable/DefaultNoData.tsx @@ -0,0 +1,5 @@ +function defaultNoDataComponent() { + return
Данные отсутствуют
; +} + +export default defaultNoDataComponent; diff --git a/rsconcept/frontend/src/components/ui/DataTable/PaginationTools.tsx b/rsconcept/frontend/src/components/ui/DataTable/PaginationTools.tsx new file mode 100644 index 00000000..e5b19dd0 --- /dev/null +++ b/rsconcept/frontend/src/components/ui/DataTable/PaginationTools.tsx @@ -0,0 +1,108 @@ +'use client'; + +import { Table } from '@tanstack/react-table'; +import clsx from 'clsx'; +import { useCallback } from 'react'; + +import { IconPageFirst, IconPageLast, IconPageLeft, IconPageRight } from '@/components/Icons'; +import { prefixes } from '@/utils/constants'; + +interface PaginationToolsProps { + id?: string; + table: Table; + paginationOptions: number[]; + onChangePaginationOption?: (newValue: number) => void; +} + +function PaginationTools({ + id, + table, + paginationOptions, + onChangePaginationOption +}: PaginationToolsProps) { + const handlePaginationOptionsChange = useCallback( + (event: React.ChangeEvent) => { + const perPage = Number(event.target.value); + table.setPageSize(perPage); + if (onChangePaginationOption) { + onChangePaginationOption(perPage); + } + }, + [onChangePaginationOption, table] + ); + + return ( +
+ + {`${table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1} + - + ${Math.min( + table.getFilteredRowModel().rows.length, + (table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize + )} + из + ${table.getFilteredRowModel().rows.length}`} + +
+ + + { + const page = event.target.value ? Number(event.target.value) - 1 : 0; + if (page + 1 <= table.getPageCount()) { + table.setPageIndex(page); + } + }} + /> + + +
+ +
+ ); +} + +export default PaginationTools; diff --git a/rsconcept/frontend/src/components/ui/DataTable/SelectAll.tsx b/rsconcept/frontend/src/components/ui/DataTable/SelectAll.tsx new file mode 100644 index 00000000..c7da72bd --- /dev/null +++ b/rsconcept/frontend/src/components/ui/DataTable/SelectAll.tsx @@ -0,0 +1,28 @@ +import { Table } from '@tanstack/react-table'; + +import CheckboxTristate from '@/components/ui/CheckboxTristate'; + +interface SelectAllProps { + table: Table; + setLastSelected: React.Dispatch>; +} + +function SelectAll({ table, setLastSelected }: SelectAllProps) { + function handleChange(value: boolean | null) { + setLastSelected(undefined); + table.toggleAllPageRowsSelected(value !== false); + } + + return ( + + ); +} + +export default SelectAll; diff --git a/rsconcept/frontend/src/components/ui/DataTable/SelectRow.tsx b/rsconcept/frontend/src/components/ui/DataTable/SelectRow.tsx new file mode 100644 index 00000000..b0a673c2 --- /dev/null +++ b/rsconcept/frontend/src/components/ui/DataTable/SelectRow.tsx @@ -0,0 +1,19 @@ +import { Row } from '@tanstack/react-table'; + +import Checkbox from '@/components/ui/Checkbox'; + +interface SelectRowProps { + row: Row; + setLastSelected: React.Dispatch>; +} + +function SelectRow({ row, setLastSelected }: SelectRowProps) { + function handleChange(value: boolean) { + setLastSelected(row.id); + row.toggleSelected(value); + } + + return ; +} + +export default SelectRow; diff --git a/rsconcept/frontend/src/components/ui/DataTable/SortingIcon.tsx b/rsconcept/frontend/src/components/ui/DataTable/SortingIcon.tsx new file mode 100644 index 00000000..d74e0d4e --- /dev/null +++ b/rsconcept/frontend/src/components/ui/DataTable/SortingIcon.tsx @@ -0,0 +1,20 @@ +import { Column } from '@tanstack/react-table'; + +import { IconSortAsc, IconSortDesc } from '@/components/Icons'; + +interface SortingIconProps { + column: Column; +} + +function SortingIcon({ column }: SortingIconProps) { + return ( + <> + {{ + desc: , + asc: + }[column.getIsSorted() as string] ?? } + + ); +} + +export default SortingIcon; diff --git a/rsconcept/frontend/src/components/ui/DataTable/TableBody.tsx b/rsconcept/frontend/src/components/ui/DataTable/TableBody.tsx new file mode 100644 index 00000000..f860fc3a --- /dev/null +++ b/rsconcept/frontend/src/components/ui/DataTable/TableBody.tsx @@ -0,0 +1,110 @@ +import { Cell, flexRender, Row, Table } from '@tanstack/react-table'; +import clsx from 'clsx'; + +import { CProps } from '@/components/props'; + +import { IConditionalStyle } from '.'; +import SelectRow from './SelectRow'; + +interface TableBodyProps { + table: Table; + dense?: boolean; + noHeader?: boolean; + enableRowSelection?: boolean; + conditionalRowStyles?: IConditionalStyle[]; + + lastSelected: string | undefined; + setLastSelected: React.Dispatch>; + + onRowClicked?: (rowData: TData, event: CProps.EventMouse) => void; + onRowDoubleClicked?: (rowData: TData, event: CProps.EventMouse) => void; +} + +function TableBody({ + table, + dense, + noHeader, + enableRowSelection, + conditionalRowStyles, + lastSelected, + setLastSelected, + onRowClicked, + onRowDoubleClicked +}: TableBodyProps) { + function handleRowClicked(target: Row, event: CProps.EventMouse) { + if (onRowClicked) { + onRowClicked(target.original, event); + } + if (enableRowSelection && target.getCanSelect()) { + if (event.shiftKey && !!lastSelected && lastSelected !== target.id) { + const { rows, rowsById } = table.getRowModel(); + const lastIndex = rowsById[lastSelected].index; + const currentIndex = target.index; + const toggleRows = rows.slice( + lastIndex > currentIndex ? currentIndex : lastIndex + 1, + lastIndex > currentIndex ? lastIndex : currentIndex + 1 + ); + const newSelection: { [key: string]: boolean } = {}; + toggleRows.forEach(row => { + newSelection[row.id] = !target.getIsSelected(); + }); + table.setRowSelection(prev => ({ ...prev, ...newSelection })); + setLastSelected(undefined); + } else { + setLastSelected(target.id); + target.toggleSelected(!target.getIsSelected()); + } + } + } + + function getRowStyles(row: Row) { + return { + ...conditionalRowStyles! + .filter(item => item.when(row.original)) + .reduce((prev, item) => ({ ...prev, ...item.style }), {}) + }; + } + + return ( + + {table.getRowModel().rows.map((row: Row, index) => ( + + {enableRowSelection ? ( + + + + ) : null} + {row.getVisibleCells().map((cell: Cell) => ( + handleRowClicked(row, event)} + onDoubleClick={event => (onRowDoubleClicked ? onRowDoubleClicked(row.original, event) : undefined)} + > + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + + ); +} + +export default TableBody; diff --git a/rsconcept/frontend/src/components/ui/DataTable/TableFooter.tsx b/rsconcept/frontend/src/components/ui/DataTable/TableFooter.tsx new file mode 100644 index 00000000..fe0e5196 --- /dev/null +++ b/rsconcept/frontend/src/components/ui/DataTable/TableFooter.tsx @@ -0,0 +1,23 @@ +import { flexRender, Header, HeaderGroup, Table } from '@tanstack/react-table'; + +interface TableFooterProps { + table: Table; +} + +function TableFooter({ table }: TableFooterProps) { + return ( + + {table.getFooterGroups().map((footerGroup: HeaderGroup) => ( + + {footerGroup.headers.map((header: Header) => ( + + {!header.isPlaceholder ? flexRender(header.column.columnDef.footer, header.getContext()) : null} + + ))} + + ))} + + ); +} + +export default TableFooter; diff --git a/rsconcept/frontend/src/components/ui/DataTable/TableHeader.tsx b/rsconcept/frontend/src/components/ui/DataTable/TableHeader.tsx new file mode 100644 index 00000000..cf3c1700 --- /dev/null +++ b/rsconcept/frontend/src/components/ui/DataTable/TableHeader.tsx @@ -0,0 +1,63 @@ +import { flexRender, Header, HeaderGroup, Table } from '@tanstack/react-table'; + +import SelectAll from './SelectAll'; +import SortingIcon from './SortingIcon'; + +interface TableHeaderProps { + table: Table; + headPosition?: string; + enableRowSelection?: boolean; + enableSorting?: boolean; + setLastSelected: React.Dispatch>; +} + +function TableHeader({ + table, + headPosition, + enableRowSelection, + enableSorting, + setLastSelected +}: TableHeaderProps) { + return ( + + {table.getHeaderGroups().map((headerGroup: HeaderGroup) => ( + + {enableRowSelection ? ( + + + + ) : null} + {headerGroup.headers.map((header: Header) => ( + + {!header.isPlaceholder ? ( + + {flexRender(header.column.columnDef.header, header.getContext())} + {enableSorting && header.column.getCanSort() ? : null} + + ) : null} + + ))} + + ))} + + ); +} + +export default TableHeader; diff --git a/rsconcept/frontend/src/components/ui/DataTable/index.tsx b/rsconcept/frontend/src/components/ui/DataTable/index.tsx new file mode 100644 index 00000000..b691c40c --- /dev/null +++ b/rsconcept/frontend/src/components/ui/DataTable/index.tsx @@ -0,0 +1,7 @@ +export { + default, + createColumnHelper, + type IConditionalStyle, + type RowSelectionState, + type VisibilityState +} from './DataTable'; diff --git a/rsconcept/frontend/src/components/ui/Divider.tsx b/rsconcept/frontend/src/components/ui/Divider.tsx new file mode 100644 index 00000000..1030cb9e --- /dev/null +++ b/rsconcept/frontend/src/components/ui/Divider.tsx @@ -0,0 +1,26 @@ +import clsx from 'clsx'; + +import { CProps } from '@/components/props'; + +interface DividerProps extends CProps.Styling { + vertical?: boolean; + margins?: string; +} + +function Divider({ vertical, margins = 'mx-2', className, ...restProps }: DividerProps) { + return ( +
+ ); +} + +export default Divider; diff --git a/rsconcept/frontend/src/components/ui/Dropdown.tsx b/rsconcept/frontend/src/components/ui/Dropdown.tsx new file mode 100644 index 00000000..c92f33b7 --- /dev/null +++ b/rsconcept/frontend/src/components/ui/Dropdown.tsx @@ -0,0 +1,43 @@ +import clsx from 'clsx'; +import { motion } from 'framer-motion'; + +import { animateDropdown } from '@/styling/animations'; + +import { CProps } from '../props'; + +interface DropdownProps extends CProps.Styling { + stretchLeft?: boolean; + isOpen: boolean; + children: React.ReactNode; +} + +function Dropdown({ isOpen, stretchLeft, className, children, ...restProps }: DropdownProps) { + return ( +
+ + {children} + +
+ ); +} + +export default Dropdown; diff --git a/rsconcept/frontend/src/components/ui/DropdownButton.tsx b/rsconcept/frontend/src/components/ui/DropdownButton.tsx new file mode 100644 index 00000000..6dd35db1 --- /dev/null +++ b/rsconcept/frontend/src/components/ui/DropdownButton.tsx @@ -0,0 +1,57 @@ +import clsx from 'clsx'; +import { motion } from 'framer-motion'; + +import { animateDropdownItem } from '@/styling/animations'; +import { globals } from '@/utils/constants'; + +import { CProps } from '../props'; + +interface DropdownButtonProps extends CProps.AnimatedButton { + text?: string; + icon?: React.ReactNode; + + children?: React.ReactNode; +} + +function DropdownButton({ + text, + icon, + className, + title, + titleHtml, + hideTitle, + onClick, + children, + ...restProps +}: DropdownButtonProps) { + return ( + + {children ? children : null} + {!children && icon ? icon : null} + {!children && text ? {text} : null} + + ); +} + +export default DropdownButton; diff --git a/rsconcept/frontend/src/components/ui/DropdownCheckbox.tsx b/rsconcept/frontend/src/components/ui/DropdownCheckbox.tsx new file mode 100644 index 00000000..128e3573 --- /dev/null +++ b/rsconcept/frontend/src/components/ui/DropdownCheckbox.tsx @@ -0,0 +1,33 @@ +import clsx from 'clsx'; +import { motion } from 'framer-motion'; + +import { animateDropdownItem } from '@/styling/animations'; + +import Checkbox from './Checkbox'; + +interface DropdownCheckboxProps { + value: boolean; + label?: string; + title?: string; + disabled?: boolean; + setValue?: (newValue: boolean) => void; +} + +function DropdownCheckbox({ title, setValue, disabled, ...restProps }: DropdownCheckboxProps) { + return ( + + + + ); +} + +export default DropdownCheckbox; diff --git a/rsconcept/frontend/src/components/ui/EmbedYoutube.tsx b/rsconcept/frontend/src/components/ui/EmbedYoutube.tsx new file mode 100644 index 00000000..edca95bf --- /dev/null +++ b/rsconcept/frontend/src/components/ui/EmbedYoutube.tsx @@ -0,0 +1,37 @@ +interface EmbedYoutubeProps { + videoID: string; + pxHeight: number; + pxWidth?: number; +} + +function EmbedYoutube({ videoID, pxHeight, pxWidth }: EmbedYoutubeProps) { + if (!pxWidth) { + pxWidth = (pxHeight * 16) / 9; + } + return ( +
+