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 @@
+
+
+
+
+
+[](https://github.com/IRBorisov/ConceptPortal/actions/workflows/backend.yml)
+[](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 (
+
+ );
+}
+
+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 (
+
+
+
+
+
+
+
+ }
+ onClick={navigateCreateNew}
+ />
+ }
+ onClick={navigateLibrary}
+ />
+ } onClick={navigateHelp} />
+
+
+
+
+ );
+}
+
+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 (
+
+ {icon ? {icon} : null}
+ {text ? {text} : null}
+
+ );
+}
+
+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[!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 (
+
+ {icon ? icon : null}
+ {text ? {text} : null}
+
+ );
+}
+
+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 (
+
+
+ {value ? (
+
+
+
+ ) : null}
+
+ {label ? {label} : null}
+
+ );
+}
+
+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 (
+
+
+ {value ? (
+
+
+
+ ) : null}
+ {value == null ? (
+
+
+
+ ) : null}
+
+ {label ? {label} : null}
+
+ );
+}
+
+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}`}
+
+
+ table.setPageIndex(0)}
+ disabled={!table.getCanPreviousPage()}
+ >
+
+
+ table.previousPage()}
+ disabled={!table.getCanPreviousPage()}
+ >
+
+
+ {
+ const page = event.target.value ? Number(event.target.value) - 1 : 0;
+ if (page + 1 <= table.getPageCount()) {
+ table.setPageIndex(page);
+ }
+ }}
+ />
+ table.nextPage()}
+ disabled={!table.getCanNextPage()}
+ >
+
+
+ table.setPageIndex(table.getPageCount() - 1)}
+ disabled={!table.getCanNextPage()}
+ >
+
+
+
+
+ {paginationOptions.map(pageSize => (
+
+ {pageSize} на стр
+
+ ))}
+
+
+ );
+}
+
+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 (
+
+ VIDEO
+
+ );
+}
+
+export default EmbedYoutube;
diff --git a/rsconcept/frontend/src/components/ui/FileInput.tsx b/rsconcept/frontend/src/components/ui/FileInput.tsx
new file mode 100644
index 00000000..87d154ef
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/FileInput.tsx
@@ -0,0 +1,54 @@
+'use client';
+
+import clsx from 'clsx';
+import { useRef, useState } from 'react';
+
+import { IconUpload } from '../Icons';
+import { CProps } from '../props';
+import Button from './Button';
+import Label from './Label';
+
+interface FileInputProps extends Omit {
+ label: string;
+
+ acceptType?: string;
+ onChange?: (event: React.ChangeEvent) => void;
+}
+
+function FileInput({ id, label, acceptType, title, className, style, onChange, ...restProps }: FileInputProps) {
+ const inputRef = useRef(null);
+ const [fileName, setFileName] = useState('');
+
+ const handleUploadClick = () => {
+ inputRef.current?.click();
+ };
+
+ const handleFileChange = (event: React.ChangeEvent) => {
+ if (event.target.files && event.target.files.length > 0) {
+ setFileName(event.target.files[0].name);
+ } else {
+ setFileName('');
+ }
+ if (onChange) {
+ onChange(event);
+ }
+ };
+
+ return (
+
+
+ } onClick={handleUploadClick} title={title} />
+
+
+ );
+}
+
+export default FileInput;
diff --git a/rsconcept/frontend/src/components/ui/FlexColumn.tsx b/rsconcept/frontend/src/components/ui/FlexColumn.tsx
new file mode 100644
index 00000000..dd822a2e
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/FlexColumn.tsx
@@ -0,0 +1,15 @@
+import clsx from 'clsx';
+
+import { CProps } from '../props';
+
+export interface FlexColumnProps extends CProps.Div {}
+
+function FlexColumn({ className, children, ...restProps }: FlexColumnProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+export default FlexColumn;
diff --git a/rsconcept/frontend/src/components/ui/GraphUI.tsx b/rsconcept/frontend/src/components/ui/GraphUI.tsx
new file mode 100644
index 00000000..ba956a69
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/GraphUI.tsx
@@ -0,0 +1,21 @@
+// Reexporting reagraph types to wrap in 'use client'.
+'use client';
+
+import { GraphCanvas as GraphUI } from 'reagraph';
+
+export {
+ type GraphEdge,
+ type GraphNode,
+ type GraphCanvasRef,
+ Sphere,
+ useSelection,
+ type CollapseProps
+} from 'reagraph';
+export { type LayoutTypes as GraphLayout } from 'reagraph';
+
+import { ThreeEvent } from '@react-three/fiber';
+
+export type GraphMouseEvent = ThreeEvent;
+export type GraphPointerEvent = ThreeEvent;
+
+export default GraphUI;
diff --git a/rsconcept/frontend/src/components/ui/Label.tsx b/rsconcept/frontend/src/components/ui/Label.tsx
new file mode 100644
index 00000000..5dcb25b8
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/Label.tsx
@@ -0,0 +1,28 @@
+import clsx from 'clsx';
+
+import { CProps } from '../props';
+
+interface LabelProps extends CProps.Label {
+ text?: string;
+}
+
+function Label({ text, className, ...restProps }: LabelProps) {
+ if (!text) {
+ return null;
+ }
+ if (restProps.htmlFor) {
+ return (
+
+ {text}
+
+ );
+ } else {
+ return (
+
+ {text}
+
+ );
+ }
+}
+
+export default Label;
diff --git a/rsconcept/frontend/src/components/ui/LabeledValue.tsx b/rsconcept/frontend/src/components/ui/LabeledValue.tsx
new file mode 100644
index 00000000..d7873a97
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/LabeledValue.tsx
@@ -0,0 +1,21 @@
+import clsx from 'clsx';
+
+import { CProps } from '../props';
+
+interface LabeledValueProps extends CProps.Styling {
+ id?: string;
+ label: string;
+ text: string | number;
+ title?: string;
+}
+
+function LabeledValue({ id, label, text, title, className, ...restProps }: LabeledValueProps) {
+ return (
+
+ {label}
+ {text}
+
+ );
+}
+
+export default LabeledValue;
diff --git a/rsconcept/frontend/src/components/ui/LinkTopic.tsx b/rsconcept/frontend/src/components/ui/LinkTopic.tsx
new file mode 100644
index 00000000..bb2c3195
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/LinkTopic.tsx
@@ -0,0 +1,15 @@
+import { urls } from '@/app/urls';
+import { HelpTopic } from '@/models/miscellaneous';
+
+import TextURL from './TextURL';
+
+interface TextURLProps {
+ text: string;
+ topic: HelpTopic;
+}
+
+function LinkTopic({ text, topic }: TextURLProps) {
+ return ;
+}
+
+export default LinkTopic;
diff --git a/rsconcept/frontend/src/components/ui/Loader.tsx b/rsconcept/frontend/src/components/ui/Loader.tsx
new file mode 100644
index 00000000..91731e98
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/Loader.tsx
@@ -0,0 +1,27 @@
+'use client';
+
+import { ThreeCircles, ThreeDots } from 'react-loader-spinner';
+
+import { useConceptOptions } from '@/context/OptionsContext';
+
+import AnimateFade from '../wrap/AnimateFade';
+
+interface LoaderProps {
+ size?: number;
+ circular?: boolean;
+}
+
+function Loader({ size = 10, circular }: LoaderProps) {
+ const { colors } = useConceptOptions();
+ return (
+
+ {circular ? (
+
+ ) : (
+
+ )}
+
+ );
+}
+
+export default Loader;
diff --git a/rsconcept/frontend/src/components/ui/MiniButton.tsx b/rsconcept/frontend/src/components/ui/MiniButton.tsx
new file mode 100644
index 00000000..cad3c927
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/MiniButton.tsx
@@ -0,0 +1,50 @@
+import clsx from 'clsx';
+
+import { globals } from '@/utils/constants';
+
+import { CProps } from '../props';
+
+interface MiniButtonProps extends CProps.Button {
+ icon: React.ReactNode;
+ noHover?: boolean;
+ noPadding?: boolean;
+}
+
+function MiniButton({
+ icon,
+ noHover,
+ noPadding,
+ tabIndex,
+ title,
+ titleHtml,
+ hideTitle,
+ className,
+ ...restProps
+}: MiniButtonProps) {
+ return (
+
+ {icon}
+
+ );
+}
+
+export default MiniButton;
diff --git a/rsconcept/frontend/src/components/ui/Modal.tsx b/rsconcept/frontend/src/components/ui/Modal.tsx
new file mode 100644
index 00000000..a6aaa2b3
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/Modal.tsx
@@ -0,0 +1,122 @@
+'use client';
+
+import clsx from 'clsx';
+import { motion } from 'framer-motion';
+import { useRef } from 'react';
+
+import useEscapeKey from '@/hooks/useEscapeKey';
+import { animateModal } from '@/styling/animations';
+import { prepareTooltip } from '@/utils/labels';
+
+import { IconClose } from '../Icons';
+import { CProps } from '../props';
+import Button from './Button';
+import MiniButton from './MiniButton';
+import Overlay from './Overlay';
+
+export interface ModalProps extends CProps.Styling {
+ header?: string;
+ submitText?: string;
+ submitInvalidTooltip?: string;
+
+ readonly?: boolean;
+ canSubmit?: boolean;
+ overflowVisible?: boolean;
+
+ hideWindow: () => void;
+ onSubmit?: () => void;
+ onCancel?: () => void;
+
+ children: React.ReactNode;
+}
+
+function Modal({
+ header,
+ hideWindow,
+ onSubmit,
+ readonly,
+ onCancel,
+ canSubmit,
+ submitInvalidTooltip,
+ className,
+ children,
+ overflowVisible,
+ submitText = 'Продолжить',
+ ...restProps
+}: ModalProps) {
+ const ref = useRef(null);
+ useEscapeKey(hideWindow);
+
+ const handleCancel = () => {
+ hideWindow();
+ if (onCancel) onCancel();
+ };
+
+ const handleSubmit = () => {
+ hideWindow();
+ if (onSubmit) onSubmit();
+ };
+
+ return (
+
+
+
+
+
+ }
+ onClick={handleCancel}
+ />
+
+
+ {header ? {header} : null}
+
+
+ {children}
+
+
+
+ {!readonly ? (
+
+ ) : null}
+
+
+
+
+ );
+}
+
+export default Modal;
diff --git a/rsconcept/frontend/src/components/ui/Overlay.tsx b/rsconcept/frontend/src/components/ui/Overlay.tsx
new file mode 100644
index 00000000..cbb009ea
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/Overlay.tsx
@@ -0,0 +1,22 @@
+import clsx from 'clsx';
+
+import { CProps } from '../props';
+
+interface OverlayProps extends CProps.Styling {
+ id?: string;
+ children: React.ReactNode;
+ position?: string;
+ layer?: string;
+}
+
+function Overlay({ children, className, position = 'top-0 right-0', layer = 'z-pop', ...restProps }: OverlayProps) {
+ return (
+
+ );
+}
+
+export default Overlay;
diff --git a/rsconcept/frontend/src/components/ui/PDFViewer/PDFViewer.tsx b/rsconcept/frontend/src/components/ui/PDFViewer/PDFViewer.tsx
new file mode 100644
index 00000000..112c9447
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/PDFViewer/PDFViewer.tsx
@@ -0,0 +1,61 @@
+'use client';
+
+import type { PDFDocumentProxy } from 'pdfjs-dist';
+import { useMemo, useState } from 'react';
+import { Document, Page } from 'react-pdf';
+
+import useWindowSize from '@/hooks/useWindowSize';
+import { graphLightT } from '@/styling/color';
+
+import Overlay from '../Overlay';
+import PageControls from './PageControls';
+
+const MAXIMUM_WIDTH = 1000;
+const MINIMUM_WIDTH = 300;
+
+interface PDFViewerProps {
+ file?: string | ArrayBuffer | Blob;
+ offsetXpx?: number;
+ minWidth?: number;
+}
+
+function PDFViewer({ file, offsetXpx, minWidth = MINIMUM_WIDTH }: PDFViewerProps) {
+ const windowSize = useWindowSize();
+
+ const [pageCount, setPageCount] = useState(0);
+ const [pageNumber, setPageNumber] = useState(1);
+
+ const pageWidth = useMemo(() => {
+ return Math.max(minWidth, Math.min((windowSize?.width ?? 0) - (offsetXpx ?? 0) - 10, MAXIMUM_WIDTH));
+ }, [windowSize, offsetXpx, minWidth]);
+
+ function onDocumentLoadSuccess({ numPages }: PDFDocumentProxy) {
+ setPageCount(numPages);
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default PDFViewer;
diff --git a/rsconcept/frontend/src/components/ui/PDFViewer/PageControls.tsx b/rsconcept/frontend/src/components/ui/PDFViewer/PageControls.tsx
new file mode 100644
index 00000000..511288e4
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/PDFViewer/PageControls.tsx
@@ -0,0 +1,51 @@
+import { IconPageFirst, IconPageLast, IconPageLeft, IconPageRight } from '@/components/Icons';
+
+interface PageControlsProps {
+ pageNumber: number;
+ pageCount: number;
+ setPageNumber: React.Dispatch>;
+}
+
+function PageControls({ pageNumber, pageCount, setPageNumber }: PageControlsProps) {
+ return (
+
+
setPageNumber(1)}
+ disabled={pageNumber < 2}
+ >
+
+
+
setPageNumber(prev => prev - 1)}
+ disabled={pageNumber < 2}
+ >
+
+
+
+ Страница {pageNumber} из {pageCount}
+
+
setPageNumber(prev => prev + 1)}
+ disabled={pageNumber >= pageCount}
+ >
+
+
+
setPageNumber(pageCount)}
+ disabled={pageNumber >= pageCount}
+ >
+
+
+
+ );
+}
+
+export default PageControls;
diff --git a/rsconcept/frontend/src/components/ui/PDFViewer/index.tsx b/rsconcept/frontend/src/components/ui/PDFViewer/index.tsx
new file mode 100644
index 00000000..38c9d624
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/PDFViewer/index.tsx
@@ -0,0 +1 @@
+export { default } from './PDFViewer';
diff --git a/rsconcept/frontend/src/components/ui/PrettyJSON.tsx b/rsconcept/frontend/src/components/ui/PrettyJSON.tsx
new file mode 100644
index 00000000..53304125
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/PrettyJSON.tsx
@@ -0,0 +1,9 @@
+interface PrettyJsonProps {
+ data: unknown;
+}
+
+function PrettyJson({ data }: PrettyJsonProps) {
+ return {JSON.stringify(data, null, 2)} ;
+}
+
+export default PrettyJson;
diff --git a/rsconcept/frontend/src/components/ui/SearchBar.tsx b/rsconcept/frontend/src/components/ui/SearchBar.tsx
new file mode 100644
index 00000000..a1b9fcfa
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/SearchBar.tsx
@@ -0,0 +1,39 @@
+import clsx from 'clsx';
+
+import { IconSearch } from '../Icons';
+import { CProps } from '../props';
+import Overlay from './Overlay';
+import TextInput from './TextInput';
+
+interface SearchBarProps extends CProps.Styling {
+ value: string;
+ noIcon?: boolean;
+ id?: string;
+ placeholder?: string;
+ onChange?: (newValue: string) => void;
+ noBorder?: boolean;
+}
+
+function SearchBar({ id, value, noIcon, onChange, noBorder, placeholder = 'Поиск', ...restProps }: SearchBarProps) {
+ return (
+
+ {!noIcon ? (
+
+
+
+ ) : null}
+ (onChange ? onChange(event.target.value) : undefined)}
+ />
+
+ );
+}
+
+export default SearchBar;
diff --git a/rsconcept/frontend/src/components/ui/SelectMulti.tsx b/rsconcept/frontend/src/components/ui/SelectMulti.tsx
new file mode 100644
index 00000000..48fe6618
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/SelectMulti.tsx
@@ -0,0 +1,131 @@
+'use client';
+
+import { useMemo } from 'react';
+import Select, {
+ ClearIndicatorProps,
+ components,
+ DropdownIndicatorProps,
+ GroupBase,
+ Props,
+ StylesConfig
+} from 'react-select';
+
+import { useConceptOptions } from '@/context/OptionsContext';
+import useWindowSize from '@/hooks/useWindowSize';
+import { selectDarkT, selectLightT } from '@/styling/color';
+
+import { IconClose, IconDropArrow, IconDropArrowUp } from '../Icons';
+
+function DropdownIndicator = GroupBase >(
+ props: DropdownIndicatorProps
+) {
+ return (
+ components.DropdownIndicator && (
+
+ {props.selectProps.menuIsOpen ? : }
+
+ )
+ );
+}
+
+function ClearIndicator = GroupBase >(
+ props: ClearIndicatorProps
+) {
+ return (
+ components.ClearIndicator && (
+
+
+
+ )
+ );
+}
+
+export interface SelectMultiProps = GroupBase >
+ extends Omit, 'theme' | 'menuPortalTarget'> {
+ noPortal?: boolean;
+}
+
+function SelectMulti = GroupBase >({
+ noPortal,
+ ...restProps
+}: SelectMultiProps ) {
+ const { darkMode, colors } = useConceptOptions();
+ const size = useWindowSize();
+ const themeColors = useMemo(() => (!darkMode ? selectLightT : selectDarkT), [darkMode]);
+
+ const adjustedStyles: StylesConfig = useMemo(
+ () => ({
+ container: defaultStyles => ({
+ ...defaultStyles,
+ borderRadius: '0.25rem'
+ }),
+ control: (styles, { isDisabled }) => ({
+ ...styles,
+ borderRadius: '0.25rem',
+ cursor: isDisabled ? 'not-allowed' : 'pointer',
+ boxShadow: 'none'
+ }),
+ option: (styles, { isSelected }) => ({
+ ...styles,
+ backgroundColor: isSelected ? colors.bgSelected : styles.backgroundColor,
+ color: isSelected ? colors.fgSelected : styles.color,
+ borderWidth: '1px',
+ borderColor: colors.border
+ }),
+ menuPortal: styles => ({
+ ...styles,
+ zIndex: 9999
+ }),
+ menuList: styles => ({
+ ...styles,
+ padding: 0
+ }),
+ input: styles => ({ ...styles }),
+ placeholder: styles => ({ ...styles }),
+ multiValue: styles => ({
+ ...styles,
+ borderRadius: '0.5rem',
+ backgroundColor: colors.bgSelected
+ }),
+ dropdownIndicator: base => ({
+ ...base,
+ paddingTop: 0,
+ paddingBottom: 0
+ }),
+ clearIndicator: base => ({
+ ...base,
+ paddingTop: 0,
+ paddingBottom: 0
+ })
+ }),
+ [colors]
+ );
+
+ return (
+ 'Список пуст'}
+ components={{ DropdownIndicator, ClearIndicator }}
+ theme={theme => ({
+ ...theme,
+ borderRadius: 0,
+ spacing: {
+ ...theme.spacing, // prettier: split-lines
+ baseUnit: size.isSmall ? 2 : 4,
+ menuGutter: size.isSmall ? 4 : 8,
+ controlHeight: size.isSmall ? 28 : 38
+ },
+ colors: {
+ ...theme.colors,
+ ...themeColors
+ }
+ })}
+ menuPortalTarget={!noPortal ? document.body : null}
+ styles={adjustedStyles}
+ classNames={{ container: () => 'focus-frame' }}
+ {...restProps}
+ />
+ );
+}
+
+export default SelectMulti;
diff --git a/rsconcept/frontend/src/components/ui/SelectSingle.tsx b/rsconcept/frontend/src/components/ui/SelectSingle.tsx
new file mode 100644
index 00000000..8a76a980
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/SelectSingle.tsx
@@ -0,0 +1,129 @@
+'use client';
+
+import { useMemo } from 'react';
+import Select, {
+ ClearIndicatorProps,
+ components,
+ DropdownIndicatorProps,
+ GroupBase,
+ Props,
+ StylesConfig
+} from 'react-select';
+
+import { useConceptOptions } from '@/context/OptionsContext';
+import useWindowSize from '@/hooks/useWindowSize';
+import { selectDarkT, selectLightT } from '@/styling/color';
+
+import { IconClose, IconDropArrow, IconDropArrowUp } from '../Icons';
+
+function DropdownIndicator = GroupBase >(
+ props: DropdownIndicatorProps
+) {
+ return (
+ components.DropdownIndicator && (
+
+ {props.selectProps.menuIsOpen ? : }
+
+ )
+ );
+}
+
+function ClearIndicator = GroupBase >(
+ props: ClearIndicatorProps
+) {
+ return (
+ components.ClearIndicator && (
+
+
+
+ )
+ );
+}
+
+export interface SelectSingleProps = GroupBase >
+ extends Omit, 'theme' | 'menuPortalTarget'> {
+ noPortal?: boolean;
+ noBorder?: boolean;
+}
+
+function SelectSingle = GroupBase >({
+ noPortal,
+ noBorder,
+ ...restProps
+}: SelectSingleProps ) {
+ const { darkMode, colors } = useConceptOptions();
+ const size = useWindowSize();
+ const themeColors = useMemo(() => (!darkMode ? selectLightT : selectDarkT), [darkMode]);
+
+ const adjustedStyles: StylesConfig = useMemo(
+ () => ({
+ container: defaultStyles => ({
+ ...defaultStyles,
+ borderRadius: '0.25rem'
+ }),
+ control: (defaultStyles, { isDisabled }) => ({
+ ...defaultStyles,
+ borderRadius: '0.25rem',
+ ...(noBorder ? { borderWidth: 0 } : {}),
+ cursor: isDisabled ? 'not-allowed' : 'pointer',
+ boxShadow: 'none'
+ }),
+ menuPortal: defaultStyles => ({
+ ...defaultStyles,
+ zIndex: 9999
+ }),
+ menuList: defaultStyles => ({
+ ...defaultStyles,
+ padding: 0
+ }),
+ option: (defaultStyles, { isSelected }) => ({
+ ...defaultStyles,
+ backgroundColor: isSelected ? colors.bgSelected : defaultStyles.backgroundColor,
+ color: isSelected ? colors.fgSelected : defaultStyles.color,
+ borderWidth: '1px',
+ borderColor: colors.border
+ }),
+ input: defaultStyles => ({ ...defaultStyles }),
+ placeholder: defaultStyles => ({ ...defaultStyles }),
+ singleValue: defaultStyles => ({ ...defaultStyles }),
+ dropdownIndicator: base => ({
+ ...base,
+ paddingTop: 0,
+ paddingBottom: 0
+ }),
+ clearIndicator: base => ({
+ ...base,
+ paddingTop: 0,
+ paddingBottom: 0
+ })
+ }),
+ [colors, noBorder]
+ );
+
+ return (
+ 'Список пуст'}
+ components={{ DropdownIndicator, ClearIndicator }}
+ theme={theme => ({
+ ...theme,
+ borderRadius: 0,
+ spacing: {
+ ...theme.spacing, // prettier: split-lines
+ baseUnit: size.isSmall ? 2 : 4,
+ menuGutter: size.isSmall ? 4 : 8,
+ controlHeight: size.isSmall ? 28 : 38
+ },
+ colors: {
+ ...theme.colors,
+ ...themeColors
+ }
+ })}
+ menuPortalTarget={!noPortal ? document.body : null}
+ styles={adjustedStyles}
+ classNames={{ container: () => 'focus-frame' }}
+ {...restProps}
+ />
+ );
+}
+
+export default SelectSingle;
diff --git a/rsconcept/frontend/src/components/ui/SelectTree.tsx b/rsconcept/frontend/src/components/ui/SelectTree.tsx
new file mode 100644
index 00000000..939a6cd2
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/SelectTree.tsx
@@ -0,0 +1,120 @@
+import clsx from 'clsx';
+import { AnimatePresence, motion } from 'framer-motion';
+import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
+
+import { animateSideAppear } from '@/styling/animations';
+import { globals } from '@/utils/constants';
+
+import { IconDropArrow, IconPageRight } from '../Icons';
+import { CProps } from '../props';
+import MiniButton from './MiniButton';
+import Overlay from './Overlay';
+
+interface SelectTreeProps extends CProps.Styling {
+ items: ItemType[];
+ value: ItemType;
+ setValue: (newItem: ItemType) => void;
+ getParent: (item: ItemType) => ItemType;
+ getLabel: (item: ItemType) => string;
+ getDescription: (item: ItemType) => string;
+ prefix: string;
+}
+
+function SelectTree({
+ items,
+ value,
+ getParent,
+ getLabel,
+ getDescription,
+ setValue,
+ prefix,
+ ...restProps
+}: SelectTreeProps) {
+ const foldable = useMemo(
+ () => new Set(items.filter(item => getParent(item) !== item).map(item => getParent(item))),
+ [items, getParent]
+ );
+ const [folded, setFolded] = useState(items);
+
+ useLayoutEffect(() => {
+ setFolded(items.filter(item => getParent(value) !== item && getParent(getParent(value)) !== item));
+ }, [value, getParent, items]);
+
+ const onFoldItem = useCallback(
+ (target: ItemType, showChildren: boolean) => {
+ setFolded(prev =>
+ items.filter(item => {
+ if (item === target) {
+ return !showChildren;
+ }
+ if (!showChildren && (getParent(item) === target || getParent(getParent(item)) === target)) {
+ return true;
+ } else {
+ return prev.includes(item);
+ }
+ })
+ );
+ },
+ [items, getParent]
+ );
+
+ const handleClickFold = useCallback(
+ (event: CProps.EventMouse, target: ItemType, showChildren: boolean) => {
+ event.preventDefault();
+ event.stopPropagation();
+ onFoldItem(target, showChildren);
+ },
+ [onFoldItem]
+ );
+
+ const handleSetValue = useCallback(
+ (event: CProps.EventMouse, target: ItemType) => {
+ event.preventDefault();
+ event.stopPropagation();
+ setValue(target);
+ },
+ [setValue]
+ );
+
+ return (
+
+
+ {items.map((item, index) =>
+ getParent(item) === item || !folded.includes(getParent(item)) ? (
+ handleSetValue(event, item)}
+ initial={{ ...animateSideAppear.initial }}
+ animate={{ ...animateSideAppear.animate }}
+ exit={{ ...animateSideAppear.exit }}
+ >
+ {foldable.has(item) ? (
+
+ : }
+ onClick={event => handleClickFold(event, item, folded.includes(item))}
+ />
+
+ ) : null}
+ {getParent(item) === item ? getLabel(item) : `- ${getLabel(item).toLowerCase()}`}
+
+ ) : null
+ )}
+
+
+ );
+}
+
+export default SelectTree;
diff --git a/rsconcept/frontend/src/components/ui/SelectorButton.tsx b/rsconcept/frontend/src/components/ui/SelectorButton.tsx
new file mode 100644
index 00000000..da38e151
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/SelectorButton.tsx
@@ -0,0 +1,54 @@
+import clsx from 'clsx';
+
+import { globals } from '@/utils/constants';
+
+import { CProps } from '../props';
+
+interface SelectorButtonProps extends CProps.Button {
+ text?: string;
+ icon?: React.ReactNode;
+
+ colors?: string;
+ transparent?: boolean;
+}
+
+function SelectorButton({
+ text,
+ icon,
+ title,
+ titleHtml,
+ colors = 'clr-btn-default',
+ className,
+ transparent,
+ hideTitle,
+ ...restProps
+}: SelectorButtonProps) {
+ return (
+
+ {icon ? icon : null}
+ {text ? {text}
: null}
+
+ );
+}
+
+export default SelectorButton;
diff --git a/rsconcept/frontend/src/components/ui/SubmitButton.tsx b/rsconcept/frontend/src/components/ui/SubmitButton.tsx
new file mode 100644
index 00000000..86c1a570
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/SubmitButton.tsx
@@ -0,0 +1,33 @@
+import clsx from 'clsx';
+
+import { CProps } from '../props';
+
+interface SubmitButtonProps extends CProps.Button {
+ text?: string;
+ loading?: boolean;
+ icon?: React.ReactNode;
+}
+
+function SubmitButton({ text = 'ОК', icon, disabled, loading, className, ...restProps }: SubmitButtonProps) {
+ return (
+
+ {icon ? {icon} : null}
+ {text ? {text} : null}
+
+ );
+}
+
+export default SubmitButton;
diff --git a/rsconcept/frontend/src/components/ui/TabLabel.tsx b/rsconcept/frontend/src/components/ui/TabLabel.tsx
new file mode 100644
index 00000000..99be8694
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/TabLabel.tsx
@@ -0,0 +1,39 @@
+import clsx from 'clsx';
+import type { TabProps as TabPropsImpl } from 'react-tabs';
+import { Tab as TabImpl } from 'react-tabs';
+
+import { globals } from '@/utils/constants';
+
+import { CProps } from '../props';
+
+interface TabLabelProps extends Omit, CProps.Titled {
+ label?: string;
+}
+
+function TabLabel({ label, title, titleHtml, hideTitle, className, ...otherProps }: TabLabelProps) {
+ return (
+
+ {label}
+
+ );
+}
+
+TabLabel.tabsRole = 'Tab';
+
+export default TabLabel;
diff --git a/rsconcept/frontend/src/components/ui/TextArea.tsx b/rsconcept/frontend/src/components/ui/TextArea.tsx
new file mode 100644
index 00000000..cfdbc0e2
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/TextArea.tsx
@@ -0,0 +1,58 @@
+import clsx from 'clsx';
+
+import { CProps } from '../props';
+import Label from './Label';
+
+export interface TextAreaProps extends CProps.Editor, CProps.Colors, CProps.TextArea {
+ dense?: boolean;
+ noResize?: boolean;
+}
+
+function TextArea({
+ id,
+ label,
+ required,
+ rows,
+ dense,
+ noBorder,
+ noOutline,
+ noResize,
+ className,
+ colors = 'clr-input',
+ ...restProps
+}: TextAreaProps) {
+ return (
+
+
+
+
+ );
+}
+
+export default TextArea;
diff --git a/rsconcept/frontend/src/components/ui/TextInput.tsx b/rsconcept/frontend/src/components/ui/TextInput.tsx
new file mode 100644
index 00000000..70c125ff
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/TextInput.tsx
@@ -0,0 +1,63 @@
+import clsx from 'clsx';
+
+import { CProps } from '../props';
+import Label from './Label';
+
+interface TextInputProps extends CProps.Editor, CProps.Colors, CProps.Input {
+ dense?: boolean;
+ allowEnter?: boolean;
+}
+
+function preventEnterCapture(event: React.KeyboardEvent) {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ }
+}
+
+function TextInput({
+ id,
+ label,
+ dense,
+ noBorder,
+ noOutline,
+ allowEnter,
+ disabled,
+ className,
+ colors = 'clr-input',
+ onKeyDown,
+ ...restProps
+}: TextInputProps) {
+ return (
+
+
+
+
+ );
+}
+
+export default TextInput;
diff --git a/rsconcept/frontend/src/components/ui/TextURL.tsx b/rsconcept/frontend/src/components/ui/TextURL.tsx
new file mode 100644
index 00000000..3c9ffcb3
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/TextURL.tsx
@@ -0,0 +1,30 @@
+import { Link } from 'react-router-dom';
+
+interface TextURLProps {
+ text: string;
+ title?: string;
+ href?: string;
+ color?: string;
+ onClick?: () => void;
+}
+
+function TextURL({ text, href, title, color = 'clr-text-url', onClick }: TextURLProps) {
+ const design = `cursor-pointer hover:underline ${color}`;
+ if (href) {
+ return (
+
+ {text}
+
+ );
+ } else if (onClick) {
+ return (
+
+ {text}
+
+ );
+ } else {
+ return null;
+ }
+}
+
+export default TextURL;
diff --git a/rsconcept/frontend/src/components/ui/Tooltip.tsx b/rsconcept/frontend/src/components/ui/Tooltip.tsx
new file mode 100644
index 00000000..627894d7
--- /dev/null
+++ b/rsconcept/frontend/src/components/ui/Tooltip.tsx
@@ -0,0 +1,55 @@
+'use client';
+
+import clsx from 'clsx';
+import { ReactNode } from 'react';
+import { createPortal } from 'react-dom';
+import { ITooltip, Tooltip as TooltipImpl } from 'react-tooltip';
+
+import { useConceptOptions } from '@/context/OptionsContext';
+
+export type { PlacesType } from 'react-tooltip';
+
+interface TooltipProps extends Omit {
+ layer?: string;
+ text?: string;
+}
+
+function Tooltip({
+ text,
+ children,
+ layer = 'z-tooltip',
+ place = 'bottom',
+ className,
+ style,
+ ...restProps
+}: TooltipProps) {
+ const { darkMode } = useConceptOptions();
+ if (typeof window === 'undefined') {
+ return null;
+ }
+ return createPortal(
+
+ {text ? text : null}
+ {children as ReactNode}
+ ,
+ document.body
+ );
+}
+
+export default Tooltip;
diff --git a/rsconcept/frontend/src/components/wrap/AnimateFade.tsx b/rsconcept/frontend/src/components/wrap/AnimateFade.tsx
new file mode 100644
index 00000000..bae27dd3
--- /dev/null
+++ b/rsconcept/frontend/src/components/wrap/AnimateFade.tsx
@@ -0,0 +1,29 @@
+import { motion } from 'framer-motion';
+
+import { animateFade } from '@/styling/animations';
+
+import { CProps } from '../props';
+
+interface AnimateFadeProps extends CProps.AnimatedDiv {
+ noFadeIn?: boolean;
+ noFadeOut?: boolean;
+ hideContent?: boolean;
+}
+
+function AnimateFade({ style, noFadeIn, noFadeOut, children, hideContent, ...restProps }: AnimateFadeProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+export default AnimateFade;
diff --git a/rsconcept/frontend/src/components/wrap/DataLoader.tsx b/rsconcept/frontend/src/components/wrap/DataLoader.tsx
new file mode 100644
index 00000000..20db7834
--- /dev/null
+++ b/rsconcept/frontend/src/components/wrap/DataLoader.tsx
@@ -0,0 +1,37 @@
+import { AnimatePresence } from 'framer-motion';
+
+import InfoError, { ErrorData } from '../info/InfoError';
+import { CProps } from '../props';
+import Loader from '../ui/Loader';
+import AnimateFade from './AnimateFade';
+
+interface DataLoaderProps extends CProps.AnimatedDiv {
+ id: string;
+
+ isLoading?: boolean;
+ error?: ErrorData;
+ hasNoData?: boolean;
+
+ children: React.ReactNode;
+}
+
+function DataLoader({ id, isLoading, hasNoData, error, children, ...restProps }: DataLoaderProps) {
+ return (
+
+ {!isLoading && !error && !hasNoData ? (
+
+ {children}
+
+ ) : null}
+ {!isLoading && !error && hasNoData ? (
+
+ Данные не загружены
+
+ ) : null}
+ {isLoading ? : null}
+ {error ? : null}
+
+ );
+}
+
+export default DataLoader;
diff --git a/rsconcept/frontend/src/components/wrap/ExpectedAnonymous.tsx b/rsconcept/frontend/src/components/wrap/ExpectedAnonymous.tsx
new file mode 100644
index 00000000..180a6989
--- /dev/null
+++ b/rsconcept/frontend/src/components/wrap/ExpectedAnonymous.tsx
@@ -0,0 +1,33 @@
+import { urls } from '@/app/urls';
+import { useAuth } from '@/context/AuthContext';
+import { useConceptNavigation } from '@/context/NavigationContext';
+
+import TextURL from '../ui/TextURL';
+
+function ExpectedAnonymous() {
+ const { user, logout } = useAuth();
+ const router = useConceptNavigation();
+
+ function logoutAndRedirect() {
+ logout(() => router.push(urls.login));
+ }
+
+ return (
+
+
{`Вы вошли в систему как ${user?.username ?? ''}`}
+
+
+ |
+
+ |
+
+ |
+
+ Выйти
+
+
+
+ );
+}
+
+export default ExpectedAnonymous;
diff --git a/rsconcept/frontend/src/components/wrap/RequireAuth.tsx b/rsconcept/frontend/src/components/wrap/RequireAuth.tsx
new file mode 100644
index 00000000..7cf30688
--- /dev/null
+++ b/rsconcept/frontend/src/components/wrap/RequireAuth.tsx
@@ -0,0 +1,27 @@
+'use client';
+
+import { useAuth } from '@/context/AuthContext';
+
+import TextURL from '../ui/TextURL';
+
+interface RequireAuthProps {
+ children: React.ReactNode;
+}
+
+function RequireAuth({ children }: RequireAuthProps) {
+ const { user } = useAuth();
+ if (user) {
+ return children;
+ } else {
+ return (
+
+
Пожалуйста войдите в систему
+
+
+
+
+ );
+ }
+}
+
+export default RequireAuth;
diff --git a/rsconcept/frontend/src/context/AccessModeContext.tsx b/rsconcept/frontend/src/context/AccessModeContext.tsx
new file mode 100644
index 00000000..1a0732a1
--- /dev/null
+++ b/rsconcept/frontend/src/context/AccessModeContext.tsx
@@ -0,0 +1,30 @@
+'use client';
+
+import { createContext, useContext, useState } from 'react';
+
+import { UserLevel } from '@/models/user';
+import { contextOutsideScope } from '@/utils/labels';
+
+interface IAccessModeContext {
+ accessLevel: UserLevel;
+ setAccessLevel: React.Dispatch>;
+}
+
+const AccessContext = createContext(null);
+export const useAccessMode = () => {
+ const context = useContext(AccessContext);
+ if (!context) {
+ throw new Error(contextOutsideScope('useAccessMode', 'AccessModeState'));
+ }
+ return context;
+};
+
+interface AccessModeStateProps {
+ children: React.ReactNode;
+}
+
+export const AccessModeState = ({ children }: AccessModeStateProps) => {
+ const [accessLevel, setAccessLevel] = useState(UserLevel.READER);
+
+ return {children} ;
+};
diff --git a/rsconcept/frontend/src/context/AuthContext.tsx b/rsconcept/frontend/src/context/AuthContext.tsx
new file mode 100644
index 00000000..073ec11d
--- /dev/null
+++ b/rsconcept/frontend/src/context/AuthContext.tsx
@@ -0,0 +1,214 @@
+'use client';
+
+import { createContext, useCallback, useContext, useLayoutEffect, useState } from 'react';
+
+import {
+ type DataCallback,
+ getAuth,
+ patchPassword,
+ postLogin,
+ postLogout,
+ postRequestPasswordReset,
+ postResetPassword,
+ postSignup,
+ postValidatePasswordToken
+} from '@/app/backendAPI';
+import { type ErrorData } from '@/components/info/InfoError';
+import {
+ ICurrentUser,
+ IPasswordTokenData,
+ IRequestPasswordData,
+ IResetPasswordData,
+ IUserInfo,
+ IUserLoginData,
+ IUserProfile,
+ IUserSignupData,
+ IUserUpdatePassword
+} from '@/models/user';
+import { contextOutsideScope } from '@/utils/labels';
+
+import { useUsers } from './UsersContext';
+
+interface IAuthContext {
+ user: ICurrentUser | undefined;
+ login: (data: IUserLoginData, callback?: DataCallback) => void;
+ logout: (callback?: DataCallback) => void;
+ signup: (data: IUserSignupData, callback?: DataCallback) => void;
+ updatePassword: (data: IUserUpdatePassword, callback?: () => void) => void;
+ requestPasswordReset: (data: IRequestPasswordData, callback?: () => void) => void;
+ validateToken: (data: IPasswordTokenData, callback?: () => void) => void;
+ resetPassword: (data: IResetPasswordData, callback?: () => void) => void;
+ loading: boolean;
+ error: ErrorData;
+ setError: (error: ErrorData) => void;
+}
+
+const AuthContext = createContext(null);
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error(contextOutsideScope('useAuth', 'AuthState'));
+ }
+ return context;
+};
+
+interface AuthStateProps {
+ children: React.ReactNode;
+}
+
+export const AuthState = ({ children }: AuthStateProps) => {
+ const { users } = useUsers();
+ const [user, setUser] = useState(undefined);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(undefined);
+
+ const reload = useCallback(
+ (callback?: () => void) => {
+ getAuth({
+ onError: () => setUser(undefined),
+ setLoading: setLoading,
+ onSuccess: currentUser => {
+ if (currentUser.id) {
+ setUser(currentUser);
+ } else {
+ setUser(undefined);
+ }
+ if (callback) callback();
+ }
+ });
+ },
+ [setUser]
+ );
+
+ function login(data: IUserLoginData, callback?: DataCallback) {
+ setError(undefined);
+ postLogin({
+ data: data,
+ showError: false,
+ setLoading: setLoading,
+ onError: setError,
+ onSuccess: newData =>
+ reload(() => {
+ if (callback) callback(newData);
+ })
+ });
+ }
+
+ function logout(callback?: DataCallback) {
+ setError(undefined);
+ postLogout({
+ showError: true,
+ onSuccess: newData =>
+ reload(() => {
+ if (callback) callback(newData);
+ })
+ });
+ }
+
+ function signup(data: IUserSignupData, callback?: DataCallback) {
+ setError(undefined);
+ postSignup({
+ data: data,
+ showError: true,
+ setLoading: setLoading,
+ onError: setError,
+ onSuccess: newData =>
+ reload(() => {
+ users.push(newData as IUserInfo);
+ if (callback) callback(newData);
+ })
+ });
+ }
+
+ const updatePassword = useCallback(
+ (data: IUserUpdatePassword, callback?: () => void) => {
+ setError(undefined);
+ patchPassword({
+ data: data,
+ showError: true,
+ setLoading: setLoading,
+ onError: setError,
+ onSuccess: () =>
+ reload(() => {
+ if (callback) callback();
+ })
+ });
+ },
+ [reload]
+ );
+
+ const requestPasswordReset = useCallback(
+ (data: IRequestPasswordData, callback?: () => void) => {
+ setError(undefined);
+ postRequestPasswordReset({
+ data: data,
+ showError: false,
+ setLoading: setLoading,
+ onError: setError,
+ onSuccess: () =>
+ reload(() => {
+ if (callback) callback();
+ })
+ });
+ },
+ [reload]
+ );
+
+ const validateToken = useCallback(
+ (data: IPasswordTokenData, callback?: () => void) => {
+ setError(undefined);
+ postValidatePasswordToken({
+ data: data,
+ showError: false,
+ setLoading: setLoading,
+ onError: setError,
+ onSuccess: () =>
+ reload(() => {
+ if (callback) callback();
+ })
+ });
+ },
+ [reload]
+ );
+
+ const resetPassword = useCallback(
+ (data: IResetPasswordData, callback?: () => void) => {
+ setError(undefined);
+ postResetPassword({
+ data: data,
+ showError: false,
+ setLoading: setLoading,
+ onError: setError,
+ onSuccess: () =>
+ reload(() => {
+ if (callback) callback();
+ })
+ });
+ },
+ [reload]
+ );
+
+ useLayoutEffect(() => {
+ reload();
+ }, [reload]);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/rsconcept/frontend/src/context/LibraryContext.tsx b/rsconcept/frontend/src/context/LibraryContext.tsx
new file mode 100644
index 00000000..a93a4ce5
--- /dev/null
+++ b/rsconcept/frontend/src/context/LibraryContext.tsx
@@ -0,0 +1,281 @@
+'use client';
+
+import { createContext, useCallback, useContext, useEffect, useState } from 'react';
+
+import {
+ DataCallback,
+ deleteLibraryItem,
+ getAdminLibrary,
+ getLibrary,
+ getRSFormDetails,
+ getTemplates,
+ postCloneLibraryItem,
+ postCreateLibraryItem,
+ postRSFormFromFile
+} from '@/app/backendAPI';
+import { ErrorData } from '@/components/info/InfoError';
+import { ILibraryItem, LibraryItemID } from '@/models/library';
+import { ILibraryCreateData } from '@/models/library';
+import { matchLibraryItem, matchLibraryItemLocation } from '@/models/libraryAPI';
+import { ILibraryFilter } from '@/models/miscellaneous';
+import { IRSForm, IRSFormCloneData, IRSFormData } from '@/models/rsform';
+import { RSFormLoader } from '@/models/RSFormLoader';
+import { contextOutsideScope } from '@/utils/labels';
+
+import { useAuth } from './AuthContext';
+import { useConceptOptions } from './OptionsContext';
+
+interface ILibraryContext {
+ items: ILibraryItem[];
+ templates: ILibraryItem[];
+ loading: boolean;
+ processing: boolean;
+ error: ErrorData;
+ setError: (error: ErrorData) => void;
+
+ applyFilter: (params: ILibraryFilter) => ILibraryItem[];
+ retrieveTemplate: (templateID: LibraryItemID, callback: (schema: IRSForm) => void) => void;
+ createItem: (data: ILibraryCreateData, callback?: DataCallback) => void;
+ cloneItem: (target: LibraryItemID, data: IRSFormCloneData, callback: DataCallback) => void;
+ destroyItem: (target: LibraryItemID, callback?: () => void) => void;
+
+ localUpdateItem: (data: ILibraryItem) => void;
+ localUpdateTimestamp: (target: LibraryItemID) => void;
+}
+
+const LibraryContext = createContext(null);
+export const useLibrary = (): ILibraryContext => {
+ const context = useContext(LibraryContext);
+ if (context === null) {
+ throw new Error(contextOutsideScope('useLibrary', 'LibraryState'));
+ }
+ return context;
+};
+
+interface LibraryStateProps {
+ children: React.ReactNode;
+}
+
+export const LibraryState = ({ children }: LibraryStateProps) => {
+ const { user } = useAuth();
+ const { adminMode } = useConceptOptions();
+
+ const [items, setItems] = useState([]);
+ const [templates, setTemplates] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [processing, setProcessing] = useState(false);
+ const [error, setError] = useState(undefined);
+ const [cachedTemplates, setCachedTemplates] = useState([]);
+
+ const applyFilter = useCallback(
+ (filter: ILibraryFilter) => {
+ let result = items;
+ if (filter.head) {
+ result = result.filter(item => item.location.startsWith(filter.head!));
+ }
+ if (filter.isVisible !== undefined) {
+ result = result.filter(item => filter.isVisible === item.visible);
+ }
+ if (filter.isOwned !== undefined) {
+ result = result.filter(item => filter.isOwned === (item.owner === user?.id));
+ }
+ if (filter.isSubscribed !== undefined) {
+ result = result.filter(item => filter.isSubscribed == user?.subscriptions.includes(item.id));
+ }
+ if (filter.isEditor !== undefined) {
+ result = result.filter(item => filter.isEditor == user?.editor.includes(item.id));
+ }
+ if (filter.query) {
+ result = result.filter(item => matchLibraryItem(item, filter.query!));
+ }
+ if (filter.path) {
+ result = result.filter(item => matchLibraryItemLocation(item, filter.path!));
+ }
+ return result;
+ },
+ [items, user]
+ );
+
+ const retrieveTemplate = useCallback(
+ (templateID: LibraryItemID, callback: (schema: IRSForm) => void) => {
+ const cached = cachedTemplates.find(schema => schema.id == templateID);
+ if (cached) {
+ callback(cached);
+ return;
+ }
+ setError(undefined);
+ getRSFormDetails(String(templateID), '', {
+ showError: true,
+ setLoading: setProcessing,
+ onError: setError,
+ onSuccess: data => {
+ const schema = new RSFormLoader(data).produceRSForm();
+ setCachedTemplates(prev => [...prev, schema]);
+ callback(schema);
+ }
+ });
+ },
+ [cachedTemplates]
+ );
+
+ const reloadItems = useCallback(
+ (callback?: () => void) => {
+ setItems([]);
+ setError(undefined);
+ if (user?.is_staff && adminMode) {
+ getAdminLibrary({
+ setLoading: setLoading,
+ showError: true,
+ onError: setError,
+ onSuccess: newData => {
+ setItems(newData);
+ if (callback) callback();
+ }
+ });
+ } else {
+ getLibrary({
+ setLoading: setLoading,
+ showError: true,
+ onError: setError,
+ onSuccess: newData => {
+ setItems(newData);
+ if (callback) callback();
+ }
+ });
+ }
+ },
+ [user, adminMode]
+ );
+
+ const reloadTemplates = useCallback(() => {
+ setTemplates([]);
+ getTemplates({
+ showError: true,
+ onSuccess: newData => setTemplates(newData)
+ });
+ }, []);
+
+ useEffect(() => {
+ reloadItems();
+ }, [reloadItems]);
+
+ useEffect(() => {
+ reloadTemplates();
+ }, [reloadTemplates]);
+
+ const localUpdateItem = useCallback(
+ (data: ILibraryItem) => {
+ const libraryItem = items.find(item => item.id === data.id);
+ if (libraryItem) Object.assign(libraryItem, data);
+ },
+ [items]
+ );
+
+ const localUpdateTimestamp = useCallback(
+ (target: LibraryItemID) => {
+ const libraryItem = items.find(item => item.id === target);
+ if (libraryItem) {
+ libraryItem.time_update = Date();
+ }
+ },
+ [items]
+ );
+
+ const createItem = useCallback(
+ (data: ILibraryCreateData, callback?: DataCallback) => {
+ const onSuccess = (newSchema: ILibraryItem) =>
+ reloadItems(() => {
+ if (user && !user.subscriptions.includes(newSchema.id)) {
+ user.subscriptions.push(newSchema.id);
+ }
+ if (callback) callback(newSchema);
+ });
+ setError(undefined);
+ if (data.file) {
+ postRSFormFromFile({
+ data: data,
+ showError: true,
+ setLoading: setProcessing,
+ onError: setError,
+ onSuccess: onSuccess
+ });
+ } else {
+ postCreateLibraryItem({
+ data: data,
+ showError: true,
+ setLoading: setProcessing,
+ onError: setError,
+ onSuccess: onSuccess
+ });
+ }
+ },
+ [reloadItems, user]
+ );
+
+ const destroyItem = useCallback(
+ (target: LibraryItemID, callback?: () => void) => {
+ setError(undefined);
+ deleteLibraryItem(String(target), {
+ showError: true,
+ setLoading: setProcessing,
+ onError: setError,
+ onSuccess: () =>
+ reloadItems(() => {
+ if (user && user.subscriptions.includes(target)) {
+ user.subscriptions.splice(
+ user.subscriptions.findIndex(item => item === target),
+ 1
+ );
+ }
+ if (callback) callback();
+ })
+ });
+ },
+ [setError, reloadItems, user]
+ );
+
+ const cloneItem = useCallback(
+ (target: LibraryItemID, data: IRSFormCloneData, callback: DataCallback) => {
+ if (!user) {
+ return;
+ }
+ setError(undefined);
+ postCloneLibraryItem(String(target), {
+ data: data,
+ showError: true,
+ setLoading: setProcessing,
+ onError: setError,
+ onSuccess: newSchema =>
+ reloadItems(() => {
+ if (user && !user.subscriptions.includes(newSchema.id)) {
+ user.subscriptions.push(newSchema.id);
+ }
+ if (callback) callback(newSchema);
+ })
+ });
+ },
+ [reloadItems, setError, user]
+ );
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/rsconcept/frontend/src/context/NavigationContext.tsx b/rsconcept/frontend/src/context/NavigationContext.tsx
new file mode 100644
index 00000000..91e0accf
--- /dev/null
+++ b/rsconcept/frontend/src/context/NavigationContext.tsx
@@ -0,0 +1,122 @@
+'use client';
+
+import { createContext, useCallback, useContext, useEffect, useState } from 'react';
+import { useLocation, useNavigate } from 'react-router-dom';
+
+import { globals } from '@/utils/constants';
+import { contextOutsideScope } from '@/utils/labels';
+
+interface INavigationContext {
+ push: (path: string, newTab?: boolean) => void;
+ replace: (path: string) => void;
+ back: () => void;
+ forward: () => void;
+
+ canBack: () => boolean;
+
+ isBlocked: boolean;
+ setIsBlocked: (value: boolean) => void;
+}
+
+const NavigationContext = createContext(null);
+export const useConceptNavigation = () => {
+ const context = useContext(NavigationContext);
+ if (!context) {
+ throw new Error(contextOutsideScope('useConceptNavigation', 'NavigationState'));
+ }
+ return context;
+};
+
+interface NavigationStateProps {
+ children: React.ReactNode;
+}
+
+export const NavigationState = ({ children }: NavigationStateProps) => {
+ const router = useNavigate();
+ const { pathname } = useLocation();
+
+ const [isBlocked, setIsBlocked] = useState(false);
+ const validate = useCallback(() => {
+ return !isBlocked || confirm('Изменения не сохранены. Вы уверены что хотите совершить переход?');
+ }, [isBlocked]);
+
+ const canBack = useCallback(() => !!window.history && window.history?.length !== 0, []);
+
+ const scrollTop = useCallback(() => {
+ window.scrollTo(0, 0);
+ const mainScroll = document.getElementById(globals.main_scroll);
+ if (mainScroll) {
+ mainScroll.scroll(0, 0);
+ }
+ }, []);
+
+ const push = useCallback(
+ (path: string, newTab?: boolean) => {
+ if (newTab) {
+ window.open(`${path}`, '_blank');
+ return;
+ }
+ if (validate()) {
+ scrollTop();
+ router(path);
+ setIsBlocked(false);
+ }
+ },
+ [router, validate, scrollTop]
+ );
+
+ const replace = useCallback(
+ (path: string) => {
+ if (validate()) {
+ scrollTop();
+ router(path, { replace: true });
+ setIsBlocked(false);
+ }
+ },
+ [router, validate, scrollTop]
+ );
+
+ const back = useCallback(() => {
+ if (validate()) {
+ scrollTop();
+ router(-1);
+ setIsBlocked(false);
+ }
+ }, [router, validate, scrollTop]);
+
+ const forward = useCallback(() => {
+ if (validate()) {
+ scrollTop();
+ router(1);
+ setIsBlocked(false);
+ }
+ }, [router, validate, scrollTop]);
+
+ useEffect(() => {
+ scrollTop();
+ }, [pathname, scrollTop]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export function useBlockNavigation(isBlocked: boolean) {
+ const router = useConceptNavigation();
+ useEffect(() => {
+ router.setIsBlocked(isBlocked);
+ return () => router.setIsBlocked(false);
+ }, [router, isBlocked]);
+}
diff --git a/rsconcept/frontend/src/context/OptionsContext.tsx b/rsconcept/frontend/src/context/OptionsContext.tsx
new file mode 100644
index 00000000..cef7672f
--- /dev/null
+++ b/rsconcept/frontend/src/context/OptionsContext.tsx
@@ -0,0 +1,161 @@
+'use client';
+
+import clsx from 'clsx';
+import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useState } from 'react';
+
+import Tooltip from '@/components/ui/Tooltip';
+import useLocalStorage from '@/hooks/useLocalStorage';
+import { FontStyle } from '@/models/miscellaneous';
+import { animationDuration } from '@/styling/animations';
+import { darkT, IColorTheme, lightT } from '@/styling/color';
+import { globals, storage } from '@/utils/constants';
+import { contextOutsideScope } from '@/utils/labels';
+
+interface IOptionsContext {
+ viewportHeight: string;
+ mainHeight: string;
+
+ colors: IColorTheme;
+
+ darkMode: boolean;
+ toggleDarkMode: () => void;
+
+ adminMode: boolean;
+ toggleAdminMode: () => void;
+
+ mathFont: FontStyle;
+ setMathFont: (value: FontStyle) => void;
+
+ noNavigationAnimation: boolean;
+ noNavigation: boolean;
+ toggleNoNavigation: () => void;
+
+ noFooter: boolean;
+ setNoFooter: React.Dispatch>;
+
+ showScroll: boolean;
+ setShowScroll: React.Dispatch>;
+
+ showHelp: boolean;
+ toggleShowHelp: () => void;
+
+ calculateHeight: (offset: string, minimum?: string) => string;
+}
+
+const OptionsContext = createContext(null);
+export const useConceptOptions = () => {
+ const context = useContext(OptionsContext);
+ if (!context) {
+ throw new Error(contextOutsideScope('useConceptTheme', 'ThemeState'));
+ }
+ return context;
+};
+
+interface OptionsStateProps {
+ children: React.ReactNode;
+}
+
+export const OptionsState = ({ children }: OptionsStateProps) => {
+ const [darkMode, setDarkMode] = useLocalStorage(storage.themeDark, false);
+ const [adminMode, setAdminMode] = useLocalStorage(storage.optionsAdmin, false);
+ const [mathFont, setMathFont] = useLocalStorage(storage.rseditFont, 'math');
+ const [showHelp, setShowHelp] = useLocalStorage(storage.optionsHelp, true);
+ const [noNavigation, setNoNavigation] = useState(false);
+
+ const [colors, setColors] = useState(lightT);
+
+ const [noNavigationAnimation, setNoNavigationAnimation] = useState(false);
+ const [noFooter, setNoFooter] = useState(false);
+ const [showScroll, setShowScroll] = useState(false);
+
+ function setDarkClass(isDark: boolean) {
+ const root = window.document.documentElement;
+ if (isDark) {
+ root.classList.add('dark');
+ } else {
+ root.classList.remove('dark');
+ }
+ root.setAttribute('data-color-scheme', !isDark ? 'light' : 'dark');
+ }
+
+ useLayoutEffect(() => {
+ setDarkClass(darkMode);
+ }, [darkMode]);
+
+ useLayoutEffect(() => {
+ setColors(darkMode ? darkT : lightT);
+ }, [darkMode, setColors]);
+
+ const toggleNoNavigation = useCallback(() => {
+ if (noNavigation) {
+ setNoNavigationAnimation(false);
+ setNoNavigation(false);
+ } else {
+ setNoNavigationAnimation(true);
+ setTimeout(() => setNoNavigation(true), animationDuration.navigationToggle);
+ }
+ }, [noNavigation]);
+
+ const calculateHeight = useCallback(
+ (offset: string, minimum: string = '0px') => {
+ if (noNavigation) {
+ return `max(calc(100dvh - (${offset})), ${minimum})`;
+ } else if (noFooter) {
+ return `max(calc(100dvh - 3rem - (${offset})), ${minimum})`;
+ } else {
+ return `max(calc(100dvh - 6.75rem - (${offset})), ${minimum})`;
+ }
+ },
+ [noNavigation, noFooter]
+ );
+
+ const toggleDarkMode = useCallback(() => {
+ setDarkMode(prev => !prev);
+ window.location.reload();
+ }, [setDarkMode]);
+
+ const mainHeight = useMemo(() => {
+ return !noNavigation ? 'calc(100dvh - 6.75rem)' : '100dvh';
+ }, [noNavigation]);
+
+ const viewportHeight = useMemo(() => {
+ return !noNavigation ? 'calc(100dvh - 3rem)' : '100dvh';
+ }, [noNavigation]);
+
+ return (
+ setAdminMode(prev => !prev),
+ toggleNoNavigation: toggleNoNavigation,
+ setNoFooter,
+ setShowScroll,
+ toggleShowHelp: () => setShowHelp(prev => !prev),
+ viewportHeight,
+ mainHeight,
+ calculateHeight
+ }}
+ >
+ <>
+
+ {children}
+ >
+
+ );
+};
diff --git a/rsconcept/frontend/src/context/OssContext.tsx b/rsconcept/frontend/src/context/OssContext.tsx
new file mode 100644
index 00000000..aa95ab43
--- /dev/null
+++ b/rsconcept/frontend/src/context/OssContext.tsx
@@ -0,0 +1,276 @@
+'use client';
+
+import { createContext, useCallback, useContext, useMemo, useState } from 'react';
+
+import {
+ type DataCallback,
+ deleteUnsubscribe,
+ patchEditorsSet as patchSetEditors,
+ patchLibraryItem,
+ patchSetAccessPolicy,
+ patchSetLocation,
+ patchSetOwner,
+ postSubscribe
+} from '@/app/backendAPI';
+import { type ErrorData } from '@/components/info/InfoError';
+import useOssDetails from '@/hooks/useOssDetails';
+import { AccessPolicy, ILibraryItem } from '@/models/library';
+import { ILibraryUpdateData } from '@/models/library';
+import { IOperationSchema } from '@/models/oss';
+import { UserID } from '@/models/user';
+import { contextOutsideScope } from '@/utils/labels';
+
+import { useAuth } from './AuthContext';
+import { useLibrary } from './LibraryContext';
+
+interface IOssContext {
+ schema?: IOperationSchema;
+ itemID: string;
+
+ loading: boolean;
+ errorLoading: ErrorData;
+ processing: boolean;
+ processingError: ErrorData;
+
+ isOwned: boolean;
+ isSubscribed: boolean;
+
+ update: (data: ILibraryUpdateData, callback?: DataCallback) => void;
+
+ subscribe: (callback?: () => void) => void;
+ unsubscribe: (callback?: () => void) => void;
+ setOwner: (newOwner: UserID, callback?: () => void) => void;
+ setAccessPolicy: (newPolicy: AccessPolicy, callback?: () => void) => void;
+ setLocation: (newLocation: string, callback?: () => void) => void;
+ setEditors: (newEditors: UserID[], callback?: () => void) => void;
+}
+
+const OssContext = createContext(null);
+export const useOSS = () => {
+ const context = useContext(OssContext);
+ if (context === null) {
+ throw new Error(contextOutsideScope('useOSS', 'OssState'));
+ }
+ return context;
+};
+
+interface OssStateProps {
+ itemID: string;
+ children: React.ReactNode;
+}
+
+export const OssState = ({ itemID, children }: OssStateProps) => {
+ const library = useLibrary();
+ const { user } = useAuth();
+ const {
+ schema: schema, // prettier: split lines
+ error: errorLoading,
+ setSchema,
+ loading
+ } = useOssDetails({
+ target: itemID
+ });
+ const [processing, setProcessing] = useState(false);
+ const [processingError, setProcessingError] = useState(undefined);
+
+ const [toggleTracking, setToggleTracking] = useState(false);
+
+ const isOwned = useMemo(() => {
+ return user?.id === schema?.owner || false;
+ }, [user, schema?.owner]);
+
+ const isSubscribed = useMemo(() => {
+ if (!user || !schema || !user.id) {
+ return false;
+ }
+ return schema.subscribers.includes(user.id);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [user, schema, toggleTracking]);
+
+ const update = useCallback(
+ (data: ILibraryUpdateData, callback?: DataCallback) => {
+ if (!schema) {
+ return;
+ }
+ setProcessingError(undefined);
+ patchLibraryItem(itemID, {
+ data: data,
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: newData => {
+ setSchema(Object.assign(schema, newData));
+ library.localUpdateItem(newData);
+ if (callback) callback(newData);
+ }
+ });
+ },
+ [itemID, setSchema, schema, library]
+ );
+
+ const subscribe = useCallback(
+ (callback?: () => void) => {
+ if (!schema || !user) {
+ return;
+ }
+ setProcessingError(undefined);
+ postSubscribe(itemID, {
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: () => {
+ if (user.id && !schema.subscribers.includes(user.id)) {
+ schema.subscribers.push(user.id);
+ }
+ if (!user.subscriptions.includes(schema.id)) {
+ user.subscriptions.push(schema.id);
+ }
+ setToggleTracking(prev => !prev);
+ if (callback) callback();
+ }
+ });
+ },
+ [itemID, schema, user]
+ );
+
+ const unsubscribe = useCallback(
+ (callback?: () => void) => {
+ if (!schema || !user) {
+ return;
+ }
+ setProcessingError(undefined);
+ deleteUnsubscribe(itemID, {
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: () => {
+ if (user.id && schema.subscribers.includes(user.id)) {
+ schema.subscribers.splice(schema.subscribers.indexOf(user.id), 1);
+ }
+ if (user.subscriptions.includes(schema.id)) {
+ user.subscriptions.splice(user.subscriptions.indexOf(schema.id), 1);
+ }
+ setToggleTracking(prev => !prev);
+ if (callback) callback();
+ }
+ });
+ },
+ [itemID, schema, user]
+ );
+
+ const setOwner = useCallback(
+ (newOwner: UserID, callback?: () => void) => {
+ if (!schema) {
+ return;
+ }
+ setProcessingError(undefined);
+ patchSetOwner(itemID, {
+ data: {
+ user: newOwner
+ },
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: () => {
+ schema.owner = newOwner;
+ library.localUpdateItem(schema);
+ if (callback) callback();
+ }
+ });
+ },
+ [itemID, schema, library]
+ );
+
+ const setAccessPolicy = useCallback(
+ (newPolicy: AccessPolicy, callback?: () => void) => {
+ if (!schema) {
+ return;
+ }
+ setProcessingError(undefined);
+ patchSetAccessPolicy(itemID, {
+ data: {
+ access_policy: newPolicy
+ },
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: () => {
+ schema.access_policy = newPolicy;
+ library.localUpdateItem(schema);
+ if (callback) callback();
+ }
+ });
+ },
+ [itemID, schema, library]
+ );
+
+ const setLocation = useCallback(
+ (newLocation: string, callback?: () => void) => {
+ if (!schema) {
+ return;
+ }
+ setProcessingError(undefined);
+ patchSetLocation(itemID, {
+ data: {
+ location: newLocation
+ },
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: () => {
+ schema.location = newLocation;
+ library.localUpdateItem(schema);
+ if (callback) callback();
+ }
+ });
+ },
+ [itemID, schema, library]
+ );
+
+ const setEditors = useCallback(
+ (newEditors: UserID[], callback?: () => void) => {
+ if (!schema) {
+ return;
+ }
+ setProcessingError(undefined);
+ patchSetEditors(itemID, {
+ data: {
+ users: newEditors
+ },
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: () => {
+ schema.editors = newEditors;
+ if (callback) callback();
+ }
+ });
+ },
+ [itemID, schema]
+ );
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/rsconcept/frontend/src/context/RSFormContext.tsx b/rsconcept/frontend/src/context/RSFormContext.tsx
new file mode 100644
index 00000000..678c40f9
--- /dev/null
+++ b/rsconcept/frontend/src/context/RSFormContext.tsx
@@ -0,0 +1,647 @@
+'use client';
+
+import { createContext, useCallback, useContext, useMemo, useState } from 'react';
+
+import {
+ type DataCallback,
+ deleteUnsubscribe,
+ deleteVersion,
+ getTRSFile,
+ patchConstituenta,
+ patchDeleteConstituenta,
+ patchEditorsSet as patchSetEditors,
+ patchInlineSynthesis,
+ patchLibraryItem,
+ patchMoveConstituenta,
+ patchProduceStructure,
+ patchRenameConstituenta,
+ patchResetAliases,
+ patchRestoreOrder,
+ patchRestoreVersion,
+ patchSetAccessPolicy,
+ patchSetLocation,
+ patchSetOwner,
+ patchSubstituteConstituents,
+ patchUploadTRS,
+ patchVersion,
+ postCreateVersion,
+ postNewConstituenta,
+ postSubscribe
+} from '@/app/backendAPI';
+import { type ErrorData } from '@/components/info/InfoError';
+import useRSFormDetails from '@/hooks/useRSFormDetails';
+import { AccessPolicy, ILibraryItem, IVersionData, VersionID } from '@/models/library';
+import { ILibraryUpdateData } from '@/models/library';
+import {
+ ConstituentaID,
+ IConstituentaList,
+ IConstituentaMeta,
+ ICstCreateData,
+ ICstMovetoData,
+ ICstRenameData,
+ ICstSubstituteData,
+ ICstUpdateData,
+ IInlineSynthesisData,
+ IRSForm,
+ IRSFormData,
+ IRSFormUploadData,
+ ITargetCst
+} from '@/models/rsform';
+import { UserID } from '@/models/user';
+import { contextOutsideScope } from '@/utils/labels';
+
+import { useAuth } from './AuthContext';
+import { useLibrary } from './LibraryContext';
+
+interface IRSFormContext {
+ schema?: IRSForm;
+ itemID: string;
+ versionID?: string;
+
+ loading: boolean;
+ errorLoading: ErrorData;
+ processing: boolean;
+ processingError: ErrorData;
+
+ isArchive: boolean;
+ isOwned: boolean;
+ isSubscribed: boolean;
+
+ update: (data: ILibraryUpdateData, callback?: DataCallback) => void;
+ download: (callback: DataCallback) => void;
+ upload: (data: IRSFormUploadData, callback: () => void) => void;
+
+ subscribe: (callback?: () => void) => void;
+ unsubscribe: (callback?: () => void) => void;
+ setOwner: (newOwner: UserID, callback?: () => void) => void;
+ setAccessPolicy: (newPolicy: AccessPolicy, callback?: () => void) => void;
+ setLocation: (newLocation: string, callback?: () => void) => void;
+ setEditors: (newEditors: UserID[], callback?: () => void) => void;
+
+ resetAliases: (callback: () => void) => void;
+ restoreOrder: (callback: () => void) => void;
+ produceStructure: (data: ITargetCst, callback?: DataCallback) => void;
+ inlineSynthesis: (data: IInlineSynthesisData, callback?: DataCallback) => void;
+
+ cstCreate: (data: ICstCreateData, callback?: DataCallback) => void;
+ cstRename: (data: ICstRenameData, callback?: DataCallback) => void;
+ cstSubstitute: (data: ICstSubstituteData, callback?: () => void) => void;
+ cstUpdate: (data: ICstUpdateData, callback?: DataCallback) => void;
+ cstDelete: (data: IConstituentaList, callback?: () => void) => void;
+ cstMoveTo: (data: ICstMovetoData, callback?: () => void) => void;
+
+ versionCreate: (data: IVersionData, callback?: (version: VersionID) => void) => void;
+ versionUpdate: (target: VersionID, data: IVersionData, callback?: () => void) => void;
+ versionDelete: (target: VersionID, callback?: () => void) => void;
+ versionRestore: (target: string, callback?: () => void) => void;
+}
+
+const RSFormContext = createContext(null);
+export const useRSForm = () => {
+ const context = useContext(RSFormContext);
+ if (context === null) {
+ throw new Error(contextOutsideScope('useRSForm', 'RSFormState'));
+ }
+ return context;
+};
+
+interface RSFormStateProps {
+ itemID: string;
+ versionID?: string;
+ children: React.ReactNode;
+}
+
+export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) => {
+ const library = useLibrary();
+ const { user } = useAuth();
+ const {
+ schema, // prettier: split lines
+ reload,
+ error: errorLoading,
+ setSchema,
+ loading
+ } = useRSFormDetails({
+ target: itemID,
+ version: versionID
+ });
+ const [processing, setProcessing] = useState(false);
+ const [processingError, setProcessingError] = useState(undefined);
+
+ const [toggleTracking, setToggleTracking] = useState(false);
+
+ const isOwned = useMemo(() => {
+ return user?.id === schema?.owner || false;
+ }, [user, schema?.owner]);
+
+ const isArchive = useMemo(() => !!versionID, [versionID]);
+
+ const isSubscribed = useMemo(() => {
+ if (!user || !schema || !user.id) {
+ return false;
+ }
+ return schema.subscribers.includes(user.id);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [user, schema, toggleTracking]);
+
+ const update = useCallback(
+ (data: ILibraryUpdateData, callback?: DataCallback) => {
+ if (!schema) {
+ return;
+ }
+ setProcessingError(undefined);
+ patchLibraryItem(itemID, {
+ data: data,
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: newData => {
+ setSchema(Object.assign(schema, newData));
+ library.localUpdateItem(newData);
+ if (callback) callback(newData);
+ }
+ });
+ },
+ [itemID, setSchema, schema, library]
+ );
+
+ const upload = useCallback(
+ (data: IRSFormUploadData, callback?: () => void) => {
+ if (!schema) {
+ return;
+ }
+ setProcessingError(undefined);
+ patchUploadTRS(itemID, {
+ data: data,
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: newData => {
+ setSchema(newData);
+ library.localUpdateItem(newData);
+ if (callback) callback();
+ }
+ });
+ },
+ [itemID, setSchema, schema, library]
+ );
+
+ const subscribe = useCallback(
+ (callback?: () => void) => {
+ if (!schema || !user) {
+ return;
+ }
+ setProcessingError(undefined);
+ postSubscribe(itemID, {
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: () => {
+ if (user.id && !schema.subscribers.includes(user.id)) {
+ schema.subscribers.push(user.id);
+ }
+ if (!user.subscriptions.includes(schema.id)) {
+ user.subscriptions.push(schema.id);
+ }
+ setToggleTracking(prev => !prev);
+ if (callback) callback();
+ }
+ });
+ },
+ [itemID, schema, user]
+ );
+
+ const unsubscribe = useCallback(
+ (callback?: () => void) => {
+ if (!schema || !user) {
+ return;
+ }
+ setProcessingError(undefined);
+ deleteUnsubscribe(itemID, {
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: () => {
+ if (user.id && schema.subscribers.includes(user.id)) {
+ schema.subscribers.splice(schema.subscribers.indexOf(user.id), 1);
+ }
+ if (user.subscriptions.includes(schema.id)) {
+ user.subscriptions.splice(user.subscriptions.indexOf(schema.id), 1);
+ }
+ setToggleTracking(prev => !prev);
+ if (callback) callback();
+ }
+ });
+ },
+ [itemID, schema, user]
+ );
+
+ const setOwner = useCallback(
+ (newOwner: UserID, callback?: () => void) => {
+ if (!schema) {
+ return;
+ }
+ setProcessingError(undefined);
+ patchSetOwner(itemID, {
+ data: {
+ user: newOwner
+ },
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: () => {
+ schema.owner = newOwner;
+ library.localUpdateItem(schema);
+ if (callback) callback();
+ }
+ });
+ },
+ [itemID, schema, library]
+ );
+
+ const setAccessPolicy = useCallback(
+ (newPolicy: AccessPolicy, callback?: () => void) => {
+ if (!schema) {
+ return;
+ }
+ setProcessingError(undefined);
+ patchSetAccessPolicy(itemID, {
+ data: {
+ access_policy: newPolicy
+ },
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: () => {
+ schema.access_policy = newPolicy;
+ library.localUpdateItem(schema);
+ if (callback) callback();
+ }
+ });
+ },
+ [itemID, schema, library]
+ );
+
+ const setLocation = useCallback(
+ (newLocation: string, callback?: () => void) => {
+ if (!schema) {
+ return;
+ }
+ setProcessingError(undefined);
+ patchSetLocation(itemID, {
+ data: {
+ location: newLocation
+ },
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: () => {
+ schema.location = newLocation;
+ library.localUpdateItem(schema);
+ if (callback) callback();
+ }
+ });
+ },
+ [itemID, schema, library]
+ );
+
+ const setEditors = useCallback(
+ (newEditors: UserID[], callback?: () => void) => {
+ if (!schema) {
+ return;
+ }
+ setProcessingError(undefined);
+ patchSetEditors(itemID, {
+ data: {
+ users: newEditors
+ },
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: () => {
+ schema.editors = newEditors;
+ if (callback) callback();
+ }
+ });
+ },
+ [itemID, schema]
+ );
+
+ const resetAliases = useCallback(
+ (callback?: () => void) => {
+ if (!schema || !user) {
+ return;
+ }
+ setProcessingError(undefined);
+ patchResetAliases(itemID, {
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: newData => {
+ setSchema(Object.assign(schema, newData));
+ library.localUpdateTimestamp(newData.id);
+ if (callback) callback();
+ }
+ });
+ },
+ [itemID, schema, library, user, setSchema]
+ );
+
+ const restoreOrder = useCallback(
+ (callback?: () => void) => {
+ if (!schema || !user) {
+ return;
+ }
+ setProcessingError(undefined);
+ patchRestoreOrder(itemID, {
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: newData => {
+ setSchema(Object.assign(schema, newData));
+ library.localUpdateTimestamp(newData.id);
+ if (callback) callback();
+ }
+ });
+ },
+ [itemID, schema, library, user, setSchema]
+ );
+
+ const produceStructure = useCallback(
+ (data: ITargetCst, callback?: DataCallback) => {
+ setProcessingError(undefined);
+ patchProduceStructure(itemID, {
+ data: data,
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: newData => {
+ setSchema(newData.schema);
+ library.localUpdateTimestamp(newData.schema.id);
+ if (callback) callback(newData.cst_list);
+ }
+ });
+ },
+ [setSchema, library, itemID]
+ );
+
+ const download = useCallback(
+ (callback: DataCallback) => {
+ setProcessingError(undefined);
+ getTRSFile(itemID, String(schema?.version ?? ''), {
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: callback
+ });
+ },
+ [itemID, schema]
+ );
+
+ const cstCreate = useCallback(
+ (data: ICstCreateData, callback?: DataCallback) => {
+ setProcessingError(undefined);
+ postNewConstituenta(itemID, {
+ data: data,
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: newData => {
+ setSchema(newData.schema);
+ library.localUpdateTimestamp(newData.schema.id);
+ if (callback) callback(newData.new_cst);
+ }
+ });
+ },
+ [itemID, library, setSchema]
+ );
+
+ const cstDelete = useCallback(
+ (data: IConstituentaList, callback?: () => void) => {
+ setProcessingError(undefined);
+ patchDeleteConstituenta(itemID, {
+ data: data,
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: newData => {
+ setSchema(newData);
+ library.localUpdateTimestamp(newData.id);
+ if (callback) callback();
+ }
+ });
+ },
+ [itemID, library, setSchema]
+ );
+
+ const cstUpdate = useCallback(
+ (data: ICstUpdateData, callback?: DataCallback) => {
+ setProcessingError(undefined);
+ patchConstituenta(String(data.id), {
+ data: data,
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: newData =>
+ reload(setProcessing, () => {
+ library.localUpdateTimestamp(Number(itemID));
+ if (callback) callback(newData);
+ })
+ });
+ },
+ [itemID, library, reload]
+ );
+
+ const cstRename = useCallback(
+ (data: ICstRenameData, callback?: DataCallback) => {
+ setProcessingError(undefined);
+ patchRenameConstituenta(itemID, {
+ data: data,
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: newData => {
+ setSchema(newData.schema);
+ library.localUpdateTimestamp(newData.schema.id);
+ if (callback) callback(newData.new_cst);
+ }
+ });
+ },
+ [setSchema, library, itemID]
+ );
+
+ const cstSubstitute = useCallback(
+ (data: ICstSubstituteData, callback?: () => void) => {
+ setProcessingError(undefined);
+ patchSubstituteConstituents(itemID, {
+ data: data,
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: newData => {
+ setSchema(newData);
+ library.localUpdateTimestamp(newData.id);
+ if (callback) callback();
+ }
+ });
+ },
+ [setSchema, library, itemID]
+ );
+
+ const cstMoveTo = useCallback(
+ (data: ICstMovetoData, callback?: () => void) => {
+ setProcessingError(undefined);
+ patchMoveConstituenta(itemID, {
+ data: data,
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: newData => {
+ setSchema(newData);
+ library.localUpdateTimestamp(Number(itemID));
+ if (callback) callback();
+ }
+ });
+ },
+ [itemID, library, setSchema]
+ );
+
+ const versionCreate = useCallback(
+ (data: IVersionData, callback?: (version: number) => void) => {
+ setProcessingError(undefined);
+ postCreateVersion(itemID, {
+ data: data,
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: newData => {
+ setSchema(newData.schema);
+ library.localUpdateTimestamp(Number(itemID));
+ if (callback) callback(newData.version);
+ }
+ });
+ },
+ [itemID, library, setSchema]
+ );
+
+ const versionUpdate = useCallback(
+ (target: number, data: IVersionData, callback?: () => void) => {
+ setProcessingError(undefined);
+ patchVersion(String(target), {
+ data: data,
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: () => {
+ schema!.versions = schema!.versions.map(prev => {
+ if (prev.id === target) {
+ prev.description = data.description;
+ prev.version = data.version;
+ return prev;
+ } else {
+ return prev;
+ }
+ });
+ setSchema(schema);
+ if (callback) callback();
+ }
+ });
+ },
+ [schema, setSchema]
+ );
+
+ const versionDelete = useCallback(
+ (target: number, callback?: () => void) => {
+ setProcessingError(undefined);
+ deleteVersion(String(target), {
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: () => {
+ schema!.versions = schema!.versions.filter(prev => prev.id !== target);
+ setSchema(schema);
+ if (callback) callback();
+ }
+ });
+ },
+ [schema, setSchema]
+ );
+
+ const versionRestore = useCallback(
+ (target: string, callback?: () => void) => {
+ setProcessingError(undefined);
+ patchRestoreVersion(target, {
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: () => {
+ setSchema(schema);
+ library.localUpdateItem(schema!);
+ if (callback) callback();
+ }
+ });
+ },
+ [schema, setSchema, library]
+ );
+
+ const inlineSynthesis = useCallback(
+ (data: IInlineSynthesisData, callback?: DataCallback) => {
+ setProcessingError(undefined);
+ patchInlineSynthesis({
+ data: data,
+ showError: true,
+ setLoading: setProcessing,
+ onError: setProcessingError,
+ onSuccess: newData => {
+ setSchema(newData);
+ library.localUpdateTimestamp(Number(itemID));
+ if (callback) callback(newData);
+ }
+ });
+ },
+ [library, itemID, setSchema]
+ );
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/rsconcept/frontend/src/context/UserProfileContext.tsx b/rsconcept/frontend/src/context/UserProfileContext.tsx
new file mode 100644
index 00000000..7185a553
--- /dev/null
+++ b/rsconcept/frontend/src/context/UserProfileContext.tsx
@@ -0,0 +1,86 @@
+'use client';
+
+import { createContext, useCallback, useContext, useEffect, useState } from 'react';
+
+import { DataCallback, getProfile, patchProfile } from '@/app/backendAPI';
+import { ErrorData } from '@/components/info/InfoError';
+import { IUserProfile, IUserUpdateData } from '@/models/user';
+import { contextOutsideScope } from '@/utils/labels';
+
+import { useUsers } from './UsersContext';
+
+interface IUserProfileContext {
+ user: IUserProfile | undefined;
+ loading: boolean;
+ processing: boolean;
+ error: ErrorData;
+ errorProcessing: ErrorData;
+ setError: (error: ErrorData) => void;
+ updateUser: (data: IUserUpdateData, callback?: DataCallback) => void;
+}
+
+const ProfileContext = createContext(null);
+
+export const useUserProfile = () => {
+ const context = useContext(ProfileContext);
+ if (!context) {
+ throw new Error(contextOutsideScope('useUserProfile', 'UserProfileState'));
+ }
+ return context;
+};
+
+interface UserProfileStateProps {
+ children: React.ReactNode;
+}
+
+export const UserProfileState = ({ children }: UserProfileStateProps) => {
+ const { users } = useUsers();
+ const [user, setUser] = useState(undefined);
+ const [loading, setLoading] = useState(false);
+ const [processing, setProcessing] = useState(false);
+ const [error, setError] = useState(undefined);
+ const [errorProcessing, setErrorProcessing] = useState(undefined);
+
+ const reload = useCallback(() => {
+ setError(undefined);
+ setUser(undefined);
+ getProfile({
+ showError: true,
+ setLoading: setLoading,
+ onError: setError,
+ onSuccess: newData => setUser(newData)
+ });
+ }, [setUser]);
+
+ const updateUser = useCallback(
+ (data: IUserUpdateData, callback?: DataCallback) => {
+ setErrorProcessing(undefined);
+ patchProfile({
+ data: data,
+ showError: true,
+ setLoading: setProcessing,
+ onError: setErrorProcessing,
+ onSuccess: newData => {
+ setUser(newData);
+ const libraryUser = users.find(item => item.id === user?.id);
+ if (libraryUser) {
+ libraryUser.first_name = newData.first_name;
+ libraryUser.last_name = newData.last_name;
+ }
+ if (callback) callback(newData);
+ }
+ });
+ },
+ [setUser, users, user?.id]
+ );
+
+ useEffect(() => {
+ reload();
+ }, [reload]);
+
+ return (
+