From 3cc2deb426b363dd4c59c0585a522ff5c4f818c2 Mon Sep 17 00:00:00 2001 From: Kumar Pallav Date: Thu, 27 Nov 2025 05:56:55 +0000 Subject: [PATCH 01/15] Adds a step to build lezer-matlab module and improves end-to-end testing framework. --- src/jupyter_matlab_labextension/package.json | 2 +- tests/e2e/tests/ui_tests.test.ts | 13 ++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/jupyter_matlab_labextension/package.json b/src/jupyter_matlab_labextension/package.json index 45f6200e..bdaaca99 100644 --- a/src/jupyter_matlab_labextension/package.json +++ b/src/jupyter_matlab_labextension/package.json @@ -29,7 +29,7 @@ "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", "build:lib": "jlpm build:lezer && tsc", - "build:lezer": "cd src/lezer-matlab && npm install", + "build:lezer": "cd src/lezer-matlab && npm install && npm run build", "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", "clean:lintcache": "rimraf .eslintcache .stylelintcache", diff --git a/tests/e2e/tests/ui_tests.test.ts b/tests/e2e/tests/ui_tests.test.ts index aa1c417b..f2d391bd 100644 --- a/tests/e2e/tests/ui_tests.test.ts +++ b/tests/e2e/tests/ui_tests.test.ts @@ -1,25 +1,20 @@ -// Copyright 2023-2024 The MathWorks, Inc. +// Copyright 2023-2025 The MathWorks, Inc. import { expect, test } from '@jupyterlab/galata'; -import { Locator } from '@playwright/test'; test.describe('MATLAB File button', () => { - let MATLABFileButton: Locator; - test.beforeEach(async ({ page }) => { - await page.launcher.waitFor({ state: 'visible' }); - MATLABFileButton = page.getByText('MATLAB File', { exact: true }); - }); - test('is visible on Launcher', async ({ page }) => { + const MATLABFileButton = page.getByRole('button', { name: 'MATLAB File' }); await expect(MATLABFileButton).toBeVisible(); }); test('takes you to .m file', async ({ page }) => { + const MATLABFileButton = page.getByRole('button', { name: 'MATLAB File' }); await MATLABFileButton.click(); // Expect a new untitled file ending in .m to be made, with optional // digits at the end of untitled, i.e. untitled1.m untitled1232.m) - await expect(page).toHaveURL(/.*untitled(\d+)?.m/); + await page.waitForURL(/.*untitled(\d+)?.m/); // MATLAB language mode selected: await expect(page.locator('#jp-main-statusbar') From 3bf4042128d244d04708bf1add38311039366ebd Mon Sep 17 00:00:00 2001 From: Prabhakar Kumar Date: Wed, 3 Dec 2025 05:50:36 +0000 Subject: [PATCH 02/15] Adding devcontainer configurations to build and test with or without MATLAB installed. --- .devcontainer/Dockerfile | 42 ++++++++++ .devcontainer/devcontainer.json | 29 +++++++ .devcontainer/with-matlab/Dockerfile | 89 +++++++++++++++++++++ .devcontainer/with-matlab/devcontainer.json | 34 ++++++++ 4 files changed, 194 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/with-matlab/Dockerfile create mode 100644 .devcontainer/with-matlab/devcontainer.json diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..b51aad16 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,42 @@ +# Copyright 2025 The MathWorks, Inc. + +# Light weight image only to test builds of matlab-proxy + +ARG PYTHON_VERSION=3.11 +ARG NODE_VERSION=20 +ARG NVM_VERSION="0.40.3" + +FROM ubuntu:24.04 + +ARG NVM_VERSION +ARG PYTHON_VERSION +ARG NODE_VERSION + +ENV DEBIAN_FRONTEND=noninteractive +# Install build dependencies, install UV (for python) and NVM (for nodejs) +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + git \ + wget \ + curl \ + unzip \ + ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +USER ubuntu + +# Updates the default shell to bash instead of sh +SHELL ["/bin/bash", "-c"] + +WORKDIR /home/ubuntu +# Set up development environment for ubuntu user with UV and NVM +RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \ + source ${HOME}/.local/bin/env && \ + uv python install ${PYTHON_VERSION} && \ + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install.sh | bash && \ + export NVM_DIR="/home/ubuntu/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && \ + nvm install ${NODE_VERSION} + +# Now the matlab-proxy directory can be mounted and tested for build + + diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..59362dd2 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,29 @@ +{ + "name": "Only build Jupyter MATLAB Proxy", + "build": { + "dockerfile": "Dockerfile", + "context": ".", + "args": { + "PYTHON_VERSION" : "3.11", + "NVM_VERSION" : "0.40.3", + "NODE_VERSION" : "24" + } + }, + "containerEnv": { + "NPM_CONFIG_REGISTRY": "${localEnv:NPM_CONFIG_REGISTRY}" + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python" + ], + "settings": { + "python.terminal.activateEnvInCurrentTerminal": true, + "python.defaultInterpreterPath": ".venv/bin/python" + } + } + }, + "postCreateCommand": "uv venv --clear && . .venv/bin/activate && uv pip install . --cache-dir ./__uvcache__", + "waitFor": "postCreateCommand" + +} \ No newline at end of file diff --git a/.devcontainer/with-matlab/Dockerfile b/.devcontainer/with-matlab/Dockerfile new file mode 100644 index 00000000..1ba308e2 --- /dev/null +++ b/.devcontainer/with-matlab/Dockerfile @@ -0,0 +1,89 @@ +# Copyright 2025 The MathWorks, Inc. + +# Full image, with MATLAB and MATLAB Proxy + +ARG PYTHON_VERSION=3.11 +ARG NODE_VERSION=20 +ARG NVM_VERSION="0.40.3" +ARG MATLAB_RELEASE="R2025b" +ARG UBUNTU_VERSION="24.04" +# Default installation directory for MATLAB +ARG MATLAB_INSTALL_LOCATION="/opt/matlab" +ARG MATLAB_PRODUCT_LIST="MATLAB" + +FROM ubuntu:${UBUNTU_VERSION} + +ARG NVM_VERSION +ARG PYTHON_VERSION +ARG NODE_VERSION +ARG MATLAB_RELEASE +ARG UBUNTU_VERSION +ARG MATLAB_INSTALL_LOCATION +ARG MATLAB_PRODUCT_LIST + +ENV DEBIAN_FRONTEND=noninteractive +# Install build dependencies, install UV (for python) and NVM (for nodejs) +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + git \ + wget \ + curl \ + unzip \ + xvfb \ + fluxbox \ + ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +USER ubuntu + +# Updates the default shell to bash instead of sh +SHELL ["/bin/bash", "-c"] + +WORKDIR /home/ubuntu +# Set up development environment for ubuntu user with UV and NVM +RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \ + source ${HOME}/.local/bin/env && \ + uv python install ${PYTHON_VERSION} && \ + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install.sh | bash && \ + export NVM_DIR="/home/ubuntu/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && \ + nvm install ${NODE_VERSION} + +# Now the matlab-proxy directory can be mounted and tested for build + +USER root + +# Install MATLAB dependencies and MATLAB using MPM +ARG MATLAB_DEPS_URL="https://raw.githubusercontent.com/mathworks-ref-arch/container-images/main/matlab-deps/${MATLAB_RELEASE}/ubuntu${UBUNTU_VERSION}/base-dependencies.txt" +ARG MATLAB_DEPENDENCIES="matlab-deps-${MATLAB_RELEASE}-base-dependencies.txt" +ARG ADDITIONAL_PACKAGES="wget curl unzip ca-certificates xvfb git vim fluxbox gettext" +RUN export DEBIAN_FRONTEND=noninteractive && apt-get update \ + && apt-get install --no-install-recommends -y ${ADDITIONAL_PACKAGES}\ + && wget $(echo ${MATLAB_DEPS_URL} | tr "[:upper:]" "[:lower:]") -O ${MATLAB_DEPENDENCIES} \ + && xargs -a ${MATLAB_DEPENDENCIES} -r apt-get install --no-install-recommends -y \ + && apt-get clean \ + && apt-get -y autoremove \ + && rm -rf /var/lib/apt/lists/* ${MATLAB_DEPENDENCIES} + +WORKDIR /matlab-install +ARG MSH_MANAGED_INSTALL_ROOT=/usr/local/MathWorks/ServiceHost/ +ARG MSH_DOWNLOAD_LOCATION=/tmp/Downloads/MathWorks/ServiceHost +# Dont need to set HOME to install Support packages as jupyter images set HOME to NB_USER in all images, even for ROOT. +RUN echo "Installing MATLAB using MPM..." +RUN wget -q https://www.mathworks.com/mpm/glnxa64/mpm && \ + chmod +x mpm \ + && ./mpm install --release=${MATLAB_RELEASE} --destination=${MATLAB_INSTALL_LOCATION} \ + --products ${MATLAB_PRODUCT_LIST} \ + || (echo "MPM Installation Failure. See below for more information:" && cat /tmp/mathworks_root.log && false)\ + && rm -f mpm /tmp/mathworks_root.log \ + && ln -s ${MATLAB_INSTALL_LOCATION}/bin/matlab /usr/local/bin/matlab \ + && git clone https://github.com/mathworks-ref-arch/administer-mathworks-service-host.git \ + && cd /matlab-install/administer-mathworks-service-host/admin-scripts/linux/admin-controlled-installation \ + && ./download_msh.sh --destination ${MSH_DOWNLOAD_LOCATION} \ + && ./install_msh.sh --source ${MSH_DOWNLOAD_LOCATION} --destination ${MSH_MANAGED_INSTALL_ROOT} --no-update-environment \ + && ./cleanup_default_msh_installation_location.sh --for-all-users \ + && cd / && rm -rf /matlab-install ${MSH_DOWNLOAD_LOCATION} + +ENV MATHWORKS_SERVICE_HOST_MANAGED_INSTALL_ROOT=${MSH_MANAGED_INSTALL_ROOT} + +USER ubuntu +WORKDIR /home/ubuntu diff --git a/.devcontainer/with-matlab/devcontainer.json b/.devcontainer/with-matlab/devcontainer.json new file mode 100644 index 00000000..877de267 --- /dev/null +++ b/.devcontainer/with-matlab/devcontainer.json @@ -0,0 +1,34 @@ +{ + "name": "MATLAB & Jupyter MATLAB Proxy", + "build": { + "dockerfile": "Dockerfile", + "context": ".", + "args": { + "PYTHON_VERSION" : "3.11", + "NVM_VERSION" : "0.40.3", + "NODE_VERSION" : "24", + "MATLAB_RELEASE" : "R2025b", + "UBUNTU_VERSION" : "24.04", + "MATLAB_PRODUCT_LIST" : "MATLAB" + } + }, + "containerEnv": { + "NPM_CONFIG_REGISTRY": "${localEnv:NPM_CONFIG_REGISTRY}", + "MWI_SESSION_NAME": "DevContainer" + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "MathWorks.language-matlab" + ], + "settings": { + "MATLAB.signIn": true, + "python.terminal.activateEnvInCurrentTerminal": true, + "python.defaultInterpreterPath": ".venv/bin/python" + } + } + }, + "postCreateCommand": "uv venv --clear && . .venv/bin/activate && uv pip install . jupyterlab --cache-dir ./__uvcache__", + "waitFor": "postCreateCommand" +} \ No newline at end of file From eb5c74e0a15719229303f08ab8f3be3b4d8015f6 Mon Sep 17 00:00:00 2001 From: Rashed Mohammed Date: Wed, 3 Dec 2025 08:12:15 +0000 Subject: [PATCH 03/15] Infrastructural updates to improve support for Symbolic Math rendering. --- src/jupyter_matlab_kernel/matlab/+jupyter/execute.m | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/jupyter_matlab_kernel/matlab/+jupyter/execute.m b/src/jupyter_matlab_kernel/matlab/+jupyter/execute.m index 31287aca..0b0f00bd 100644 --- a/src/jupyter_matlab_kernel/matlab/+jupyter/execute.m +++ b/src/jupyter_matlab_kernel/matlab/+jupyter/execute.m @@ -214,7 +214,12 @@ persistent idler; if isempty(webwindow) - url = 'toolbox/matlab/codetools/liveeditor/index.html'; + % Use liveeditor index.html in older MATLAB versions + if isMATLABReleaseOlderThan("R2026a") + url = 'toolbox/matlab/codetools/liveeditor/index.html'; + else + url = 'toolbox/matlab/editor/application/index.html'; + end % MATLAB versions R2020b and R2021a requires specifying the base url. % Not doing so results in the URL not being loaded with the error From 1958dd74e0ebd19494d42ede56bbbbdd298ee09e Mon Sep 17 00:00:00 2001 From: Shushant Singh Date: Mon, 8 Dec 2025 06:47:43 +0000 Subject: [PATCH 04/15] Introduces support for python 3.12, 3.13 and deprecates support for python 3.8 and 3.9 --- .github/workflows/publish-jupyter-matlab-proxy.yml | 4 ++-- .github/workflows/run-e2e-tests.yml | 2 +- .github/workflows/run-integration-tests.yml | 2 +- .github/workflows/run-unit-tests.yml | 2 +- README.md | 2 +- src/jupyter_matlab_proxy/README.md | 6 ++++-- tests/unit/jupyter_matlab_kernel/test_kernelspec.py | 2 +- 7 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/publish-jupyter-matlab-proxy.yml b/.github/workflows/publish-jupyter-matlab-proxy.yml index c437da22..9bbeb404 100644 --- a/.github/workflows/publish-jupyter-matlab-proxy.yml +++ b/.github/workflows/publish-jupyter-matlab-proxy.yml @@ -30,10 +30,10 @@ jobs: with: ref: ${{ github.sha }} - - name: Set up Python 3.8 + - name: Set up Python 3.10 uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: 3.10 - name: Install Python build dependencies run: | diff --git a/.github/workflows/run-e2e-tests.yml b/.github/workflows/run-e2e-tests.yml index 9e463417..2f5c8edd 100644 --- a/.github/workflows/run-e2e-tests.yml +++ b/.github/workflows/run-e2e-tests.yml @@ -12,7 +12,7 @@ jobs: working-directory: tests/e2e env: NODE_VERSION: 18 - PYTHON_VERSION: 3.8 + PYTHON_VERSION: 3.10 steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 98826e36..43d16230 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -13,7 +13,7 @@ jobs: matrix: #TODO: Add test coverage for macOs os: [ubuntu-latest, windows-latest] - python-version: ["3.8", "3.11"] + python-version: ["3.10", "3.13"] # The minimum matlab-release is set to 21b as 20b and 21a are not supported by matlab-actions/setup-matlab. # See https://github.com/matlab-actions/setup-matlab/issues/76 diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index dcb5f57e..a55ff0c7 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -67,7 +67,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.8", "3.11"] + python-version: ["3.10", "3.13"] runs-on: ${{ matrix.os }} steps: diff --git a/README.md b/README.md index 5007df57..1769840d 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ From your Jupyter notebook or JupyterLab, you can also open the MATLAB developme - Windows® (supported from [v0.6.0](https://github.com/mathworks/jupyter-matlab-proxy/releases/tag/v0.6.0)). - Windows Subsystem for Linux (WSL 2) [Installation Guide](https://github.com/mathworks/jupyter-matlab-proxy/blob/main/install_guides/wsl2/README.md). -* Python versions: 3.8 | 3.9 | 3.10 | 3.11 +* Python versions: 3.10 | 3.11 | 3.12 | 3.13 * MATLAB R2020b or later, installed and on the system PATH. ```bash diff --git a/src/jupyter_matlab_proxy/README.md b/src/jupyter_matlab_proxy/README.md index 069ea121..9ce0ceec 100644 --- a/src/jupyter_matlab_proxy/README.md +++ b/src/jupyter_matlab_proxy/README.md @@ -52,8 +52,10 @@ To report any issues or suggestions, see the [Feedback](https://github.com/mathw | Help | Open a help pop-up for a detailed description of the options.| ## Limitations -This package supports the same subset of MATLAB features and commands as MATLAB® Online, except there is no support for Simulink® Online. -[Click here for a full list of Specifications and Limitations for MATLAB Online](https://www.mathworks.com/products/matlab-online/limitations.html). +This package supports the same set of MATLAB features and commands as MATLAB® Online. For the full list, see +[Specifications and Limitations for MATLAB Online](https://www.mathworks.com/products/matlab-online/limitations.html). + +Simulink Online is supported exclusively on Linux platforms starting from MATLAB R2024b. If you need to use functionality that is not yet supported, or for versions of MATLAB earlier than R2020b, you can use the alternative [MATLAB Integration for Jupyter using VNC](https://github.com/mathworks/jupyter-matlab-vnc-proxy). diff --git a/tests/unit/jupyter_matlab_kernel/test_kernelspec.py b/tests/unit/jupyter_matlab_kernel/test_kernelspec.py index f338119f..5246731a 100644 --- a/tests/unit/jupyter_matlab_kernel/test_kernelspec.py +++ b/tests/unit/jupyter_matlab_kernel/test_kernelspec.py @@ -28,7 +28,7 @@ def test_get_kernel_spec_custom_executable(): """ Test that get_kernel_spec() uses a custom executable when provided. """ - custom_executable = "/usr/bin/python3.9" + custom_executable = "/usr/bin/python3.10" kernelspec = get_kernel_spec(executable=custom_executable) assert kernelspec["argv"][0] == custom_executable From b12129c79f81aca85b61c151603df148d13c016d Mon Sep 17 00:00:00 2001 From: Kumar Pallav Date: Mon, 8 Dec 2025 06:55:30 +0000 Subject: [PATCH 05/15] Added timeout of 15 minutes and removed testing on macos for GitHub Action workflows. --- .github/workflows/run-e2e-tests.yml | 5 +++-- .github/workflows/run-integration-tests.yml | 6 +++--- .github/workflows/run-unit-tests.yml | 10 +++++++--- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/run-e2e-tests.yml b/.github/workflows/run-e2e-tests.yml index 2f5c8edd..ca121cbc 100644 --- a/.github/workflows/run-e2e-tests.yml +++ b/.github/workflows/run-e2e-tests.yml @@ -1,4 +1,4 @@ -# Copyright 2023-2024 The MathWorks, Inc. +# Copyright 2023-2025 The MathWorks, Inc. name: End-to-End Tests for MATLAB Integration for Jupyter on: @@ -6,6 +6,7 @@ on: jobs: playwright_e2e_tests: + timeout-minutes: 15 runs-on: ubuntu-latest defaults: run: @@ -101,4 +102,4 @@ jobs: with: name: e2e_test_results path: ./tests/e2e/zipped-e2e-test-results.zip - retention-days: 5 + retention-days: 5 \ No newline at end of file diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 43d16230..d14c9b0f 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -1,4 +1,4 @@ -# Copyright 2023-2024 The MathWorks, Inc. +# Copyright 2023-2025 The MathWorks, Inc. name: Integration testing MATLAB Integration for Jupyter @@ -8,10 +8,10 @@ on: jobs: python_integration_tests: + timeout-minutes: 15 strategy: fail-fast: false matrix: - #TODO: Add test coverage for macOs os: [ubuntu-latest, windows-latest] python-version: ["3.10", "3.13"] @@ -69,4 +69,4 @@ jobs: name: MATLAB Proxy Integration Test Log File ${{ matrix.os }} matlab-${{ matrix.matlab-release }} python-${{ matrix.python-version }} path: | tests/integration/integ_logs.log - licensing-screenshot-failed.png + licensing-screenshot-failed.png \ No newline at end of file diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index a55ff0c7..72317500 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -1,4 +1,4 @@ -# Copyright 2020-2024 The MathWorks, Inc. +# Copyright 2020-2025 The MathWorks, Inc. name: Unit Testing MATLAB Integration for Jupyter @@ -10,6 +10,7 @@ on: jobs: matlab_unit_tests: + timeout-minutes: 15 strategy: fail-fast: false matrix: @@ -60,13 +61,14 @@ jobs: select-by-folder: tests/matlab-tests python_unit_tests: + timeout-minutes: 15 env: code-cov-py: "3.11" code-cov-os: "ubuntu-latest" strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest, windows-latest] python-version: ["3.10", "3.13"] runs-on: ${{ matrix.os }} @@ -108,10 +110,11 @@ jobs: retention-days: 5 lezer_unit_tests: + timeout-minutes: 15 strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} defaults: @@ -134,6 +137,7 @@ jobs: run: npm test upload_code_coverage: + timeout-minutes: 15 name: "Upload Code Coverage using codecov" needs: [python_unit_tests] if: success() From 0c8ac9ef1faf55286d1000740c4405f0f583c550 Mon Sep 17 00:00:00 2001 From: Prabhakar Kumar Date: Mon, 8 Dec 2025 07:00:52 +0000 Subject: [PATCH 06/15] Fixes vulnerabilities in package-lock.json files, and introduces git commit pre-hooks to sanitize package-lock files. --- .githooks/pre-commit | 37 + .githooks/scripts/sanitize-npm-registry.sh | 32 + .gitignore | 3 + .../src/lezer-matlab/package-lock.json | 15 +- tests/e2e/package-lock.json | 4451 +++++++++++++---- 5 files changed, 3549 insertions(+), 989 deletions(-) create mode 100755 .githooks/pre-commit create mode 100755 .githooks/scripts/sanitize-npm-registry.sh diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 00000000..b3dcb86d --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,37 @@ +#!/bin/sh +# Copyright 2025 The MathWorks, Inc. +# Pre-commit hook to run all scripts in the scripts folder +# Called by "git commit" with no arguments. + +# Path to the scripts directory +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SCRIPTS_DIR="$SCRIPT_DIR/scripts" + +# Check if the scripts directory exists +if [ ! -d "$SCRIPTS_DIR" ]; then + echo "Error: Scripts directory not found at $SCRIPTS_DIR" + exit 1 +fi + +echo "Running pre-commit scripts from $SCRIPTS_DIR" + +# Find all executable scripts in the scripts directory and sort them alphabetically +for script in $(find "$SCRIPTS_DIR" -type f -executable | sort); do + script_name=$(basename "$script") + echo "Running $script_name..." + + # Execute the script + sh "$script" + + # Check the exit code + if [ $? -ne 0 ]; then + echo "Error: $script_name failed. Commit aborted." + exit 1 + fi + + echo "$script_name completed successfully." +done + +echo "All pre-commit scripts completed successfully." +exit 0 diff --git a/.githooks/scripts/sanitize-npm-registry.sh b/.githooks/scripts/sanitize-npm-registry.sh new file mode 100755 index 00000000..ac6aa022 --- /dev/null +++ b/.githooks/scripts/sanitize-npm-registry.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Copyright 2025 The MathWorks, Inc. +# Pre-commit hook to replace MathWorks NPM registry URLs with public NPM registry + +# Find all package.json, .npmrc, and other relevant files +echo "Executing pre-commit hook: sanitize-npm-registry" +FILES=$( git diff --cached --name-only --diff-filter=ACM | grep -E '(.*.json)') + +if [ -z "$FILES" ]; then + exit 0 +fi + +# Replace the MathWorks NPM registry URL with the public NPM registry +for FILE in $FILES; do + # Skip if file doesn't exist (it may have been deleted) + [ -f "$FILE" ] || continue + + echo "Sanitizing NPM registry URL in $FILE" + + # Replace the URL in the file + sed -i.bak 's|https://.*/artifactory/api/npm/npm-repos/|https://registry.npmjs.org/|g' "$FILE" + + # Remove backup file + rm -f "${FILE}.bak" + + # Stage the modified file + git add "$FILE" + + echo "Sanitization complete for $FILE" +done + +exit 0 diff --git a/.gitignore b/.gitignore index adc18549..d4ca2d4e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ *.egg-info .eggs __pycache__ +__uvcache__ +.ipynb_checkpoints/ +*.ipynb build dist .venv diff --git a/src/jupyter_matlab_labextension/src/lezer-matlab/package-lock.json b/src/jupyter_matlab_labextension/src/lezer-matlab/package-lock.json index f049cc66..e9408058 100644 --- a/src/jupyter_matlab_labextension/src/lezer-matlab/package-lock.json +++ b/src/jupyter_matlab_labextension/src/lezer-matlab/package-lock.json @@ -391,10 +391,11 @@ } }, "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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -838,10 +839,11 @@ } }, "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==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -1060,6 +1062,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", "dev": true, + "peer": true, "dependencies": { "@types/estree": "1.0.5" }, diff --git a/tests/e2e/package-lock.json b/tests/e2e/package-lock.json index 885ce17a..63bee2ca 100644 --- a/tests/e2e/package-lock.json +++ b/tests/e2e/package-lock.json @@ -19,29 +19,125 @@ "typescript": "^5.0.0" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@antfu/utils": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-9.3.0.tgz", + "integrity": "sha512-9hFT4RauhcUzqOE4f1+frMKLZrgNog5b06I7VmZQV1BkvwvqrbC8EBZf3L1eEL2AKb6rNKjER0sEvJiSP1FXEA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz", + "integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", + "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", + "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", + "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", + "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", + "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@codemirror/autocomplete": { - "version": "6.18.3", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.3.tgz", - "integrity": "sha512-1dNIOmiM0z4BIBwxmxEfA1yoxh1MF/6KPBbh20a5vphGV0ictKlgQsbJs6D6SkR6iJpGbpwRsa6PFMNlg9T9pQ==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", + "integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==", "dev": true, + "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.7.1", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.7.1.tgz", - "integrity": "sha512-llTrboQYw5H4THfhN4U3qCnSZ1SOJ60ohhz+SzU0ADGtwlc533DtklQP0vSFaQuCPDn3BPpOd1GbbnUtwNjsrw==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.0.tgz", + "integrity": "sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w==", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", @@ -50,20 +146,22 @@ } }, "node_modules/@codemirror/lang-cpp": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.2.tgz", - "integrity": "sha512-6oYEYUKHvrnacXxWxYa6t4puTlbN3dgV662BDfSH8+MfjQjVmP697/KYTDOqpxgerkvoNm7q5wlFMBeX8ZMocg==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.3.tgz", + "integrity": "sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/cpp": "^1.0.0" } }, "node_modules/@codemirror/lang-css": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.0.tgz", - "integrity": "sha512-CyR4rUNG9OYcXDZwMPvJdtb6PHbBDKUc/6Na2BIwZ6dKab1JQqKa4di+RNRY9Myn7JB81vayKwJeQ7jEdmNVDA==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", + "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", @@ -73,10 +171,11 @@ } }, "node_modules/@codemirror/lang-html": { - "version": "6.4.9", - "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz", - "integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==", + "version": "6.4.11", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", + "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", @@ -86,24 +185,26 @@ "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/css": "^1.1.0", - "@lezer/html": "^1.3.0" + "@lezer/html": "^1.3.12" } }, "node_modules/@codemirror/lang-java": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.1.tgz", - "integrity": "sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.2.tgz", + "integrity": "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/java": "^1.0.0" } }, "node_modules/@codemirror/lang-javascript": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz", - "integrity": "sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz", + "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", @@ -115,20 +216,22 @@ } }, "node_modules/@codemirror/lang-json": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz", - "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", + "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/json": "^1.0.0" } }, "node_modules/@codemirror/lang-markdown": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.3.1.tgz", - "integrity": "sha512-y3sSPuQjBKZQbQwe3ZJKrSW6Silyl9PnrU/Mf0m2OQgIlPoSYTtOvEL7xs94SVMkb8f4x+SQFnzXPdX4Wk2lsg==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz", + "integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.7.1", "@codemirror/lang-html": "^6.0.0", @@ -140,10 +243,11 @@ } }, "node_modules/@codemirror/lang-php": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.1.tgz", - "integrity": "sha512-ublojMdw/PNWa7qdN5TMsjmqkNuTBD3k6ndZ4Z0S25SBAiweFGyY68AS3xNcIOlb6DDFDvKlinLQ40vSLqf8xA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.2.tgz", + "integrity": "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", @@ -153,10 +257,11 @@ } }, "node_modules/@codemirror/lang-python": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.6.tgz", - "integrity": "sha512-ai+01WfZhWqM92UqjnvorkxosZ2aq2u28kHvr+N3gu012XqY2CThD67JPMHnGceRfXPDBmn1HnyqowdpF57bNg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.1.tgz", + "integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.3.2", "@codemirror/language": "^6.8.0", @@ -166,20 +271,22 @@ } }, "node_modules/@codemirror/lang-rust": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.1.tgz", - "integrity": "sha512-344EMWFBzWArHWdZn/NcgkwMvZIWUR1GEBdwG8FEp++6o6vT6KL9V7vGs2ONsKxxFUPXKI0SPcWhyYyl2zPYxQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.2.tgz", + "integrity": "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/rust": "^1.0.0" } }, "node_modules/@codemirror/lang-sql": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.8.0.tgz", - "integrity": "sha512-aGLmY4OwGqN3TdSx3h6QeA1NrvaYtF7kkoWR/+W7/JzB0gQtJ+VJxewlnE3+VImhA4WVlhmkJr109PefOOhjLg==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz", + "integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", @@ -194,6 +301,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/lang-wast/-/lang-wast-6.0.2.tgz", "integrity": "sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", @@ -206,6 +314,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz", "integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.4.0", @@ -216,10 +325,11 @@ } }, "node_modules/@codemirror/language": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.3.tgz", - "integrity": "sha512-kDqEU5sCP55Oabl6E7m5N+vZRoc0iWqgDVhEKifcHzPzjqCegcO4amfrYVL9PmPZpl4G0yjkpTpUO/Ui8CzO8A==", + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", + "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -230,30 +340,33 @@ } }, "node_modules/@codemirror/legacy-modes": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.4.2.tgz", - "integrity": "sha512-HsvWu08gOIIk303eZQCal4H4t65O/qp1V4ul4zVa3MHK5FJ0gz3qz3O55FIkm+aQUcshUOjBx38t2hPiJwW5/g==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.2.tgz", + "integrity": "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0" } }, "node_modules/@codemirror/lint": { - "version": "6.8.2", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.2.tgz", - "integrity": "sha512-PDFG5DjHxSEjOXk9TQYYVjZDqlZTFaDBfhQixHnQOEVDDNHUbEh/hstAjcQJaA6FQdZTD1hquXTK0rVBLADR1g==", + "version": "6.9.2", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.2.tgz", + "integrity": "sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", + "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "node_modules/@codemirror/search": { - "version": "6.5.7", - "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.7.tgz", - "integrity": "sha512-6+iLsXvITWKHYlkgHPCs/qiX4dNzn8N78YfhOFvPtPYCkuXqZq10rAfsUMhOq7O/1VjJqdXRflyExlfVcu/9VQ==", + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz", + "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", @@ -261,18 +374,24 @@ } }, "node_modules/@codemirror/state": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", - "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==", - "dev": true + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", + "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } }, "node_modules/@codemirror/view": { - "version": "6.34.3", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.34.3.tgz", - "integrity": "sha512-Ph5d+u8DxIeSgssXEakaakImkzBV4+slwIbcxl9oc9evexJhImeu/G8TK7+zp+IFK9KuJ0BdSn6kTBJeH2CHvA==", + "version": "6.38.8", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.8.tgz", + "integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==", "dev": true, + "license": "MIT", "dependencies": { - "@codemirror/state": "^6.4.0", + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } @@ -344,10 +463,11 @@ } }, "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==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -409,6 +529,7 @@ "integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==", "dev": true, "hasInstallScript": true, + "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", "engines": { "node": ">=6" } @@ -429,10 +550,11 @@ } }, "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==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -493,11 +615,139 @@ "deprecated": "Use @eslint/object-schema instead", "dev": true }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.0.2.tgz", + "integrity": "sha512-EfJS0rLfVuRuJRn4psJHtK2A9TqVnkxPpHY6lYHiB9+8eSuudsxbwMiavocG45ujOo6FJ+CIRlRnlOGinzkaGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@antfu/utils": "^9.2.0", + "@iconify/types": "^2.0.0", + "debug": "^4.4.1", + "globals": "^15.15.0", + "kolorist": "^1.8.0", + "local-pkg": "^1.1.1", + "mlly": "^1.7.4" + } + }, + "node_modules/@iconify/utils/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@iconify/utils/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@iconify/utils/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "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/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/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/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/@jupyter/react-components": { "version": "0.16.7", "resolved": "https://registry.npmjs.org/@jupyter/react-components/-/react-components-0.16.7.tgz", "integrity": "sha512-BKIPkJ9V011uhtdq1xBOu2M3up59CqsRbDS4aq8XhnHR4pwqfRV6k6irE5YBOETCoIwWZZ5RZO+cJcZ3DcsT5A==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@jupyter/web-components": "^0.16.7", "react": ">=17.0.0 <19.0.0" @@ -508,6 +758,7 @@ "resolved": "https://registry.npmjs.org/@jupyter/web-components/-/web-components-0.16.7.tgz", "integrity": "sha512-1a8awgvvP9J9pCV5vBRuQxdBk29764qiMJsJYEndrWH3cB/FlaO+sZIBm4OTf56Eqdgl8R3/ZSLM1+3mgXOkPg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@microsoft/fast-colors": "^5.3.1", "@microsoft/fast-element": "^1.12.0", @@ -516,10 +767,11 @@ } }, "node_modules/@jupyter/ydoc": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@jupyter/ydoc/-/ydoc-3.0.1.tgz", - "integrity": "sha512-zO6PEe/kNpb9oRPhxytLdnL4zclVDJVCIEavprIlIs/qCxGbgC/BvIoK6N/Ny525Ljrev8Ku9B+y6P2qFn3zqg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@jupyter/ydoc/-/ydoc-3.3.1.tgz", + "integrity": "sha512-q//mryEjBYgS83oxRoVkW9gRKp/34u0KtLguOjFvqwJDOn5Ksg2BtSwwMjcIru8i9MMtPqZywD7ZXSUAk3oJnw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@jupyterlab/nbformat": "^3.0.0 || ^4.0.0-alpha.21 || ^4.0.0", "@lumino/coreutils": "^1.11.0 || ^2.0.0", @@ -530,396 +782,415 @@ } }, "node_modules/@jupyterlab/application": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/application/-/application-4.3.1.tgz", - "integrity": "sha512-+Nzs99Umz36lH2a1E5u7ie68e4L1GnZX0YJHvFoREVvTXxwvGWvwrrAmS360GrlLD1kszg5LjAKV4H1hfoXXJQ==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/application/-/application-4.5.0.tgz", + "integrity": "sha512-8Xqu5sfEWhJkXqv5XY5kKhhtuvvrlXiUU3jw20gHNWVgy7m/RqJODEq1FMkcFpij0hW4cl62dkmmEjlQU8TGmQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@fortawesome/fontawesome-free": "^5.12.0", - "@jupyterlab/apputils": "^4.4.1", - "@jupyterlab/coreutils": "^6.3.1", - "@jupyterlab/docregistry": "^4.3.1", - "@jupyterlab/rendermime": "^4.3.1", - "@jupyterlab/rendermime-interfaces": "^3.11.1", - "@jupyterlab/services": "^7.3.1", - "@jupyterlab/statedb": "^4.3.1", - "@jupyterlab/translation": "^4.3.1", - "@jupyterlab/ui-components": "^4.3.1", - "@lumino/algorithm": "^2.0.2", - "@lumino/application": "^2.4.1", - "@lumino/commands": "^2.3.1", - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3", - "@lumino/messaging": "^2.0.2", - "@lumino/polling": "^2.1.3", - "@lumino/properties": "^2.0.2", - "@lumino/signaling": "^2.1.3", - "@lumino/widgets": "^2.5.0" + "@jupyterlab/apputils": "^4.6.0", + "@jupyterlab/coreutils": "^6.5.0", + "@jupyterlab/docregistry": "^4.5.0", + "@jupyterlab/rendermime": "^4.5.0", + "@jupyterlab/rendermime-interfaces": "^3.13.0", + "@jupyterlab/services": "^7.5.0", + "@jupyterlab/statedb": "^4.5.0", + "@jupyterlab/translation": "^4.5.0", + "@jupyterlab/ui-components": "^4.5.0", + "@lumino/algorithm": "^2.0.4", + "@lumino/application": "^2.4.5", + "@lumino/commands": "^2.3.3", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/messaging": "^2.0.4", + "@lumino/polling": "^2.1.5", + "@lumino/properties": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/widgets": "^2.7.2" } }, "node_modules/@jupyterlab/apputils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/apputils/-/apputils-4.4.1.tgz", - "integrity": "sha512-Yc66aJaTrktvJAuNP5/nQFeqZqiYRoMs7J1zbbPbPVn8+bRiuSF/0eCaCYY6X4U0cC0mZjCVmS6zrPHNthnktg==", - "dev": true, - "dependencies": { - "@jupyterlab/coreutils": "^6.3.1", - "@jupyterlab/observables": "^5.3.1", - "@jupyterlab/rendermime-interfaces": "^3.11.1", - "@jupyterlab/services": "^7.3.1", - "@jupyterlab/settingregistry": "^4.3.1", - "@jupyterlab/statedb": "^4.3.1", - "@jupyterlab/statusbar": "^4.3.1", - "@jupyterlab/translation": "^4.3.1", - "@jupyterlab/ui-components": "^4.3.1", - "@lumino/algorithm": "^2.0.2", - "@lumino/commands": "^2.3.1", - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3", - "@lumino/domutils": "^2.0.2", - "@lumino/messaging": "^2.0.2", - "@lumino/signaling": "^2.1.3", - "@lumino/virtualdom": "^2.0.2", - "@lumino/widgets": "^2.5.0", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/apputils/-/apputils-4.6.0.tgz", + "integrity": "sha512-vdT7mdt+fQUE/iKFwx7UAmSIXtbBopzFastnA6xeJOrHVyJfrnTblJEVeNmGhFjh9zhAhzSfer9GZgZhjFoetw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jupyterlab/coreutils": "^6.5.0", + "@jupyterlab/observables": "^5.5.0", + "@jupyterlab/rendermime-interfaces": "^3.13.0", + "@jupyterlab/services": "^7.5.0", + "@jupyterlab/settingregistry": "^4.5.0", + "@jupyterlab/statedb": "^4.5.0", + "@jupyterlab/statusbar": "^4.5.0", + "@jupyterlab/translation": "^4.5.0", + "@jupyterlab/ui-components": "^4.5.0", + "@lumino/algorithm": "^2.0.4", + "@lumino/commands": "^2.3.3", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/domutils": "^2.0.4", + "@lumino/messaging": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/virtualdom": "^2.0.4", + "@lumino/widgets": "^2.7.2", "@types/react": "^18.0.26", "react": "^18.2.0", "sanitize-html": "~2.12.1" } }, "node_modules/@jupyterlab/attachments": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/attachments/-/attachments-4.3.1.tgz", - "integrity": "sha512-QLrL2o3NIlvPyIOZFzeCu8Pqis/rDP3V5AbQBbxLKBVQGjPHipRo6Xa9idXdgHdUdYbCi1h19MU3mYQajI0+/g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/attachments/-/attachments-4.5.0.tgz", + "integrity": "sha512-MJSi5SWi7BCjASNPNiJNWTORk1hcmzyuR5oMJ0L1GHmJvykA8EjL/5n1MgksSLV7N2QpT9wDTv2Ycgh3VVYEhQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@jupyterlab/nbformat": "^4.3.1", - "@jupyterlab/observables": "^5.3.1", - "@jupyterlab/rendermime": "^4.3.1", - "@jupyterlab/rendermime-interfaces": "^3.11.1", - "@lumino/disposable": "^2.1.3", - "@lumino/signaling": "^2.1.3" + "@jupyterlab/nbformat": "^4.5.0", + "@jupyterlab/observables": "^5.5.0", + "@jupyterlab/rendermime": "^4.5.0", + "@jupyterlab/rendermime-interfaces": "^3.13.0", + "@lumino/disposable": "^2.1.5", + "@lumino/signaling": "^2.1.5" } }, "node_modules/@jupyterlab/cells": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/cells/-/cells-4.3.1.tgz", - "integrity": "sha512-/HUBYJSU/p20d4WeZCmr0Oj/ykwmpZwnESV2+D7V2EWGt1k7OiyEFQvJuLi1Nyt8znME7+p7ulUA118VXse6Bg==", - "dev": true, - "dependencies": { - "@codemirror/state": "^6.4.1", - "@codemirror/view": "^6.26.3", - "@jupyter/ydoc": "^3.0.0", - "@jupyterlab/apputils": "^4.4.1", - "@jupyterlab/attachments": "^4.3.1", - "@jupyterlab/codeeditor": "^4.3.1", - "@jupyterlab/codemirror": "^4.3.1", - "@jupyterlab/coreutils": "^6.3.1", - "@jupyterlab/documentsearch": "^4.3.1", - "@jupyterlab/filebrowser": "^4.3.1", - "@jupyterlab/nbformat": "^4.3.1", - "@jupyterlab/observables": "^5.3.1", - "@jupyterlab/outputarea": "^4.3.1", - "@jupyterlab/rendermime": "^4.3.1", - "@jupyterlab/services": "^7.3.1", - "@jupyterlab/toc": "^6.3.1", - "@jupyterlab/translation": "^4.3.1", - "@jupyterlab/ui-components": "^4.3.1", - "@lumino/algorithm": "^2.0.2", - "@lumino/coreutils": "^2.2.0", - "@lumino/domutils": "^2.0.2", - "@lumino/dragdrop": "^2.1.5", - "@lumino/messaging": "^2.0.2", - "@lumino/polling": "^2.1.3", - "@lumino/signaling": "^2.1.3", - "@lumino/virtualdom": "^2.0.2", - "@lumino/widgets": "^2.5.0", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/cells/-/cells-4.5.0.tgz", + "integrity": "sha512-53Vhj4I6LFTCy9UklUxNQM308VFSLebVXuNsP0w6OYQHZ3F4IYmJvK8pt+HZDxQI7FszleDu/iEbVsKmV1DTSw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@codemirror/state": "^6.5.2", + "@codemirror/view": "^6.38.1", + "@jupyter/ydoc": "^3.1.0", + "@jupyterlab/apputils": "^4.6.0", + "@jupyterlab/attachments": "^4.5.0", + "@jupyterlab/codeeditor": "^4.5.0", + "@jupyterlab/codemirror": "^4.5.0", + "@jupyterlab/coreutils": "^6.5.0", + "@jupyterlab/documentsearch": "^4.5.0", + "@jupyterlab/filebrowser": "^4.5.0", + "@jupyterlab/nbformat": "^4.5.0", + "@jupyterlab/observables": "^5.5.0", + "@jupyterlab/outputarea": "^4.5.0", + "@jupyterlab/rendermime": "^4.5.0", + "@jupyterlab/services": "^7.5.0", + "@jupyterlab/toc": "^6.5.0", + "@jupyterlab/translation": "^4.5.0", + "@jupyterlab/ui-components": "^4.5.0", + "@lumino/algorithm": "^2.0.4", + "@lumino/coreutils": "^2.2.2", + "@lumino/domutils": "^2.0.4", + "@lumino/dragdrop": "^2.1.7", + "@lumino/messaging": "^2.0.4", + "@lumino/polling": "^2.1.5", + "@lumino/signaling": "^2.1.5", + "@lumino/virtualdom": "^2.0.4", + "@lumino/widgets": "^2.7.2", "react": "^18.2.0" } }, "node_modules/@jupyterlab/codeeditor": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/codeeditor/-/codeeditor-4.3.1.tgz", - "integrity": "sha512-E7puhQOkxegQ4qDeA+xc7V+ien9bXndjflIw0tJCOPkN21XNcsPhm7ULuy/Kij/t+0w+a3NNoAbSROS4wnhEUw==", - "dev": true, - "dependencies": { - "@codemirror/state": "^6.4.1", - "@jupyter/ydoc": "^3.0.0", - "@jupyterlab/apputils": "^4.4.1", - "@jupyterlab/coreutils": "^6.3.1", - "@jupyterlab/nbformat": "^4.3.1", - "@jupyterlab/observables": "^5.3.1", - "@jupyterlab/statusbar": "^4.3.1", - "@jupyterlab/translation": "^4.3.1", - "@jupyterlab/ui-components": "^4.3.1", - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3", - "@lumino/dragdrop": "^2.1.5", - "@lumino/messaging": "^2.0.2", - "@lumino/signaling": "^2.1.3", - "@lumino/widgets": "^2.5.0", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/codeeditor/-/codeeditor-4.5.0.tgz", + "integrity": "sha512-CkETFC1IoaCk6sMyJfVFCYSN8Pw9/LxRzsZ1CDqWFCXHv9y8s9nuCj0cgYYXFJEdR9116C5EqZEkJrFl430bOQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@codemirror/state": "^6.5.2", + "@jupyter/ydoc": "^3.1.0", + "@jupyterlab/apputils": "^4.6.0", + "@jupyterlab/coreutils": "^6.5.0", + "@jupyterlab/nbformat": "^4.5.0", + "@jupyterlab/observables": "^5.5.0", + "@jupyterlab/statusbar": "^4.5.0", + "@jupyterlab/translation": "^4.5.0", + "@jupyterlab/ui-components": "^4.5.0", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/dragdrop": "^2.1.7", + "@lumino/messaging": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/widgets": "^2.7.2", "react": "^18.2.0" } }, "node_modules/@jupyterlab/codemirror": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/codemirror/-/codemirror-4.3.1.tgz", - "integrity": "sha512-0k8qts5p3GK0I8h3l7gvOcb+5HB9ExBTzWiLE0YWMAS7C/WTBKVdKOs1HwZJqt18nPfF6T3Q4kK/ZDBBNCE1LA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/codemirror/-/codemirror-4.5.0.tgz", + "integrity": "sha512-tV/F/W23QqrjiqWoZGOM571/hM0OqNxwR5WZ6yp8mJPPgsuXY0bzfexX/JDRcHOuB9oZIc47S7i8O0lbMZRCgg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@codemirror/autocomplete": "^6.16.0", - "@codemirror/commands": "^6.5.0", + "@codemirror/autocomplete": "^6.18.6", + "@codemirror/commands": "^6.8.1", "@codemirror/lang-cpp": "^6.0.2", - "@codemirror/lang-css": "^6.2.1", + "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-html": "^6.4.9", "@codemirror/lang-java": "^6.0.1", - "@codemirror/lang-javascript": "^6.2.2", + "@codemirror/lang-javascript": "^6.2.3", "@codemirror/lang-json": "^6.0.1", - "@codemirror/lang-markdown": "^6.2.5", + "@codemirror/lang-markdown": "^6.3.2", "@codemirror/lang-php": "^6.0.1", - "@codemirror/lang-python": "^6.1.6", + "@codemirror/lang-python": "^6.2.0", "@codemirror/lang-rust": "^6.0.1", - "@codemirror/lang-sql": "^6.6.4", + "@codemirror/lang-sql": "^6.8.0", "@codemirror/lang-wast": "^6.0.2", "@codemirror/lang-xml": "^6.1.0", - "@codemirror/language": "^6.10.1", - "@codemirror/legacy-modes": "^6.4.0", - "@codemirror/search": "^6.5.6", - "@codemirror/state": "^6.4.1", - "@codemirror/view": "^6.26.3", - "@jupyter/ydoc": "^3.0.0", - "@jupyterlab/codeeditor": "^4.3.1", - "@jupyterlab/coreutils": "^6.3.1", - "@jupyterlab/documentsearch": "^4.3.1", - "@jupyterlab/nbformat": "^4.3.1", - "@jupyterlab/translation": "^4.3.1", + "@codemirror/language": "^6.11.0", + "@codemirror/legacy-modes": "^6.5.1", + "@codemirror/search": "^6.5.10", + "@codemirror/state": "^6.5.2", + "@codemirror/view": "^6.38.1", + "@jupyter/ydoc": "^3.1.0", + "@jupyterlab/codeeditor": "^4.5.0", + "@jupyterlab/coreutils": "^6.5.0", + "@jupyterlab/documentsearch": "^4.5.0", + "@jupyterlab/nbformat": "^4.5.0", + "@jupyterlab/translation": "^4.5.0", "@lezer/common": "^1.2.1", "@lezer/generator": "^1.7.0", "@lezer/highlight": "^1.2.0", "@lezer/markdown": "^1.3.0", - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3", - "@lumino/signaling": "^2.1.3", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/signaling": "^2.1.5", "yjs": "^13.5.40" } }, "node_modules/@jupyterlab/console": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/console/-/console-4.3.1.tgz", - "integrity": "sha512-rl5jXBgW0US8zucBQmABrLsUulWdW5Wssv/E1ZPNYyppaqd9rbiW8LsSSYxyi2pBFvnDbAZVf+MVh06HBivH6g==", - "dev": true, - "dependencies": { - "@jupyter/ydoc": "^3.0.0", - "@jupyterlab/apputils": "^4.4.1", - "@jupyterlab/cells": "^4.3.1", - "@jupyterlab/codeeditor": "^4.3.1", - "@jupyterlab/coreutils": "^6.3.1", - "@jupyterlab/nbformat": "^4.3.1", - "@jupyterlab/observables": "^5.3.1", - "@jupyterlab/rendermime": "^4.3.1", - "@jupyterlab/services": "^7.3.1", - "@jupyterlab/translation": "^4.3.1", - "@jupyterlab/ui-components": "^4.3.1", - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3", - "@lumino/dragdrop": "^2.1.5", - "@lumino/messaging": "^2.0.2", - "@lumino/signaling": "^2.1.3", - "@lumino/widgets": "^2.5.0" + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/console/-/console-4.5.0.tgz", + "integrity": "sha512-qqz+Jhf4uuoAlOzhTumLK4LigDNB4dWZ8t+xnKAWCuZczgzlos7mvQNdw0Q12AZEPQmvAklsmY/C9Z3L7HaFVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jupyter/ydoc": "^3.1.0", + "@jupyterlab/apputils": "^4.6.0", + "@jupyterlab/cells": "^4.5.0", + "@jupyterlab/codeeditor": "^4.5.0", + "@jupyterlab/coreutils": "^6.5.0", + "@jupyterlab/nbformat": "^4.5.0", + "@jupyterlab/observables": "^5.5.0", + "@jupyterlab/rendermime": "^4.5.0", + "@jupyterlab/services": "^7.5.0", + "@jupyterlab/translation": "^4.5.0", + "@jupyterlab/ui-components": "^4.5.0", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/dragdrop": "^2.1.7", + "@lumino/messaging": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/widgets": "^2.7.2" } }, "node_modules/@jupyterlab/coreutils": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-6.3.1.tgz", - "integrity": "sha512-72Lyg2XA0Fsyn+62FpVIf67TGrxQZ4CNvldDvmlkixyY/n63bB38AcC0zvvCFDRv7QwXQzp7DaA0xfvAPoQ3bw==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-6.5.0.tgz", + "integrity": "sha512-lowpjjZMRupNNvBxG3IwQ4pSKWGtNaDPA//cL5PxiUd5Y1xkUcUy+DPq8HjcSWvDe3viCwpi7rCII8v4btPtuw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3", - "@lumino/signaling": "^2.1.3", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/signaling": "^2.1.5", "minimist": "~1.2.0", "path-browserify": "^1.0.0", "url-parse": "~1.5.4" } }, "node_modules/@jupyterlab/debugger": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/debugger/-/debugger-4.3.1.tgz", - "integrity": "sha512-ao1zZ8/A1c+fbJQH7xshlEemB77W7tYdJUg3Jek9udeC84fBiGQ99iq9KXje2tg5DrMS3gSoS1RVLYrF4JFtLQ==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/debugger/-/debugger-4.5.0.tgz", + "integrity": "sha512-0jpf+OLM6VEAKWYypGHFqrsKBLvyafi8bW843K04v13U6jKHXCql8GkuIvqN/1mnxnu6DTvL8TXYncQXEvvmqw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@codemirror/state": "^6.4.1", - "@codemirror/view": "^6.26.3", + "@codemirror/state": "^6.5.2", + "@codemirror/view": "^6.38.1", "@jupyter/react-components": "^0.16.6", - "@jupyter/ydoc": "^3.0.0", - "@jupyterlab/application": "^4.3.1", - "@jupyterlab/apputils": "^4.4.1", - "@jupyterlab/cells": "^4.3.1", - "@jupyterlab/codeeditor": "^4.3.1", - "@jupyterlab/codemirror": "^4.3.1", - "@jupyterlab/console": "^4.3.1", - "@jupyterlab/coreutils": "^6.3.1", - "@jupyterlab/docregistry": "^4.3.1", - "@jupyterlab/fileeditor": "^4.3.1", - "@jupyterlab/notebook": "^4.3.1", - "@jupyterlab/observables": "^5.3.1", - "@jupyterlab/rendermime": "^4.3.1", - "@jupyterlab/services": "^7.3.1", - "@jupyterlab/translation": "^4.3.1", - "@jupyterlab/ui-components": "^4.3.1", - "@lumino/algorithm": "^2.0.2", - "@lumino/commands": "^2.3.1", - "@lumino/coreutils": "^2.2.0", - "@lumino/datagrid": "^2.4.1", - "@lumino/disposable": "^2.1.3", - "@lumino/messaging": "^2.0.2", - "@lumino/polling": "^2.1.3", - "@lumino/signaling": "^2.1.3", - "@lumino/widgets": "^2.5.0", + "@jupyter/ydoc": "^3.1.0", + "@jupyterlab/application": "^4.5.0", + "@jupyterlab/apputils": "^4.6.0", + "@jupyterlab/cells": "^4.5.0", + "@jupyterlab/codeeditor": "^4.5.0", + "@jupyterlab/codemirror": "^4.5.0", + "@jupyterlab/console": "^4.5.0", + "@jupyterlab/coreutils": "^6.5.0", + "@jupyterlab/docregistry": "^4.5.0", + "@jupyterlab/fileeditor": "^4.5.0", + "@jupyterlab/notebook": "^4.5.0", + "@jupyterlab/observables": "^5.5.0", + "@jupyterlab/rendermime": "^4.5.0", + "@jupyterlab/services": "^7.5.0", + "@jupyterlab/settingregistry": "^4.5.0", + "@jupyterlab/translation": "^4.5.0", + "@jupyterlab/ui-components": "^4.5.0", + "@lumino/algorithm": "^2.0.4", + "@lumino/commands": "^2.3.3", + "@lumino/coreutils": "^2.2.2", + "@lumino/datagrid": "^2.5.3", + "@lumino/disposable": "^2.1.5", + "@lumino/messaging": "^2.0.4", + "@lumino/polling": "^2.1.5", + "@lumino/signaling": "^2.1.5", + "@lumino/widgets": "^2.7.2", "@vscode/debugprotocol": "^1.51.0", "react": "^18.2.0" } }, "node_modules/@jupyterlab/docmanager": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/docmanager/-/docmanager-4.3.1.tgz", - "integrity": "sha512-k2KzicklzXYQcgJGdrlyJcbFTAwwKN2Eo3KSRT+Zh6hJgqWlIaMP0D9M5rUsgd1ml3b41yK2UZGeULjpoZa6GA==", - "dev": true, - "dependencies": { - "@jupyterlab/apputils": "^4.4.1", - "@jupyterlab/coreutils": "^6.3.1", - "@jupyterlab/docregistry": "^4.3.1", - "@jupyterlab/services": "^7.3.1", - "@jupyterlab/statedb": "^4.3.1", - "@jupyterlab/statusbar": "^4.3.1", - "@jupyterlab/translation": "^4.3.1", - "@jupyterlab/ui-components": "^4.3.1", - "@lumino/algorithm": "^2.0.2", - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3", - "@lumino/messaging": "^2.0.2", - "@lumino/polling": "^2.1.3", - "@lumino/properties": "^2.0.2", - "@lumino/signaling": "^2.1.3", - "@lumino/widgets": "^2.5.0", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/docmanager/-/docmanager-4.5.0.tgz", + "integrity": "sha512-gQnXVPV1tAaMD77PVe67rjQYcnhM3Dsst6ffuE9Yf5hw1OjnU9ZLSHwqXUBWIRnD5J0vUrEtJLVOsfjHtq2mPg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jupyterlab/apputils": "^4.6.0", + "@jupyterlab/coreutils": "^6.5.0", + "@jupyterlab/docregistry": "^4.5.0", + "@jupyterlab/rendermime": "^4.5.0", + "@jupyterlab/services": "^7.5.0", + "@jupyterlab/statedb": "^4.5.0", + "@jupyterlab/statusbar": "^4.5.0", + "@jupyterlab/translation": "^4.5.0", + "@jupyterlab/ui-components": "^4.5.0", + "@lumino/algorithm": "^2.0.4", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/messaging": "^2.0.4", + "@lumino/polling": "^2.1.5", + "@lumino/properties": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/widgets": "^2.7.2", "react": "^18.2.0" } }, "node_modules/@jupyterlab/docregistry": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/docregistry/-/docregistry-4.3.1.tgz", - "integrity": "sha512-aPNYbs/gnvs3BChGvVRkPV8cAUpP3Xg4wS91beVIEzr84/YdZ+ekqQvL5wnIOjyKPoi6KmGmgQC2Tlz37W2ZdA==", - "dev": true, - "dependencies": { - "@jupyter/ydoc": "^3.0.0", - "@jupyterlab/apputils": "^4.4.1", - "@jupyterlab/codeeditor": "^4.3.1", - "@jupyterlab/coreutils": "^6.3.1", - "@jupyterlab/observables": "^5.3.1", - "@jupyterlab/rendermime": "^4.3.1", - "@jupyterlab/rendermime-interfaces": "^3.11.1", - "@jupyterlab/services": "^7.3.1", - "@jupyterlab/translation": "^4.3.1", - "@jupyterlab/ui-components": "^4.3.1", - "@lumino/algorithm": "^2.0.2", - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3", - "@lumino/messaging": "^2.0.2", - "@lumino/properties": "^2.0.2", - "@lumino/signaling": "^2.1.3", - "@lumino/widgets": "^2.5.0", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/docregistry/-/docregistry-4.5.0.tgz", + "integrity": "sha512-0K4lxmRrVp/dlR39Z5VQXlTIvXNo1ziIGrkggkSx8E6HRNeoURhUj37/CJ6cPzxY5H+fUAiTPVYlicq/T5N7QA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jupyter/ydoc": "^3.1.0", + "@jupyterlab/apputils": "^4.6.0", + "@jupyterlab/codeeditor": "^4.5.0", + "@jupyterlab/coreutils": "^6.5.0", + "@jupyterlab/observables": "^5.5.0", + "@jupyterlab/rendermime": "^4.5.0", + "@jupyterlab/rendermime-interfaces": "^3.13.0", + "@jupyterlab/services": "^7.5.0", + "@jupyterlab/translation": "^4.5.0", + "@jupyterlab/ui-components": "^4.5.0", + "@lumino/algorithm": "^2.0.4", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/messaging": "^2.0.4", + "@lumino/properties": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/widgets": "^2.7.2", "react": "^18.2.0" } }, "node_modules/@jupyterlab/documentsearch": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/documentsearch/-/documentsearch-4.3.1.tgz", - "integrity": "sha512-oeEdx3CLOzIweRqgOBb5VUfOSkBs34lxC6Ih5fKgg4Ql+wuWAu61Bj0baykXA/q2637kPeDrELjOK9zB6bkd2g==", - "dev": true, - "dependencies": { - "@jupyterlab/apputils": "^4.4.1", - "@jupyterlab/translation": "^4.3.1", - "@jupyterlab/ui-components": "^4.3.1", - "@lumino/commands": "^2.3.1", - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3", - "@lumino/messaging": "^2.0.2", - "@lumino/polling": "^2.1.3", - "@lumino/signaling": "^2.1.3", - "@lumino/widgets": "^2.5.0", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/documentsearch/-/documentsearch-4.5.0.tgz", + "integrity": "sha512-Fdh18NLFyEXHQPPwnPgpKoFxTKNz5n1GiU3e2AgkE4Yzt2BVdM3xC0LoDd6uiWyg+FRtWVuAP+9qZHniOJCnHw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jupyterlab/apputils": "^4.6.0", + "@jupyterlab/translation": "^4.5.0", + "@jupyterlab/ui-components": "^4.5.0", + "@lumino/commands": "^2.3.3", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/messaging": "^2.0.4", + "@lumino/polling": "^2.1.5", + "@lumino/signaling": "^2.1.5", + "@lumino/widgets": "^2.7.2", "react": "^18.2.0" } }, "node_modules/@jupyterlab/filebrowser": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/filebrowser/-/filebrowser-4.3.1.tgz", - "integrity": "sha512-LNxXBdQBLn3ttuZ1PThMK1+saazSDZFrwUkGyQBU+NxY4nN1tPuwTrJaOCr2+4CzI6VY3asOEJ7grDILXqY0jg==", - "dev": true, - "dependencies": { - "@jupyterlab/apputils": "^4.4.1", - "@jupyterlab/coreutils": "^6.3.1", - "@jupyterlab/docmanager": "^4.3.1", - "@jupyterlab/docregistry": "^4.3.1", - "@jupyterlab/services": "^7.3.1", - "@jupyterlab/statedb": "^4.3.1", - "@jupyterlab/statusbar": "^4.3.1", - "@jupyterlab/translation": "^4.3.1", - "@jupyterlab/ui-components": "^4.3.1", - "@lumino/algorithm": "^2.0.2", - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3", - "@lumino/domutils": "^2.0.2", - "@lumino/dragdrop": "^2.1.5", - "@lumino/messaging": "^2.0.2", - "@lumino/polling": "^2.1.3", - "@lumino/signaling": "^2.1.3", - "@lumino/virtualdom": "^2.0.2", - "@lumino/widgets": "^2.5.0", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/filebrowser/-/filebrowser-4.5.0.tgz", + "integrity": "sha512-KWKyThGUBGahUzAefw0tjjP+lbzLcx7k870FTBpSf92neRSCsoQ3Qkso6zPFExM4T0vTo0gv6vq7cLE8tzWnmw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jupyterlab/apputils": "^4.6.0", + "@jupyterlab/coreutils": "^6.5.0", + "@jupyterlab/docmanager": "^4.5.0", + "@jupyterlab/docregistry": "^4.5.0", + "@jupyterlab/services": "^7.5.0", + "@jupyterlab/statedb": "^4.5.0", + "@jupyterlab/statusbar": "^4.5.0", + "@jupyterlab/translation": "^4.5.0", + "@jupyterlab/ui-components": "^4.5.0", + "@lumino/algorithm": "^2.0.4", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/domutils": "^2.0.4", + "@lumino/dragdrop": "^2.1.7", + "@lumino/messaging": "^2.0.4", + "@lumino/polling": "^2.1.5", + "@lumino/signaling": "^2.1.5", + "@lumino/virtualdom": "^2.0.4", + "@lumino/widgets": "^2.7.2", + "jest-environment-jsdom": "^29.3.0", "react": "^18.2.0" } }, "node_modules/@jupyterlab/fileeditor": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/fileeditor/-/fileeditor-4.3.1.tgz", - "integrity": "sha512-gMGZZ4cvpJyIGsapfTequzWqMs5pWCuU8Z2kn+INKq0U+xpimZoz/0mzND6veQHzXQkZwST2CTJOllnMLEVz4Q==", - "dev": true, - "dependencies": { - "@jupyter/ydoc": "^3.0.0", - "@jupyterlab/apputils": "^4.4.1", - "@jupyterlab/codeeditor": "^4.3.1", - "@jupyterlab/codemirror": "^4.3.1", - "@jupyterlab/coreutils": "^6.3.1", - "@jupyterlab/docregistry": "^4.3.1", - "@jupyterlab/documentsearch": "^4.3.1", - "@jupyterlab/lsp": "^4.3.1", - "@jupyterlab/statusbar": "^4.3.1", - "@jupyterlab/toc": "^6.3.1", - "@jupyterlab/translation": "^4.3.1", - "@jupyterlab/ui-components": "^4.3.1", - "@lumino/commands": "^2.3.1", - "@lumino/coreutils": "^2.2.0", - "@lumino/messaging": "^2.0.2", - "@lumino/widgets": "^2.5.0", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/fileeditor/-/fileeditor-4.5.0.tgz", + "integrity": "sha512-IqfeiBetowWuQHshRiDAeQeB1hkLeRNfWp8COkceh8ghHYjDIfu7e2kJzQ6bkAg1lhqvtw7JmxySqub/1JEIhw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jupyter/ydoc": "^3.1.0", + "@jupyterlab/apputils": "^4.6.0", + "@jupyterlab/codeeditor": "^4.5.0", + "@jupyterlab/codemirror": "^4.5.0", + "@jupyterlab/coreutils": "^6.5.0", + "@jupyterlab/docregistry": "^4.5.0", + "@jupyterlab/documentsearch": "^4.5.0", + "@jupyterlab/lsp": "^4.5.0", + "@jupyterlab/rendermime": "^4.5.0", + "@jupyterlab/statusbar": "^4.5.0", + "@jupyterlab/toc": "^6.5.0", + "@jupyterlab/translation": "^4.5.0", + "@jupyterlab/ui-components": "^4.5.0", + "@lumino/commands": "^2.3.3", + "@lumino/coreutils": "^2.2.2", + "@lumino/messaging": "^2.0.4", + "@lumino/widgets": "^2.7.2", "react": "^18.2.0", "regexp-match-indices": "^1.0.2" } }, "node_modules/@jupyterlab/galata": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/galata/-/galata-5.3.1.tgz", - "integrity": "sha512-6k9A8wlADtHlkKwXRsf+3bx0cusbIx7U38c5MARpkaUS5rvimtAWPpJXjMbHGMFUbtsCLczv7zJdrwl/mNEy0Q==", - "dev": true, - "dependencies": { - "@jupyterlab/application": "^4.3.1", - "@jupyterlab/apputils": "^4.4.1", - "@jupyterlab/coreutils": "^6.3.1", - "@jupyterlab/debugger": "^4.3.1", - "@jupyterlab/docmanager": "^4.3.1", - "@jupyterlab/nbformat": "^4.3.1", - "@jupyterlab/notebook": "^4.3.1", - "@jupyterlab/services": "^7.3.1", - "@jupyterlab/settingregistry": "^4.3.1", - "@lumino/coreutils": "^2.2.0", - "@playwright/test": "^1.48.0", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/galata/-/galata-5.5.0.tgz", + "integrity": "sha512-1BvcmBUvpMYxdsZ5tAUfojCLoZAbiJn1RrYVpPFlJqkdF00H/ikf8fGrgxiTgrfUU5qizDVLqspFVk+hRb4uAA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jupyterlab/application": "^4.5.0", + "@jupyterlab/apputils": "^4.6.0", + "@jupyterlab/coreutils": "^6.5.0", + "@jupyterlab/debugger": "^4.5.0", + "@jupyterlab/docmanager": "^4.5.0", + "@jupyterlab/nbformat": "^4.5.0", + "@jupyterlab/notebook": "^4.5.0", + "@jupyterlab/services": "^7.5.0", + "@jupyterlab/settingregistry": "^4.5.0", + "@lumino/coreutils": "^2.2.2", + "@playwright/test": "^1.56.0", "@stdlib/stats": "~0.0.13", "fs-extra": "^10.1.0", "json5": "^2.2.3", @@ -931,171 +1202,215 @@ } }, "node_modules/@jupyterlab/lsp": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/lsp/-/lsp-4.3.1.tgz", - "integrity": "sha512-XpMBRq2kSnlZKPCo46uVonc7d7YXJQhyu5uqxoikbbp95oLE+LNcF90iJ1pFqXYGV4JpxZhvH9FO5+YP2uTiQQ==", - "dev": true, - "dependencies": { - "@jupyterlab/apputils": "^4.4.1", - "@jupyterlab/codeeditor": "^4.3.1", - "@jupyterlab/codemirror": "^4.3.1", - "@jupyterlab/coreutils": "^6.3.1", - "@jupyterlab/docregistry": "^4.3.1", - "@jupyterlab/services": "^7.3.1", - "@jupyterlab/translation": "^4.3.1", - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3", - "@lumino/signaling": "^2.1.3", - "@lumino/widgets": "^2.5.0", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/lsp/-/lsp-4.5.0.tgz", + "integrity": "sha512-Hz3mm4eJejKmZi0aLMHfW2FsiOynvetfMoXfQ8spcgPQ1yu76YjW6e8Djs417sukZ8XEOXhd/WoLL+69Q+8k6A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jupyterlab/apputils": "^4.6.0", + "@jupyterlab/codeeditor": "^4.5.0", + "@jupyterlab/codemirror": "^4.5.0", + "@jupyterlab/coreutils": "^6.5.0", + "@jupyterlab/docregistry": "^4.5.0", + "@jupyterlab/services": "^7.5.0", + "@jupyterlab/translation": "^4.5.0", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/signaling": "^2.1.5", + "@lumino/widgets": "^2.7.2", "lodash.mergewith": "^4.6.1", "vscode-jsonrpc": "^6.0.0", "vscode-languageserver-protocol": "^3.17.0", "vscode-ws-jsonrpc": "~1.0.2" } }, + "node_modules/@jupyterlab/markedparser-extension": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/markedparser-extension/-/markedparser-extension-4.5.0.tgz", + "integrity": "sha512-F/DjHsXNmzmbv+V5yHhGFyLbnNRZM5fYCmC6Gx0FScN0tLtkVZlFpIiw6HZCm/dRPFH9zKyCqgsdP2Al8IQ1Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jupyterlab/application": "^4.5.0", + "@jupyterlab/codemirror": "^4.5.0", + "@jupyterlab/coreutils": "^6.5.0", + "@jupyterlab/mermaid": "^4.5.0", + "@jupyterlab/rendermime": "^4.5.0", + "@lumino/coreutils": "^2.2.2", + "marked": "^16.2.1", + "marked-gfm-heading-id": "^4.1.2", + "marked-mangle": "^1.1.11" + } + }, + "node_modules/@jupyterlab/mermaid": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/mermaid/-/mermaid-4.5.0.tgz", + "integrity": "sha512-R2jin3NI8sFMqL5Bx//Q/+3lyLPBbfaVaERprCSgGUzizK6q6GXoeQuJh4Hl5NafqPgKyUtiwBwmQZZJsbCEwQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jupyterlab/apputils": "^4.6.0", + "@jupyterlab/coreutils": "^6.5.0", + "@jupyterlab/rendermime-interfaces": "^3.13.0", + "@lumino/coreutils": "^2.2.2", + "@lumino/widgets": "^2.7.2", + "@mermaid-js/layout-elk": "^0.2.0", + "mermaid": "^11.12.1" + } + }, "node_modules/@jupyterlab/nbformat": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/nbformat/-/nbformat-4.3.1.tgz", - "integrity": "sha512-9dIcagn5xQMKijPiu08MGAnfWQzk5mkqHRltHUfYbydV33lWgTjFBEpugHncEZGIxIJz+iqw2H9KJ8HSb7aJqw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/nbformat/-/nbformat-4.5.0.tgz", + "integrity": "sha512-yNG4EGawtyM398VEyfXlDvJP5mRJMUs06oVBhigUDsKdyhc7xO++z2zMSj9sJOUfV27qh6B/4RppFzIl8vvWBA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@lumino/coreutils": "^2.2.0" + "@lumino/coreutils": "^2.2.2" } }, "node_modules/@jupyterlab/notebook": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/notebook/-/notebook-4.3.1.tgz", - "integrity": "sha512-NjtAsZczoVPp+mdJFFykqeCCGEXqzUKgab27dbJRqG4aWpHVQjrLxLGOv1mde70je836lEyMKZet9fxnlDOgKg==", - "dev": true, - "dependencies": { - "@jupyter/ydoc": "^3.0.0", - "@jupyterlab/apputils": "^4.4.1", - "@jupyterlab/cells": "^4.3.1", - "@jupyterlab/codeeditor": "^4.3.1", - "@jupyterlab/codemirror": "^4.3.1", - "@jupyterlab/coreutils": "^6.3.1", - "@jupyterlab/docregistry": "^4.3.1", - "@jupyterlab/documentsearch": "^4.3.1", - "@jupyterlab/lsp": "^4.3.1", - "@jupyterlab/nbformat": "^4.3.1", - "@jupyterlab/observables": "^5.3.1", - "@jupyterlab/rendermime": "^4.3.1", - "@jupyterlab/services": "^7.3.1", - "@jupyterlab/settingregistry": "^4.3.1", - "@jupyterlab/statusbar": "^4.3.1", - "@jupyterlab/toc": "^6.3.1", - "@jupyterlab/translation": "^4.3.1", - "@jupyterlab/ui-components": "^4.3.1", - "@lumino/algorithm": "^2.0.2", - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3", - "@lumino/domutils": "^2.0.2", - "@lumino/dragdrop": "^2.1.5", - "@lumino/messaging": "^2.0.2", - "@lumino/polling": "^2.1.3", - "@lumino/properties": "^2.0.2", - "@lumino/signaling": "^2.1.3", - "@lumino/virtualdom": "^2.0.2", - "@lumino/widgets": "^2.5.0", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/notebook/-/notebook-4.5.0.tgz", + "integrity": "sha512-I5FY13yru5PytY5wDoUoe+Df1U0wMI6EUykPOCQCSqQfyQ/rlzAbjwdba1hG8CZoN0c4l10i1SCNffcjuNeqOw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jupyter/ydoc": "^3.1.0", + "@jupyterlab/apputils": "^4.6.0", + "@jupyterlab/cells": "^4.5.0", + "@jupyterlab/codeeditor": "^4.5.0", + "@jupyterlab/codemirror": "^4.5.0", + "@jupyterlab/coreutils": "^6.5.0", + "@jupyterlab/docregistry": "^4.5.0", + "@jupyterlab/documentsearch": "^4.5.0", + "@jupyterlab/lsp": "^4.5.0", + "@jupyterlab/markedparser-extension": "^4.5.0", + "@jupyterlab/nbformat": "^4.5.0", + "@jupyterlab/observables": "^5.5.0", + "@jupyterlab/rendermime": "^4.5.0", + "@jupyterlab/services": "^7.5.0", + "@jupyterlab/settingregistry": "^4.5.0", + "@jupyterlab/statusbar": "^4.5.0", + "@jupyterlab/toc": "^6.5.0", + "@jupyterlab/translation": "^4.5.0", + "@jupyterlab/ui-components": "^4.5.0", + "@lumino/algorithm": "^2.0.4", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/domutils": "^2.0.4", + "@lumino/dragdrop": "^2.1.7", + "@lumino/messaging": "^2.0.4", + "@lumino/polling": "^2.1.5", + "@lumino/properties": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/virtualdom": "^2.0.4", + "@lumino/widgets": "^2.7.2", "react": "^18.2.0" } }, "node_modules/@jupyterlab/observables": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/observables/-/observables-5.3.1.tgz", - "integrity": "sha512-iS67+D/aMz9elySBic+453RUOYbIM9/zCZboF4OoBJ9f8ClY5SUIwvAlkkcMK8rn8tphSbh3Ik7U7Ea/HSdzjw==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/observables/-/observables-5.5.0.tgz", + "integrity": "sha512-DWNtwNRqVhrX+ebKhebAZYDsUiuDBDo7SZcsAkx3lXEhmbDkSwlwiZrmdZ/9C3fpyAPAjdbEDpGDBMBwt+6spQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@lumino/algorithm": "^2.0.2", - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3", - "@lumino/messaging": "^2.0.2", - "@lumino/signaling": "^2.1.3" + "@lumino/algorithm": "^2.0.4", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/messaging": "^2.0.4", + "@lumino/signaling": "^2.1.5" } }, "node_modules/@jupyterlab/outputarea": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/outputarea/-/outputarea-4.3.1.tgz", - "integrity": "sha512-zGeIDe1xpoXve9VAj8qHW6sKSOZo9sW5gJbDfATguYHS44cX1XGNkLHAfW2GozP4y+M5De0v5cKf2EVnTfL6OA==", - "dev": true, - "dependencies": { - "@jupyterlab/apputils": "^4.4.1", - "@jupyterlab/nbformat": "^4.3.1", - "@jupyterlab/observables": "^5.3.1", - "@jupyterlab/rendermime": "^4.3.1", - "@jupyterlab/rendermime-interfaces": "^3.11.1", - "@jupyterlab/services": "^7.3.1", - "@jupyterlab/translation": "^4.3.1", - "@lumino/algorithm": "^2.0.2", - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3", - "@lumino/messaging": "^2.0.2", - "@lumino/properties": "^2.0.2", - "@lumino/signaling": "^2.1.3", - "@lumino/widgets": "^2.5.0" + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/outputarea/-/outputarea-4.5.0.tgz", + "integrity": "sha512-xt/tq+3sTWzbuRjifmA+kSaa9SHHX3Da/E96OIQhC9eckdfIaKnUJ8PCTrnHxor3OKLxpc9NE04nJMonXBkDPA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jupyterlab/apputils": "^4.6.0", + "@jupyterlab/nbformat": "^4.5.0", + "@jupyterlab/observables": "^5.5.0", + "@jupyterlab/rendermime": "^4.5.0", + "@jupyterlab/rendermime-interfaces": "^3.13.0", + "@jupyterlab/services": "^7.5.0", + "@jupyterlab/translation": "^4.5.0", + "@lumino/algorithm": "^2.0.4", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/messaging": "^2.0.4", + "@lumino/properties": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/widgets": "^2.7.2" } }, "node_modules/@jupyterlab/rendermime": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/rendermime/-/rendermime-4.3.1.tgz", - "integrity": "sha512-b44RZKrdYkM/CqUWNT36Tl6Q/sYeyb1oCECH1Jb4wRNZC88SG57373rENGj28BqWdhk+0aYAvxbB9r2gTy8csA==", - "dev": true, - "dependencies": { - "@jupyterlab/apputils": "^4.4.1", - "@jupyterlab/coreutils": "^6.3.1", - "@jupyterlab/nbformat": "^4.3.1", - "@jupyterlab/observables": "^5.3.1", - "@jupyterlab/rendermime-interfaces": "^3.11.1", - "@jupyterlab/services": "^7.3.1", - "@jupyterlab/translation": "^4.3.1", - "@lumino/coreutils": "^2.2.0", - "@lumino/messaging": "^2.0.2", - "@lumino/signaling": "^2.1.3", - "@lumino/widgets": "^2.5.0", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/rendermime/-/rendermime-4.5.0.tgz", + "integrity": "sha512-3+q+OtSKaX27BapoyJJco17jnBU8gCW7/tfQTms+zH0cVhtSTvK+VGTERlwp/229UlZvnYmC6iROIirZ9jVwig==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jupyterlab/apputils": "^4.6.0", + "@jupyterlab/coreutils": "^6.5.0", + "@jupyterlab/nbformat": "^4.5.0", + "@jupyterlab/observables": "^5.5.0", + "@jupyterlab/rendermime-interfaces": "^3.13.0", + "@jupyterlab/services": "^7.5.0", + "@jupyterlab/translation": "^4.5.0", + "@lumino/coreutils": "^2.2.2", + "@lumino/messaging": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/widgets": "^2.7.2", "lodash.escape": "^4.0.1" } }, "node_modules/@jupyterlab/rendermime-interfaces": { - "version": "3.11.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/rendermime-interfaces/-/rendermime-interfaces-3.11.1.tgz", - "integrity": "sha512-T2eThoZ34qqEOiVdjqGbWyWoF4tTQUpdOFuqFEQMTgXWuagyMGr91aLapB0ABMuUH8R0TBfS6HjWCIQ+wD5AOw==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/rendermime-interfaces/-/rendermime-interfaces-3.13.0.tgz", + "integrity": "sha512-FfGpT7qjBHIgdMJO/wJgQws1X7DxpyKEUvnzqzXp5dKOwUuE4SSINAa4vKR9cZ/jkKNKOEyeOTvF58OQxF0dYQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@lumino/coreutils": "^1.11.0 || ^2.2.0", - "@lumino/widgets": "^1.37.2 || ^2.5.0" + "@lumino/coreutils": "^1.11.0 || ^2.2.2", + "@lumino/widgets": "^1.37.2 || ^2.7.2" } }, "node_modules/@jupyterlab/services": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/services/-/services-7.3.1.tgz", - "integrity": "sha512-ZXRW2wDV7kIoFRj8M3nkbaSxC6pYn4QlRtwJJEz6Ewges9x+EJEKoN7V0AVQ2zDG1XSmMvRLAvv1YvLgEk4Bug==", - "dev": true, - "dependencies": { - "@jupyter/ydoc": "^3.0.0", - "@jupyterlab/coreutils": "^6.3.1", - "@jupyterlab/nbformat": "^4.3.1", - "@jupyterlab/settingregistry": "^4.3.1", - "@jupyterlab/statedb": "^4.3.1", - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3", - "@lumino/polling": "^2.1.3", - "@lumino/properties": "^2.0.2", - "@lumino/signaling": "^2.1.3", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/services/-/services-7.5.0.tgz", + "integrity": "sha512-PzOAFp8BxFHyc7ZHrQSWJ5S/9wFNUqdMyPVb+cM5B3Hveh9pBpRd7A41ua5mV8xt56J6lPUgo5+FIVUGV7vHbA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jupyter/ydoc": "^3.1.0", + "@jupyterlab/coreutils": "^6.5.0", + "@jupyterlab/nbformat": "^4.5.0", + "@jupyterlab/settingregistry": "^4.5.0", + "@jupyterlab/statedb": "^4.5.0", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/polling": "^2.1.5", + "@lumino/properties": "^2.0.4", + "@lumino/signaling": "^2.1.5", "ws": "^8.11.0" } }, "node_modules/@jupyterlab/settingregistry": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/settingregistry/-/settingregistry-4.3.1.tgz", - "integrity": "sha512-tlRYh+CouyyDY3nGsCcD9hQKftdYKMwkTSV4X09vHlIEZoMNrbXtkyADgErgQGaIMwx7gtaLU4qCWK2LLMGyDw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/settingregistry/-/settingregistry-4.5.0.tgz", + "integrity": "sha512-hnuAODpFVQEHrkaVBvJLjOm2aurIglv2HUS+GIKN2VcHehRVgEi30XUgCEuf7YVH+KkRLXDIZlCGDBfItw3SBw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@jupyterlab/nbformat": "^4.3.1", - "@jupyterlab/statedb": "^4.3.1", - "@lumino/commands": "^2.3.1", - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3", - "@lumino/signaling": "^2.1.3", + "@jupyterlab/nbformat": "^4.5.0", + "@jupyterlab/statedb": "^4.5.0", + "@lumino/commands": "^2.3.3", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/signaling": "^2.1.5", "@rjsf/utils": "^5.13.4", "ajv": "^8.12.0", "json5": "^2.2.3" @@ -1105,92 +1420,97 @@ } }, "node_modules/@jupyterlab/statedb": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/statedb/-/statedb-4.3.1.tgz", - "integrity": "sha512-Uph0kEPgCZvI6XnRsf7Md4WWA+4WnJMzwubc7cxk7B2KjdRYOqJHVtQbdJk4ZUA2ARhyS8tBtit/BZb9e3wLZw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/statedb/-/statedb-4.5.0.tgz", + "integrity": "sha512-OERjbE0LV1wYhEH71eF71hpdCPbM/Y+LkGK+pvKJzYy9NqaSbUDTT9GMurT+43tfdUYeUE/nls5Ibr8ZkTakDA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@lumino/commands": "^2.3.1", - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3", - "@lumino/properties": "^2.0.2", - "@lumino/signaling": "^2.1.3" + "@lumino/commands": "^2.3.3", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/properties": "^2.0.4", + "@lumino/signaling": "^2.1.5" } }, "node_modules/@jupyterlab/statusbar": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/statusbar/-/statusbar-4.3.1.tgz", - "integrity": "sha512-5XZJsIvdWENto4of4p1c2hrd4pJuQW5FYevCETMRhIeVnI6vM+JB67LCe84+nKjEt4wiYJhF7I8Bf9No6fUFTw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/statusbar/-/statusbar-4.5.0.tgz", + "integrity": "sha512-vT0rSVJDA3gdxioNIYQ6upiPYa3BjDztgKFIbCYEdOJmp7d64rOyPpDPaonbFewu+6L+V2s1oJEky/6jSw+CgQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@jupyterlab/ui-components": "^4.3.1", - "@lumino/algorithm": "^2.0.2", - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3", - "@lumino/messaging": "^2.0.2", - "@lumino/signaling": "^2.1.3", - "@lumino/widgets": "^2.5.0", + "@jupyterlab/ui-components": "^4.5.0", + "@lumino/algorithm": "^2.0.4", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/messaging": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/widgets": "^2.7.2", "react": "^18.2.0" } }, "node_modules/@jupyterlab/toc": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/toc/-/toc-6.3.1.tgz", - "integrity": "sha512-+QOChkEJ8xCI407MSUWMOAu/rb0SigGmeKPvxycaWZZY2RQnTQvavEUYBimgg0gh80XWqDfpR1SB56dtZp8ifw==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/toc/-/toc-6.5.0.tgz", + "integrity": "sha512-1UDT1PRWFKgsJhLRyyBXVA1zKUgJRFBTU8X4uglhshNEC2XtlNWQ2CvW/nlH3glW5p6lyVKIfqKIRCfF7U522A==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@jupyter/react-components": "^0.16.6", - "@jupyterlab/apputils": "^4.4.1", - "@jupyterlab/coreutils": "^6.3.1", - "@jupyterlab/docregistry": "^4.3.1", - "@jupyterlab/observables": "^5.3.1", - "@jupyterlab/rendermime": "^4.3.1", - "@jupyterlab/rendermime-interfaces": "^3.11.1", - "@jupyterlab/translation": "^4.3.1", - "@jupyterlab/ui-components": "^4.3.1", - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3", - "@lumino/messaging": "^2.0.2", - "@lumino/signaling": "^2.1.3", - "@lumino/widgets": "^2.5.0", + "@jupyterlab/apputils": "^4.6.0", + "@jupyterlab/coreutils": "^6.5.0", + "@jupyterlab/docregistry": "^4.5.0", + "@jupyterlab/observables": "^5.5.0", + "@jupyterlab/rendermime": "^4.5.0", + "@jupyterlab/rendermime-interfaces": "^3.13.0", + "@jupyterlab/translation": "^4.5.0", + "@jupyterlab/ui-components": "^4.5.0", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/messaging": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/widgets": "^2.7.2", "react": "^18.2.0" } }, "node_modules/@jupyterlab/translation": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/translation/-/translation-4.3.1.tgz", - "integrity": "sha512-hFlzzVAliO+0vrGGCD9us23GZ1WwF7vWDHBAnMJdIelpydOvzIBASoZJrGMIohq+7cO/eOlHqQguhUaBbBzh/g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/translation/-/translation-4.5.0.tgz", + "integrity": "sha512-hCtLfkJdxseTbuhFZSYLCLz7lQl/uRbVzhmEAN642J5BSAuHE6DJDtV0beIDnkk1OgmGiy5qsSGtW+Sa98D2qw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@jupyterlab/coreutils": "^6.3.1", - "@jupyterlab/rendermime-interfaces": "^3.11.1", - "@jupyterlab/services": "^7.3.1", - "@jupyterlab/statedb": "^4.3.1", - "@lumino/coreutils": "^2.2.0" + "@jupyterlab/coreutils": "^6.5.0", + "@jupyterlab/rendermime-interfaces": "^3.13.0", + "@jupyterlab/services": "^7.5.0", + "@jupyterlab/statedb": "^4.5.0", + "@lumino/coreutils": "^2.2.2" } }, "node_modules/@jupyterlab/ui-components": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/ui-components/-/ui-components-4.3.1.tgz", - "integrity": "sha512-csWs6iG6Qg2NDAJ1uFzqGBInE6lDmqEAONqP7uFvCK247xhj5Ps4tUeOWOfKs9sH5Pv4wvCVmB+eNcxoZy3QJA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/ui-components/-/ui-components-4.5.0.tgz", + "integrity": "sha512-Vh1STa9CoaK6jdCj5W3hx3gC+U/6DQT1pTp3ApSLp+ZzT6QWbIp+Gf5+gEYrCdnZK8MS68sS9Imp6r1gmvYLlA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@jupyter/react-components": "^0.16.6", "@jupyter/web-components": "^0.16.6", - "@jupyterlab/coreutils": "^6.3.1", - "@jupyterlab/observables": "^5.3.1", - "@jupyterlab/rendermime-interfaces": "^3.11.1", - "@jupyterlab/translation": "^4.3.1", - "@lumino/algorithm": "^2.0.2", - "@lumino/commands": "^2.3.1", - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3", - "@lumino/messaging": "^2.0.2", - "@lumino/polling": "^2.1.3", - "@lumino/properties": "^2.0.2", - "@lumino/signaling": "^2.1.3", - "@lumino/virtualdom": "^2.0.2", - "@lumino/widgets": "^2.5.0", + "@jupyterlab/coreutils": "^6.5.0", + "@jupyterlab/observables": "^5.5.0", + "@jupyterlab/rendermime-interfaces": "^3.13.0", + "@jupyterlab/translation": "^4.5.0", + "@lumino/algorithm": "^2.0.4", + "@lumino/commands": "^2.3.3", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/messaging": "^2.0.4", + "@lumino/polling": "^2.1.5", + "@lumino/properties": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/virtualdom": "^2.0.4", + "@lumino/widgets": "^2.7.2", "@rjsf/core": "^5.13.4", "@rjsf/utils": "^5.13.4", "react": "^18.2.0", @@ -1202,16 +1522,18 @@ } }, "node_modules/@lezer/common": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", - "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", - "dev": true + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.3.0.tgz", + "integrity": "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==", + "dev": true, + "license": "MIT" }, "node_modules/@lezer/cpp": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.1.2.tgz", - "integrity": "sha512-macwKtyeUO0EW86r3xWQCzOV9/CF8imJLpJlPv3sDY57cPGeUZ8gXWOWNlJr52TVByMV3PayFQCA5SHEERDmVQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.1.3.tgz", + "integrity": "sha512-ykYvuFQKGsRi6IcE+/hCSGUhb/I4WPjd3ELhEblm2wS2cOznDFzO+ubK2c+ioysOnlZ3EduV+MVQFCPzAIoY3w==", "dev": true, + "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", @@ -1219,21 +1541,23 @@ } }, "node_modules/@lezer/css": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.9.tgz", - "integrity": "sha512-TYwgljcDv+YrV0MZFFvYFQHCfGgbPMR6nuqLabBdmZoFH3EP1gvw8t0vae326Ne3PszQkbXfVBjCnf3ZVCr0bA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.0.tgz", + "integrity": "sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==", "dev": true, + "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" + "@lezer/lr": "^1.3.0" } }, "node_modules/@lezer/generator": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@lezer/generator/-/generator-1.7.1.tgz", - "integrity": "sha512-MgPJN9Si+ccxzXl3OAmCeZuUKw4XiPl4y664FX/hnnyG9CTqUPq65N3/VGPA2jD23D7QgMTtNqflta+cPN+5mQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@lezer/generator/-/generator-1.8.0.tgz", + "integrity": "sha512-/SF4EDWowPqV1jOgoGSGTIFsE7Ezdr7ZYxyihl5eMKVO5tlnpIhFcDavgm1hHY5GEonoOAEnJ0CU0x+tvuAuUg==", "dev": true, + "license": "MIT", "dependencies": { "@lezer/common": "^1.1.0", "@lezer/lr": "^1.3.0" @@ -1243,19 +1567,21 @@ } }, "node_modules/@lezer/highlight": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", - "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", "dev": true, + "license": "MIT", "dependencies": { - "@lezer/common": "^1.0.0" + "@lezer/common": "^1.3.0" } }, "node_modules/@lezer/html": { - "version": "1.3.10", - "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz", - "integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==", + "version": "1.3.12", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.12.tgz", + "integrity": "sha512-RJ7eRWdaJe3bsiiLLHjCFT1JMk8m1YP9kaUbvu2rMLEoOnke9mcTVDyfOslsln0LtujdWespjJ39w6zo+RsQYw==", "dev": true, + "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", @@ -1267,6 +1593,7 @@ "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.1.3.tgz", "integrity": "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==", "dev": true, + "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", @@ -1274,10 +1601,11 @@ } }, "node_modules/@lezer/javascript": { - "version": "1.4.19", - "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.19.tgz", - "integrity": "sha512-j44kbR1QL26l6dMunZ1uhKBFteVGLVCBGNUD2sUaMnic+rbTviVuoK0CD1l9FTW31EueWvFFswCKMH7Z+M3JRA==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", + "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", "dev": true, + "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.1.3", @@ -1285,10 +1613,11 @@ } }, "node_modules/@lezer/json": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.2.tgz", - "integrity": "sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz", + "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", "dev": true, + "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", @@ -1296,29 +1625,32 @@ } }, "node_modules/@lezer/lr": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", - "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.3.tgz", + "integrity": "sha512-yenN5SqAxAPv/qMnpWW0AT7l+SxVrgG+u0tNsRQWqbrz66HIl8DnEbBObvy21J5K7+I1v7gsAnlE2VQ5yYVSeA==", "dev": true, + "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0" } }, "node_modules/@lezer/markdown": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.3.2.tgz", - "integrity": "sha512-Wu7B6VnrKTbBEohqa63h5vxXjiC4pO5ZQJ/TDbhJxPQaaIoRD/6UVDhSDtVsCwVZV12vvN9KxuLL3ATMnlG0oQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.0.tgz", + "integrity": "sha512-AXb98u3M6BEzTnreBnGtQaF7xFTiMA92Dsy5tqEjpacbjRxDSFdN4bKJo9uvU4cEEOS7D2B9MT7kvDgOEIzJSw==", "dev": true, + "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0", "@lezer/highlight": "^1.0.0" } }, "node_modules/@lezer/php": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.2.tgz", - "integrity": "sha512-GN7BnqtGRpFyeoKSEqxvGvhJQiI4zkgmYnDk/JIyc7H7Ifc1tkPnUn/R2R8meH3h/aBf5rzjvU8ZQoyiNDtDrA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.5.tgz", + "integrity": "sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==", "dev": true, + "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", @@ -1326,10 +1658,11 @@ } }, "node_modules/@lezer/python": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.14.tgz", - "integrity": "sha512-ykDOb2Ti24n76PJsSa4ZoDF0zH12BSw1LGfQXCYJhJyOGiFTfGaX0Du66Ze72R+u/P35U+O6I9m8TFXov1JzsA==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz", + "integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==", "dev": true, + "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", @@ -1341,6 +1674,7 @@ "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.2.tgz", "integrity": "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==", "dev": true, + "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", @@ -1348,10 +1682,11 @@ } }, "node_modules/@lezer/xml": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.5.tgz", - "integrity": "sha512-VFouqOzmUWfIg+tfmpcdV33ewtK+NSwd4ngSe1aG7HFb4BN0ExyY1b8msp+ndFrnlG4V4iC8yXacjFtrwERnaw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz", + "integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==", "dev": true, + "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", @@ -1359,185 +1694,235 @@ } }, "node_modules/@lumino/algorithm": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@lumino/algorithm/-/algorithm-2.0.2.tgz", - "integrity": "sha512-cI8yJ2+QK1yM5ZRU3Kuaw9fJ/64JEDZEwWWp7+U0cd/mvcZ44BGdJJ29w+tIet1QXxPAvnsUleWyQ5qm4qUouA==", - "dev": true + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lumino/algorithm/-/algorithm-2.0.4.tgz", + "integrity": "sha512-gddBhESPqu25KWLeAK9Kz8tS9Ph7P45i0CNG7Ia4XMhK9PHLtTsBdJTC9jP+MqhbzC8zDT/4ekvYRV9ojRPj7Q==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@lumino/application": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@lumino/application/-/application-2.4.1.tgz", - "integrity": "sha512-XdCAlNajcsGdK6ep+s6QC70EY+uBnP3kDiWthFLl3EMkvkYwmjOPzIPGlwLEd9Hu0XCO+1Vd2PlpeTnxw5D3/g==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@lumino/application/-/application-2.4.5.tgz", + "integrity": "sha512-b6JLzjQ0qAV7wJWS0v6z+9FeY8HTuteKjdXaHZQToLutGb67leOtp+/JoKI37HFn1FQdeMgoMHIBXxrT58xXGA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@lumino/commands": "^2.3.1", - "@lumino/coreutils": "^2.2.0", - "@lumino/widgets": "^2.5.0" + "@lumino/commands": "^2.3.3", + "@lumino/coreutils": "^2.2.2", + "@lumino/widgets": "^2.7.2" } }, "node_modules/@lumino/collections": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@lumino/collections/-/collections-2.0.2.tgz", - "integrity": "sha512-o0QmfV1D3WhAeA8GI1/YmEPaK89JtHVa764rQ5T0LdbDEwUtUDbjavHs1E/+y66tNTXz9RUJ4D2rcSb9tysYsg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lumino/collections/-/collections-2.0.4.tgz", + "integrity": "sha512-D/Py9L5HET6+XUYGxFqDEEth4B65X2c7B/GQVRR8q5Fl7EArVL6e98ZXw8BMkuPcTNa0zlENpCKXzlcoJZxXgQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@lumino/algorithm": "^2.0.2" + "@lumino/algorithm": "^2.0.4" } }, "node_modules/@lumino/commands": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@lumino/commands/-/commands-2.3.1.tgz", - "integrity": "sha512-DpX1kkE4PhILpvK1T4ZnaFb6UP4+YTkdZifvN3nbiomD64O2CTd+wcWIBpZMgy6MMgbVgrE8dzHxHk1EsKxNxw==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@lumino/commands/-/commands-2.3.3.tgz", + "integrity": "sha512-7Ci0QdFzt4NKFMhULr19sJPpOLHJw/oYlq6Pb0/Kq1s05+cIoLimr5wiyjkbAlNoGO/8A8SEBGHy3uctZz6G3A==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@lumino/algorithm": "^2.0.2", - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3", - "@lumino/domutils": "^2.0.2", - "@lumino/keyboard": "^2.0.2", - "@lumino/signaling": "^2.1.3", - "@lumino/virtualdom": "^2.0.2" + "@lumino/algorithm": "^2.0.4", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/domutils": "^2.0.4", + "@lumino/keyboard": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/virtualdom": "^2.0.4" } }, "node_modules/@lumino/coreutils": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@lumino/coreutils/-/coreutils-2.2.0.tgz", - "integrity": "sha512-x5wnQ/GjWBayJ6vXVaUi6+Q6ETDdcUiH9eSfpRZFbgMQyyM6pi6baKqJBK2CHkCc/YbAEl6ipApTgm3KOJ/I3g==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@lumino/coreutils/-/coreutils-2.2.2.tgz", + "integrity": "sha512-zaKJaK7rawPATn2BGHkbMrR6oK3s9PxNe9KreLwWF2dB4ZBHDiEmNLRyHRorfJ7XqVOEXAsAAj0jFn+qJPC/4Q==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@lumino/algorithm": "^2.0.2" + "@lumino/algorithm": "^2.0.4" } }, "node_modules/@lumino/datagrid": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@lumino/datagrid/-/datagrid-2.4.1.tgz", - "integrity": "sha512-9sJg8UU/hqcKDqO5Rd0Blm2JYKT9nyAK/kuPYeaQc4ZLvtgn4SoOenNaShLDr3Wp54quBM8npAlk1mWG+yYC2g==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@lumino/datagrid/-/datagrid-2.5.3.tgz", + "integrity": "sha512-iSwZWYV8qVZVSfOUUNMOEc0EbTwOUPmUKju2xTZ3vN39P80gPEaEhmwZFolIe8rJgpsYu+kacYU0HAb3dYVpKw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@lumino/algorithm": "^2.0.2", - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3", - "@lumino/domutils": "^2.0.2", - "@lumino/dragdrop": "^2.1.5", - "@lumino/keyboard": "^2.0.2", - "@lumino/messaging": "^2.0.2", - "@lumino/signaling": "^2.1.3", - "@lumino/widgets": "^2.5.0" + "@lumino/algorithm": "^2.0.4", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/domutils": "^2.0.4", + "@lumino/dragdrop": "^2.1.7", + "@lumino/keyboard": "^2.0.4", + "@lumino/messaging": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/widgets": "^2.7.2" } }, "node_modules/@lumino/disposable": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@lumino/disposable/-/disposable-2.1.3.tgz", - "integrity": "sha512-k5KXy/+T3UItiWHY4WwQawnsJnGo3aNtP5CTRKqo4+tbTNuhc3rTSvygJlNKIbEfIZXW2EWYnwfFDozkYx95eA==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@lumino/disposable/-/disposable-2.1.5.tgz", + "integrity": "sha512-hO9AkJK0oEGzxopuxI8LaZqwzSNwXJTGCdr5K4gh6al+zxpN7rOCh6Aq3zDxkIHJU4zybxv8r02ardx9XJsG3A==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@lumino/signaling": "^2.1.3" + "@lumino/signaling": "^2.1.5" } }, "node_modules/@lumino/domutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@lumino/domutils/-/domutils-2.0.2.tgz", - "integrity": "sha512-2Kp6YHaMNI1rKB0PrALvOsZBHPy2EvVVAvJLWjlCm8MpWOVETjFp0MA9QpMubT9I76aKbaI5s1o1NJyZ8Y99pQ==", - "dev": true + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lumino/domutils/-/domutils-2.0.4.tgz", + "integrity": "sha512-naYGUQn3e0CLtz/tjKOZP8SOBg0SW7EguhkxLpNUXlVUvx7rVsfr0VI22FVL+jgI0FbxXpEkxpSMxtK73jxJAg==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@lumino/dragdrop": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@lumino/dragdrop/-/dragdrop-2.1.5.tgz", - "integrity": "sha512-zqwR4GakrQBKZOW6S5pj2nfrQDurOErAoe9x3HS3BKLa1AzWA+t9PD5NESOKd81NqXFHjiMirSyFkTUs6pw+uA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@lumino/dragdrop/-/dragdrop-2.1.7.tgz", + "integrity": "sha512-oa5EtBC37EiiJsuIFVcK1DywpEb4dnQCMzWnymvjlBXG3/fAIC+65Q/iLoNUWkMz1pc3ET2SZWDporlznxlEbw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3" + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5" } }, "node_modules/@lumino/keyboard": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@lumino/keyboard/-/keyboard-2.0.2.tgz", - "integrity": "sha512-icRUpvswDaFjqmAJNbQRb/aTu6Iugo6Y2oC08TiIwhQtLS9W+Ee9VofdqvbPSvCm6DkyP+DCWMuA3KXZ4V4g4g==", - "dev": true + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lumino/keyboard/-/keyboard-2.0.4.tgz", + "integrity": "sha512-kIVkdSz8F5wtZr8hZp0CMX+E0eMCOnFH6XCT7j2UBQ80ERJHFy0eX+IbNo3dtRQ7+CcDhBV4hQquFNFa+/04QQ==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@lumino/messaging": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@lumino/messaging/-/messaging-2.0.2.tgz", - "integrity": "sha512-2sUF07cYA0f3mDil41Eh5sfBk0aGAH/mOh1I4+vyRUsKyBqp4WTUtpJFd8xVJGAntygxwnebIygkIaXXTIQvxA==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lumino/messaging/-/messaging-2.0.4.tgz", + "integrity": "sha512-NbZnchAPOciSe9Qn/g6EzG0LRaw7bygFIXbCD440ZhzvugdBeAerwYhrA795jkXPNrrl3olp5AlO0cBB/XZNtg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@lumino/algorithm": "^2.0.2", - "@lumino/collections": "^2.0.2" + "@lumino/algorithm": "^2.0.4", + "@lumino/collections": "^2.0.4" } }, "node_modules/@lumino/polling": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@lumino/polling/-/polling-2.1.3.tgz", - "integrity": "sha512-WEZk96ddK6eHEhdDkFUAAA40EOLit86QVbqQqnbPmhdGwFogek26Kq9b1U273LJeirv95zXCATOJAkjRyb7D+w==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@lumino/polling/-/polling-2.1.5.tgz", + "integrity": "sha512-YhQRWTNRVSi5R5uatwh1jkxASY5JKyAGWmtnfQOZWLDUFmsIjOTsS8NaYg1BgneZjWM3fbA18dCDDT7PPs5X1g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3", - "@lumino/signaling": "^2.1.3" + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/signaling": "^2.1.5" } }, "node_modules/@lumino/properties": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@lumino/properties/-/properties-2.0.2.tgz", - "integrity": "sha512-b312oA3Bh97WFK8efXejYmC3DVJmvzJk72LQB7H3fXhfqS5jUWvL7MSnNmgcQvGzl9fIhDWDWjhtSTi0KGYYBg==", - "dev": true + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lumino/properties/-/properties-2.0.4.tgz", + "integrity": "sha512-XsL2qLZk+1FbfuTrkyjciI8PMDw3YcaBkqVQ+iv7OOJf9bUlrmTpCMY0Hu5d3hV2W3TWlRsdbvRRLEBJSKv0iA==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@lumino/signaling": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@lumino/signaling/-/signaling-2.1.3.tgz", - "integrity": "sha512-9Wd4iMk8F1i6pYjy65bqKuPlzQMicyL9xy1/ccS20kovPcfD074waneL/7BVe+3M8i+fGa3x2qjbWrBzOdTdNw==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@lumino/signaling/-/signaling-2.1.5.tgz", + "integrity": "sha512-Wkx6WR45ynmKBlW0GBEoh4xk9+QluKr1JHuMftqcStBHSQBCnN54UKRRDbySXHGRhhx6p4neu7sGomgQSlQK8w==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@lumino/algorithm": "^2.0.2", - "@lumino/coreutils": "^2.2.0" + "@lumino/algorithm": "^2.0.4", + "@lumino/coreutils": "^2.2.2" } }, "node_modules/@lumino/virtualdom": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@lumino/virtualdom/-/virtualdom-2.0.2.tgz", - "integrity": "sha512-HYZThOtZSoknjdXA102xpy5CiXtTFCVz45EXdWeYLx3NhuEwuAIX93QBBIhupalmtFlRg1yhdDNV40HxJ4kcXg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lumino/virtualdom/-/virtualdom-2.0.4.tgz", + "integrity": "sha512-7MFthA9KUsqZTGm/D98FZt1QupjIGyd3XyB4SIugn6DQAqhjBiyykCZydnRq3qmuMHybQel33dNIbHpzyNyQwA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@lumino/algorithm": "^2.0.2" + "@lumino/algorithm": "^2.0.4" } }, "node_modules/@lumino/widgets": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@lumino/widgets/-/widgets-2.5.0.tgz", - "integrity": "sha512-RSRpc6aIEiuw79jqWUHYWXLJ2GBy7vhwuqgo94UVzg6oeh3XBECX0OvXGjK2k7N2BhmRrIs9bXky7Dm861S6mQ==", + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@lumino/widgets/-/widgets-2.7.2.tgz", + "integrity": "sha512-svp4Si10PcTr6Hfd1hZgkue1rKPYpnuCheGWkou/RlMZih94mVbQoDO1xppo/haYQg4GX2IgCFpUKsNb19oBAg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@lumino/algorithm": "^2.0.4", + "@lumino/commands": "^2.3.3", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/domutils": "^2.0.4", + "@lumino/dragdrop": "^2.1.7", + "@lumino/keyboard": "^2.0.4", + "@lumino/messaging": "^2.0.4", + "@lumino/properties": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/virtualdom": "^2.0.4" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@mermaid-js/layout-elk": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/layout-elk/-/layout-elk-0.2.0.tgz", + "integrity": "sha512-vjjYGnCCjYlIA/rR7M//eFi0rHM6dsMyN1JQKfckpt30DTC/esrw36hcrvA2FNPHaqh3Q/SyBWzddyaky8EtUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "elkjs": "^0.9.3" + }, + "peerDependencies": { + "mermaid": "^11.0.2" + } + }, + "node_modules/@mermaid-js/parser": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.3.tgz", + "integrity": "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==", "dev": true, + "license": "MIT", "dependencies": { - "@lumino/algorithm": "^2.0.2", - "@lumino/commands": "^2.3.1", - "@lumino/coreutils": "^2.2.0", - "@lumino/disposable": "^2.1.3", - "@lumino/domutils": "^2.0.2", - "@lumino/dragdrop": "^2.1.5", - "@lumino/keyboard": "^2.0.2", - "@lumino/messaging": "^2.0.2", - "@lumino/properties": "^2.0.2", - "@lumino/signaling": "^2.1.3", - "@lumino/virtualdom": "^2.0.2" + "langium": "3.3.1" } }, "node_modules/@microsoft/fast-colors": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@microsoft/fast-colors/-/fast-colors-5.3.1.tgz", "integrity": "sha512-72RZXVfCbwQzvo5sXXkuLXLT7rMeYaSf5r/6ewQiv/trBtqpWRm4DEH2EilHw/iWTBKOXs1qZNQndgUMa5n4LA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@microsoft/fast-element": { "version": "1.14.0", "resolved": "https://registry.npmjs.org/@microsoft/fast-element/-/fast-element-1.14.0.tgz", "integrity": "sha512-zXvuSOzvsu8zDTy9eby8ix8VqLop2rwKRgp++ZN2kTCsoB3+QJVoaGD2T/Cyso2ViZQFXNpiNCVKfnmxBvmWkQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@microsoft/fast-foundation": { "version": "2.50.0", "resolved": "https://registry.npmjs.org/@microsoft/fast-foundation/-/fast-foundation-2.50.0.tgz", "integrity": "sha512-8mFYG88Xea1jZf2TI9Lm/jzZ6RWR8x29r24mGuLojNYqIR2Bl8+hnswoV6laApKdCbGMPKnsAL/O68Q0sRxeVg==", "dev": true, + "license": "MIT", "dependencies": { "@microsoft/fast-element": "^1.14.0", "@microsoft/fast-web-utilities": "^5.4.1", @@ -1550,6 +1935,7 @@ "resolved": "https://registry.npmjs.org/@microsoft/fast-web-utilities/-/fast-web-utilities-5.4.1.tgz", "integrity": "sha512-ReWYncndjV3c8D8iq9tp7NcFNc1vbVHvcBFPME2nNFKNbS1XCesYZGlIlf3ot5EmuOXPlrzUHOWzQ2vFpIkqDg==", "dev": true, + "license": "MIT", "dependencies": { "exenv-es6": "^1.1.1" } @@ -1590,12 +1976,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0.tgz", - "integrity": "sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw==", + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "playwright": "1.49.0" + "playwright": "1.57.0" }, "bin": { "playwright": "cli.js" @@ -1605,30 +1992,32 @@ } }, "node_modules/@rjsf/core": { - "version": "5.22.4", - "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.22.4.tgz", - "integrity": "sha512-0QjAVPXDi/19jR/E44ULDzOkvC4Px5zcZhpGtBFNWNWWmb9UgyjPuvJYga2obzHU46P+5maLvUQEZVAeFwDuqQ==", + "version": "5.24.13", + "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.24.13.tgz", + "integrity": "sha512-ONTr14s7LFIjx2VRFLuOpagL76sM/HPy6/OhdBfq6UukINmTIs6+aFN0GgcR0aXQHFDXQ7f/fel0o/SO05Htdg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "lodash": "^4.17.21", "lodash-es": "^4.17.21", "markdown-to-jsx": "^7.4.1", - "nanoid": "^3.3.7", "prop-types": "^15.8.1" }, "engines": { "node": ">=14" }, "peerDependencies": { - "@rjsf/utils": "^5.22.x", + "@rjsf/utils": "^5.24.x", "react": "^16.14.0 || >=17" } }, "node_modules/@rjsf/utils": { - "version": "5.22.4", - "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.22.4.tgz", - "integrity": "sha512-yQTdz5ryiYy258xCVthVPQ3DeaMzrRNrFcO8xvGHorp0/bLUxdTZ0iidXop49m3y8SaxxTZd398ZKWg2cqxiIA==", + "version": "5.24.13", + "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.24.13.tgz", + "integrity": "sha512-rNF8tDxIwTtXzz5O/U23QU73nlhgQNYJ+Sv5BAwQOIyhIE2Z3S5tUiSVMwZHt0julkv/Ryfwi+qsD4FiE5rOuw==", "dev": true, + "license": "Apache-2.0", + "peer": true, "dependencies": { "json-schema-merge-allof": "^0.8.1", "jsonpointer": "^5.0.1", @@ -1649,6 +2038,33 @@ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "dev": true }, + "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/@stdlib/array": { "version": "0.0.12", "resolved": "https://registry.npmjs.org/@stdlib/array/-/array-0.0.12.tgz", @@ -2502,116 +2918,490 @@ "url": "https://www.patreon.com/athan" } }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } }, - "node_modules/@types/geojson": { - "version": "7946.0.4", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.4.tgz", - "integrity": "sha512-MHmwBtCb7OCv1DSivz2UNJXPGU/1btAWRKlqJ2saEhVJkpkvqHMMaOpKg0v4sAbDWSQekHGvPVMM8nQ+Jen03Q==", + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", "dev": true, "license": "MIT" }, - "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 - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true - }, - "node_modules/@types/node": { - "version": "20.17.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.6.tgz", - "integrity": "sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==", + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "@types/d3-selection": "*" } }, - "node_modules/@types/prop-types": { - "version": "15.7.13", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", - "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", - "dev": true - }, - "node_modules/@types/react": { - "version": "18.3.12", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", - "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", "dev": true, + "license": "MIT", "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" + "@types/d3-selection": "*" } }, - "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 + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "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==", + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", "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 - } + "@types/d3-array": "*", + "@types/geojson": "*" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "@types/d3-selection": "*" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ms": { - "version": "2.1.3", + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@types/geojson": { + "version": "7946.0.4", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.4.tgz", + "integrity": "sha512-MHmwBtCb7OCv1DSivz2UNJXPGU/1btAWRKlqJ2saEhVJkpkvqHMMaOpKg0v4sAbDWSQekHGvPVMM8nQ+Jen03Q==", + "dev": true, + "license": "MIT" + }, + "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/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.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 + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.17.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.6.tgz", + "integrity": "sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "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 + }, + "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/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "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, + "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/eslint-plugin/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ms": { + "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true @@ -2621,6 +3411,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -2850,13 +3641,24 @@ "version": "1.68.0", "resolved": "https://registry.npmjs.org/@vscode/debugprotocol/-/debugprotocol-1.68.0.tgz", "integrity": "sha512-2J27dysaXmvnfuhFGhfeuxfHRXunqNPxtBoR3koiTOA9rdxWNDTa1zIFLCFMSHJ9MPTPKFcBeblsyaCJCIlQxg==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2864,6 +3666,17 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -2873,11 +3686,63 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.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==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3078,6 +3943,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -3100,10 +3972,11 @@ "dev": true }, "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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -3148,6 +4021,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3173,6 +4060,51 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chevrotain": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", + "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", + "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^11.0.0" + } + }, + "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/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -3205,6 +4137,19 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -3244,17 +4189,35 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "dev": true, + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/crelt": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -3270,11 +4233,135 @@ "node": ">= 8" } }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cytoscape": { + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", + "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "dev": true, + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } }, "node_modules/d3-array": { "version": "3.2.4", @@ -3288,6 +4375,46 @@ "node": ">=12" } }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-color": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", @@ -3298,6 +4425,19 @@ "node": ">=12" } }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-delaunay": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", @@ -3321,6 +4461,20 @@ "node": ">=12" } }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-dsv": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", @@ -3347,6 +4501,29 @@ "node": ">=12" } }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-force": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", @@ -3440,6 +4617,16 @@ "node": ">=12" } }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-quadtree": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", @@ -3450,6 +4637,61 @@ "node": ">=12" } }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "dev": true, + "license": "ISC" + }, "node_modules/d3-scale": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", @@ -3481,6 +4723,17 @@ "node": ">=12" } }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "dev": true, + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-shape": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", @@ -3530,6 +4783,106 @@ "node": ">=12" } }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.13.tgz", + "integrity": "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -3581,6 +4934,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -3590,6 +4950,13 @@ "ms": "2.0.0" } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3601,6 +4968,7 @@ "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" } @@ -3644,9 +5012,19 @@ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", "dev": true, - "license": "ISC", - "dependencies": { - "robust-predicates": "^3.0.2" + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" } }, "node_modules/dir-glob": { @@ -3678,6 +5056,7 @@ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, + "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -3687,6 +5066,19 @@ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/domelementtype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", @@ -3697,13 +5089,39 @@ "type": "github", "url": "https://github.com/sponsors/fb55" } - ] + ], + "license": "BSD-2-Clause" + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/domexception/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } }, "node_modules/domhandler": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" }, @@ -3714,11 +5132,22 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz", + "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==", + "dev": true, + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", @@ -3740,6 +5169,28 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/elkjs": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", + "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==", + "dev": true, + "license": "EPL-2.0" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -3747,10 +5198,11 @@ "dev": true }, "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -3828,13 +5280,11 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -3875,10 +5325,11 @@ } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -3887,14 +5338,16 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -3947,12 +5400,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/eslint": { "version": "8.57.1", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4162,6 +5638,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -4191,10 +5668,11 @@ } }, "node_modules/eslint-plugin-import/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==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4253,6 +5731,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.7.0.tgz", "integrity": "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==", "dev": true, + "peer": true, "dependencies": { "builtins": "^5.0.1", "eslint-plugin-es": "^4.1.0", @@ -4274,10 +5753,11 @@ } }, "node_modules/eslint-plugin-n/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==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4300,6 +5780,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.6.0.tgz", "integrity": "sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==", "dev": true, + "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -4315,6 +5796,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.2.tgz", "integrity": "sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w==", "dev": true, + "peer": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -4343,10 +5825,11 @@ } }, "node_modules/eslint-plugin-react/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==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4474,10 +5957,11 @@ } }, "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==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4541,6 +6025,20 @@ "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.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -4587,7 +6085,15 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/exenv-es6/-/exenv-es6-1.1.1.tgz", "integrity": "sha512-vlVu3N8d6yEMpMsEm+7sUBAI81aqYYuEvfK0jNqmdb/OPXzzH7QWDDnVjMvDSY47JdHEqx/dfC/q8WkfoTmpGQ==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "dev": true, + "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", @@ -4636,10 +6142,21 @@ "dev": true }, "node_modules/fast-uri": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", - "dev": true + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" }, "node_modules/fastq": { "version": "1.17.1", @@ -4719,11 +6236,29 @@ "is-callable": "^1.1.3" } }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/free-style": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/free-style/-/free-style-3.1.0.tgz", "integrity": "sha512-vJujYSIyT30iDoaoeigNAxX4yB1RUrh+N2ZMhIElMr3BvCuGXOw7XNJMEEJkDUeamK2Rnb/IKFGKRKlTWIGRWA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fs-extra": { "version": "10.1.0", @@ -4751,6 +6286,7 @@ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -4805,16 +6341,22 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -4823,6 +6365,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stdin": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", @@ -4852,6 +6408,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "dev": true, + "license": "ISC" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4886,10 +6449,11 @@ } }, "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==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4959,12 +6523,13 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4982,6 +6547,13 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "dev": true, + "license": "MIT" + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -5025,10 +6597,11 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5063,24 +6636,130 @@ "node": ">= 0.4" } }, - "node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } + "license": "MIT" }, "node_modules/iconv-lite": { "version": "0.6.3", @@ -5410,10 +7089,18 @@ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -5559,6 +7246,7 @@ "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", "dev": true, + "license": "MIT", "funding": { "type": "GitHub Sponsors ❤", "url": "https://github.com/sponsors/dmonad" @@ -5580,6 +7268,88 @@ "node": ">= 0.4" } }, + "node_modules/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "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-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-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/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5587,10 +7357,11 @@ "dev": true }, "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==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -5598,6 +7369,89 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -5615,6 +7469,7 @@ "resolved": "https://registry.npmjs.org/json-schema-compare/-/json-schema-compare-0.2.2.tgz", "integrity": "sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ==", "dev": true, + "license": "MIT", "dependencies": { "lodash": "^4.17.4" } @@ -5624,6 +7479,7 @@ "resolved": "https://registry.npmjs.org/json-schema-merge-allof/-/json-schema-merge-allof-0.8.1.tgz", "integrity": "sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w==", "dev": true, + "license": "MIT", "dependencies": { "compute-lcm": "^1.1.2", "json-schema-compare": "^0.2.2", @@ -5637,7 +7493,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -5646,16 +7503,18 @@ "dev": true }, "node_modules/json-stringify-pretty-compact": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-3.0.0.tgz", - "integrity": "sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA==", - "dev": true + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "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==", "dev": true, + "license": "MIT", "bin": { "json5": "lib/cli.js" }, @@ -5680,6 +7539,7 @@ "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5699,6 +7559,33 @@ "node": ">=4.0" } }, + "node_modules/katex": { + "version": "0.16.25", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.25.tgz", + "integrity": "sha512-woHRUZ/iF23GBP1dkDQMh1QBad9dmr8/PAwNA54VrSOVYgI12MAcE14TqnDdQOdzyEonGzMepYnqBMYdsoAr8Q==", + "dev": true, + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -5708,6 +7595,43 @@ "json-buffer": "3.0.1" } }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==", + "dev": true + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/langium": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz", + "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chevrotain": "~11.0.3", + "chevrotain-allstar": "~0.3.0", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.0.8" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "dev": true, + "license": "MIT" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5722,10 +7646,11 @@ } }, "node_modules/lib0": { - "version": "0.2.98", - "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.98.tgz", - "integrity": "sha512-XteTiNO0qEXqqweWx+b21p/fBnNHUA1NwAtJNJek1oPrewEZs2uiT4gWivHKr9GqCjDPAhchz0UQO8NwU3bBNA==", + "version": "0.2.114", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz", + "integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==", "dev": true, + "license": "MIT", "dependencies": { "isomorphic.js": "^0.2.4" }, @@ -5767,6 +7692,24 @@ "node": ">=6" } }, + "node_modules/local-pkg": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", + "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5786,19 +7729,22 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.escape": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", "integrity": "sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -5810,7 +7756,8 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/loose-envify": { "version": "1.4.0", @@ -5825,15 +7772,68 @@ } }, "node_modules/markdown-to-jsx": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.7.0.tgz", - "integrity": "sha512-130nIMbJY+woOQJ11xTqEtYko60t6EpNkZuqjKMferL3udtob3nRfzXOdsiA26NPemiR7w/hR8M3/B9yiYPGZg==", + "version": "7.7.17", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.7.17.tgz", + "integrity": "sha512-7mG/1feQ0TX5I7YyMZVDgCC/y2I3CiEhIRQIhyov9nGBP5eoVrOXXHuL5ZP8GRfxVZKRiXWJgwXkb9It+nQZfQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10" }, "peerDependencies": { "react": ">= 0.14.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/marked-gfm-heading-id": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/marked-gfm-heading-id/-/marked-gfm-heading-id-4.1.3.tgz", + "integrity": "sha512-aR0i63LmFbuxU/gAgrgz1Ir+8HK6zAIFXMlckeKHpV+qKbYaOP95L4Ux5Gi+sKmCZU5qnN2rdKpvpb7PnUBIWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "github-slugger": "^2.0.0" + }, + "peerDependencies": { + "marked": ">=13 <18" + } + }, + "node_modules/marked-mangle": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/marked-mangle/-/marked-mangle-1.1.12.tgz", + "integrity": "sha512-bRrqNcfU9v3iRECb7YPvA+/xKZMjHojd9R92YwHbFjdPQ+Wc7vozkbGKAv4U8AUl798mNUuY3DTBQkedsV3TeQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "marked": ">=4 <18" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" } }, "node_modules/merge2": { @@ -5845,6 +7845,36 @@ "node": ">= 8" } }, + "node_modules/mermaid": { + "version": "11.12.1", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.1.tgz", + "integrity": "sha512-UlIZrRariB11TY1RtTgUWp65tphtBv4CSq7vyS2ZZ2TgoMjs2nloq+wFqxiwcxlhHUvs7DPGgMjs2aeQxz5h9g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@braintree/sanitize-url": "^7.1.1", + "@iconify/utils": "^3.0.1", + "@mermaid-js/parser": "^0.6.3", + "@types/d3": "^7.4.3", + "cytoscape": "^3.29.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.13", + "dayjs": "^1.11.18", + "dompurify": "^3.2.5", + "katex": "^0.16.22", + "khroma": "^2.1.0", + "lodash-es": "^4.17.21", + "marked": "^16.2.1", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -5858,6 +7888,29 @@ "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", @@ -5882,6 +7935,38 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -5889,9 +7974,9 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -5934,6 +8019,13 @@ } } }, + "node_modules/nwsapi": { + "version": "2.2.22", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", + "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6110,6 +8202,13 @@ "node": ">=6" } }, + "node_modules/package-manager-detector": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.5.0.tgz", + "integrity": "sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==", + "dev": true, + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6139,7 +8238,21 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } }, "node_modules/path": { "version": "0.12.7", @@ -6155,7 +8268,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "dev": true, + "license": "MIT" }, "node_modules/path-exists": { "version": "4.0.0", @@ -6199,11 +8320,19 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", @@ -6300,13 +8429,26 @@ "node": ">=4" } }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, "node_modules/playwright": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz", - "integrity": "sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==", + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.49.0" + "playwright-core": "1.57.0" }, "bin": { "playwright": "cli.js" @@ -6319,10 +8461,11 @@ } }, "node_modules/playwright-core": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0.tgz", - "integrity": "sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA==", + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", "dev": true, + "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, @@ -6330,6 +8473,24 @@ "node": ">=18" } }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "dev": true, + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -6340,9 +8501,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -6358,8 +8519,9 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -6376,6 +8538,34 @@ "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/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -6408,6 +8598,19 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -6417,11 +8620,29 @@ "node": ">=6" } }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/queue-microtask": { "version": "1.2.3", @@ -6448,6 +8669,8 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -6460,6 +8683,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dev": true, + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -6472,7 +8696,8 @@ "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==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/readable-stream": { "version": "2.3.8", @@ -6515,6 +8740,7 @@ "resolved": "https://registry.npmjs.org/regexp-match-indices/-/regexp-match-indices-1.0.2.tgz", "integrity": "sha512-DwZuAkt8NF5mKwGGER1EGh2PRqyvhRhhLviH+R8y8dIuaQROlUfXjt4s9ZTXstIsSkptf06BSvwcEmmfheJJWQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "regexp-tree": "^0.1.11" } @@ -6524,6 +8750,7 @@ "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", "dev": true, + "license": "MIT", "bin": { "regexp-tree": "bin/regexp-tree" } @@ -6572,6 +8799,7 @@ "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==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -6580,7 +8808,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/resolve": { "version": "1.22.8", @@ -6641,6 +8870,19 @@ "dev": true, "license": "Unlicense" }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6730,6 +8972,7 @@ "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.12.1.tgz", "integrity": "sha512-Plh+JAn0UVDpBRP/xEjsk+xDCoOvMBwQUf/K+/cBAVuTbtX8bj2VB7S1sL1dssVpykqp0/KPSesHrqXtokVBpA==", "dev": true, + "license": "MIT", "dependencies": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", @@ -6739,11 +8982,25 @@ "postcss": "^8.3.11" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "dev": true, + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" } @@ -6840,15 +9097,50 @@ "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", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, + "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/standard": { "version": "17.1.2", "resolved": "https://registry.npmjs.org/standard/-/standard-17.1.2.tgz", @@ -7057,10 +9349,18 @@ } }, "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==", - "dev": true + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "dev": true, + "license": "MIT" }, "node_modules/supports-color": { "version": "7.2.0", @@ -7086,6 +9386,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/systeminformation": { "version": "5.23.25", "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.23.25.tgz", @@ -7117,7 +9424,8 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz", "integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/text-table": { "version": "0.2.0", @@ -7125,6 +9433,16 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -7159,6 +9477,32 @@ "dev": true, "license": "MIT" }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -7178,6 +9522,16 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -7206,7 +9560,8 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true + "dev": true, + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", @@ -7220,6 +9575,16 @@ "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.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -7310,6 +9675,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7323,6 +9689,7 @@ "resolved": "https://registry.npmjs.org/typestyle/-/typestyle-2.4.0.tgz", "integrity": "sha512-/d1BL6Qi+YlMLEydnUEB8KL/CAjAN8cyt3/UyGnOyBrWf7bLGcR/6yhmsaUstO2IcYwZfagjE7AIzuI2vUW9mg==", "dev": true, + "license": "MIT", "dependencies": { "csstype": "3.0.10", "free-style": "3.1.0" @@ -7332,7 +9699,15 @@ "version": "3.0.10", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz", "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "dev": true, + "license": "MIT" }, "node_modules/unbox-primitive": { "version": "1.0.2", @@ -7378,6 +9753,7 @@ "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", "dev": true, + "license": "MIT", "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" @@ -7404,11 +9780,26 @@ "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", "dev": true }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/validate.io-array": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/validate.io-array/-/validate.io-array-1.0.6.tgz", "integrity": "sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/validate.io-function": { "version": "1.0.2", @@ -7529,13 +9920,14 @@ "dev": true }, "node_modules/vega-expression": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-5.1.1.tgz", - "integrity": "sha512-zv9L1Hm0KHE9M7mldHyz8sXbGu3KmC0Cdk7qfHkcTNS75Jpsem6jkbu6ZAwx5cNUeW91AxUQOu77r4mygq2wUQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-5.1.2.tgz", + "integrity": "sha512-fFeDTh4UtOxlZWL54jf1ZqJHinyerWq/ROiqrQxqLkNJRJ86RmxYTgXwt65UoZ/l4VUv9eAd2qoJeDEf610Umw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@types/estree": "^1.0.0", - "vega-util": "^1.17.2" + "vega-util": "^1.17.3" } }, "node_modules/vega-force": { @@ -7585,14 +9977,14 @@ } }, "node_modules/vega-functions/node_modules/vega-expression": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-5.2.0.tgz", - "integrity": "sha512-WRMa4ny3iZIVAzDlBh3ipY2QUuLk2hnJJbfbncPgvTF7BUgbIbKq947z+JicWksYbokl8n1JHXJoqi3XvpG0Zw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-5.2.1.tgz", + "integrity": "sha512-9KKbI2q9qTI55NSjD/dVWg3aeCtw+gwyWCiLMM47ha6iXrAN9pQ+EKRJfxOHuoDfCTlJJTaUfnnXgbqm0HEszg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "@types/estree": "^1.0.0", - "vega-util": "^1.17.3" + "vega-util": "^1.17.4" } }, "node_modules/vega-geo": { @@ -7638,13 +10030,14 @@ } }, "node_modules/vega-lite": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/vega-lite/-/vega-lite-5.21.0.tgz", - "integrity": "sha512-hNxM9nuMqpI1vkUOhEx6ewEf23WWLmJxSFJ4TA86AW43ixJyqcLV+iSCO0NipuVTE0rlDcc2e8joSewWyOlEwA==", + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/vega-lite/-/vega-lite-5.23.0.tgz", + "integrity": "sha512-l4J6+AWE3DIjvovEoHl2LdtCUkfm4zs8Xxx7INwZEAv+XVb6kR6vIN1gt3t2gN2gs/y4DYTs/RPoTeYAuEg6mA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "json-stringify-pretty-compact": "~3.0.0", - "tslib": "~2.6.3", + "json-stringify-pretty-compact": "~4.0.0", + "tslib": "~2.8.1", "vega-event-selector": "~3.0.1", "vega-expression": "~5.1.1", "vega-util": "~1.17.2", @@ -7664,10 +10057,11 @@ } }, "node_modules/vega-lite/node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" }, "node_modules/vega-loader": { "version": "4.5.3", @@ -7776,14 +10170,14 @@ } }, "node_modules/vega-selections/node_modules/vega-expression": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-5.2.0.tgz", - "integrity": "sha512-WRMa4ny3iZIVAzDlBh3ipY2QUuLk2hnJJbfbncPgvTF7BUgbIbKq947z+JicWksYbokl8n1JHXJoqi3XvpG0Zw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-5.2.1.tgz", + "integrity": "sha512-9KKbI2q9qTI55NSjD/dVWg3aeCtw+gwyWCiLMM47ha6iXrAN9pQ+EKRJfxOHuoDfCTlJJTaUfnnXgbqm0HEszg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "@types/estree": "^1.0.0", - "vega-util": "^1.17.3" + "vega-util": "^1.17.4" } }, "node_modules/vega-statistics": { @@ -7835,20 +10229,20 @@ } }, "node_modules/vega-typings/node_modules/vega-expression": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-5.2.0.tgz", - "integrity": "sha512-WRMa4ny3iZIVAzDlBh3ipY2QUuLk2hnJJbfbncPgvTF7BUgbIbKq947z+JicWksYbokl8n1JHXJoqi3XvpG0Zw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-5.2.1.tgz", + "integrity": "sha512-9KKbI2q9qTI55NSjD/dVWg3aeCtw+gwyWCiLMM47ha6iXrAN9pQ+EKRJfxOHuoDfCTlJJTaUfnnXgbqm0HEszg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "@types/estree": "^1.0.0", - "vega-util": "^1.17.3" + "vega-util": "^1.17.4" } }, "node_modules/vega-util": { - "version": "1.17.3", - "resolved": "https://registry.npmjs.org/vega-util/-/vega-util-1.17.3.tgz", - "integrity": "sha512-nSNpZLUrRvFo46M5OK4O6x6f08WD1yOcEzHNlqivF+sDLSsVpstaF6fdJYwrbf/debFi2L9Tkp4gZQtssup9iQ==", + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/vega-util/-/vega-util-1.17.4.tgz", + "integrity": "sha512-+y3ZW7dEqM8Ck+KRsd+jkMfxfE7MrQxUyIpNjkfhIpGEreym+aTn7XUw1DKXqclr8mqTQvbilPo16B3lnBr0wA==", "dev": true, "license": "BSD-3-Clause" }, @@ -7908,14 +10302,14 @@ } }, "node_modules/vega/node_modules/vega-expression": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-5.2.0.tgz", - "integrity": "sha512-WRMa4ny3iZIVAzDlBh3ipY2QUuLk2hnJJbfbncPgvTF7BUgbIbKq947z+JicWksYbokl8n1JHXJoqi3XvpG0Zw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-5.2.1.tgz", + "integrity": "sha512-9KKbI2q9qTI55NSjD/dVWg3aeCtw+gwyWCiLMM47ha6iXrAN9pQ+EKRJfxOHuoDfCTlJJTaUfnnXgbqm0HEszg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "@types/estree": "^1.0.0", - "vega-util": "^1.17.3" + "vega-util": "^1.17.4" } }, "node_modules/version-guard": { @@ -7932,15 +10326,30 @@ "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.0.0 || >=10.0.0" } }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, "node_modules/vscode-languageserver-protocol": { "version": "3.17.5", "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", "dev": true, + "license": "MIT", "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" @@ -7951,21 +10360,38 @@ "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.0.0" } }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "dev": true, + "license": "MIT" + }, "node_modules/vscode-languageserver-types": { "version": "3.17.5", "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "dev": true, + "license": "MIT" }, "node_modules/vscode-ws-jsonrpc": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/vscode-ws-jsonrpc/-/vscode-ws-jsonrpc-1.0.2.tgz", "integrity": "sha512-09OpRC0RcqZs4DleJRgs+R+7gQkwb4tgvsL43lzVZwW4N5NO3H/9sLNeKPBt83k7WyA8qBZjrzM6X7tKFpFrjQ==", "dev": true, + "license": "MIT", "dependencies": { "vscode-jsonrpc": "^8.0.2" } @@ -7975,6 +10401,7 @@ "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz", "integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -7983,7 +10410,21 @@ "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } }, "node_modules/webidl-conversions": { "version": "3.0.1", @@ -7992,6 +10433,29 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -8136,10 +10600,11 @@ "dev": true }, "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -8165,11 +10630,29 @@ "node": ">=8" } }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/y-protocols": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz", "integrity": "sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==", "dev": true, + "license": "MIT", "dependencies": { "lib0": "^0.2.85" }, @@ -8222,12 +10705,14 @@ } }, "node_modules/yjs": { - "version": "13.6.20", - "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.20.tgz", - "integrity": "sha512-Z2YZI+SYqK7XdWlloI3lhMiKnCdFCVC4PchpdO+mCYwtiTwncjUbnRK9R1JmkNfdmHyDXuWN3ibJAt0wsqTbLQ==", + "version": "13.6.27", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz", + "integrity": "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "lib0": "^0.2.98" + "lib0": "^0.2.99" }, "engines": { "node": ">=16.0.0", From b19d28050e88575ad915c3253bfdf82cde656200 Mon Sep 17 00:00:00 2001 From: Prabhakar Kumar Date: Mon, 8 Dec 2025 17:12:54 +0530 Subject: [PATCH 07/15] Update yml files to use newer versions of actions and update to use node 24, set min and max versions of supported python to pyproject.toml --- .github/workflows/publish-jupyter-matlab-proxy.yml | 2 +- .github/workflows/publish-tljh-matlab.yml | 4 ++-- .github/workflows/run-e2e-tests.yml | 10 ++++++---- .github/workflows/run-integration-tests.yml | 2 +- .github/workflows/run-unit-tests.yml | 6 +++--- .github/workflows/test-tljh-matlab.yml | 2 +- pyproject.toml | 4 ++-- 7 files changed, 16 insertions(+), 14 deletions(-) diff --git a/.github/workflows/publish-jupyter-matlab-proxy.yml b/.github/workflows/publish-jupyter-matlab-proxy.yml index 9bbeb404..ad8e0ca7 100644 --- a/.github/workflows/publish-jupyter-matlab-proxy.yml +++ b/.github/workflows/publish-jupyter-matlab-proxy.yml @@ -31,7 +31,7 @@ jobs: ref: ${{ github.sha }} - name: Set up Python 3.10 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.10 diff --git a/.github/workflows/publish-tljh-matlab.yml b/.github/workflows/publish-tljh-matlab.yml index 6cc8ff78..00ae320c 100644 --- a/.github/workflows/publish-tljh-matlab.yml +++ b/.github/workflows/publish-tljh-matlab.yml @@ -1,4 +1,4 @@ -# Copyright 2024 The MathWorks, Inc. +# Copyright 2024-2025 The MathWorks, Inc. name: Upload Python Package for tljh-matlab on: @@ -22,7 +22,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: '3.x' diff --git a/.github/workflows/run-e2e-tests.yml b/.github/workflows/run-e2e-tests.yml index ca121cbc..5a60a142 100644 --- a/.github/workflows/run-e2e-tests.yml +++ b/.github/workflows/run-e2e-tests.yml @@ -12,17 +12,19 @@ jobs: run: working-directory: tests/e2e env: - NODE_VERSION: 18 - PYTHON_VERSION: 3.10 + NODE_VERSION: 24 + PYTHON_VERSION: '3.10' + steps: - name: Checkout uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - name: Set up Node ${{ env.NODE_VERSION }} + uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} - - name: Set up Python + - name: Set up Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index d14c9b0f..08b36b51 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -32,7 +32,7 @@ jobs: products: MATLAB Symbolic_Math_Toolbox - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 72317500..b8d7ff50 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -77,7 +77,7 @@ jobs: uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -121,12 +121,12 @@ jobs: run: working-directory: src/jupyter_matlab_labextension/src/lezer-matlab/test/ env: - NODE_VERSION: 18 + NODE_VERSION: 24 steps: - name: Checkout uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} diff --git a/.github/workflows/test-tljh-matlab.yml b/.github/workflows/test-tljh-matlab.yml index c1e751a7..0acca298 100644 --- a/.github/workflows/test-tljh-matlab.yml +++ b/.github/workflows/test-tljh-matlab.yml @@ -19,7 +19,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python 3.10 - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: "3.10" diff --git a/pyproject.toml b/pyproject.toml index 4054bdbc..87ea0757 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ version = "0.17.2" description = "MATLAB Integration for Jupyter" readme = "README.md" license = { file = "LICENSE.md" } -requires-python = ">=3.8" +requires-python = ">=3.10, <3.14" authors = [ { name = "The MathWorks Inc.", email = "jupyter-support@mathworks.com" }, ] @@ -74,7 +74,7 @@ features = [ ] [[tool.hatch.envs.hatch-test.matrix]] -python = ["3.9", "3.10", "3.11", "3.12", "3.13"] +python = ["3.10", "3.11", "3.12", "3.13"] [project.entry-points.jupyter_serverproxy_servers] matlab = "jupyter_matlab_proxy:setup_matlab" From c6f14266f384e8808a5f3da737faaa28761931dd Mon Sep 17 00:00:00 2001 From: Prabhakar Kumar Date: Mon, 8 Dec 2025 17:14:19 +0530 Subject: [PATCH 08/15] Update to v0.17.3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 87ea0757..0e6777dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "jupyter-matlab-proxy" -version = "0.17.2" +version = "0.17.3" description = "MATLAB Integration for Jupyter" readme = "README.md" license = { file = "LICENSE.md" } From 4d2bfa5cc35e1fdcf0894ac94b69a8464310c02f Mon Sep 17 00:00:00 2001 From: Prabhakar Kumar Date: Mon, 8 Dec 2025 19:26:28 +0530 Subject: [PATCH 09/15] Update codecov action --- .github/workflows/run-unit-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index b8d7ff50..1a42fcf2 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -63,7 +63,7 @@ jobs: python_unit_tests: timeout-minutes: 15 env: - code-cov-py: "3.11" + code-cov-py: "3.13" code-cov-os: "ubuntu-latest" strategy: fail-fast: false From 64b055cf9d755c1ac7cf89d15dd2c4544629c9ba Mon Sep 17 00:00:00 2001 From: Prabhakar Kumar Date: Mon, 8 Dec 2025 19:27:10 +0530 Subject: [PATCH 10/15] Update to v0.17.4 --- .github/workflows/publish-jupyter-matlab-proxy.yml | 2 +- .github/workflows/test-tljh-matlab.yml | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish-jupyter-matlab-proxy.yml b/.github/workflows/publish-jupyter-matlab-proxy.yml index ad8e0ca7..ea081ccc 100644 --- a/.github/workflows/publish-jupyter-matlab-proxy.yml +++ b/.github/workflows/publish-jupyter-matlab-proxy.yml @@ -33,7 +33,7 @@ jobs: - name: Set up Python 3.10 uses: actions/setup-python@v5 with: - python-version: 3.10 + python-version: '3.10' - name: Install Python build dependencies run: | diff --git a/.github/workflows/test-tljh-matlab.yml b/.github/workflows/test-tljh-matlab.yml index 0acca298..ebc01597 100644 --- a/.github/workflows/test-tljh-matlab.yml +++ b/.github/workflows/test-tljh-matlab.yml @@ -21,7 +21,7 @@ jobs: - name: Set up Python 3.10 uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: '3.10' - name: Install dependencies run: | diff --git a/pyproject.toml b/pyproject.toml index 0e6777dd..450004eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "jupyter-matlab-proxy" -version = "0.17.3" +version = "0.17.4" description = "MATLAB Integration for Jupyter" readme = "README.md" license = { file = "LICENSE.md" } From 1a8affb7d6bb1267b18b8bf74b69aa774abc0879 Mon Sep 17 00:00:00 2001 From: Krishan Sharma Date: Mon, 8 Dec 2025 15:26:58 +0000 Subject: [PATCH 11/15] Fixes pylance warnings when parsing magic command arguments. --- src/jupyter_matlab_kernel/magic_execution_engine.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jupyter_matlab_kernel/magic_execution_engine.py b/src/jupyter_matlab_kernel/magic_execution_engine.py index e7cd176a..58811a2f 100644 --- a/src/jupyter_matlab_kernel/magic_execution_engine.py +++ b/src/jupyter_matlab_kernel/magic_execution_engine.py @@ -1,4 +1,4 @@ -# Copyright 2024 The MathWorks, Inc. +# Copyright 2024-2025 The MathWorks, Inc. import importlib import re @@ -144,7 +144,7 @@ def get_magics_from_cell(cell_code): magic_dict = magic_matches.groupdict() params = magic_dict["params"].strip() if params: - magic_dict["params"] = re.split("\s+", params) + magic_dict["params"] = re.split(r"\s+", params) # transform ? parameter into a help magic, such that %%file? becomes %%help file if params.startswith("?"): # Ignore additional parameters after the ? parameter From c142f19c609c50a1e9b84f02ea7e332406fe60a2 Mon Sep 17 00:00:00 2001 From: Sourabh Kondapaka Date: Mon, 8 Dec 2025 15:30:18 +0000 Subject: [PATCH 12/15] Infrastructure to enable communication between LabExtension and Kernel. --- src/jupyter_matlab_kernel/__main__.py | 2 +- src/jupyter_matlab_kernel/base_kernel.py | 16 +- src/jupyter_matlab_kernel/comms/__init__.py | 3 + .../comms/labextension/__init__.py | 3 + .../comms/labextension/labextension.py | 55 +++ src/jupyter_matlab_kernel/jsp_kernel.py | 2 +- src/jupyter_matlab_kernel/mwi_comm_helpers.py | 182 ++++++--- .../.eslintrc.json | 60 ++- .../.styleintrc.json | 0 .../jest.config.js | 20 +- src/jupyter_matlab_labextension/package.json | 2 + .../codemirror-lang-matlab.ts | 14 +- .../codemirror-lang-matlab/indent-matlab.ts | 30 +- src/jupyter_matlab_labextension/src/index.ts | 16 +- .../src/lezer-matlab/rollup.config.js | 31 +- .../src/lezer-matlab/src/highlight.js | 24 +- .../src/lezer-matlab/src/parse_comments.js | 252 ++++++------ .../test/test-long-matlab-files.js | 60 +-- .../src/lezer-matlab/test/test-matlab.js | 28 +- .../matlabCM6Mode.ts} | 15 +- .../src/plugins/matlabCommunication.ts | 186 +++++++++ .../matlabFiles.ts} | 12 +- .../matlabToolbarButton.ts} | 21 +- .../src/tests/jest-setup.ts | 17 + .../src/tests/matlabCommunication.test.ts | 145 +++++++ .../src/tests/matlabToolbarButton.test.ts | 286 ++++++++++++++ .../src/tests/mocks/ui-components.js | 15 + .../src/utils/notebook.ts | 132 +++++++ .../style/index.css | 3 +- .../style/index.js | 3 +- src/jupyter_matlab_labextension/tsconfig.json | 2 +- src/jupyter_matlab_labextension/yarn.lock | 372 ++++++++++++++++++ .../comms/test_labextension.py | 208 ++++++++++ .../mocks/mock_http_responses.py | 49 ++- .../unit/jupyter_matlab_kernel/test_kernel.py | 7 +- .../jupyter_matlab_kernel/test_mpm_kernel.py | 3 +- .../test_mwi_comm_helpers.py | 286 +++++++++++++- 37 files changed, 2243 insertions(+), 319 deletions(-) create mode 100644 src/jupyter_matlab_kernel/comms/__init__.py create mode 100644 src/jupyter_matlab_kernel/comms/labextension/__init__.py create mode 100644 src/jupyter_matlab_kernel/comms/labextension/labextension.py create mode 100644 src/jupyter_matlab_labextension/.styleintrc.json rename src/jupyter_matlab_labextension/src/{matlab_cm6_mode.ts => plugins/matlabCM6Mode.ts} (72%) create mode 100644 src/jupyter_matlab_labextension/src/plugins/matlabCommunication.ts rename src/jupyter_matlab_labextension/src/{matlab_files.ts => plugins/matlabFiles.ts} (89%) rename src/jupyter_matlab_labextension/src/{matlab_browser_button.ts => plugins/matlabToolbarButton.ts} (77%) create mode 100644 src/jupyter_matlab_labextension/src/tests/jest-setup.ts create mode 100644 src/jupyter_matlab_labextension/src/tests/matlabCommunication.test.ts create mode 100644 src/jupyter_matlab_labextension/src/tests/matlabToolbarButton.test.ts create mode 100644 src/jupyter_matlab_labextension/src/tests/mocks/ui-components.js create mode 100644 src/jupyter_matlab_labextension/src/utils/notebook.ts create mode 100644 tests/unit/jupyter_matlab_kernel/comms/test_labextension.py diff --git a/src/jupyter_matlab_kernel/__main__.py b/src/jupyter_matlab_kernel/__main__.py index 0d0b5884..65a401bb 100644 --- a/src/jupyter_matlab_kernel/__main__.py +++ b/src/jupyter_matlab_kernel/__main__.py @@ -1,4 +1,4 @@ -# Copyright 2023-2024 The MathWorks, Inc. +# Copyright 2023-2025 The MathWorks, Inc. # Use ipykernel infrastructure to launch the MATLAB Kernel. if __name__ == "__main__": diff --git a/src/jupyter_matlab_kernel/base_kernel.py b/src/jupyter_matlab_kernel/base_kernel.py index 7afa7612..ec0369e0 100644 --- a/src/jupyter_matlab_kernel/base_kernel.py +++ b/src/jupyter_matlab_kernel/base_kernel.py @@ -13,7 +13,6 @@ import time from logging import Logger from pathlib import Path -from typing import Optional import aiohttp import aiohttp.client_exceptions @@ -28,6 +27,9 @@ ) from jupyter_matlab_kernel.mwi_exceptions import MATLABConnectionError +from jupyter_matlab_kernel.comms import LabExtensionCommunication + + _MATLAB_STARTUP_TIMEOUT = mwi_settings.get_process_startup_timeout() @@ -141,6 +143,18 @@ def __init__(self, *args, **kwargs): # Communication helper for interaction with backend MATLAB proxy self.mwi_comm_helper = None + self.labext_comm = LabExtensionCommunication(self) + + # Custom handling of comm messages for jupyterlab extension communication. + # https://jupyter-client.readthedocs.io/en/latest/messaging.html#custom-messages + + # Override only comm handlers to keep implementation clean by separating + # JupyterLab extension communication logic from core kernel functionality. + # Other handlers (interrupt_request, execute_request, etc.) remain in base class. + self.shell_handlers["comm_open"] = self.labext_comm.comm_open + self.shell_handlers["comm_msg"] = self.labext_comm.comm_msg + self.shell_handlers["comm_close"] = self.labext_comm.comm_close + # ipykernel Interface API # https://ipython.readthedocs.io/en/stable/development/wrapperkernels.html diff --git a/src/jupyter_matlab_kernel/comms/__init__.py b/src/jupyter_matlab_kernel/comms/__init__.py new file mode 100644 index 00000000..c5f51be7 --- /dev/null +++ b/src/jupyter_matlab_kernel/comms/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2025 The MathWorks, Inc. + +from .labextension import LabExtensionCommunication diff --git a/src/jupyter_matlab_kernel/comms/labextension/__init__.py b/src/jupyter_matlab_kernel/comms/labextension/__init__.py new file mode 100644 index 00000000..c5f51be7 --- /dev/null +++ b/src/jupyter_matlab_kernel/comms/labextension/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2025 The MathWorks, Inc. + +from .labextension import LabExtensionCommunication diff --git a/src/jupyter_matlab_kernel/comms/labextension/labextension.py b/src/jupyter_matlab_kernel/comms/labextension/labextension.py new file mode 100644 index 00000000..aba744b8 --- /dev/null +++ b/src/jupyter_matlab_kernel/comms/labextension/labextension.py @@ -0,0 +1,55 @@ +# Copyright 2025 The MathWorks, Inc. + +from ipykernel.comm import Comm + + +class LabExtensionCommunication: + def __init__(self, kernel): + self.comms = {} + self.kernel = kernel + self.log = kernel.log + + def comm_open(self, stream, ident, msg): + """Handler to execute when labextension sends a message with 'comm_open' type .""" + + # As per jupyter messaging protocol https://jupyter-client.readthedocs.io/en/latest/messaging.html#custom-messages + # 'content' will be present in msg, 'comm_id' and 'target_name' will be present in content. + + content = msg["content"] + comm_id = content["comm_id"] + target_name = content["target_name"] + self.log.debug( + f"Received comm_open message with id: {comm_id} and target_name: {target_name}" + ) + comm = Comm(comm_id=comm_id, primary=False, target_name=target_name) + self.comms[comm_id] = comm + self.log.debug( + f"Successfully created communication channel with labextension on: {comm_id}" + ) + + async def comm_msg(self, stream, ident, msg): + """Handler to execute when labextension sends a message with 'comm_msg' type.""" + # As per jupyter messaging protocol https://jupyter-client.readthedocs.io/en/latest/messaging.html#custom-messages + # 'content' will be present in msg, 'comm_id' and 'data' will be present in content. + payload = msg["content"]["data"] + action_type, action_data = payload["action"], payload["data"] + + self.log.debug( + f"Received action_type:{action_type} with data:{action_data} from the lab extension" + ) + + def comm_close(self, stream, ident, msg): + """Handler to execute when labextension sends a message with 'comm_close' type.""" + + # As per jupyter messaging protocol https://jupyter-client.readthedocs.io/en/latest/messaging.html#custom-messages + # 'content' will be present in msg, 'comm_id' and 'data' will be present in content. + content = msg["content"] + comm_id = content["comm_id"] + comm = self.comms.get(comm_id) + + if comm: + self.log.info(f"Comm closed with id: {comm_id}") + del self.comms[comm_id] + + else: + self.log.debug(f"Attempted to close unknown comm_id: {comm_id}") diff --git a/src/jupyter_matlab_kernel/jsp_kernel.py b/src/jupyter_matlab_kernel/jsp_kernel.py index cb1afceb..dd0c4793 100644 --- a/src/jupyter_matlab_kernel/jsp_kernel.py +++ b/src/jupyter_matlab_kernel/jsp_kernel.py @@ -1,4 +1,4 @@ -# Copyright 2024 The MathWorks, Inc. +# Copyright 2024-2025 The MathWorks, Inc. """This module contains derived class implementation of MATLABKernel that uses Jupyter Server to manage interactions with matlab-proxy & MATLAB. diff --git a/src/jupyter_matlab_kernel/mwi_comm_helpers.py b/src/jupyter_matlab_kernel/mwi_comm_helpers.py index 8365a9b4..95df8105 100644 --- a/src/jupyter_matlab_kernel/mwi_comm_helpers.py +++ b/src/jupyter_matlab_kernel/mwi_comm_helpers.py @@ -1,4 +1,4 @@ -# Copyright 2023-2024 The MathWorks, Inc. +# Copyright 2023-2025 The MathWorks, Inc. # Helper functions to communicate with matlab-proxy and MATLAB import http @@ -179,6 +179,13 @@ async def send_shutdown_request_to_matlab(self): ) async def send_interrupt_request_to_matlab(self): + """Send an interrupt request to MATLAB to stop current execution. + + The interrupt request is sent through the control channel using a specific message format. + + Raises: + HTTPError: If the interrupt request fails or matlab-proxy communication errors occur + """ self.logger.debug("Sending interrupt request to MATLAB") req_body = { "messages": { @@ -201,6 +208,24 @@ async def send_interrupt_request_to_matlab(self): resp.raise_for_status() async def _send_feval_request_to_matlab(self, http_client, fname, nargout, *args): + """Execute a MATLAB function call (feval) through the matlab-proxy. + + Sends a function evaluation request to MATLAB, handling path setup and synchronous execution. + + Args: + http_client (aiohttp.ClientSession): HTTP client for sending the request + fname (str): Name of the MATLAB function to call + nargout (int): Number of output arguments expected + *args: Variable arguments to pass to the MATLAB function + + Returns: + list: Results from the MATLAB function execution if successful + Empty list if no outputs or nargout=0 + + Raises: + MATLABConnectionError: If MATLAB connection is lost or response is invalid + Exception: If function execution fails or is interrupted by user + """ self.logger.debug("Sending FEval request to MATLAB") # Add the MATLAB code shipped with kernel to the Path path = [str(pathlib.Path(__file__).parent / "matlab")] @@ -264,7 +289,36 @@ async def _send_feval_request_to_matlab(self, http_client, fname, nargout, *args self.logger.error("Error occurred during communication with matlab-proxy") raise resp.raise_for_status() + async def send_eval_request_to_matlab(self, mcode): + """Send an evaluation request to MATLAB using the shell client. + + Args: + mcode (str): MATLAB code to be evaluated + + Returns: + dict: The evaluation response from MATLAB containing results or error information + + Raises: + MATLABConnectionError: If MATLAB connection is not available + HTTPError: If there is an error in communication with matlab-proxy + """ + return await self._send_eval_request_to_matlab(self._http_shell_client, mcode) + async def _send_eval_request_to_matlab(self, http_client, mcode): + """Internal method to send and process an evaluation request to MATLAB. + + Args: + http_client (aiohttp.ClientSession): HTTP client to use for the request + mcode (str): MATLAB code to be evaluated + + Returns: + dict: The evaluation response containing results or error information + from the MATLAB execution + + Raises: + MATLABConnectionError: If MATLAB connection is not available or response is invalid + HTTPError: If there is an error in communication with matlab-proxy + """ self.logger.debug("Sending Eval request to MATLAB") # Add the MATLAB code shipped with kernel to the Path path = str(pathlib.Path(__file__).parent / "matlab") @@ -286,6 +340,7 @@ async def _send_eval_request_to_matlab(self, http_client, mcode): self.logger.debug(f"Response:\n{response_data}") try: eval_response = response_data["messages"]["EvalResponse"][0] + except KeyError: # In certain cases when the HTTPResponse is received, it does not # contain the expected data. In these cases most likely MATLAB has @@ -296,54 +351,27 @@ async def _send_eval_request_to_matlab(self, http_client, mcode): ) raise MATLABConnectionError() - # If the eval request succeeded, return the json decoded result. - if not eval_response["isError"]: - result_filepath = eval_response["responseStr"].strip() - - # If the filepath in the response is not empty, read the result from - # file and delete the file. - if result_filepath != "": - self.logger.debug(f"Found file with results: {result_filepath}") - self.logger.debug("Reading contents of the file") - with open(result_filepath, "r") as f: - result = f.read().strip() - self.logger.debug("Reading completed") - try: - import os - - self.logger.debug(f"Deleting file: {result_filepath}") - os.remove(result_filepath) - except Exception: - self.logger.error("Deleting file failed") - else: - self.logger.debug("No result in EvalResponse") - result = "" - - # If result is empty, populate dummy json - if result == "": - result = "[]" - return json.loads(result) - - # Handle the error cases - if eval_response["messageFaults"]: - # This happens when "Interrupt Kernel" is issued from a different - # kernel. There may be other cases also. - self.logger.error( - f'Error during execution of Eval request in MATLAB:\n{eval_response["messageFaults"][0]["message"]}' - ) - error_message = ( - "Failed to execute. Operation may have been interrupted by user." - ) - else: - # This happens when "Interrupt Kernel" is issued from the same kernel. - # The responseStr contains the error message - error_message = eval_response["responseStr"].strip() - raise Exception(error_message) + return eval_response + else: self.logger.error("Error during communication with matlab-proxy") raise resp.raise_for_status() async def _send_jupyter_request_to_matlab(self, request_type, inputs, http_client): + """Process and send a Jupyter request to MATLAB using either feval or eval execution. + + Args: + request_type (str): Type of request (execute, complete, shutdown) + inputs (list): List of input arguments for the request + http_client (aiohttp.ClientSession): HTTP client to use for the request + + Returns: + dict: Response from MATLAB containing results of the request execution + + Raises: + MATLABConnectionError: If MATLAB connection is not available + Exception: If request execution fails or is interrupted + """ execution_request_type = "feval" inputs.insert(0, request_type) @@ -353,10 +381,14 @@ async def _send_jupyter_request_to_matlab(self, request_type, inputs, http_clien f"Using {execution_request_type} request type for communication with EC" ) + resp = None if execution_request_type == "feval": resp = await self._send_feval_request_to_matlab( http_client, "processJupyterKernelRequest", 1, *inputs ) + + # The 'else' condition is an artifact and is present here incase we ever want to test + # eval execution. else: user_mcode = inputs[2] # Construct a string which can be evaluated in MATLAB. For example @@ -376,6 +408,66 @@ async def _send_jupyter_request_to_matlab(self, request_type, inputs, http_clien args = args + "," + str(cursor_pos) eval_mcode = f"processJupyterKernelRequest({args})" - resp = await self._send_eval_request_to_matlab(http_client, eval_mcode) + eval_response = await self._send_eval_request_to_matlab( + http_client, eval_mcode + ) + resp = await self._read_eval_response_from_file(eval_response) return resp + + async def _read_eval_response_from_file(self, eval_response): + """Read and process MATLAB evaluation results from a response file. + + Args: + eval_response (dict): Response dictionary from MATLAB eval request containing + file path and error information + + Returns: + dict: JSON decoded results from the response file + + Raises: + Exception: If evaluation failed or was interrupted by user + """ + # If the eval request succeeded, return the json decoded result. + if not eval_response["isError"]: + result_filepath = eval_response["responseStr"].strip() + + # If the filepath in the response is not empty, read the result from + # file and delete the file. + if result_filepath != "": + self.logger.debug(f"Found file with results: {result_filepath}") + self.logger.debug("Reading contents of the file") + with open(result_filepath, "r") as f: + result = f.read().strip() + self.logger.debug("Reading completed") + try: + import os + + self.logger.debug(f"Deleting file: {result_filepath}") + os.remove(result_filepath) + except Exception: + self.logger.error("Deleting file failed") + else: + self.logger.debug("No result in EvalResponse") + result = "" + + # If result is empty, populate dummy json + if result == "": + result = "[]" + return json.loads(result) + + # Handle the error cases + if eval_response["messageFaults"]: + # This happens when "Interrupt Kernel" is issued from a different + # kernel. There may be other cases also. + self.logger.error( + f'Error during execution of Eval request in MATLAB:\n{eval_response["messageFaults"][0]["message"]}' + ) + error_message = ( + "Failed to execute. Operation may have been interrupted by user." + ) + else: + # This happens when "Interrupt Kernel" is issued from the same kernel. + # The responseStr contains the error message + error_message = eval_response["responseStr"].strip() + raise Exception(error_message) diff --git a/src/jupyter_matlab_labextension/.eslintrc.json b/src/jupyter_matlab_labextension/.eslintrc.json index b5c169fc..983f3e9d 100644 --- a/src/jupyter_matlab_labextension/.eslintrc.json +++ b/src/jupyter_matlab_labextension/.eslintrc.json @@ -1,10 +1,9 @@ - { "root": true, "parser": "@typescript-eslint/parser", "parserOptions": { - "ecmaVersion": 6, - "sourceType": "module" + "ecmaVersion": 6, + "sourceType": "module" }, "env": { "jest": true @@ -12,33 +11,32 @@ "plugins": ["@typescript-eslint"], "extends": ["standard"], "rules": { - "indent": ["error", 4, { "SwitchCase": 1 }], - "semi": ["error", "always"], - "no-extra-semi": "error", - "quote-props": "warn", - "dot-notation": "warn", - "object-curly-newline": "warn", - "multiline-ternary": "warn", - "prefer-const": "warn", - "no-prototype-builtins": "warn", - "array-callback-return": "warn", - "array-bracket-spacing": "warn", - "quotes": "warn", - "lines-between-class-members": "warn", - "no-empty": "warn", - "prefer-regex-literals": "warn", - "no-useless-catch": "warn", - "no-case-declarations": "warn", - "computed-property-spacing": "warn", - "no-async-promise-executor": "warn", - "no-unused-vars": "warn", - "no-unreachable-loop": "warn", - "no-void": "warn", - "import/no-webpack-loader-syntax": "warn", - "node/no-callback-literal": ["off", "warn"], - "node/handle-callback-err": ["off", "warn"], - "node/no-deprecated-api": ["off", "warn"] + "indent": ["error", 4, { "SwitchCase": 1 }], + "semi": ["error", "always"], + "no-extra-semi": "error", + "quote-props": "warn", + "dot-notation": "warn", + "object-curly-newline": "warn", + "multiline-ternary": "warn", + "prefer-const": "warn", + "no-prototype-builtins": "warn", + "array-callback-return": "warn", + "array-bracket-spacing": "warn", + "quotes": "warn", + "lines-between-class-members": "warn", + "no-empty": "warn", + "prefer-regex-literals": "warn", + "no-useless-catch": "warn", + "no-case-declarations": "warn", + "computed-property-spacing": "warn", + "no-async-promise-executor": "warn", + "no-unused-vars": "warn", + "no-unreachable-loop": "warn", + "no-void": "warn", + "import/no-webpack-loader-syntax": "warn", + "node/no-callback-literal": ["off", "warn"], + "node/handle-callback-err": ["off", "warn"], + "node/no-deprecated-api": ["off", "warn"], + "no-multiple-empty-lines": "warn" } } - - diff --git a/src/jupyter_matlab_labextension/.styleintrc.json b/src/jupyter_matlab_labextension/.styleintrc.json new file mode 100644 index 00000000..e69de29b diff --git a/src/jupyter_matlab_labextension/jest.config.js b/src/jupyter_matlab_labextension/jest.config.js index ab6e1bba..2f10abe1 100644 --- a/src/jupyter_matlab_labextension/jest.config.js +++ b/src/jupyter_matlab_labextension/jest.config.js @@ -1,7 +1,19 @@ // Copyright 2025 The MathWorks, Inc. module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'], - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + preset: "ts-jest", + testEnvironment: "node", + testMatch: ["src/tests/**/*.ts?(x)", "**/?(*.)+(spec|test).ts?(x)"], + testPathIgnorePatterns: ["/node_modules/", "/src/tests/jest-setup.ts"], + moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], + setupFilesAfterEnv: ["/src/tests/jest-setup.ts"], + transform: { + "^.+\\.(ts|tsx)$": "ts-jest", + }, + transformIgnorePatterns: [ + "/node_modules/(?!(@jupyterlab)/)", // Transform @jupyterlab packages + ], + moduleNameMapper: { + // Mock @jupyterlab/ui-components to avoid ES modules issues + "@jupyterlab/ui-components": "/src/tests/mocks/ui-components.js", + }, }; diff --git a/src/jupyter_matlab_labextension/package.json b/src/jupyter_matlab_labextension/package.json index bdaaca99..3f10baca 100644 --- a/src/jupyter_matlab_labextension/package.json +++ b/src/jupyter_matlab_labextension/package.json @@ -60,6 +60,7 @@ "@jupyterlab/coreutils": "^6.0.0", "@jupyterlab/docregistry": "^4.0.0", "@jupyterlab/launcher": "^4.0.0", + "@jupyterlab/mainmenu": "^4.3.4", "@jupyterlab/notebook": "^4.0.0", "@jupyterlab/ui-components": "^4.0.0", "@lumino/coreutils": "^2.0.0", @@ -68,6 +69,7 @@ "devDependencies": { "@jupyterlab/builder": ">=4.0.0", "@types/jest": "^29.5.14", + "@types/node": "^24.0.4", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "cross-spawn": "^6.0.6", diff --git a/src/jupyter_matlab_labextension/src/codemirror-lang-matlab/codemirror-lang-matlab.ts b/src/jupyter_matlab_labextension/src/codemirror-lang-matlab/codemirror-lang-matlab.ts index d1203fb9..bd94c11b 100644 --- a/src/jupyter_matlab_labextension/src/codemirror-lang-matlab/codemirror-lang-matlab.ts +++ b/src/jupyter_matlab_labextension/src/codemirror-lang-matlab/codemirror-lang-matlab.ts @@ -1,13 +1,21 @@ // Copyright 2024-2025 The MathWorks, Inc. import { parser } from '../lezer-matlab/dist/index'; -import { indentNodeProp, LanguageSupport, LRLanguage, TreeIndentContext } from '@codemirror/language'; +import { + indentNodeProp, + LanguageSupport, + LRLanguage, + TreeIndentContext +} from '@codemirror/language'; import { lineIndent, getDedentPattern } from './indent-matlab'; function determineLineIndent (context: TreeIndentContext) { - if (context.pos === 0) { return null; } + if (context.pos === 0) { + return null; + } const currentLine = context.lineAt(context.pos); - const previousLine = currentLine.text.length === 0 + const previousLine = + currentLine.text.length === 0 ? context.lineAt(context.pos, -1) // Look to the left of the simulated line break. : context.lineAt(context.pos - 1); // Not on a simulated line break, so step back to the previous line. if (previousLine === null || currentLine === null) { diff --git a/src/jupyter_matlab_labextension/src/codemirror-lang-matlab/indent-matlab.ts b/src/jupyter_matlab_labextension/src/codemirror-lang-matlab/indent-matlab.ts index d25be0b1..83dce4b0 100644 --- a/src/jupyter_matlab_labextension/src/codemirror-lang-matlab/indent-matlab.ts +++ b/src/jupyter_matlab_labextension/src/codemirror-lang-matlab/indent-matlab.ts @@ -1,7 +1,8 @@ // Copyright 2025 The MathWorks, Inc. // Indent after these keywords unless the line ends with "end". -const indentPattern = /^(?:\s*)(arguments|case|catch|classdef|else|elseif|enumeration|for|function|if|methods|otherwise|parfor|properties|switch|try|while)\b(?!.*\bend;?$)/; +const indentPattern = + /^(?:\s*)(arguments|case|catch|classdef|else|elseif|enumeration|for|function|if|methods|otherwise|parfor|properties|switch|try|while)\b(?!.*\bend;?$)/; const dedentPattern = /^(?:\s*)(case|catch|else|end|otherwise)\b$/; const leadingWhitespacePattern = /^\s*/; @@ -9,16 +10,31 @@ export function getDedentPattern (): RegExp { return new RegExp(dedentPattern); } -export function lineIndent (indentUnit: number, currentLineText: string, previousLineText: string) { - const prevLeadingWhitespace = previousLineText.match(leadingWhitespacePattern); - const prevLineIndent = prevLeadingWhitespace ? prevLeadingWhitespace[0].length : 0; - if (currentLineText.match(/^(?:\s*)(case)\b$/) && previousLineText.match(/^(?:\s*)(switch)\b/)) { +export function lineIndent ( + indentUnit: number, + currentLineText: string, + previousLineText: string +) { + const prevLeadingWhitespace = previousLineText.match( + leadingWhitespacePattern + ); + const prevLineIndent = prevLeadingWhitespace + ? prevLeadingWhitespace[0].length + : 0; + if ( + currentLineText.match(/^(?:\s*)(case)\b$/) && + previousLineText.match(/^(?:\s*)(switch)\b/) + ) { // First case in a switch statement. return prevLineIndent + indentUnit; } else if (currentLineText.match(/^(?:\s*)(end)\b$/)) { // Treat "end" separately to avoid mistakenly correcting the end of a switch statement. - const currentLeadingWhitespace = currentLineText.match(leadingWhitespacePattern); - const currentLineIndent = currentLeadingWhitespace ? currentLeadingWhitespace[0].length : 0; + const currentLeadingWhitespace = currentLineText.match( + leadingWhitespacePattern + ); + const currentLineIndent = currentLeadingWhitespace + ? currentLeadingWhitespace[0].length + : 0; const indentMatch = previousLineText.match(indentPattern); if (indentMatch) { return Math.min(prevLineIndent, currentLineIndent); diff --git a/src/jupyter_matlab_labextension/src/index.ts b/src/jupyter_matlab_labextension/src/index.ts index bc2d8c76..946310e8 100644 --- a/src/jupyter_matlab_labextension/src/index.ts +++ b/src/jupyter_matlab_labextension/src/index.ts @@ -1,9 +1,15 @@ -// Copyright 2023-2024 The MathWorks, Inc. +// Copyright 2023-2025 The MathWorks, Inc. import { JupyterFrontEndPlugin } from '@jupyterlab/application'; -import { matlabToolbarButtonPlugin } from './matlab_browser_button'; -import { matlabMFilesPlugin } from './matlab_files'; -import { matlabCodeMirror6Plugin } from './matlab_cm6_mode'; +import { matlabToolbarButtonPlugin } from './plugins/matlabToolbarButton'; +import { matlabMFilesPlugin } from './plugins/matlabFiles'; +import { matlabCodeMirror6Plugin } from './plugins/matlabCM6Mode'; +import { matlabCommPlugin } from './plugins/matlabCommunication'; -const plugins: JupyterFrontEndPlugin[] = [matlabToolbarButtonPlugin, matlabMFilesPlugin, matlabCodeMirror6Plugin]; +const plugins: JupyterFrontEndPlugin[] = [ + matlabToolbarButtonPlugin, + matlabMFilesPlugin, + matlabCodeMirror6Plugin, + matlabCommPlugin +]; export default plugins; diff --git a/src/jupyter_matlab_labextension/src/lezer-matlab/rollup.config.js b/src/jupyter_matlab_labextension/src/lezer-matlab/rollup.config.js index b285d451..8e3b4f68 100644 --- a/src/jupyter_matlab_labextension/src/lezer-matlab/rollup.config.js +++ b/src/jupyter_matlab_labextension/src/lezer-matlab/rollup.config.js @@ -1,26 +1,27 @@ -// Copyright 2024 The MathWorks, Inc. +// Copyright 2024-2025 The MathWorks, Inc. -import { nodeResolve } from '@rollup/plugin-node-resolve'; -import path from 'path'; +import { nodeResolve } from "@rollup/plugin-node-resolve"; +import path from "path"; -const entryModule = './src/parser.js'; +const entryModule = "./src/parser.js"; export default { input: entryModule, - output: [{ - format: "cjs", - file: "./dist/index.cjs" - }, { - format: "es", - file: "./dist/index.js" - }], + output: [ + { + format: "cjs", + file: "./dist/index.cjs", + }, + { + format: "es", + file: "./dist/index.js", + }, + ], external(id) { if (id === path.resolve(entryModule)) { return false; } return !/^[\.\/]/.test(id); }, - plugins: [ - nodeResolve() - ] -} + plugins: [nodeResolve()], +}; diff --git a/src/jupyter_matlab_labextension/src/lezer-matlab/src/highlight.js b/src/jupyter_matlab_labextension/src/lezer-matlab/src/highlight.js index d15b6819..4c289ce3 100644 --- a/src/jupyter_matlab_labextension/src/lezer-matlab/src/highlight.js +++ b/src/jupyter_matlab_labextension/src/lezer-matlab/src/highlight.js @@ -1,18 +1,18 @@ -// Copyright 2024 The MathWorks, Inc. +// Copyright 2024-2025 The MathWorks, Inc. -import { styleTags, tags as t } from '@lezer/highlight'; +import { styleTags, tags as t } from "@lezer/highlight"; // Associate nodes in the Lezer tree with styles. // https://lezer.codemirror.net/docs/ref/#highlight.styleTags export const matlabHighlighting = styleTags({ - Keyword: t.keyword, - Identifier: t.variableName, - LineComment: t.comment, - MultilineComment: t.comment, - SystemCommand: t.meta, - String: t.string, - Magic: t.monospace, - '( )': t.paren, - '[ ]': t.squareBracket, - '{ }': t.brace + Keyword: t.keyword, + Identifier: t.variableName, + LineComment: t.comment, + MultilineComment: t.comment, + SystemCommand: t.meta, + String: t.string, + Magic: t.monospace, + "( )": t.paren, + "[ ]": t.squareBracket, + "{ }": t.brace, }); diff --git a/src/jupyter_matlab_labextension/src/lezer-matlab/src/parse_comments.js b/src/jupyter_matlab_labextension/src/lezer-matlab/src/parse_comments.js index 29f4d91e..ef8e65d9 100644 --- a/src/jupyter_matlab_labextension/src/lezer-matlab/src/parse_comments.js +++ b/src/jupyter_matlab_labextension/src/lezer-matlab/src/parse_comments.js @@ -1,16 +1,16 @@ -// Copyright 2024 The MathWorks, Inc. +// Copyright 2024-2025 The MathWorks, Inc. -import { ExternalTokenizer } from '@lezer/lr'; +import { ExternalTokenizer } from "@lezer/lr"; // This file is created by lezer-generator during the build. -import { MultilineComment, LineComment, Magic } from './parser.terms.js'; +import { MultilineComment, LineComment, Magic } from "./parser.terms.js"; -const percent = '%'.charCodeAt(0); -const openBrace = '{'.charCodeAt(0); -const closeBrace = '}'.charCodeAt(0); +const percent = "%".charCodeAt(0); +const openBrace = "{".charCodeAt(0); +const closeBrace = "}".charCodeAt(0); const fileStart = -1; const fileEnd = -1; -const newline = '\n'.charCodeAt(0); -const carriageReturn = '\r'.charCodeAt(0); +const newline = "\n".charCodeAt(0); +const carriageReturn = "\r".charCodeAt(0); const isAlphabetical = (char) => /^[a-zA-Z]$/.test(String.fromCharCode(char)); @@ -19,142 +19,144 @@ const lineEndArray = [newline, carriageReturn, fileEnd, fileStart]; const isWhitespace = (char) => /\s/.test(String.fromCharCode(char)); const precededByWhitespaceOnly = (input) => { - // Scan from current position to start of line. - // Return False if non-whitespace found. - // Always return input back to where it started. - const startPos = input.pos; - let onlyWhitespace = true; - while (!lineEndArray.includes(input.peek(-1))) { - if (isWhitespace(input.peek(-1))) { - input.advance(-1); - } else { - onlyWhitespace = false; - break; - } + // Scan from current position to start of line. + // Return False if non-whitespace found. + // Always return input back to where it started. + const startPos = input.pos; + let onlyWhitespace = true; + while (!lineEndArray.includes(input.peek(-1))) { + if (isWhitespace(input.peek(-1))) { + input.advance(-1); + } else { + onlyWhitespace = false; + break; } - while (input.pos < startPos) { input.advance(1); } - return onlyWhitespace; + } + while (input.pos < startPos) { + input.advance(1); + } + return onlyWhitespace; }; const followedByWhitespaceOnly = (input) => { - // Scan from current position to end of line. - // Return False if non-whitespace found. - // Always return input back to where it started. - const startPos = input.pos; - let onlyWhitespace = true; - while (!lineEndArray.includes(input.peek(0))) { - if (isWhitespace(input.peek(0))) { - input.advance(1); - } else { - onlyWhitespace = false; - break; - } + // Scan from current position to end of line. + // Return False if non-whitespace found. + // Always return input back to where it started. + const startPos = input.pos; + let onlyWhitespace = true; + while (!lineEndArray.includes(input.peek(0))) { + if (isWhitespace(input.peek(0))) { + input.advance(1); + } else { + onlyWhitespace = false; + break; } - while (input.pos > startPos) { input.advance(-1); } - return onlyWhitespace; + } + while (input.pos > startPos) { + input.advance(-1); + } + return onlyWhitespace; }; const validMultiLineCommentStart = (input) => { - if (input.peek(0) !== percent || input.peek(1) !== openBrace) { - return false; - } - if (!precededByWhitespaceOnly(input)) { - return false; - } - // Consume the %{ - input.advance(2); - if (!followedByWhitespaceOnly(input)) { - return false; - } - input.advance(-2); - return true; + if (input.peek(0) !== percent || input.peek(1) !== openBrace) { + return false; + } + if (!precededByWhitespaceOnly(input)) { + return false; + } + // Consume the %{ + input.advance(2); + if (!followedByWhitespaceOnly(input)) { + return false; + } + input.advance(-2); + return true; }; const validMultiLineCommentEnd = (input) => { - if (input.peek(0) !== percent || input.peek(1) !== closeBrace) { - return false; - } - if (!precededByWhitespaceOnly(input)) { - return false; - } - // Consume the %} - input.advance(2); - if (!followedByWhitespaceOnly(input)) { - return false; - } - input.advance(-2); - return true; + if (input.peek(0) !== percent || input.peek(1) !== closeBrace) { + return false; + } + if (!precededByWhitespaceOnly(input)) { + return false; + } + // Consume the %} + input.advance(2); + if (!followedByWhitespaceOnly(input)) { + return false; + } + input.advance(-2); + return true; }; const validMagic = (input) => { - if (input.notMagic !== undefined) { - return false; - } - var isMagic = false; - if ( - input.peek(0) === percent && - input.peek(1) === percent && - isAlphabetical(input.peek(2)) - ) { - isMagic = true; - } - return isMagic; + if (input.notMagic !== undefined) { + return false; + } + var isMagic = false; + if ( + input.peek(0) === percent && + input.peek(1) === percent && + isAlphabetical(input.peek(2)) + ) { + isMagic = true; + } + return isMagic; }; export const parseComments = new ExternalTokenizer((input) => { - // Tokenize only if the line is a comment, multiline comment - // or a magic and starts with a percentage. - if (input.peek(0) !== percent) { - // If the line starts with anything other than a percentage then it is MATLAB Code. - // If the input.input.string exists then check it's length otherwise ignore the keys by returning true. - if ( - !lineEndArray.includes(input.peek(0)) && - ( - !('input' in input) || - !('string' in input.input) || - input.input.string.length !== 0 - ) - ) { - input.notMagic = true; - } - return; - } else if (validMagic(input)) { - while (!lineEndArray.includes(input.peek(0))) { - input.advance(1); - } - input.acceptToken(Magic); - return; - } else if (validMultiLineCommentStart(input)) { - // Consume the %{ + // Tokenize only if the line is a comment, multiline comment + // or a magic and starts with a percentage. + if (input.peek(0) !== percent) { + // If the line starts with anything other than a percentage then it is MATLAB Code. + // If the input.input.string exists then check it's length otherwise ignore the keys by returning true. + if ( + !lineEndArray.includes(input.peek(0)) && + (!("input" in input) || + !("string" in input.input) || + input.input.string.length !== 0) + ) { + input.notMagic = true; + } + return; + } else if (validMagic(input)) { + while (!lineEndArray.includes(input.peek(0))) { + input.advance(1); + } + input.acceptToken(Magic); + return; + } else if (validMultiLineCommentStart(input)) { + // Consume the %{ + input.advance(2); + // Multiline comments are treated as MATLAB Code. + input.notMagic = true; + // Now we know we've started a multiline comment, so + // continue until the end of the input or until the comment is closed. + // We need to keep track of the depth of nested multiline comments. + let depth = 1; + while (input.peek(0) !== fileEnd) { + if (validMultiLineCommentEnd(input)) { input.advance(2); - // Multiline comments are treated as MATLAB Code. - input.notMagic = true; - // Now we know we've started a multiline comment, so - // continue until the end of the input or until the comment is closed. - // We need to keep track of the depth of nested multiline comments. - let depth = 1; - while (input.peek(0) !== fileEnd) { - if (validMultiLineCommentEnd(input)) { - input.advance(2); - depth--; - if (depth === 0) { - break; - } - } else if (validMultiLineCommentStart(input)) { - depth++; - } - input.advance(1); + depth--; + if (depth === 0) { + break; } + } else if (validMultiLineCommentStart(input)) { + depth++; + } + input.advance(1); + } - // Emit the token for the entire multiline comment - input.acceptToken(MultilineComment); - } else { - // Comments are also treated as MATLAB Code. - input.notMagic = true; - while (!lineEndArray.includes(input.peek(0))) { - input.advance(1); - } - input.acceptToken(LineComment); - return; + // Emit the token for the entire multiline comment + input.acceptToken(MultilineComment); + } else { + // Comments are also treated as MATLAB Code. + input.notMagic = true; + while (!lineEndArray.includes(input.peek(0))) { + input.advance(1); } + input.acceptToken(LineComment); + return; + } }); diff --git a/src/jupyter_matlab_labextension/src/lezer-matlab/test/test-long-matlab-files.js b/src/jupyter_matlab_labextension/src/lezer-matlab/test/test-long-matlab-files.js index 173a4ae7..c5bbd785 100644 --- a/src/jupyter_matlab_labextension/src/lezer-matlab/test/test-long-matlab-files.js +++ b/src/jupyter_matlab_labextension/src/lezer-matlab/test/test-long-matlab-files.js @@ -1,46 +1,58 @@ -// Copyright 2024 The MathWorks, Inc. +// Copyright 2024-2025 The MathWorks, Inc. -import {parser} from "../dist/index.js" -import {fileTests} from "@lezer/generator/dist/test" +import { parser } from "../dist/index.js"; +import { fileTests } from "@lezer/generator/dist/test"; -let N = 10000 +let N = 10000; -let long_file_spec = `Script(${"Keyword,Identifier,Symbol,Identifier,Symbol,Identifier,Identifier,Symbol,Identifier,Symbol,Symbol,Keyword,".repeat(N)}LineComment)` +let long_file_spec = `Script(${"Keyword,Identifier,Symbol,Identifier,Symbol,Identifier,Identifier,Symbol,Identifier,Symbol,Symbol,Keyword,".repeat( + N +)}LineComment)`; let long_file_input = ` ${"for c = 1:100\n\tdisp(c);\nend\n".repeat(N)} % Long file - ` + `; -let long_line_spec = `Script(${"Keyword,Identifier,Symbol,Identifier,Symbol,Identifier,Symbol,Identifier,Symbol,Identifier,Symbol,Symbol,Keyword,Symbol,".repeat(N)}LineComment)` +let long_line_spec = `Script(${"Keyword,Identifier,Symbol,Identifier,Symbol,Identifier,Symbol,Identifier,Symbol,Identifier,Symbol,Symbol,Keyword,Symbol,".repeat( + N +)}LineComment)`; let long_line_input = ` ${"for c = 1:100;\tdisp(c);end;".repeat(N)} % Long line - ` + `; describe("Long file", () => { it("parses long files correctly", () => { - let tree = parser.parse(long_file_input) - if (tree.toString() != long_file_spec) throw new Error("Parsed tree does not match long file spec") - }) -}) + let tree = parser.parse(long_file_input); + if (tree.toString() != long_file_spec) + throw new Error("Parsed tree does not match long file spec"); + }); +}); describe("Long line", () => { it("parses long lines correctly", () => { - let tree = parser.parse(long_line_input) - if (tree.toString() != long_line_spec) throw new Error("Parsed tree does not match long line spec") - }) -}) + let tree = parser.parse(long_line_input); + if (tree.toString() != long_line_spec) + throw new Error("Parsed tree does not match long line spec"); + }); +}); describe("Long file (multiline comment)", () => { it("parses long files correctly", () => { - let tree = parser.parse(`%{\n${long_file_input}\n%}`) - if (tree.toString() != "Script(MultilineComment)") throw new Error("Parsed tree does not match long file spec (multiline comment)") - }) -}) + let tree = parser.parse(`%{\n${long_file_input}\n%}`); + if (tree.toString() != "Script(MultilineComment)") + throw new Error( + "Parsed tree does not match long file spec (multiline comment)" + ); + }); +}); describe("Long line (multiline comment)", () => { it("parses long lines correctly", () => { - let tree = parser.parse(`%{\n${long_line_input}\n%}`) - if (tree.toString() != "Script(MultilineComment)") throw new Error("Parsed tree does not match long line spec (multiline comment)") - }) -}) + let tree = parser.parse(`%{\n${long_line_input}\n%}`); + if (tree.toString() != "Script(MultilineComment)") + throw new Error( + "Parsed tree does not match long line spec (multiline comment)" + ); + }); +}); diff --git a/src/jupyter_matlab_labextension/src/lezer-matlab/test/test-matlab.js b/src/jupyter_matlab_labextension/src/lezer-matlab/test/test-matlab.js index 2562f180..a83590cc 100644 --- a/src/jupyter_matlab_labextension/src/lezer-matlab/test/test-matlab.js +++ b/src/jupyter_matlab_labextension/src/lezer-matlab/test/test-matlab.js @@ -1,24 +1,20 @@ -// Copyright 2024 The MathWorks, Inc. +// Copyright 2024-2025 The MathWorks, Inc. -import {parser} from "../dist/index.js" -import {fileTests} from "@lezer/generator/dist/test" +import { parser } from "../dist/index.js"; +import { fileTests } from "@lezer/generator/dist/test"; import { describe, it } from "mocha"; -import * as fs from "fs" -import * as path from "path" +import * as fs from "fs"; +import * as path from "path"; -const testFileNames = [ - "basic_terms", - "nested_terms", - "additional_cases" -]; +const testFileNames = ["basic_terms", "nested_terms", "additional_cases"]; for (const testFileName of testFileNames) { - describe(`MATLAB Lezer grammar ${testFileName}`, function() { + describe(`MATLAB Lezer grammar ${testFileName}`, function () { const testFileContent = fs.readFileSync(`test/${testFileName}.txt`, "utf8"); - - for (let {name, run} of fileTests(testFileContent, testFileName)) { - it(name, () => run(parser)) + + for (let { name, run } of fileTests(testFileContent, testFileName)) { + it(name, () => run(parser)); } - }) -}; + }); +} diff --git a/src/jupyter_matlab_labextension/src/matlab_cm6_mode.ts b/src/jupyter_matlab_labextension/src/plugins/matlabCM6Mode.ts similarity index 72% rename from src/jupyter_matlab_labextension/src/matlab_cm6_mode.ts rename to src/jupyter_matlab_labextension/src/plugins/matlabCM6Mode.ts index 0a39bfea..30ea0498 100644 --- a/src/jupyter_matlab_labextension/src/matlab_cm6_mode.ts +++ b/src/jupyter_matlab_labextension/src/plugins/matlabCM6Mode.ts @@ -1,4 +1,4 @@ -// Copyright 2023-2024 The MathWorks, Inc. +// Copyright 2023-2025 The MathWorks, Inc. // Set up CodeMirror for the MATLAB language. @@ -7,9 +7,7 @@ import { JupyterFrontEndPlugin } from '@jupyterlab/application'; -import { - IEditorLanguageRegistry -} from '@jupyterlab/codemirror'; +import { IEditorLanguageRegistry } from '@jupyterlab/codemirror'; /** Register language with CodeMirror */ export function addMATLABCodeMirror (languageRegistry: IEditorLanguageRegistry) { @@ -20,7 +18,9 @@ export function addMATLABCodeMirror (languageRegistry: IEditorLanguageRegistry) extensions: ['m', 'mlx'], filename: /^[a-zA-Z][a-zA-Z0-9_]*\.m$/, async load () { - const m = await import('./codemirror-lang-matlab/codemirror-lang-matlab'); + const m = await import( + '../codemirror-lang-matlab/codemirror-lang-matlab' + ); return m.matlab(); } }); @@ -30,10 +30,7 @@ export const matlabCodeMirror6Plugin: JupyterFrontEndPlugin = { id: '@mathworks/matlabCodeMirror6Plugin', autoStart: true, requires: [IEditorLanguageRegistry], - activate: ( - app: JupyterFrontEnd, - codeMirror: IEditorLanguageRegistry - ) => { + activate: (app: JupyterFrontEnd, codeMirror: IEditorLanguageRegistry) => { addMATLABCodeMirror(codeMirror); } }; diff --git a/src/jupyter_matlab_labextension/src/plugins/matlabCommunication.ts b/src/jupyter_matlab_labextension/src/plugins/matlabCommunication.ts new file mode 100644 index 00000000..2968eb12 --- /dev/null +++ b/src/jupyter_matlab_labextension/src/plugins/matlabCommunication.ts @@ -0,0 +1,186 @@ +// Copyright 2025 The MathWorks, Inc. + +import { + JupyterFrontEnd, + JupyterFrontEndPlugin +} from '@jupyterlab/application'; + +import { DocumentRegistry } from '@jupyterlab/docregistry'; +import { + INotebookModel, + INotebookTracker, + NotebookPanel +} from '@jupyterlab/notebook'; +import { KernelMessage, Kernel } from '@jupyterlab/services'; +import { JSONObject, JSONValue, Token } from '@lumino/coreutils'; +import { DisposableDelegate } from '@lumino/disposable'; +import { NotebookInfo } from '../utils/notebook'; + +// Add more action types as needed +type CommunicationData = { + action: string; + data: JSONValue; +}; + +export interface ICommunicationChannel { + readonly commId: string; + readonly targetName: string; + readonly isDisposed: boolean; + onMsg: (msg: KernelMessage.ICommMsgMsg) => void | PromiseLike; + onClose: (msg: KernelMessage.ICommCloseMsg) => void | PromiseLike; + close: ( + data?: JSONValue, + metadata?: JSONObject, + buffers?: (ArrayBuffer | ArrayBufferView)[] + ) => void; + send: ( + data: CommunicationData, + metadata?: JSONObject, + buffers?: (ArrayBuffer | ArrayBufferView)[], + disposeOnDone?: boolean + ) => void; +} +export interface ICommunicationService { + getComm(notebookID: string): ICommunicationChannel; +} + +export class MatlabCommunicationExtension +implements + DocumentRegistry.IWidgetExtension, + ICommunicationService { + private _comms = new Map(); + + /* + * Attempts to open a comm channel with a retry mechanism. + * @param kernel The kernel for which a comm channel is being created. + + * @returns A promise that resolves when the comm is open. + */ + private async _createAndOpenCommWithRetry (kernel: Kernel.IKernelConnection, channelName: string): Promise { + let attempt = 1; + let delayInMS = 200; + const maxRetries = 5; + + while (attempt <= maxRetries) { + try { + // Creates comm object on the client side + const comm = kernel.createComm(channelName); + + // Attempts to open a channel with the kernel + await comm.open().done; + console.log('Communication channel opened successfully with ID:', comm.commId); + return comm; + } catch (error) { + console.error('Error opening communication channel', error); + console.error(`Attempt #${attempt} failed. Waiting ${delayInMS}ms before next attempt.`); + } + // Wait for the delay + await new Promise(resolve => setTimeout(resolve, delayInMS)); + + // Update + delayInMS *= 2; + attempt += 1; + } + + console.error(`Failed to create communication channel after ${attempt} attempts.`); + return null; + } + + createNew ( + panel: NotebookPanel, + context: DocumentRegistry.IContext + ): DisposableDelegate { + panel.sessionContext.ready + .then(async () => { + const kernel = panel.sessionContext.session?.kernel; + // If kernel is available, create channel and set up listeners. + if (!kernel) { + console.error("Kernel not ready! Can't create communication channel"); + return new DisposableDelegate(() => {}); + } + + const notebookInfo = new NotebookInfo(); + await notebookInfo.update(panel); + + if (!notebookInfo.isMatlabNotebook()) { + console.debug('Not a MATLAB notebook, skipping communication setup'); + return new DisposableDelegate(() => {}); + } + + console.log('MATLAB Communication plugin activated for ', panel.id); + + // Create a unique channel name for this notebook + const channelName = 'matlab_comm_' + panel.id; + console.log( + 'Attempting to establish communication with the kernel' + ); + + const comm = await this._createAndOpenCommWithRetry(kernel, channelName); + if (!comm) { + return new DisposableDelegate(() => {}); + } + + // Listen for messages from the kernel + comm.onMsg = (msg: KernelMessage.ICommMsgMsg) => { + const data = msg.content.data as CommunicationData; + console.debug('Recieved data from kernel: ', data); + }; + + // Handle comm close + comm.onClose = (msg) => { + console.debug(`Received data:${msg} for comm close event.`); + console.log(`Comm with ID:${comm.commId} closed.`); + }; + + this._comms.set(panel.id, comm); + }) + .catch((error) => { + console.error('Notebook panel was not ready', error); + }); + + return new DisposableDelegate(() => { + const comm = this._comms.get(panel.id); + if (comm && !comm.isDisposed) { + comm.close(); + this._comms.delete(panel.id); + } + }); + } + + getComm (notebookId: string): ICommunicationChannel { + const commChannel = this._comms.get(notebookId); + if (!commChannel) { + throw new Error( + `No communication channel found for notebook ID: ${notebookId}` + ); + } + return commChannel; + } + + deleteComms (): void { + this._comms.clear(); + } +} + +// A unique token for the comm service +export const IMatlabCommunication = new Token('@mathworks/matlab-comm:IMatlabCommunication'); + +export const matlabCommPlugin: JupyterFrontEndPlugin = + { + id: '@mathworks/matlabCommPlugin', + autoStart: true, + requires: [INotebookTracker], + provides: IMatlabCommunication, + activate: (app: JupyterFrontEnd): MatlabCommunicationExtension => { + const matlabCommExtension = new MatlabCommunicationExtension(); + app.docRegistry.addWidgetExtension('Notebook', matlabCommExtension); + + // Dispose resources created by this plugin when the page unloads. + // Need to handle this separately for the case when jupyterlab tab is closed directly + window.addEventListener('beforeunload', () => { + matlabCommExtension.deleteComms(); + }); + + return matlabCommExtension; + } + }; diff --git a/src/jupyter_matlab_labextension/src/matlab_files.ts b/src/jupyter_matlab_labextension/src/plugins/matlabFiles.ts similarity index 89% rename from src/jupyter_matlab_labextension/src/matlab_files.ts rename to src/jupyter_matlab_labextension/src/plugins/matlabFiles.ts index 17cf655d..47bdf086 100644 --- a/src/jupyter_matlab_labextension/src/matlab_files.ts +++ b/src/jupyter_matlab_labextension/src/plugins/matlabFiles.ts @@ -1,4 +1,4 @@ -// Copyright 2023-2024 The MathWorks, Inc. +// Copyright 2023-2025 The MathWorks, Inc. // Create a command to open a new .m file. // Add this command to the Launcher (under "Other"), @@ -14,17 +14,21 @@ import { ILauncher } from '@jupyterlab/launcher'; import { ReadonlyPartialJSONObject } from '@lumino/coreutils'; -import { newMFileIcon, matlabIcon } from './icons'; +import { newMFileIcon, matlabIcon } from '../icons'; const FACTORY = 'Editor'; const PALETTE_CATEGORY = 'Other'; const command = 'matlab:new-matlab-file'; -function registerMFiles (app: JupyterFrontEnd, launcher: ILauncher | null, palette: ICommandPalette | null) { +function registerMFiles ( + app: JupyterFrontEnd, + launcher: ILauncher | null, + palette: ICommandPalette | null +) { const { commands } = app; const createNewMatlabFile = async (args: ReadonlyPartialJSONObject) => { /** Get the directory in which the MATLAB file must be created; - * otherwise take the current filebrowser directory. */ + * otherwise take the current filebrowser directory. */ const cwd = args.cwd; /** Create a new untitled MATLAB file. */ diff --git a/src/jupyter_matlab_labextension/src/matlab_browser_button.ts b/src/jupyter_matlab_labextension/src/plugins/matlabToolbarButton.ts similarity index 77% rename from src/jupyter_matlab_labextension/src/matlab_browser_button.ts rename to src/jupyter_matlab_labextension/src/plugins/matlabToolbarButton.ts index 5f061755..abefac94 100644 --- a/src/jupyter_matlab_labextension/src/matlab_browser_button.ts +++ b/src/jupyter_matlab_labextension/src/plugins/matlabToolbarButton.ts @@ -1,4 +1,4 @@ -// Copyright 2023 The MathWorks, Inc. +// Copyright 2023-2025 The MathWorks, Inc. // Registers the button which allows access to MATLAB in a browser, which will // appear in the notebook toolbar. @@ -14,18 +14,25 @@ import { INotebookModel, NotebookPanel } from '@jupyterlab/notebook'; import { IDisposable } from '@lumino/disposable'; -import { matlabIcon } from './icons'; +import { matlabIcon } from '../icons'; /** Wait until the kernel has loaded, then check if it is a MATLAB kernel. */ -const insertButton = async (panel: NotebookPanel, matlabToolbarButton: ToolbarButton): Promise => { +export const insertButton = async ( + panel: NotebookPanel, + matlabToolbarButton: ToolbarButton +): Promise => { await panel.sessionContext.ready; if (panel.sessionContext.kernelDisplayName === 'MATLAB Kernel') { panel.toolbar.insertItem(10, 'matlabToolbarButton', matlabToolbarButton); } }; -class MatlabToolbarButtonExtension implements DocumentRegistry.IWidgetExtension { - createNew (panel: NotebookPanel, context: DocumentRegistry.IContext): IDisposable { +export class MatlabToolbarButtonExtension +implements DocumentRegistry.IWidgetExtension { + createNew ( + panel: NotebookPanel, + context: DocumentRegistry.IContext + ): IDisposable { /** Create the toolbar button to open MATLAB in a browser. */ const matlabToolbarButton = new ToolbarButton({ className: 'openMATLABButton', @@ -46,9 +53,7 @@ class MatlabToolbarButtonExtension implements DocumentRegistry.IWidgetExtension< export const matlabToolbarButtonPlugin: JupyterFrontEndPlugin = { id: '@mathworks/matlabToolbarButtonPlugin', autoStart: true, - activate: ( - app: JupyterFrontEnd - ) => { + activate: (app: JupyterFrontEnd) => { const matlabToolbarButton = new MatlabToolbarButtonExtension(); app.docRegistry.addWidgetExtension('Notebook', matlabToolbarButton); } diff --git a/src/jupyter_matlab_labextension/src/tests/jest-setup.ts b/src/jupyter_matlab_labextension/src/tests/jest-setup.ts new file mode 100644 index 00000000..c0b71c40 --- /dev/null +++ b/src/jupyter_matlab_labextension/src/tests/jest-setup.ts @@ -0,0 +1,17 @@ +// Copyright 2025 The MathWorks, Inc. + +// Mock global objects that might not be available in the Node.js environment + +// Mock window object if needed +// Tests run in a node environment, where 'window' is not defined. +// This mock ensures that 'window' is defined during tests. +if (typeof window === 'undefined') { + (global as any).window = { + open: jest.fn() + }; +} + +// Reset mocks before each test +beforeEach(() => { + jest.clearAllMocks(); +}); diff --git a/src/jupyter_matlab_labextension/src/tests/matlabCommunication.test.ts b/src/jupyter_matlab_labextension/src/tests/matlabCommunication.test.ts new file mode 100644 index 00000000..fa2b809e --- /dev/null +++ b/src/jupyter_matlab_labextension/src/tests/matlabCommunication.test.ts @@ -0,0 +1,145 @@ +// Copyright 2025 The MathWorks, Inc. + +// Mock dependencies from JupyterLab and other modules +import { MatlabCommunicationExtension } from '../plugins/matlabCommunication'; +import { NotebookPanel } from '@jupyterlab/notebook'; +import { DocumentRegistry } from '@jupyterlab/docregistry'; + +jest.mock('@jupyterlab/services', () => ({ + KernelMessage: { + createMessage: jest.fn() + } +})); + +jest.mock('@jupyterlab/notebook', () => ({ + NotebookPanel: jest.fn() +})); + +jest.mock('../utils/notebook', () => ({ + NotebookInfo: jest.fn().mockImplementation(() => ({ + update: jest.fn(), + isMatlabNotebook: jest.fn(() => true) + })) +})); + +// Begin testing MatlabCommunicationExtension +describe('MatlabCommunicationExtension', () => { + let panel: NotebookPanel; + let context: DocumentRegistry.IContext; + let extension: MatlabCommunicationExtension; + + beforeEach(() => { + jest.clearAllMocks(); + + const notebookInfoMock = require('../utils/notebook').NotebookInfo; + notebookInfoMock.mockImplementation(() => ({ + update: jest.fn(), + isMatlabNotebook: jest.fn(() => true) // Reset to true for default behavior + })); + + // Mock NotebookPanel and context + panel = { + id: 'notebook-1', + sessionContext: { + ready: Promise.resolve(), + session: { + kernel: { + createComm: jest.fn(() => ({ + commId: 'test-comm-id', + targetName: 'matlab', + onMsg: jest.fn(), + onClose: jest.fn(), + open: () => ({ + done: { + then: jest.fn().mockImplementation((cb) => cb()), + catch: jest.fn().mockImplementation((cb) => cb()) + } + }), + close: jest.fn() + })) + } + } + }, + disposed: { + connect: jest.fn() + } + } as unknown as NotebookPanel; + + context = {} as DocumentRegistry.IContext; + + extension = new MatlabCommunicationExtension(); + }); + + it('should create a new communication channel for MATLAB notebooks', async () => { + const disposable = extension.createNew(panel, context); + + // Wait for async operations to complete + await panel.sessionContext.ready; + + const { kernel } = panel.sessionContext.session!; + + // Wait a bit more to ensure the async then block has executed + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(kernel?.createComm).toHaveBeenCalled(); + expect(disposable.dispose).toBeDefined(); + }); + + it('should not create a communication channel for non-MATLAB notebooks', async () => { + const notebookInfoMock = require('../utils/notebook').NotebookInfo; + notebookInfoMock.mockImplementation(() => ({ + update: jest.fn(), + isMatlabNotebook: jest.fn(() => false) + })); + + const disposable = extension.createNew(panel, context); + + await panel.sessionContext.ready; + + // Ensure no channel is created + expect( + panel.sessionContext.session!.kernel!.createComm + ).not.toHaveBeenCalled(); + expect(disposable.dispose).toBeDefined(); + }); + + it('should clean up communication channels when the panel is disposed', async () => { + const disposable = extension.createNew(panel, context); + + await panel.sessionContext.ready; + await new Promise((resolve) => setTimeout(resolve, 10)); + + // Verify a comm was created + const { kernel } = panel.sessionContext.session!; + expect(kernel?.createComm).toHaveBeenCalled(); + + // Comm should exist before disposal + expect(() => extension.getComm(panel.id)).not.toThrow(); + + // Simulate disposal via DisposableDelegate + disposable.dispose(); + + // Comm should now be cleaned up + expect(() => extension.getComm(panel.id)).toThrow(); + }); + + it('should throw an error if getComm is called with an invalid notebook ID', async () => { + // First, create a communication channel for a valid notebook + extension.createNew(panel, context); + await panel.sessionContext.ready; + await new Promise((resolve) => setTimeout(resolve, 0)); + + // Verify that the valid notebook ID works (doesn't throw) + expect(() => extension.getComm(panel.id)).not.toThrow(); + + // Now test that an invalid ID throws the expected error + expect(() => extension.getComm('invalid-id')).toThrowError( + 'No communication channel found for notebook ID: invalid-id' + ); + }); + + // it('should delete all communication channels during cleanup', () => { + // extension.deleteComms(); + // expect(() => extension.getComm(panel.id)).toThrowError(); + // }); +}); diff --git a/src/jupyter_matlab_labextension/src/tests/matlabToolbarButton.test.ts b/src/jupyter_matlab_labextension/src/tests/matlabToolbarButton.test.ts new file mode 100644 index 00000000..97fff9c9 --- /dev/null +++ b/src/jupyter_matlab_labextension/src/tests/matlabToolbarButton.test.ts @@ -0,0 +1,286 @@ +// Copyright 2025 The MathWorks, Inc. + +// Mock the icons module +import { + insertButton, + MatlabToolbarButtonExtension, + matlabToolbarButtonPlugin +} from '../plugins/matlabToolbarButton'; +import { NotebookPanel, INotebookModel } from '@jupyterlab/notebook'; +import { JupyterFrontEnd } from '@jupyterlab/application'; +import { DocumentRegistry } from '@jupyterlab/docregistry'; + +jest.mock('../icons', () => ({ + matlabIcon: { + name: 'matlab-icon', + svgstr: '' + } +})); + +// Get the mocked matlabIcon +const { matlabIcon } = jest.requireMock('../icons'); + +// Mock JupyterLab dependencies +jest.mock('@jupyterlab/apputils', () => ({ + ToolbarButton: jest.fn().mockImplementation((options: any) => ({ + ...options, + dispose: jest.fn() + })) +})); + +jest.mock('@jupyterlab/coreutils', () => ({ + PageConfig: { + getBaseUrl: jest.fn().mockReturnValue('http://localhost:8888/') + } +})); + +// Mock window.open +const originalWindowOpen = window.open; +window.open = jest.fn(); + +// Mock for NotebookPanel +const createMockNotebookPanel = (kernelDisplayName = 'MATLAB Kernel') => ({ + sessionContext: { + ready: Promise.resolve(), + kernelDisplayName, + session: null, + initialize: jest.fn(), + isReady: true, + isTerminating: false, + // Add other required methods as + dispose: jest.fn() + }, + toolbar: { + insertItem: jest.fn(), + names: [] + // addItem: jest.fn(), + // insertAfter: jest.fn(), + // insertBefore: jest.fn() + } +}); + +// Mock for ToolbarButton +const createMockToolbarButton = () => ({ + className: 'openMATLABButton', + icon: matlabIcon, + label: 'Open MATLAB', + tooltip: 'Open MATLAB', + onClick: expect.any(Function), + dispose: jest.fn() +}); + +// Mock for JupyterFrontEnd +const createMockJupyterFrontEnd = () => ({ + docRegistry: { + addWidgetExtension: jest.fn(), + // Add other required properties with mock implementations + changed: { connect: jest.fn() }, + isDisposed: false, + dispose: jest.fn(), + addWidgetFactory: jest.fn() + } +}); + +describe('matlab_browser_button', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + window.open = originalWindowOpen; + }); + + describe('insertButton', () => { + test('should insert button when kernel is MATLAB Kernel', async () => { + // Arrange + const panel = createMockNotebookPanel('MATLAB Kernel'); + const button = createMockToolbarButton(); + + // Act + await insertButton(panel as unknown as NotebookPanel, button as any); + + // Assert + expect(panel.toolbar!.insertItem).toHaveBeenCalledWith( + 10, + 'matlabToolbarButton', + button + ); + }); + + test('should not insert button when kernel is not MATLAB Kernel', async () => { + // Arrange + const panel = createMockNotebookPanel('Python 3'); + const button = createMockToolbarButton(); + + // Act + await insertButton(panel as unknown as NotebookPanel, button as any); + + // Assert + expect(panel.toolbar!.insertItem).not.toHaveBeenCalled(); + }); + + test('should wait for session context to be ready before checking kernel', async () => { + // Arrange + const readyPromise = new Promise((resolve) => + setTimeout(resolve, 10) + ); + const panel = { + sessionContext: { + ready: readyPromise, + kernelDisplayName: 'MATLAB Kernel' + }, + toolbar: { + insertItem: jest.fn() + } + }; + const button = createMockToolbarButton(); + + // Act + const insertPromise = insertButton(panel as any, button as any); + + // Assert - insertItem should not be called before ready resolves + expect(panel.toolbar.insertItem).not.toHaveBeenCalled(); + + // Wait for ready promise to resolve + await insertPromise; + + // Now insertItem should have been called + expect(panel.toolbar.insertItem).toHaveBeenCalledWith( + 10, + 'matlabToolbarButton', + button + ); + }); + }); + + describe('MatlabToolbarButtonExtension', () => { + let extension: MatlabToolbarButtonExtension; + let panel: any; + let context: any; + + beforeEach(() => { + extension = new MatlabToolbarButtonExtension(); + panel = createMockNotebookPanel(); + context = {}; + }); + + test('should create a toolbar button with correct properties', () => { + // Act + const result = extension.createNew(panel, context); + + // Assert + expect(result).toEqual( + expect.objectContaining({ + className: 'openMATLABButton', + icon: matlabIcon, + label: 'Open MATLAB', + tooltip: 'Open MATLAB' + }) + ); + }); + + test('should return a disposable object', () => { + // Act + const result = extension.createNew(panel, context); + + // Assert + expect(result.dispose).toBeDefined(); + expect(typeof result.dispose).toBe('function'); + }); + + test('button onClick should open MATLAB in a new tab', () => { + // Arrange + const ToolbarButtonMock = jest.requireMock( + '@jupyterlab/apputils' + ).ToolbarButton; + let capturedOnClick: () => void = () => {}; // Initialize with empty function + + // Capture the onClick handler when ToolbarButton is constructed + ToolbarButtonMock.mockImplementationOnce((options: any) => { + capturedOnClick = options.onClick; + return { + ...options, + dispose: jest.fn() + }; + }); + + // Act + extension.createNew( + panel as unknown as NotebookPanel, + context as unknown as DocumentRegistry.IContext + ); + // Manually call the onClick handler + capturedOnClick(); + + // Assert + expect(window.open).toHaveBeenCalledWith( + 'http://localhost:8888/matlab', + '_blank' + ); + }); + + test('should call insertButton with panel and button', () => { + // Arrange + // Import the module using ES modules syntax for TypeScript compatibility + const matlabButtonModule = require('../plugins/matlabToolbarButton'); + const spy = jest + .spyOn(matlabButtonModule, 'insertButton') + .mockImplementation(() => Promise.resolve()); + + // Act + const button = extension.createNew( + panel as unknown as NotebookPanel, + context as unknown as DocumentRegistry.IContext + ); + + // Assert + expect(spy).toHaveBeenCalledWith(panel, button); + + // Cleanup + spy.mockRestore(); + }); + }); + + describe('matlabToolbarButtonPlugin', () => { + test('should have correct id and autoStart properties', () => { + // Assert + expect(matlabToolbarButtonPlugin.id).toBe( + '@mathworks/matlabToolbarButtonPlugin' + ); + expect(matlabToolbarButtonPlugin.autoStart).toBe(true); + }); + + test('should register extension with docRegistry on activation', () => { + // Arrange + const app = createMockJupyterFrontEnd(); + + // Act + matlabToolbarButtonPlugin.activate(app as unknown as JupyterFrontEnd); + + // Assert + expect(app.docRegistry!.addWidgetExtension).toHaveBeenCalledWith( + 'Notebook', + expect.any(MatlabToolbarButtonExtension) + ); + }); + + test('should create a MatlabToolbarButtonExtension instance on activation', () => { + // Arrange + const app = createMockJupyterFrontEnd(); + + // Act + matlabToolbarButtonPlugin.activate(app as unknown as JupyterFrontEnd); + + // Assert - Check if addWidgetExtension was called with an instance of MatlabToolbarButtonExtension + expect(app.docRegistry!.addWidgetExtension).toHaveBeenCalledWith( + 'Notebook', + expect.any(MatlabToolbarButtonExtension) + ); + + // Additional check - verify the argument is an instance of MatlabToolbarButtonExtension + const extensionArg = (app.docRegistry!.addWidgetExtension as jest.Mock) + .mock.calls[0][1]; + expect(extensionArg).toBeInstanceOf(MatlabToolbarButtonExtension); + }); + }); +}); diff --git a/src/jupyter_matlab_labextension/src/tests/mocks/ui-components.js b/src/jupyter_matlab_labextension/src/tests/mocks/ui-components.js new file mode 100644 index 00000000..6ebde50c --- /dev/null +++ b/src/jupyter_matlab_labextension/src/tests/mocks/ui-components.js @@ -0,0 +1,15 @@ +// Copyright 2025 The MathWorks, Inc. + +// Mock for @jupyterlab/ui-components +module.exports = { + LabIcon: class LabIcon { + constructor(name, options) { + this.name = name; + this.svgstr = options?.svgstr || ""; + } + + static resolve(icon) { + return icon; + } + }, +}; diff --git a/src/jupyter_matlab_labextension/src/utils/notebook.ts b/src/jupyter_matlab_labextension/src/utils/notebook.ts new file mode 100644 index 00000000..1eecd78f --- /dev/null +++ b/src/jupyter_matlab_labextension/src/utils/notebook.ts @@ -0,0 +1,132 @@ +// Copyright 2025 The MathWorks, Inc. + +import path from 'path'; +import { NotebookPanel } from '@jupyterlab/notebook'; +import { PageConfig } from '@jupyterlab/coreutils'; + +/** + * Tracks metadata and kernel state for the currently active Jupyter notebook panel. + * Provides helpers to determine whether the notebook is a MATLAB notebook, whether + * its kernel is busy, resolve the notebook's file path, and control the kernel. + */ +export class NotebookInfo { + private _notebookName: string | undefined = undefined; + private _isMatlabNotebook: boolean = false; + private _isBusy: boolean = false; + private _panel: NotebookPanel | null = null; + + /* + * Whether the current notebook’s kernelspec indicates MATLAB. + * + * @returns True if the current notebook is a MATLAB notebook; otherwise false. + */ + isMatlabNotebook (): boolean { + return this._isMatlabNotebook; + } + + /* + * Whether the kernel is busy, but only for MATLAB notebooks. + * If the current notebook is not MATLAB, returns false. + * + * @returns True if the notebook is MATLAB and the kernel is busy; otherwise false. + */ + isBusy (): boolean { + return this._isMatlabNotebook ? this._isBusy : false; + } + + /* + * Absolute path to the current notebook on the filesystem. + * + * Combines the Jupyter server root with the notebook's relative path. + * + * @returns The absolute file path if available; otherwise undefined. + */ + getCurrentFilePath (): string | undefined { + if (this._notebookName) { + return path.join(PageConfig.getOption('serverRoot'), this._notebookName); + } else { + return undefined; + } + } + + /* + * Waits until the associated kernel reaches the 'idle' status. + * + * @throws Error if no notebook panel has been set via update(). + * @returns A promise that resolves when the kernel status becomes 'idle'. + */ + async waitForIdleStatus (): Promise { + if (!this._panel) { + throw Error('No notebook panel provided'); + } else { + return new Promise((resolve) => { + if (this._panel!.sessionContext.session?.kernel?.status === 'idle') { + resolve(); + } else { + const onStatusChanged = (connection: any, status: string) => { + if (status === 'idle') { + // Disconnect listener from statusChanged signal so that it doesn't get called again. + connection.statusChanged.disconnect(onStatusChanged); + resolve(); + } + }; + this._panel!.sessionContext.session?.kernel?.statusChanged.connect( + onStatusChanged + ); + } + }); + } + } + + /* + * Updates the tracked notebook panel and refreshes its derived state: + * - whether it is a MATLAB notebook (via kernelspec metadata), + * - whether the kernel is currently busy, + * - the notebook’s path. + * + * If panel is null, clears all tracked state. + * + * Note: Waits for the session context to be ready before reading kernel status. + * + * @param panel The active NotebookPanel to track, or null to reset. + * @returns A promise that resolves when the state has been updated. + */ + async update (panel: NotebookPanel | null): Promise { + if (panel) { + // Wait for session context to be ready + if (!panel.sessionContext.isReady) { + await panel.sessionContext.ready; + } + this._panel = panel; + this._isMatlabNotebook = panel.sessionContext.kernelDisplayName === 'MATLAB Kernel'; + const context = panel.context; + this._isBusy = panel.sessionContext.session?.kernel?.status === 'busy'; + this._notebookName = context.path; + } else { + this._notebookName = undefined; + this._isMatlabNotebook = false; + this._isBusy = false; + this._panel = null; + } + } + + /* + * Sends an interrupt to the associated kernel, if available. + * No-op if there is no tracked panel/session/kernel. + */ + interrupt (): void { + if (this._panel) { + this._panel.sessionContext.session?.kernel?.interrupt(); + console.log('Kernel interupted'); + } + } + + /* + * Returns the current notebook’s path relative to the server root. + * + * @returns The relative path (e.g., 'folder/notebook.ipynb') or undefined if none is set. + */ + getCurrentFilename (): string | undefined { + return this._notebookName; + } +} diff --git a/src/jupyter_matlab_labextension/style/index.css b/src/jupyter_matlab_labextension/style/index.css index 8a7ea29e..01d5beb9 100644 --- a/src/jupyter_matlab_labextension/style/index.css +++ b/src/jupyter_matlab_labextension/style/index.css @@ -1 +1,2 @@ -@import url('base.css'); +/* Copyright 2025 The MathWorks, Inc. */ +@import url("base.css"); diff --git a/src/jupyter_matlab_labextension/style/index.js b/src/jupyter_matlab_labextension/style/index.js index a028a764..e216e587 100644 --- a/src/jupyter_matlab_labextension/style/index.js +++ b/src/jupyter_matlab_labextension/style/index.js @@ -1 +1,2 @@ -import './base.css'; +// Copyright 2025 The MathWorks, Inc. +import "./base.css"; diff --git a/src/jupyter_matlab_labextension/tsconfig.json b/src/jupyter_matlab_labextension/tsconfig.json index fbfef6df..45d01695 100644 --- a/src/jupyter_matlab_labextension/tsconfig.json +++ b/src/jupyter_matlab_labextension/tsconfig.json @@ -19,7 +19,7 @@ "strict": true, "strictNullChecks": true, "target": "es2018", - "types": ["jest"], + "types": ["jest", "node"], "lib": ["DOM", "ES2018", "ES2020.Intl"] }, "include": ["src"] diff --git a/src/jupyter_matlab_labextension/yarn.lock b/src/jupyter_matlab_labextension/yarn.lock index 16a5f9e3..9c8dd694 100644 --- a/src/jupyter_matlab_labextension/yarn.lock +++ b/src/jupyter_matlab_labextension/yarn.lock @@ -1081,6 +1081,20 @@ __metadata: languageName: node linkType: hard +"@jupyter/ydoc@npm:^3.0.4": + version: 3.0.5 + resolution: "@jupyter/ydoc@npm:3.0.5" + dependencies: + "@jupyterlab/nbformat": ^3.0.0 || ^4.0.0-alpha.21 || ^4.0.0 + "@lumino/coreutils": ^1.11.0 || ^2.0.0 + "@lumino/disposable": ^1.10.0 || ^2.0.0 + "@lumino/signaling": ^1.10.0 || ^2.0.0 + y-protocols: ^1.0.5 + yjs: ^13.5.40 + checksum: a4f8074790e34b649e581e093806ec84ccfdcd676735d35efdba74e93114c5ff3d40e5909322ce7fc7acd0faf379ecfb8979ab88af1db9705d74b0eff4e1c75c + languageName: node + linkType: hard + "@jupyterlab/application@npm:^4.0.0": version: 4.3.4 resolution: "@jupyterlab/application@npm:4.3.4" @@ -1138,6 +1152,35 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/apputils@npm:^4.5.3": + version: 4.5.3 + resolution: "@jupyterlab/apputils@npm:4.5.3" + dependencies: + "@jupyterlab/coreutils": ^6.4.3 + "@jupyterlab/observables": ^5.4.3 + "@jupyterlab/rendermime-interfaces": ^3.12.3 + "@jupyterlab/services": ^7.4.3 + "@jupyterlab/settingregistry": ^4.4.3 + "@jupyterlab/statedb": ^4.4.3 + "@jupyterlab/statusbar": ^4.4.3 + "@jupyterlab/translation": ^4.4.3 + "@jupyterlab/ui-components": ^4.4.3 + "@lumino/algorithm": ^2.0.3 + "@lumino/commands": ^2.3.2 + "@lumino/coreutils": ^2.2.1 + "@lumino/disposable": ^2.1.4 + "@lumino/domutils": ^2.0.3 + "@lumino/messaging": ^2.0.3 + "@lumino/signaling": ^2.1.4 + "@lumino/virtualdom": ^2.0.3 + "@lumino/widgets": ^2.7.1 + "@types/react": ^18.0.26 + react: ^18.2.0 + sanitize-html: ~2.12.1 + checksum: 02a642db18a7efb2cff01e52d5ca52fb36fcecbed678ba9379455ff00ac6e6437d03439991f9312e0d1cf89eec3018e72065eb615d747642820c7dfa5af6a33a + languageName: node + linkType: hard + "@jupyterlab/attachments@npm:^4.3.4": version: 4.3.4 resolution: "@jupyterlab/attachments@npm:4.3.4" @@ -1309,6 +1352,20 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/coreutils@npm:^6.4.3": + version: 6.4.3 + resolution: "@jupyterlab/coreutils@npm:6.4.3" + dependencies: + "@lumino/coreutils": ^2.2.1 + "@lumino/disposable": ^2.1.4 + "@lumino/signaling": ^2.1.4 + minimist: ~1.2.0 + path-browserify: ^1.0.0 + url-parse: ~1.5.4 + checksum: f9ffd692d4e30f4813e4985972ff4323752dd30497877b58a8a791f71eda8655efe2297c38cb83b6b0ed057707c8bf50633d7bdd9c2d388a400d986458307cf4 + languageName: node + linkType: hard + "@jupyterlab/docmanager@npm:^4.3.4": version: 4.3.4 resolution: "@jupyterlab/docmanager@npm:4.3.4" @@ -1448,6 +1505,21 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/mainmenu@npm:^4.3.4": + version: 4.4.3 + resolution: "@jupyterlab/mainmenu@npm:4.4.3" + dependencies: + "@jupyterlab/apputils": ^4.5.3 + "@jupyterlab/translation": ^4.4.3 + "@jupyterlab/ui-components": ^4.4.3 + "@lumino/algorithm": ^2.0.3 + "@lumino/commands": ^2.3.2 + "@lumino/coreutils": ^2.2.1 + "@lumino/widgets": ^2.7.1 + checksum: 99c3efe7556c4a704813ad4d5ebef61caada4e1384214730b45b3666e6cc6c21e894be485582a25b2d4c39ed35f07274b8b848616e1dd2bc2c2ae216a3895e44 + languageName: node + linkType: hard + "@jupyterlab/nbformat@npm:^3.0.0 || ^4.0.0-alpha.21 || ^4.0.0, @jupyterlab/nbformat@npm:^4.3.4": version: 4.3.4 resolution: "@jupyterlab/nbformat@npm:4.3.4" @@ -1457,6 +1529,15 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/nbformat@npm:^4.4.3": + version: 4.4.3 + resolution: "@jupyterlab/nbformat@npm:4.4.3" + dependencies: + "@lumino/coreutils": ^2.2.1 + checksum: 2e743fcf41fa7e0bbbe06fb417467b32b3679544f5b6ebf33623ce92e04e0d545c879e5eead6b201a83e2d8aa503df9c95050be67244cc4a6c65355120f9b0fe + languageName: node + linkType: hard + "@jupyterlab/notebook@npm:^4.0.0": version: 4.3.4 resolution: "@jupyterlab/notebook@npm:4.3.4" @@ -1508,6 +1589,19 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/observables@npm:^5.4.3": + version: 5.4.3 + resolution: "@jupyterlab/observables@npm:5.4.3" + dependencies: + "@lumino/algorithm": ^2.0.3 + "@lumino/coreutils": ^2.2.1 + "@lumino/disposable": ^2.1.4 + "@lumino/messaging": ^2.0.3 + "@lumino/signaling": ^2.1.4 + checksum: 97189f20cc95e3eaed94c4a2915e778be5282f7dbf04d99d1e0a348ee57601800b80244c5d7502f3c8d61ea496aeaa9d24c081deec8a6bd4c7f247d6a3c43fdc + languageName: node + linkType: hard + "@jupyterlab/outputarea@npm:^4.3.4": version: 4.3.4 resolution: "@jupyterlab/outputarea@npm:4.3.4" @@ -1540,6 +1634,16 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/rendermime-interfaces@npm:^3.12.3": + version: 3.12.3 + resolution: "@jupyterlab/rendermime-interfaces@npm:3.12.3" + dependencies: + "@lumino/coreutils": ^1.11.0 || ^2.2.1 + "@lumino/widgets": ^1.37.2 || ^2.7.1 + checksum: 182560f1ba710a996d886a64c9b035a4a9da3bf65460ab451d0b4b0c1d46cba1339b682446d67f408bce32ae631a7090fba041777d4c93621249300522b9172d + languageName: node + linkType: hard + "@jupyterlab/rendermime@npm:^4.3.4": version: 4.3.4 resolution: "@jupyterlab/rendermime@npm:4.3.4" @@ -1579,6 +1683,25 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/services@npm:^7.4.3": + version: 7.4.3 + resolution: "@jupyterlab/services@npm:7.4.3" + dependencies: + "@jupyter/ydoc": ^3.0.4 + "@jupyterlab/coreutils": ^6.4.3 + "@jupyterlab/nbformat": ^4.4.3 + "@jupyterlab/settingregistry": ^4.4.3 + "@jupyterlab/statedb": ^4.4.3 + "@lumino/coreutils": ^2.2.1 + "@lumino/disposable": ^2.1.4 + "@lumino/polling": ^2.1.4 + "@lumino/properties": ^2.0.3 + "@lumino/signaling": ^2.1.4 + ws: ^8.11.0 + checksum: 83c3e903c54e665e73d5beec7bf0de8785a124974eefce13b146fcd9afa5f353e5a379097d7913d87f92800097f95e104ce224061bd113776092d31bb07bd8b4 + languageName: node + linkType: hard + "@jupyterlab/settingregistry@npm:^4.3.4": version: 4.3.4 resolution: "@jupyterlab/settingregistry@npm:4.3.4" @@ -1598,6 +1721,25 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/settingregistry@npm:^4.4.3": + version: 4.4.3 + resolution: "@jupyterlab/settingregistry@npm:4.4.3" + dependencies: + "@jupyterlab/nbformat": ^4.4.3 + "@jupyterlab/statedb": ^4.4.3 + "@lumino/commands": ^2.3.2 + "@lumino/coreutils": ^2.2.1 + "@lumino/disposable": ^2.1.4 + "@lumino/signaling": ^2.1.4 + "@rjsf/utils": ^5.13.4 + ajv: ^8.12.0 + json5: ^2.2.3 + peerDependencies: + react: ">=16" + checksum: d9a6a4d130d7e7190633d08bb9d28c8273aba967b350f6d55c6281f36cda8a3cd71942daee34b32f47b94b2a1cd215b3c434b9243af54c494be1b86c2eea39e9 + languageName: node + linkType: hard + "@jupyterlab/statedb@npm:^4.3.4": version: 4.3.4 resolution: "@jupyterlab/statedb@npm:4.3.4" @@ -1611,6 +1753,19 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/statedb@npm:^4.4.3": + version: 4.4.3 + resolution: "@jupyterlab/statedb@npm:4.4.3" + dependencies: + "@lumino/commands": ^2.3.2 + "@lumino/coreutils": ^2.2.1 + "@lumino/disposable": ^2.1.4 + "@lumino/properties": ^2.0.3 + "@lumino/signaling": ^2.1.4 + checksum: ae50806cac848c752f4ecda6906d6566c626f28739a276438445a87a3f8a206ce920623486c499f433ae57d818a98e815b89bda34479c9d4bbfef86c784ad8b0 + languageName: node + linkType: hard + "@jupyterlab/statusbar@npm:^4.3.4": version: 4.3.4 resolution: "@jupyterlab/statusbar@npm:4.3.4" @@ -1627,6 +1782,22 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/statusbar@npm:^4.4.3": + version: 4.4.3 + resolution: "@jupyterlab/statusbar@npm:4.4.3" + dependencies: + "@jupyterlab/ui-components": ^4.4.3 + "@lumino/algorithm": ^2.0.3 + "@lumino/coreutils": ^2.2.1 + "@lumino/disposable": ^2.1.4 + "@lumino/messaging": ^2.0.3 + "@lumino/signaling": ^2.1.4 + "@lumino/widgets": ^2.7.1 + react: ^18.2.0 + checksum: 9d9bf4de8222e93997a65969fe110ec7687b3948da01a788cc2685015d0fc56f7aaf8c5665e98c790426594db79a175f276984fa68ecfb0136e5fa65c9080bdd + languageName: node + linkType: hard + "@jupyterlab/toc@npm:^6.3.4": version: 6.3.4 resolution: "@jupyterlab/toc@npm:6.3.4" @@ -1663,6 +1834,19 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/translation@npm:^4.4.3": + version: 4.4.3 + resolution: "@jupyterlab/translation@npm:4.4.3" + dependencies: + "@jupyterlab/coreutils": ^6.4.3 + "@jupyterlab/rendermime-interfaces": ^3.12.3 + "@jupyterlab/services": ^7.4.3 + "@jupyterlab/statedb": ^4.4.3 + "@lumino/coreutils": ^2.2.1 + checksum: 252eba345ca627c39e8253d4776677e1bb83e629cad09cd702b0cccd5c132f386c1bf68c9bfb976934960c355104c03f4ca94ad4f4b59c7123eb2566102e7c28 + languageName: node + linkType: hard + "@jupyterlab/ui-components@npm:^4.0.0, @jupyterlab/ui-components@npm:^4.3.4": version: 4.3.4 resolution: "@jupyterlab/ui-components@npm:4.3.4" @@ -1694,6 +1878,37 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/ui-components@npm:^4.4.3": + version: 4.4.3 + resolution: "@jupyterlab/ui-components@npm:4.4.3" + dependencies: + "@jupyter/react-components": ^0.16.6 + "@jupyter/web-components": ^0.16.6 + "@jupyterlab/coreutils": ^6.4.3 + "@jupyterlab/observables": ^5.4.3 + "@jupyterlab/rendermime-interfaces": ^3.12.3 + "@jupyterlab/translation": ^4.4.3 + "@lumino/algorithm": ^2.0.3 + "@lumino/commands": ^2.3.2 + "@lumino/coreutils": ^2.2.1 + "@lumino/disposable": ^2.1.4 + "@lumino/messaging": ^2.0.3 + "@lumino/polling": ^2.1.4 + "@lumino/properties": ^2.0.3 + "@lumino/signaling": ^2.1.4 + "@lumino/virtualdom": ^2.0.3 + "@lumino/widgets": ^2.7.1 + "@rjsf/core": ^5.13.4 + "@rjsf/utils": ^5.13.4 + react: ^18.2.0 + react-dom: ^18.2.0 + typestyle: ^2.0.4 + peerDependencies: + react: ^18.2.0 + checksum: a7a1ef9e5a48754d4545fdbe1908c361604cabb5665541f3a1a42d7fcf0ec0c6bc78e9c3c83999fb356f3542a01666ba9e036dcef2819ae435f47e83628ae3ca + languageName: node + linkType: hard + "@lezer/common@npm:^1.0.0, @lezer/common@npm:^1.0.2, @lezer/common@npm:^1.1.0, @lezer/common@npm:^1.2.0, @lezer/common@npm:^1.2.1": version: 1.2.3 resolution: "@lezer/common@npm:1.2.3" @@ -1858,6 +2073,13 @@ __metadata: languageName: node linkType: hard +"@lumino/algorithm@npm:^2.0.3": + version: 2.0.3 + resolution: "@lumino/algorithm@npm:2.0.3" + checksum: 03932cdc39d612a00579ee40bafb0b1d8bf5f8a12449f777a1ae7201843ddefb557bc3f9260aa6b9441d87bfc43e53cced854e71c4737de59e32cd00d4ac1394 + languageName: node + linkType: hard + "@lumino/application@npm:^2.4.1": version: 2.4.1 resolution: "@lumino/application@npm:2.4.1" @@ -1878,6 +2100,15 @@ __metadata: languageName: node linkType: hard +"@lumino/collections@npm:^2.0.3": + version: 2.0.3 + resolution: "@lumino/collections@npm:2.0.3" + dependencies: + "@lumino/algorithm": ^2.0.3 + checksum: 1c7aca239731e6c7379ce593318fd3f646b38c1903e81e884e36ed01f61017498f6699ba58848c43191f4825a9968b7f9c94e9355f1614c9baee84ce9ea6221f + languageName: node + linkType: hard + "@lumino/commands@npm:^2.3.1": version: 2.3.1 resolution: "@lumino/commands@npm:2.3.1" @@ -1893,6 +2124,21 @@ __metadata: languageName: node linkType: hard +"@lumino/commands@npm:^2.3.2": + version: 2.3.2 + resolution: "@lumino/commands@npm:2.3.2" + dependencies: + "@lumino/algorithm": ^2.0.3 + "@lumino/coreutils": ^2.2.1 + "@lumino/disposable": ^2.1.4 + "@lumino/domutils": ^2.0.3 + "@lumino/keyboard": ^2.0.3 + "@lumino/signaling": ^2.1.4 + "@lumino/virtualdom": ^2.0.3 + checksum: 090454bcc07aeb71f0791d6ca86ca4857b16bb6286a47ab6e59c3046e7f99cd3ef27c36d2dd35de7cf2bdeeaf5fc00ae8f29246a39e276eac2d186ae3cd7023e + languageName: node + linkType: hard + "@lumino/coreutils@npm:^1.11.0 || ^2.0.0, @lumino/coreutils@npm:^1.11.0 || ^2.2.0, @lumino/coreutils@npm:^2.0.0, @lumino/coreutils@npm:^2.2.0": version: 2.2.0 resolution: "@lumino/coreutils@npm:2.2.0" @@ -1902,6 +2148,15 @@ __metadata: languageName: node linkType: hard +"@lumino/coreutils@npm:^1.11.0 || ^2.2.1, @lumino/coreutils@npm:^2.2.1": + version: 2.2.1 + resolution: "@lumino/coreutils@npm:2.2.1" + dependencies: + "@lumino/algorithm": ^2.0.3 + checksum: d08570d1ebcf6bca973ba3af0836fb19a5a7a5b24979e90aab0fb4acb245e9619a0db356a78d67f618ae565435bb2aaf7c158c5bc0ae1ef9e9f1638ebfa05484 + languageName: node + linkType: hard + "@lumino/disposable@npm:^1.10.0 || ^2.0.0, @lumino/disposable@npm:^2.0.0, @lumino/disposable@npm:^2.1.3": version: 2.1.3 resolution: "@lumino/disposable@npm:2.1.3" @@ -1911,6 +2166,15 @@ __metadata: languageName: node linkType: hard +"@lumino/disposable@npm:^2.1.4": + version: 2.1.4 + resolution: "@lumino/disposable@npm:2.1.4" + dependencies: + "@lumino/signaling": ^2.1.4 + checksum: 0274c1cd81683f0d37c79795ed683fe49929452e6f075b9027b62dee376b5c6aa5f27b279236c4e1621bcbdcb844d5be0bbde3a065ab39159deb995244d1d2a7 + languageName: node + linkType: hard + "@lumino/domutils@npm:^2.0.2": version: 2.0.2 resolution: "@lumino/domutils@npm:2.0.2" @@ -1918,6 +2182,13 @@ __metadata: languageName: node linkType: hard +"@lumino/domutils@npm:^2.0.3": + version: 2.0.3 + resolution: "@lumino/domutils@npm:2.0.3" + checksum: 46cbcbd38f6abb53eab1b6de0a2ea8a9fa5e28b0f5aa4b058c35f2380cb8ec881fe7616c7468ba200b785f95357ac8cbac6b64512f9945f5973d1d425864b163 + languageName: node + linkType: hard + "@lumino/dragdrop@npm:^2.1.5": version: 2.1.5 resolution: "@lumino/dragdrop@npm:2.1.5" @@ -1928,6 +2199,16 @@ __metadata: languageName: node linkType: hard +"@lumino/dragdrop@npm:^2.1.6": + version: 2.1.6 + resolution: "@lumino/dragdrop@npm:2.1.6" + dependencies: + "@lumino/coreutils": ^2.2.1 + "@lumino/disposable": ^2.1.4 + checksum: 5a746ee0644e2fa02cba47d6ef45f3fb09ebc3391ac0f478f6f3073864a9637e13fcee666038c751ab8f17bc69c55299c85a88f526ea645cc3240a367490c8ca + languageName: node + linkType: hard + "@lumino/keyboard@npm:^2.0.2": version: 2.0.2 resolution: "@lumino/keyboard@npm:2.0.2" @@ -1935,6 +2216,13 @@ __metadata: languageName: node linkType: hard +"@lumino/keyboard@npm:^2.0.3": + version: 2.0.3 + resolution: "@lumino/keyboard@npm:2.0.3" + checksum: ca648cf978ddcf15fe3af2b8c8beb8aff153dfe616099df5a8bc7f43124420f77c358dbd33a988911b82f68debe07268d630c1777618b182ef7b520962d653e7 + languageName: node + linkType: hard + "@lumino/messaging@npm:^2.0.2": version: 2.0.2 resolution: "@lumino/messaging@npm:2.0.2" @@ -1945,6 +2233,16 @@ __metadata: languageName: node linkType: hard +"@lumino/messaging@npm:^2.0.3": + version: 2.0.3 + resolution: "@lumino/messaging@npm:2.0.3" + dependencies: + "@lumino/algorithm": ^2.0.3 + "@lumino/collections": ^2.0.3 + checksum: 9c2bea2a31d3922a29276df751b651e6bd41d1ed3a5f61ba94d3e90d454c53f07fc4dac7d435867fb8480415222a3d45d74188dd73e9c89c43110ebbee0ff301 + languageName: node + linkType: hard + "@lumino/polling@npm:^2.1.3": version: 2.1.3 resolution: "@lumino/polling@npm:2.1.3" @@ -1956,6 +2254,17 @@ __metadata: languageName: node linkType: hard +"@lumino/polling@npm:^2.1.4": + version: 2.1.4 + resolution: "@lumino/polling@npm:2.1.4" + dependencies: + "@lumino/coreutils": ^2.2.1 + "@lumino/disposable": ^2.1.4 + "@lumino/signaling": ^2.1.4 + checksum: e08d07d11eb030fed83bea232dba91af4ea40ef8f6ec7b8fe61722ebbd29faba10c67d269596c19c515c920f607c73bb64cdc9319af9ecef4619cddfd92ea764 + languageName: node + linkType: hard + "@lumino/properties@npm:^2.0.2": version: 2.0.2 resolution: "@lumino/properties@npm:2.0.2" @@ -1963,6 +2272,13 @@ __metadata: languageName: node linkType: hard +"@lumino/properties@npm:^2.0.3": + version: 2.0.3 + resolution: "@lumino/properties@npm:2.0.3" + checksum: a575d821f994090907abb567d3af21a828f528ae5f329ada92719eba9818bbb2b0955e675b91bd392043a5d835c345d7b500994a77157c5ea317f36442ce570e + languageName: node + linkType: hard + "@lumino/signaling@npm:^1.10.0 || ^2.0.0, @lumino/signaling@npm:^2.1.3": version: 2.1.3 resolution: "@lumino/signaling@npm:2.1.3" @@ -1973,6 +2289,16 @@ __metadata: languageName: node linkType: hard +"@lumino/signaling@npm:^2.1.4": + version: 2.1.4 + resolution: "@lumino/signaling@npm:2.1.4" + dependencies: + "@lumino/algorithm": ^2.0.3 + "@lumino/coreutils": ^2.2.1 + checksum: 554a5135c8742ed3f61a4923b1f26cb29b55447ca5939df70033449cfb654a37048d7a3e2fd0932497099cd24501a3819b85cd1fdf4e76023ba0af747c171d53 + languageName: node + linkType: hard + "@lumino/virtualdom@npm:^2.0.2": version: 2.0.2 resolution: "@lumino/virtualdom@npm:2.0.2" @@ -1982,6 +2308,15 @@ __metadata: languageName: node linkType: hard +"@lumino/virtualdom@npm:^2.0.3": + version: 2.0.3 + resolution: "@lumino/virtualdom@npm:2.0.3" + dependencies: + "@lumino/algorithm": ^2.0.3 + checksum: 66c18494fdfc1b87e76286140cd256b3616aede262641912646a18395226e200048ddeaa6d1644dff3f597b1cde8e583968cb973d64a9e9d4f45e2b24c1e2c7c + languageName: node + linkType: hard + "@lumino/widgets@npm:^1.37.2 || ^2.5.0, @lumino/widgets@npm:^2.5.0": version: 2.5.0 resolution: "@lumino/widgets@npm:2.5.0" @@ -2001,6 +2336,25 @@ __metadata: languageName: node linkType: hard +"@lumino/widgets@npm:^1.37.2 || ^2.7.1, @lumino/widgets@npm:^2.7.1": + version: 2.7.1 + resolution: "@lumino/widgets@npm:2.7.1" + dependencies: + "@lumino/algorithm": ^2.0.3 + "@lumino/commands": ^2.3.2 + "@lumino/coreutils": ^2.2.1 + "@lumino/disposable": ^2.1.4 + "@lumino/domutils": ^2.0.3 + "@lumino/dragdrop": ^2.1.6 + "@lumino/keyboard": ^2.0.3 + "@lumino/messaging": ^2.0.3 + "@lumino/properties": ^2.0.3 + "@lumino/signaling": ^2.1.4 + "@lumino/virtualdom": ^2.0.3 + checksum: c57f7e6cfbaddbd830e14db55242dcbdf531524cdf8641214ce737f43a6684004219eb58a572838f99f78af433bb8f9f19fd2ac6f0ffab4a635bd20164b75cec + languageName: node + linkType: hard + "@marijn/find-cluster-break@npm:^1.0.0": version: 1.0.2 resolution: "@marijn/find-cluster-break@npm:1.0.2" @@ -2332,6 +2686,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^24.0.4": + version: 24.0.4 + resolution: "@types/node@npm:24.0.4" + dependencies: + undici-types: ~7.8.0 + checksum: db036e3f1fea5a92fd6cd76064cf07d517b245ace73d6f46bcd8e35f38ff856f86f3311aabc7c7406de2bdae9f8308070541dacfce20fec2510d42b3cd00bb9e + languageName: node + linkType: hard + "@types/prop-types@npm:*": version: 15.7.14 resolution: "@types/prop-types@npm:15.7.14" @@ -6155,11 +6518,13 @@ __metadata: "@jupyterlab/coreutils": ^6.0.0 "@jupyterlab/docregistry": ^4.0.0 "@jupyterlab/launcher": ^4.0.0 + "@jupyterlab/mainmenu": ^4.3.4 "@jupyterlab/notebook": ^4.0.0 "@jupyterlab/ui-components": ^4.0.0 "@lumino/coreutils": ^2.0.0 "@lumino/disposable": ^2.0.0 "@types/jest": ^29.5.14 + "@types/node": ^24.0.4 "@typescript-eslint/eslint-plugin": ^5.62.0 "@typescript-eslint/parser": ^5.62.0 cross-spawn: ^6.0.6 @@ -8496,6 +8861,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~7.8.0": + version: 7.8.0 + resolution: "undici-types@npm:7.8.0" + checksum: 59521a5b9b50e72cb838a29466b3557b4eacbc191a83f4df5a2f7b156bc8263072b145dc4bb8ec41da7d56a7e9b178892458da02af769243d57f801a50ac5751 + languageName: node + linkType: hard + "unique-filename@npm:^4.0.0": version: 4.0.0 resolution: "unique-filename@npm:4.0.0" diff --git a/tests/unit/jupyter_matlab_kernel/comms/test_labextension.py b/tests/unit/jupyter_matlab_kernel/comms/test_labextension.py new file mode 100644 index 00000000..45493cfa --- /dev/null +++ b/tests/unit/jupyter_matlab_kernel/comms/test_labextension.py @@ -0,0 +1,208 @@ +# Copyright 2025 The MathWorks, Inc. + +import pytest +from jupyter_matlab_kernel.comms.labextension import ( + LabExtensionCommunication, +) + +from unittest.mock import call + + +@pytest.fixture +def mock_kernel(mocker): + """Create a mock kernel for testing.""" + kernel = mocker.MagicMock() + return kernel + + +@pytest.fixture +def mock_stream(mocker): + """Create a mock stream for testing.""" + stream = mocker.MagicMock() + return stream + + +@pytest.fixture +def mock_ident(mocker): + """Create a mock ident for testing.""" + ident = mocker.MagicMock() + return ident + + +@pytest.fixture +def mock_comm(mocker): + """Create a mock comm object for testing.""" + comm = mocker.MagicMock() + return comm + + +@pytest.fixture +def labext_comm(mock_kernel): + """Create a LabExtensionCommunication instance for testing.""" + return LabExtensionCommunication(mock_kernel) + + +def test_init(mock_kernel): + """Test that LabExtensionCommunication initializes correctly.""" + labext_comm = LabExtensionCommunication(mock_kernel) + + assert labext_comm.comms == {} + assert labext_comm.kernel is mock_kernel + assert labext_comm.log is mock_kernel.log + + +def test_comm_open_creates_comm( + labext_comm, mocker, mock_stream, mock_ident, mock_comm +): + """Test that comm_open creates a communication channel.""" + # Arrange + # Mock the Comm class + mock_comm_class = mocker.patch( + "jupyter_matlab_kernel.comms.labextension.labextension.Comm", + return_value=mock_comm, + ) + + test_comm_id = "test-comm-id" + test_target_name = "test-target" + msg = {"content": {"comm_id": test_comm_id, "target_name": test_target_name}} + + # Act + labext_comm.comm_open(mock_stream, mock_ident, msg) + + # Assert + mock_comm_class.assert_called_once_with( + comm_id=test_comm_id, primary=False, target_name=test_target_name + ) + + # Verify comm is set + assert labext_comm.comms[test_comm_id] is mock_comm + # Verify debug is called twice and with the right messages + assert labext_comm.log.debug.call_count == 2 + expected_calls = [ + call( + f"Received comm_open message with id: {test_comm_id} and target_name: {test_target_name}" + ), + call( + f"Successfully created communication channel with labextension on: {test_comm_id}" + ), + ] + labext_comm.log.debug.assert_has_calls(expected_calls, any_order=True) + + +@pytest.mark.asyncio +async def test_comm_msg_with_valid_comm( + labext_comm, mock_stream, mock_ident, mock_comm +): + """Test that comm_msg processes messages when comm is available.""" + # Arrange + comm_id = "test-comm-id" + labext_comm.comms[comm_id] = mock_comm + + test_action = "test-action" + test_data = {"key": "value"} + msg = { + "content": { + "comm_id": comm_id, + "data": {"action": test_action, "data": test_data}, + } + } + + # Act + await labext_comm.comm_msg(mock_stream, mock_ident, msg) + + # Assert + labext_comm.log.debug.assert_called_once_with( + f"Received action_type:{test_action} with data:{test_data} from the lab extension" + ) + + +def test_comm_close_with_valid_comm_id(labext_comm, mock_stream, mock_ident, mock_comm): + """Test that comm_close closes the correct communication channel.""" + # Arrange + # Set up a mock comm with matching ID + comm_id = "test-comm-id" + mock_comm.comm_id = comm_id + labext_comm.comms = {comm_id: mock_comm} + + msg = {"content": {"comm_id": comm_id}} + + # Act + labext_comm.comm_close(mock_stream, mock_ident, msg) + + # Assert + # Verify comm is set to None + assert labext_comm.comms == {} + + # Verify logging + labext_comm.log.info.assert_called_once_with(f"Comm closed with id: {comm_id}") + + +def test_comm_close_with_non_matching_comm_id( + labext_comm, mock_stream, mock_ident, mock_comm +): + """Test that comm_close warns when trying to close unknown comm.""" + # Arrange + # Set up a mock comm with different ID + mock_comm.comm_id = "different-comm-id" + comm_id = "test-comm-id" + different_comm_id = "different-comm-id" + labext_comm.comms = {different_comm_id: mock_comm} + + msg = {"content": {"comm_id": comm_id}} + + # Act + labext_comm.comm_close(mock_stream, mock_ident, msg) + + # Assert + # Verify comm is not changed + assert labext_comm.comms[different_comm_id] is mock_comm + + # Verify warning logging + labext_comm.log.debug.assert_called_once_with( + f"Attempted to close unknown comm_id: {comm_id}" + ) + + +def test_comm_close_with_no_comm(labext_comm, mock_stream, mock_ident): + """Test that comm_close warns when no comm exists.""" + # Arrange + # Ensure comms is empty + labext_comm.comms = {} + test_comm_id = "test-comm-id" + msg = {"content": {"comm_id": test_comm_id}} + + # Act + labext_comm.comm_close(mock_stream, mock_ident, msg) + + # Assert + # Verify comm remains None + assert labext_comm.comms == {} + + # Verify warning logging + labext_comm.log.debug.assert_called_once_with( + f"Attempted to close unknown comm_id: {test_comm_id}" + ) + + +@pytest.mark.asyncio +async def test_comm_msg_extracts_data_correctly( + labext_comm, mock_stream, mock_ident, mock_comm +): + """Test that comm_msg correctly extracts action and data from message.""" + # Arrange + comm_id = "test-comm-id" + labext_comm.comms = {comm_id: mock_comm} + action_type = "execute_code" + data = {"code": "x = 1 + 1", "cell_id": "abc123"} + + msg = { + "content": {"comm_id": comm_id, "data": {"action": action_type, "data": data}} + } + + # Call the method + await labext_comm.comm_msg(mock_stream, mock_ident, msg) + + # Verify logging with correct extracted data + labext_comm.log.debug.assert_called_once_with( + f"Received action_type:{action_type} with data:{data} from the lab extension" + ) diff --git a/tests/unit/jupyter_matlab_kernel/mocks/mock_http_responses.py b/tests/unit/jupyter_matlab_kernel/mocks/mock_http_responses.py index 77284f22..6414c8ef 100644 --- a/tests/unit/jupyter_matlab_kernel/mocks/mock_http_responses.py +++ b/tests/unit/jupyter_matlab_kernel/mocks/mock_http_responses.py @@ -1,4 +1,4 @@ -# Copyright 2023-2024 The MathWorks, Inc. +# Copyright 2023-2025 The MathWorks, Inc. """Mock matlab-proxy HTTP Responses.""" import http @@ -136,3 +136,50 @@ class MockError(Exception): """ pass + + +class MockEvalResponse: + """A mock of a successful eval response from matlab-proxy.""" + + def __init__(self, is_error=False, response_str="", message_faults=None): + """Construct a mock eval response. + + Args: + is_error (bool): indicates if the eval had an error. + response_str (str): the response string or file path. + message_faults (list): list of message faults if any. + """ + self.is_error = is_error + self.response_str = response_str + self.message_faults = message_faults or [] + + status = http.HTTPStatus.OK + + async def json(self): + """Return a matlab-proxy eval JSON response.""" + return { + "messages": { + "EvalResponse": [ + { + "isError": self.is_error, + "responseStr": self.response_str, + "messageFaults": self.message_faults, + } + ] + } + } + + +class MockEvalResponseMissingData: + """A mock of an eval response missing EvalResponse data.""" + + status = http.HTTPStatus.OK + + def raise_for_status(self): + """Raise a HTTPError for missing data.""" + raise aiohttp.client_exceptions.ClientError("Mock exception") + + @staticmethod + async def json(): + """Return a matlab-proxy response missing EvalResponse.""" + return {"messages": {}} diff --git a/tests/unit/jupyter_matlab_kernel/test_kernel.py b/tests/unit/jupyter_matlab_kernel/test_kernel.py index 9c833f71..18ef551b 100644 --- a/tests/unit/jupyter_matlab_kernel/test_kernel.py +++ b/tests/unit/jupyter_matlab_kernel/test_kernel.py @@ -1,4 +1,4 @@ -# Copyright 2023-2024 The MathWorks, Inc. +# Copyright 2023-2025 The MathWorks, Inc. # This file contains tests for jupyter_matlab_kernel.kernel import mocks.mock_jupyter_server as MockJupyterServer @@ -6,7 +6,10 @@ from jupyter_server import serverapp from mocks.mock_jupyter_server import MockJupyterServerFixture -from jupyter_matlab_kernel.jsp_kernel import MATLABKernelUsingJSP, start_matlab_proxy +from jupyter_matlab_kernel.jsp_kernel import ( + MATLABKernelUsingJSP, + start_matlab_proxy, +) from jupyter_matlab_kernel.mwi_exceptions import MATLABConnectionError diff --git a/tests/unit/jupyter_matlab_kernel/test_mpm_kernel.py b/tests/unit/jupyter_matlab_kernel/test_mpm_kernel.py index 495eca8f..1bf01d1b 100644 --- a/tests/unit/jupyter_matlab_kernel/test_mpm_kernel.py +++ b/tests/unit/jupyter_matlab_kernel/test_mpm_kernel.py @@ -19,7 +19,8 @@ def mpm_kernel_instance(mocker) -> MATLABKernelUsingMPM: return_value=uuid.uuid4().hex, ) mocker.patch( - "jupyter_matlab_kernel.base_kernel.BaseMATLABKernel.log", new=mock_logger + "jupyter_matlab_kernel.base_kernel.BaseMATLABKernel.log", + new=mock_logger, ) return MATLABKernelUsingMPM() diff --git a/tests/unit/jupyter_matlab_kernel/test_mwi_comm_helpers.py b/tests/unit/jupyter_matlab_kernel/test_mwi_comm_helpers.py index 4646b8de..4b7ee6f4 100644 --- a/tests/unit/jupyter_matlab_kernel/test_mwi_comm_helpers.py +++ b/tests/unit/jupyter_matlab_kernel/test_mwi_comm_helpers.py @@ -1,8 +1,11 @@ -# Copyright 2023-2024 The MathWorks, Inc. +# Copyright 2023-2025 The MathWorks, Inc. # This file contains tests for jupyter_matlab_kernel.mwi_comm_helpers import asyncio import http +import json +import tempfile +import os import aiohttp import aiohttp.client_exceptions @@ -11,6 +14,8 @@ MockMatlabProxyStatusResponse, MockSimpleBadResponse, MockUnauthorisedRequestResponse, + MockEvalResponse, + MockEvalResponseMissingData, ) from jupyter_matlab_kernel.mwi_comm_helpers import MWICommHelper @@ -225,3 +230,282 @@ async def mock_post(*args, **kwargs): pytest.fail("Unexpected failured in execution request") assert "Mock results from feval" in outputs + + +# Testing send_eval_request_to_matlab +async def test_send_eval_request_to_matlab_success(monkeypatch, matlab_proxy_fixture): + """Test that send_eval_request_to_matlab returns eval response correctly.""" + + # Arrange + async def mock_post(*args, **kwargs): + return MockEvalResponse(is_error=False, response_str="", message_faults=[]) + + monkeypatch.setattr(aiohttp.ClientSession, "post", mock_post) + + mcode = "x = 1 + 1" + + # Act + result = await matlab_proxy_fixture.send_eval_request_to_matlab(mcode) + + # Assert + # Verify the eval response is returned as-is + expected_response = {"isError": False, "responseStr": "", "messageFaults": []} + assert result == expected_response + + +async def test_send_eval_request_to_matlab_with_error( + monkeypatch, matlab_proxy_fixture +): + """Test that send_eval_request_to_matlab returns error response correctly.""" + + # Arrange + async def mock_post(*args, **kwargs): + return MockEvalResponse( + is_error=True, + response_str="Error occurred", + message_faults=[{"message": "Syntax error"}], + ) + + monkeypatch.setattr(aiohttp.ClientSession, "post", mock_post) + + mcode = "invalid_syntax" + + # Act + result = await matlab_proxy_fixture.send_eval_request_to_matlab(mcode) + + # Assert + # Verify the error response is returned as-is + expected_response = { + "isError": True, + "responseStr": "Error occurred", + "messageFaults": [{"message": "Syntax error"}], + } + assert result == expected_response + + +async def test_send_eval_request_to_matlab_bad_request( + monkeypatch, matlab_proxy_fixture +): + """Test that send_eval_request_to_matlab raises exception for bad HTTP request.""" + # Arrange + mock_exception_message = "Mock exception thrown due to bad request status." + + async def mock_post(*args, **kwargs): + return MockSimpleBadResponse(mock_exception_message) + + monkeypatch.setattr(aiohttp.ClientSession, "post", mock_post) + + mcode = "x = 1 + 1" + + # Act + with pytest.raises(aiohttp.client_exceptions.ClientError) as exceptionInfo: + await matlab_proxy_fixture.send_eval_request_to_matlab(mcode) + + # Assert + assert mock_exception_message in str(exceptionInfo.value) + + +async def test_send_eval_request_to_matlab_missing_eval_response( + monkeypatch, matlab_proxy_fixture +): + """Test that send_eval_request_to_matlab raises MATLABConnectionError for missing EvalResponse.""" + + # Arrange + async def mock_post(*args, **kwargs): + return MockEvalResponseMissingData() + + monkeypatch.setattr(aiohttp.ClientSession, "post", mock_post) + + mcode = "x = 1 + 1" + with pytest.raises(MATLABConnectionError): + await matlab_proxy_fixture.send_eval_request_to_matlab(mcode) + + +# Testing _read_eval_response_from_file +async def test_read_eval_response_from_file_success_with_file(matlab_proxy_fixture): + """Test _read_eval_response_from_file with successful response and file.""" + # Arrange + # Create a temporary file with test data + test_data = [ + {"type": "stream", "content": {"name": "stdout", "text": "Hello World"}} + ] + with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".json") as f: + json.dump(test_data, f) + temp_file_path = f.name + + try: + eval_response = { + "isError": False, + "responseStr": temp_file_path, + "messageFaults": [], + } + + # Act + result = await matlab_proxy_fixture._read_eval_response_from_file(eval_response) + + # Assert + # Verify the result + assert result == test_data + + # Verify the file was deleted + assert not os.path.exists(temp_file_path) + + finally: + # Clean up in case the test failed + if os.path.exists(temp_file_path): + os.remove(temp_file_path) + + +async def test_read_eval_response_from_file_success_without_file(matlab_proxy_fixture): + """Test _read_eval_response_from_file with successful response but no file.""" + # Arrange + eval_response = { + "isError": False, + "responseStr": "", # Empty file path + "messageFaults": [], + } + + # Act + result = await matlab_proxy_fixture._read_eval_response_from_file(eval_response) + + # Assert + # Verify empty result returns empty list + assert result == [] + + +async def test_read_eval_response_from_file_error_with_message_faults( + matlab_proxy_fixture, +): + """Test _read_eval_response_from_file with error response containing message faults.""" + # Arrange + eval_response = { + "isError": True, + "responseStr": "Error occurred", + "messageFaults": [{"message": "Syntax error in code"}], + } + + # Act + with pytest.raises( + Exception, + match="Failed to execute. Operation may have been interrupted by user.", + ): + await matlab_proxy_fixture._read_eval_response_from_file(eval_response) + + +async def test_read_eval_response_from_file_error_without_message_faults( + matlab_proxy_fixture, +): + """Test _read_eval_response_from_file with error response without message faults.""" + + eval_response = { + "isError": True, + "responseStr": "Custom error message", + "messageFaults": [], + } + + with pytest.raises(Exception, match="Custom error message"): + await matlab_proxy_fixture._read_eval_response_from_file(eval_response) + + +async def test_read_eval_response_from_file_handles_file_deletion_error( + matlab_proxy_fixture, monkeypatch +): + """Test _read_eval_response_from_file handles file deletion errors gracefully.""" + + # Create a temporary file with test data + test_data = [ + {"type": "stream", "content": {"name": "stdout", "text": "Hello World"}} + ] + with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".json") as f: + json.dump(test_data, f) + temp_file_path = f.name + + # Mock os.remove to raise an exception + original_remove = os.remove + + def mock_remove(path): + if path == temp_file_path: + raise OSError("Permission denied") + return original_remove(path) + + monkeypatch.setattr(os, "remove", mock_remove) + + try: + eval_response = { + "isError": False, + "responseStr": temp_file_path, + "messageFaults": [], + } + + # Should not raise exception even if file deletion fails + result = await matlab_proxy_fixture._read_eval_response_from_file(eval_response) + + # Verify the result is still correct + assert result == test_data + + finally: + # Clean up manually since mocked remove failed + if os.path.exists(temp_file_path): + original_remove(temp_file_path) + + +async def test_read_eval_response_from_file_with_empty_file_content( + matlab_proxy_fixture, +): + """Test _read_eval_response_from_file with empty file content.""" + + # Create a temporary file with empty content + with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".json") as f: + f.write("") # Empty content + temp_file_path = f.name + + try: + eval_response = { + "isError": False, + "responseStr": temp_file_path, + "messageFaults": [], + } + + result = await matlab_proxy_fixture._read_eval_response_from_file(eval_response) + + # Verify empty content returns empty list + assert result == [] + + # Verify the file was deleted + assert not os.path.exists(temp_file_path) + + finally: + # Clean up in case the test failed + if os.path.exists(temp_file_path): + os.remove(temp_file_path) + + +async def test_read_eval_response_from_file_with_whitespace_only_content( + matlab_proxy_fixture, +): + """Test _read_eval_response_from_file with whitespace-only file content.""" + + # Create a temporary file with whitespace content + with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".json") as f: + f.write(" \n\t ") # Only whitespace + temp_file_path = f.name + + try: + eval_response = { + "isError": False, + "responseStr": temp_file_path, + "messageFaults": [], + } + + result = await matlab_proxy_fixture._read_eval_response_from_file(eval_response) + + # Verify whitespace-only content returns empty list + assert result == [] + + # Verify the file was deleted + assert not os.path.exists(temp_file_path) + + finally: + # Clean up in case the test failed + if os.path.exists(temp_file_path): + os.remove(temp_file_path) From d208385e994ddc78b6aa9767dd862f600839bcd7 Mon Sep 17 00:00:00 2001 From: Prabhakar Kumar Date: Mon, 8 Dec 2025 21:01:14 +0530 Subject: [PATCH 13/15] Update to v0.17.5 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 450004eb..40813ca2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "jupyter-matlab-proxy" -version = "0.17.4" +version = "0.17.5" description = "MATLAB Integration for Jupyter" readme = "README.md" license = { file = "LICENSE.md" } From 3fa7c1e7ad9eb7d3aa72d3e6942ddffd287ea201 Mon Sep 17 00:00:00 2001 From: Krishan Sharma Date: Thu, 11 Dec 2025 13:07:53 +0000 Subject: [PATCH 14/15] Adds support for notebook magic `%matlab [OPTIONS]`. Enables notebooks to specify the use of a dedicated MATLAB. For more information, see (Technical Overview of MATLAB Kernel for Jupyter)[https://github.com/mathworks/jupyter-matlab-proxy/tree/main/src/jupyter_matlab_kernel#technical-overview] fixes mathworks/jupyter-matlab-proxy#103 fixes mathworks/jupyter-matlab-proxy#44 --- README.md | 4 +- img/kernel-architecture-dedicated.png | Bin 0 -> 183123 bytes img/kernel-architecture.png | Bin 33567 -> 0 bytes src/jupyter_matlab_kernel/README.md | 21 +- src/jupyter_matlab_kernel/base_kernel.py | 225 +++++++++++----- src/jupyter_matlab_kernel/jsp_kernel.py | 25 +- .../magic_execution_engine.py | 2 +- src/jupyter_matlab_kernel/magics/README.md | 4 +- .../magics/base/matlab_magic.py | 7 +- src/jupyter_matlab_kernel/magics/matlab.py | 219 ++++++++++++++++ src/jupyter_matlab_kernel/mpm_kernel.py | 60 +---- src/jupyter_matlab_kernel/mwi_comm_helpers.py | 104 ++++++-- src/jupyter_matlab_kernel/mwi_exceptions.py | 4 +- .../src/plugins/matlabToolbarButton.ts | 93 +++++-- .../src/tests/matlabToolbarButton.test.ts | 244 +++++++++++------- .../style/base.css | 6 + .../utils/integration_test_utils.py | 35 ++- .../magics/test_matlab.py | 202 +++++++++++++++ .../unit/jupyter_matlab_kernel/test_kernel.py | 23 +- .../jupyter_matlab_kernel/test_mpm_kernel.py | 25 +- .../test_mwi_comm_helpers.py | 108 ++++---- 21 files changed, 1074 insertions(+), 337 deletions(-) create mode 100644 img/kernel-architecture-dedicated.png delete mode 100644 img/kernel-architecture.png create mode 100644 src/jupyter_matlab_kernel/magics/matlab.py create mode 100644 tests/unit/jupyter_matlab_kernel/magics/test_matlab.py diff --git a/README.md b/README.md index 1769840d..d4432d6e 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,8 @@ This opens a Jupyter notebook that supports MATLAB. - **Licensing:** When you execute MATLAB code in a notebook for the first time, enter your MATLAB license information in the dialog box that appears. For details, see [Licensing](https://github.com/mathworks/matlab-proxy/blob/main/MATLAB-Licensing-Info.md). The MATLAB session can take a few minutes to start. +- **Sharing MATLAB across notebooks:** By default, multiple notebooks running on a Jupyter server share the underlying MATLAB process, so executing code in one notebook affects the workspace in others. To use a dedicated MATLAB for your kernel instead, use the magic `%%matlab new_session`. For details, see [Magic Commands for MATLAB Kernel](https://github.com/mathworks/jupyter-matlab-proxy/blob/main/src/jupyter_matlab_kernel/magics/README.md). To learn more about the kernel architecture, see [MATLAB Kernel for Jupyter](https://github.com/mathworks/jupyter-matlab-proxy/blob/main/src/jupyter_matlab_kernel/README.md). + - **MATLAB Kernel:** The MATLAB kernel supports tab completion and rich outputs: * Inline static plot images * LaTeX representation for symbolic expressions @@ -181,7 +183,7 @@ This opens a Jupyter notebook that supports MATLAB. For a technical overview of the MATLAB kernel, see [MATLAB Kernel for Jupyter](https://github.com/mathworks/jupyter-matlab-proxy/blob/main/src/jupyter_matlab_kernel/README.md). -- **Multiple notebooks:** Multiple notebooks running on a Jupyter server share the underlying MATLAB process, so executing code in one notebook affects the workspace in others. If you work in several notebooks simultaneously, be aware they share a workspace. For details, see [MATLAB Kernel for Jupyter](https://github.com/mathworks/jupyter-matlab-proxy/blob/main/src/jupyter_matlab_kernel/README.md). + - **Local functions:** With MATLAB R2022b and later, you can define a local function at the end of the cell where you want to call it:

diff --git a/img/kernel-architecture-dedicated.png b/img/kernel-architecture-dedicated.png new file mode 100644 index 0000000000000000000000000000000000000000..cb9a1fcb4b711ea3f387fb6ed418915e8f03f1e1 GIT binary patch literal 183123 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

Xw- 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 literal 0 HcmV?d00001 diff --git a/img/kernel-architecture.png b/img/kernel-architecture.png deleted file mode 100644 index 367fb79622f0b9b8e2bcdf53a412c602e0ce83a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33567 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

| -|--| -|The diagram above illustrates that multiple Jupyter notebooks communicate with a shared MATLAB process, through the Jupyter notebook server.| +

-Start a Jupyter notebook to create a MATLAB kernel. When you run MATLAB code in a notebook for the first time, you see a licensing screen to enter your MATLAB license details. If a MATLAB process is not already running, Jupyter will start one. +### Shared MATLAB Workspace (Default Behavior) -Multiple notebooks share the same MATLAB workspace. MATLAB processes commands from multiple notebooks in on a first-in, first-out basis. +By default, multiple notebooks share the same MATLAB workspace. MATLAB processes commands from multiple notebooks on a first-in, first-out basis. -You can use kernel interrupts to stop MATLAB from processing a request. Remember that if cells from multiple notebooks are being run at the same time, the execution request you interrupt may not be from the notebook where you initated the interrupt. +You can use kernel interrupts to stop MATLAB from processing a request. Remember that if cells from multiple notebooks are being run at the same time, the execution request you interrupt may not be from the notebook where you initiated the interrupt. + +### Dedicated MATLAB Workspace (Optional Behavior) + +You can now create a dedicated MATLAB session for your notebook by using the magic command `%%matlab new_session` in a cell. This starts a separate MATLAB process exclusively for that notebook, providing an isolated workspace that is not shared with other notebooks. + +This is useful when you need to avoid conflicts with other notebooks or require an independent execution environment. + +Once created, all subsequent MATLAB code in that notebook will execute in the dedicated session. Each dedicated session operates independently with its own workspace and execution queue. ## Limitations @@ -33,6 +40,6 @@ To request an enhancement or technical support, [create a GitHub issue](https:// ---- -Copyright 2023-2024 The MathWorks, Inc. +Copyright 2023-2025 The MathWorks, Inc. ---- diff --git a/src/jupyter_matlab_kernel/base_kernel.py b/src/jupyter_matlab_kernel/base_kernel.py index ec0369e0..253a48ed 100644 --- a/src/jupyter_matlab_kernel/base_kernel.py +++ b/src/jupyter_matlab_kernel/base_kernel.py @@ -25,6 +25,7 @@ MagicExecutionEngine, get_completion_result_for_magics, ) +from jupyter_matlab_kernel.mwi_comm_helpers import MWICommHelper from jupyter_matlab_kernel.mwi_exceptions import MATLABConnectionError from jupyter_matlab_kernel.comms import LabExtensionCommunication @@ -141,7 +142,22 @@ def __init__(self, *args, **kwargs): self.magic_engine = MagicExecutionEngine(self.log) # Communication helper for interaction with backend MATLAB proxy - self.mwi_comm_helper = None + self.mwi_comm_helper: Optional[MWICommHelper] = None + + # Used to detect if this Kernel has been assigned a MATLAB-proxy server or not + self.is_matlab_assigned = False + + # Flag indicating whether this kernel is using a shared MATLAB instance + self.is_shared_matlab: bool = True + + # Keeps track of MATLAB version information for the MATLAB assigned to this Kernel + self.matlab_version = None + + # Keeps track of MATLAB root path information for the MATLAB assigned to this Kernel + self.matlab_root_path = None + + # Keeps track of the MATLAB licensing mode information for the MATLAB assigned to this Kernel + self.licensing_mode = None self.labext_comm = LabExtensionCommunication(self) @@ -165,8 +181,9 @@ async def interrupt_request(self, stream, ident, parent): """ self.log.debug("Received interrupt request from Jupyter") try: - # Send interrupt request to MATLAB - await self.mwi_comm_helper.send_interrupt_request_to_matlab() + if self.is_matlab_assigned and self.mwi_comm_helper: + # Send interrupt request to MATLAB + await self.mwi_comm_helper.send_interrupt_request_to_matlab() # Set the response to interrupt request. content = {"status": "ok"} @@ -184,29 +201,6 @@ async def interrupt_request(self, stream, ident, parent): self.session.send(stream, "interrupt_reply", content, parent, ident=ident) - def modify_kernel(self, states_to_modify): - """ - Used to modify MATLAB Kernel state - Args: - states_to_modify (dict): A key value pair of all the states to be modified. - - """ - self.log.debug(f"Modifying the kernel with {states_to_modify}") - for key, value in states_to_modify.items(): - if hasattr(self, key): - self.log.debug(f"set the value of {key} to {value}") - setattr(self, key, value) - - def handle_magic_output(self, output, outputs=None): - if output["type"] == "modify_kernel": - self.modify_kernel(output) - else: - self.display_output(output) - if outputs is not None and not self.startup_checks_completed: - # Outputs are cleared after startup_check. - # Storing the magic outputs to display them after startup_check completes. - outputs.append(output) - async def do_execute( self, code, @@ -222,18 +216,19 @@ async def do_execute( https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute """ self.log.debug(f"Received execution request from Jupyter with code:\n{code}") + try: - accumulated_magic_outputs = [] performed_startup_checks = False - - for output in self.magic_engine.process_before_cell_execution( - code, self.execution_count - ): - self.handle_magic_output(output, accumulated_magic_outputs) + accumulated_magic_outputs = await self._perform_before_cell_execution(code) skip_cell_execution = self.magic_engine.skip_cell_execution() self.log.debug(f"Skipping cell execution is set to {skip_cell_execution}") + # Start a shared matlab-proxy (default) if not already started + if not self.is_matlab_assigned and not skip_cell_execution: + await self.start_matlab_proxy_and_comm_helper() + self.is_matlab_assigned = True + # Complete one-time startup checks before sending request to MATLAB. # Blocking call, returns after MATLAB is started. if not skip_cell_execution: @@ -275,9 +270,8 @@ async def do_execute( ) # Display all the outputs produced during the execution of code. - for idx in range(len(outputs)): - data = outputs[idx] - self.log.debug(f"Displaying output {idx+1}:\n{data}") + for idx, data in enumerate(outputs): + self.log.debug(f"Displaying output {idx + 1}:\n{data}") # Ignore empty values returned from MATLAB. if not data: @@ -286,7 +280,7 @@ async def do_execute( # Execute post execution of MAGICs for output in self.magic_engine.process_after_cell_execution(): - self.handle_magic_output(output) + await self._handle_magic_output(output) except Exception as e: self.log.error( @@ -418,6 +412,119 @@ async def do_history( # Helper functions + def _get_kernel_info(self): + return { + "is_shared_matlab": self.is_shared_matlab, + "matlab_version": self.matlab_version, + "matlab_root_path": self.matlab_root_path, + "licensing_mode": self.licensing_mode, + } + + def _modify_kernel(self, states_to_modify): + """ + Used to modify MATLAB Kernel state + Args: + states_to_modify (dict): A key value pair of all the states to be modified. + + """ + self.log.info(f"Modifying the kernel with {states_to_modify}") + for key, value in states_to_modify.items(): + if hasattr(self, key): + self.log.debug(f"set the value of {key} to {value}") + setattr(self, key, value) + else: + self.log.warning(f"Attribute with name: {key} not found in kernel") + + async def _handle_magic_output(self, output): + """ + Handle the output from magic commands. + + Args: + output (dict): The output from a magic command. + + Returns: + dict or None: Returns the output if startup checks are not completed, + otherwise returns None. + + This method processes the output from magic commands. It handles kernel + modifications, stores outputs before startup checks are completed, and + displays outputs after startup checks are done. + """ + if output["type"] == "modify_kernel": + self.log.debug("Handling modify_kernel output") + self._modify_kernel(output) + elif output["type"] == "callback": + self.log.debug("Handling callback output") + await self._invoke_callback_function(output.get("callback_function")) + else: + self.display_output(output) + + if not self.startup_checks_completed: + # Outputs are cleared after startup_check. + # Storing the magic outputs to display them after startup_check completes. + return output + return None + + async def _invoke_callback_function(self, callback_fx): + """ + Handles the invocation of callback function supplied by the magic command. Kernel injects + itself as a parameter. + + Args: + callback_fx: Function to be called. Currently only supports calling async or async generator functions. + """ + if callback_fx: + import inspect + + if inspect.isasyncgenfunction(callback_fx): + async for result in callback_fx(self): + self.display_output(result) + else: + result = await callback_fx(self) + if result: + self.display_output(result) + self.log.debug(f"Callback function {callback_fx} executed successfully") + return None + + async def start_matlab_proxy_and_comm_helper(self): + """ + Start MATLAB proxy and communication helper. + + This method is intended to be overridden by subclasses to perform + any necessary setup for matlab-proxy startup. The default implementation + does nothing. + + Returns: + None + + Raises: + NotImplementedError: Always raised as this method must be implemented by subclasses. + """ + raise NotImplementedError("Subclasses should implement this method") + + async def _perform_before_cell_execution(self, code) -> list: + """ + Perform actions before cell execution and handle magic outputs. + + This method processes magic commands before cell execution and accumulates + their outputs. + + Args: + code (str): The code to be executed. + + Returns: + list: A list of accumulated magic outputs. + """ + accumulated_magic_outputs = [] + for magic_output in self.magic_engine.process_before_cell_execution( + code, self.execution_count + ): + output = await self._handle_magic_output(magic_output) + if output: + accumulated_magic_outputs.append(output) + + return accumulated_magic_outputs + def display_output(self, out): """ Common function to send execution outputs to Jupyter UI. @@ -472,11 +579,8 @@ async def perform_startup_checks( self.log.error(f"Found a startup error: {self.startup_error}") raise self.startup_error - ( - is_matlab_licensed, - matlab_status, - matlab_proxy_has_error, - ) = await self.mwi_comm_helper.fetch_matlab_proxy_status() + # Query matlab-proxy for its current status + matlab_proxy_status = await self.mwi_comm_helper.fetch_matlab_proxy_status() # Display iframe containing matlab-proxy to show login window if MATLAB # is not licensed using matlab-proxy. The iframe is removed after MATLAB @@ -486,7 +590,7 @@ async def perform_startup_checks( # as src for iframe to avoid hardcoding any hostname/domain information. This is done to # ensure the kernel works in Jupyter deployments. VS Code however does not work the same way # as other browser based Jupyter clients. - if not is_matlab_licensed: + if not matlab_proxy_status.is_matlab_licensed: if not jupyter_base_url: # happens for non-jupyter environments (like VSCode), we expect licensing to # be completed before hand @@ -519,22 +623,13 @@ async def perform_startup_checks( ) # Wait until MATLAB is started before sending requests. - await self.poll_for_matlab_startup( - is_matlab_licensed, matlab_status, matlab_proxy_has_error - ) + await self.poll_for_matlab_startup(matlab_proxy_status) - async def poll_for_matlab_startup( - self, is_matlab_licensed, matlab_status, matlab_proxy_has_error - ): - """Wait until MATLAB has started or time has run out" + async def poll_for_matlab_startup(self, matlab_proxy_status): + """Wait until MATLAB has started or time has run out Args: - is_matlab_licensed (bool): A flag indicating whether MATLAB is - licensed and eligible to start. - matlab_status (str): A string representing the current status - of the MATLAB startup process. - matlab_proxy_has_error (bool): A flag indicating whether there - is an error in the MATLAB proxy process during startup. + matlab_proxy_status: The status object from matlab-proxy Raises: MATLABConnectionError: If an error occurs while attempting to @@ -544,11 +639,12 @@ async def poll_for_matlab_startup( self.log.debug("Waiting until MATLAB is started") timeout = 0 while ( - matlab_status != "up" + matlab_proxy_status + and matlab_proxy_status.matlab_status != "up" and timeout != _MATLAB_STARTUP_TIMEOUT - and not matlab_proxy_has_error + and not matlab_proxy_status.matlab_proxy_has_error ): - if is_matlab_licensed: + if matlab_proxy_status.is_matlab_licensed: if timeout == 0: self.log.debug("Licensing completed. Clearing output area") self.display_output( @@ -565,11 +661,7 @@ async def poll_for_matlab_startup( ) timeout += 1 time.sleep(1) - ( - is_matlab_licensed, - matlab_status, - matlab_proxy_has_error, - ) = await self.mwi_comm_helper.fetch_matlab_proxy_status() + matlab_proxy_status = await self.mwi_comm_helper.fetch_matlab_proxy_status() # If MATLAB is not available after 15 seconds of licensing information # being available either through user input or through matlab-proxy cache, @@ -580,10 +672,15 @@ async def poll_for_matlab_startup( ) raise MATLABConnectionError - if matlab_proxy_has_error: + if not matlab_proxy_status or matlab_proxy_status.matlab_proxy_has_error: self.log.error("matlab-proxy encountered error.") raise MATLABConnectionError + # Update the kernel state with information from matlab proxy server + self.licensing_mode = matlab_proxy_status.licensing_mode + self.matlab_version = matlab_proxy_status.matlab_version + self.matlab_root_path = await self.mwi_comm_helper.fetch_matlab_root_path() + self.log.debug("MATLAB is running, startup checks completed.") def _extract_kernel_id_from_sys_args(self, args) -> str: diff --git a/src/jupyter_matlab_kernel/jsp_kernel.py b/src/jupyter_matlab_kernel/jsp_kernel.py index dd0c4793..8efbc73a 100644 --- a/src/jupyter_matlab_kernel/jsp_kernel.py +++ b/src/jupyter_matlab_kernel/jsp_kernel.py @@ -186,19 +186,24 @@ def __init__(self, *args, **kwargs): async def do_shutdown(self, restart): self.log.debug("Received shutdown request from Jupyter") - try: - await self.mwi_comm_helper.send_shutdown_request_to_matlab() - await self.mwi_comm_helper.disconnect() - except ( - MATLABConnectionError, - aiohttp.client_exceptions.ClientResponseError, - ) as e: - self.log.error( - f"Exception occurred while sending shutdown request to MATLAB:\n{e}" - ) + if self.is_matlab_assigned: + try: + await self.mwi_comm_helper.send_shutdown_request_to_matlab() + await self.mwi_comm_helper.disconnect() + except ( + MATLABConnectionError, + aiohttp.client_exceptions.ClientResponseError, + ) as e: + self.log.error( + f"Exception occurred while sending shutdown request to MATLAB:\n{e}" + ) return super().do_shutdown(restart) async def perform_startup_checks(self): """Overriding base function to provide a different iframe source""" await super().perform_startup_checks(self.jupyter_base_url, "matlab") + + async def start_matlab_proxy_and_comm_helper(self): + """Default implementation assumes that matlab is assigned""" + self.is_matlab_assigned = True diff --git a/src/jupyter_matlab_kernel/magic_execution_engine.py b/src/jupyter_matlab_kernel/magic_execution_engine.py index 58811a2f..9169aadf 100644 --- a/src/jupyter_matlab_kernel/magic_execution_engine.py +++ b/src/jupyter_matlab_kernel/magic_execution_engine.py @@ -181,7 +181,7 @@ def magic_executor(magics_for_execution, magic_execution_function): for output_from_method in magic_method(): if output_from_method: if "type" in output_from_method: - yield output_from_method + yield (output_from_method) else: raise MagicError( f"Invalid result returned by a Magic command. Contact Magic Author to fix. \n Error: {output_from_method}\n Does not contain a key called type." diff --git a/src/jupyter_matlab_kernel/magics/README.md b/src/jupyter_matlab_kernel/magics/README.md index 117bfe43..038f8f8d 100644 --- a/src/jupyter_matlab_kernel/magics/README.md +++ b/src/jupyter_matlab_kernel/magics/README.md @@ -15,6 +15,8 @@ This table lists the predefined magic commands you can use: |---|---|---|---|---| |`?` and `help`| Display documentation of given magic command.|Name of magic command.||`%%lsmagic?` or `%%help lsmagic`| |`lsmagic`|List predefined magic commands.|||`%%lsmagic`| +|`matlab new_session`|Starts a new MATLAB dedicated to the kernel instead of being shared across kernels.

Note: To change from a shared MATLAB to a dedicated MATLAB after you have already run MATLAB code in a notebook, you must first restart the kernel.|||`%%matlab new_session`| +|`matlab info`|Print a summary of the MATLAB session currently being used for the kernel. The summary includes the MATLAB version, root path, licensing mode, and whether the MATLAB is shared or dedicated to a kernel. |||`%%matlab info`| |`time`|Display time taken to execute a cell.|||`%%time`| |`file`|Save contents of cell as a file in the notebook folder. You can use this command to define and save new functions. For details, see the section below on how to [Create New Functions Using the %%file Magic Command](#create-new-functions-using-the-the-file-magic-command)|Name of saved file.|The file magic command will save the contents of the cell, but not execute them in MATLAB.|`%%file myfile.m`| @@ -63,6 +65,6 @@ Note: to use your function in MATLAB, remember to add the Jupyter notebook folde --- -Copyright 2024 The MathWorks, Inc. +Copyright 2024-2025 The MathWorks, Inc. --- diff --git a/src/jupyter_matlab_kernel/magics/base/matlab_magic.py b/src/jupyter_matlab_kernel/magics/base/matlab_magic.py index 51031089..8015399f 100644 --- a/src/jupyter_matlab_kernel/magics/base/matlab_magic.py +++ b/src/jupyter_matlab_kernel/magics/base/matlab_magic.py @@ -1,4 +1,4 @@ -# Copyright 2024 The MathWorks, Inc. +# Copyright 2024-2025 The MathWorks, Inc. from jupyter_matlab_kernel import mwi_logger @@ -83,6 +83,11 @@ def before_cell_execute(self): "murl": new_url, "headers": new_headers, } + 4. To invoke the callback function: + { + "type": "callback", + "callback_function": callback_function_name, + } default: Empty dict ({}). """ yield {} diff --git a/src/jupyter_matlab_kernel/magics/matlab.py b/src/jupyter_matlab_kernel/magics/matlab.py new file mode 100644 index 00000000..5a207cbf --- /dev/null +++ b/src/jupyter_matlab_kernel/magics/matlab.py @@ -0,0 +1,219 @@ +# Copyright 2025 The MathWorks, Inc. + +from jupyter_matlab_kernel.magics.base.matlab_magic import MATLABMagic +from jupyter_matlab_kernel.mpm_kernel import MATLABKernelUsingMPM +from jupyter_matlab_kernel.mwi_exceptions import MagicError + +# Module constants +LICENSING_MODES = { + "mhlm": "Online Licensing", + "nlm": "Network License Manager", + "existing_license": "Existing License", +} +CMD_NEW_SESSION = "new_session" +CMD_INFO = "info" +EXISTING_NEW_SESSION_ERROR = "This kernel is already using a dedicated MATLAB.\n" +DEDICATED_SESSION_CONFIRMATION_MSG = ( + "A dedicated MATLAB session has been started for this kernel.\n" +) + + +async def handle_new_matlab_session(kernel: MATLABKernelUsingMPM): + """ + Handles the creation of a new dedicated MATLAB session for the kernel. + Args: + kernel: The kernel instance. + + Yields: + dict: Result dictionary containing execution status and confirmation message. + """ + # Validations + if kernel.is_matlab_assigned: + if not kernel.is_shared_matlab: + # No-op if already in an isolated MATLAB session + yield { + "type": "execute_result", + "mimetype": ["text/plain", "text/html"], + "value": [ + EXISTING_NEW_SESSION_ERROR, + f"
{EXISTING_NEW_SESSION_ERROR}
", + ], + } + return + + else: + # Shared MATLAB session is already assigned + kernel.log.warning( + "Cannot start a new MATLAB session while an existing session is active." + ) + raise MagicError( + "This notebook is currently linked to Default MATLAB session." + "To proceed, restart the kernel and run this magic command before any other MATLAB commands." + ) + # Starting new dedicated MATLAB session + try: + kernel.is_shared_matlab = False + await kernel.start_matlab_proxy_and_comm_helper() + kernel.is_matlab_assigned = True + + # Raises MATLABConnectionError if matlab-proxy failed to start in previous step + await kernel.perform_startup_checks() + kernel.startup_checks_completed = True + kernel.display_output({"type": "clear_output", "content": {"wait": False}}) + except Exception as ex: + _reset_kernel_state(kernel) + + # Try and cleanup the matlab-proxy process if it was started + await kernel.cleanup_matlab_proxy() + + # Raising here so that matlab magic output can display the error + raise MagicError(str(ex)) from ex + + yield { + "type": "execute_result", + "mimetype": ["text/plain", "text/html"], + "value": [ + DEDICATED_SESSION_CONFIRMATION_MSG, + f"
{DEDICATED_SESSION_CONFIRMATION_MSG}
", + ], + } + + +def _reset_kernel_state(kernel: MATLABKernelUsingMPM): + """ + Resets the kernel to its initial state for MATLAB session management. + + Args: + kernel (MATLABKernelUsingMPM): The MATLAB kernel instance whose state + needs to be reset. + """ + kernel.is_shared_matlab = True + kernel.is_matlab_assigned = False + kernel.startup_checks_completed = False + + +async def get_kernel_info(kernel): + """ + Provides information about the current MATLAB kernel state related to MATLAB. + + :param kernel: kernel object containing MATLAB information + """ + output = _format_info(kernel._get_kernel_info()) + yield { + "type": "execute_result", + "mimetype": ["text/plain", "text/html"], + "value": [ + output, + f"
{output}
", + ], + } + + +def _format_info(info) -> str: + """ + Formats MATLAB information into a formatted string. + + Args: + info: Dictionary containing MATLAB information. + + Returns: + str: Formatted string with MATLAB information. + """ + info_text = f'MATLAB Version: {info.get("matlab_version")}\n' + info_text += f'MATLAB Root Path: {info.get("matlab_root_path")}\n' + info_text += f'Licensing Mode: {LICENSING_MODES.get(info.get("licensing_mode"), "Unknown")}\n' + info_text += f'MATLAB Shared With Other Notebooks: {info.get("is_shared_matlab")}\n' + return info_text + + +class matlab(MATLABMagic): + info_about_magic = f""" + Starts a new MATLAB that is dedicated to the current kernel, instead of being shared across kernels. + + Usage: %%matlab {CMD_NEW_SESSION} or %%matlab {CMD_INFO} + + Note: To change from a shared MATLAB to a dedicated MATLAB after you have already run MATLAB code in a notebook, you must first restart the kernel. + """ + skip_matlab_execution = False + + def before_cell_execute(self): + """ + Processes the MATLAB magic command before cell execution. + + This method validates the parameters passed to the MATLAB magic command, + and yields appropriate callbacks based on the command type. + + Raises: + MagicError: If the number of parameters is not exactly one or if an unknown argument is provided. + + Yields: + dict: A dictionary containing callback information for the kernel to process. + The dictionary must contain a key called "type". Kernel injects itself into the callback function while + making the call. This ensures Kernel object is available to magic instance. + + Examples: To start a new matlab session or to display information about assigned MATLAB: + { + "type": "callback", + "callback_function": "function local to this module to be called from kernel", + } + """ + if len(self.parameters) != 1: + raise MagicError( + f"matlab magic expects only one argument. Received: {self.parameters}. Choose one of: {[arg for arg in self.get_supported_arguments()]}" + ) + + command = self.parameters[0] + # Handles "new_session" argument + if command == CMD_NEW_SESSION: + yield { + "type": "callback", + "callback_function": handle_new_matlab_session, + } + + # Handles "info" argument + elif command == CMD_INFO: + yield { + "type": "callback", + "callback_function": get_kernel_info, + } + + # Handles unknown arguments + else: + raise MagicError( + f"Unknown argument {command}. Choose one of: {[arg for arg in self.get_supported_arguments()]}" + ) + + def do_complete(self, parameters, parameter_pos, cursor_pos): + """ + Provides autocompletion for the matlab magic command. + + Args: + parameters (list): The parameters passed to the magic command + parameter_pos (int): The position of the parameter being completed + cursor_pos (int): The cursor position within the parameter + + Returns: + list: A list of possible completions + """ + matches = [] + if parameter_pos == 1: + # Show all the arguments under matlab magic + if cursor_pos == 0: + matches = self.get_supported_arguments() + # For partial input, match arguments that start with the current input + else: + matches = [ + s + for s in self.get_supported_arguments() + if s.startswith(parameters[0][:cursor_pos]) + ] + return matches + + def get_supported_arguments(self) -> list: + """ + Returns a list of supported arguments for the MATLAB magic command. + + Returns: + list: A list of supported arguments + """ + return [CMD_NEW_SESSION, CMD_INFO] diff --git a/src/jupyter_matlab_kernel/mpm_kernel.py b/src/jupyter_matlab_kernel/mpm_kernel.py index 1e2a9604..e52140a8 100644 --- a/src/jupyter_matlab_kernel/mpm_kernel.py +++ b/src/jupyter_matlab_kernel/mpm_kernel.py @@ -18,9 +18,6 @@ class MATLABKernelUsingMPM(base.BaseMATLABKernel): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Used to detect if this Kernel has been assigned a MATLAB-proxy server or not - self.is_matlab_assigned = False - # Serves as the auth token to secure communication between Jupyter Server and MATLAB proxy manager self.mpm_auth_token = None @@ -35,41 +32,9 @@ def __init__(self, *args, **kwargs): # ipykernel Interface API # https://ipython.readthedocs.io/en/stable/development/wrapperkernels.html - async def do_execute( - self, - code, - silent, - store_history=True, - user_expressions=None, - allow_stdin=False, - *, - cell_id=None, - ): - """ - Used by ipykernel infrastructure for execution. For more info, look at - https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute - """ - self.log.debug(f"Received execution request from Jupyter with code:\n{code}") - - # Starts the matlab proxy process if this kernel hasn't yet been assigned a - # matlab proxy and sets the attributes on kernel to talk to the correct backend. - if not self.is_matlab_assigned: - self.log.debug("Starting matlab-proxy") - await self._start_matlab_proxy_and_comm_helper() - self.is_matlab_assigned = True - - return await super().do_execute( - code=code, - silent=silent, - store_history=store_history, - user_expressions=user_expressions, - allow_stdin=allow_stdin, - cell_id=cell_id, - ) - async def do_shutdown(self, restart): self.log.debug("Received shutdown request from Jupyter") - if self.is_matlab_assigned: + if self.is_matlab_assigned and self.mwi_comm_helper: try: # Cleans up internal live editor state, client session await self.mwi_comm_helper.send_shutdown_request_to_matlab() @@ -80,25 +45,28 @@ async def do_shutdown(self, restart): f"Exception occurred while sending shutdown request to MATLAB:\n{e}" ) except Exception as e: - self.log.debug("Exception during shutdown", e) + self.log.debug("Exception during shutdown: %s", e) finally: - # Shuts down matlab assigned to this Kernel (based on satisfying certain criteria) - await mpm_lib.shutdown( - self.parent_pid, self.kernel_id, self.mpm_auth_token - ) - self.is_matlab_assigned = False + await self.cleanup_matlab_proxy() return super().do_shutdown(restart) + # Helper functions + + async def cleanup_matlab_proxy(self): + # Shuts down matlab-proxy and MATLAB assigned to this Kernel. + # matlab-proxy process is cleaned up when this Kernel process is the + # only reference to the assigned matlab-proxy instance + await mpm_lib.shutdown(self.parent_pid, self.kernel_id, self.mpm_auth_token) + self.is_matlab_assigned = False + async def perform_startup_checks(self): """Overriding base function to provide a different iframe source""" await super().perform_startup_checks( self.jupyter_base_url, f"{self.matlab_proxy_base_url}/" ) - # Helper functions - - async def _start_matlab_proxy_and_comm_helper(self) -> None: + async def start_matlab_proxy_and_comm_helper(self) -> None: """ Starts the MATLAB proxy using the proxy manager and fetches its status. """ @@ -137,7 +105,7 @@ async def _initialize_matlab_proxy_with_mpm(self, _logger: Logger): response = await mpm_lib.start_matlab_proxy_for_kernel( caller_id=self.kernel_id, parent_id=self.parent_pid, - is_shared_matlab=True, + is_shared_matlab=self.is_shared_matlab, base_url_prefix=self.jupyter_base_url, ) err = response.get("errors") diff --git a/src/jupyter_matlab_kernel/mwi_comm_helpers.py b/src/jupyter_matlab_kernel/mwi_comm_helpers.py index 95df8105..f3f54281 100644 --- a/src/jupyter_matlab_kernel/mwi_comm_helpers.py +++ b/src/jupyter_matlab_kernel/mwi_comm_helpers.py @@ -4,6 +4,8 @@ import http import json import pathlib +from dataclasses import dataclass +from typing import Optional import aiohttp from matlab_proxy.util.mwi.embedded_connector.helpers import ( @@ -27,6 +29,27 @@ def check_licensing_status(data): return licensing_status +@dataclass +class MATLABStatus: + """Represents the status of MATLAB and its licensing information. + + Attributes: + is_matlab_licensed (bool): Indicates whether MATLAB is properly licensed. + matlab_status (str): Current status of the MATLAB instance. + matlab_proxy_has_error (bool): Whether the MATLAB proxy has encountered an error. Defaults to False. + licensing_mode (str): The type of licensing being used. Defaults to an empty string. + matlab_version (str): Version of the MATLAB instance. Defaults to an empty string. + matlab_root_path (str): Root installation path of MATLAB. Defaults to an empty string. + """ + + is_matlab_licensed: bool + matlab_status: str + matlab_proxy_has_error: bool = False + licensing_mode: str = "" + matlab_version: str = "" + matlab_root_path: str = "" + + class MWICommHelper: def __init__( self, kernel_id, url, shell_loop, control_loop, headers=None, logger=_logger @@ -88,35 +111,84 @@ async def disconnect(self): if self._http_control_client: await self._http_control_client.close() - async def fetch_matlab_proxy_status(self): + async def fetch_matlab_root_path(self) -> Optional[str]: + """ + Fetches the MATLAB root path from the matlab-proxy server. + + Sends an HTTP GET request to the /get_env_config endpoint of matlab-proxy + to retrieve the filesystem path to the MATLAB installation. + + Returns: + Optional[str]: The filesystem path to the MATLAB installation root directory, + or None if the path could not be retrieved. + """ + self.logger.debug("Fetching MATLAB root path from matlab-proxy") + resp = await self._http_shell_client.get(self.url + "/get_env_config") + self.logger.debug( + f"Received status code for matlab-proxy get-env-config request: {resp.status}" + ) + + if resp.status == http.HTTPStatus.OK: + data = await resp.json() + self.logger.debug(f"get-env-config data:\n{data}") + matlab_data = data.get("matlab") or {} + return matlab_data.get("rootPath", None) + + self.logger.warning( + "Error occurred during retrieving environment config for matlab-proxy" + ) + return None + + async def fetch_matlab_proxy_status(self) -> Optional[MATLABStatus]: """ - Sends HTTP request to /get_status endpoint of matlab-proxy and returns - license and MATLAB status. + Fetches the current status of the MATLAB proxy server. + + Sends an HTTP GET request to the /get_status endpoint of matlab-proxy + to retrieve information about MATLAB licensing, runtime status, and any + errors that may have occurred. Returns: - Tuple (bool, string): - is_matlab_licensed (bool): True if matlab-proxy has license information, else False. - matlab_status (string): Status of MATLAB. Values could be "up", "down" and "starting" - matlab_proxy_has_error (bool): True if matlab-proxy faced any issues and unable to - start MATLAB + Optional[MATLABStatus]: A MATLABStatus object containing: + - is_matlab_licensed (bool): True if MATLAB has valid license + information, False otherwise. + - matlab_status (str): Current MATLAB state. Possible values: + "up" (running), "down" (stopped). + - matlab_proxy_has_error (bool): True if matlab-proxy encountered + errors preventing MATLAB startup, False otherwise. + - licensing_mode (str): The type of licensing being used + (e.g., "mhlm", "nlm", "existing_license"). + - matlab_version (str): The version string of the MATLAB installation + (e.g., "R2024a"). + Raises: - HTTPError: Occurs when connection to matlab-proxy cannot be established. + HTTPError: If the HTTP request fails or matlab-proxy returns a + non-200 status code, indicating connection issues or + server errors. + + Example: + >>> status = await comm_helper.fetch_matlab_proxy_status() + >>> if status and status.matlab_status == "up": + ... print(f"MATLAB {status.matlab_version} is running") """ self.logger.debug("Fetching matlab-proxy status") resp = await self._http_shell_client.get(self.url + "/get_status") self.logger.debug(f"Received status code: {resp.status}") if resp.status == http.HTTPStatus.OK: data = await resp.json() - self.logger.debug(f"Response:\n{data}") - is_matlab_licensed = check_licensing_status(data) - - matlab_status = data["matlab"]["status"] - matlab_proxy_has_error = data["error"] is not None - return is_matlab_licensed, matlab_status, matlab_proxy_has_error + self.logger.debug(f"matlab-proxy status:\n{data}") + matlab_data = data.get("matlab") or {} + return MATLABStatus( + is_matlab_licensed=check_licensing_status(data), + matlab_status=matlab_data.get("status", ""), + matlab_proxy_has_error=data.get("error") is not None, + licensing_mode=(data.get("licensing") or {}).get("type", ""), + matlab_version=matlab_data.get("version", ""), + ) else: self.logger.error("Error occurred during communication with matlab-proxy") resp.raise_for_status() + return None async def send_execution_request_to_matlab(self, code): """ @@ -281,7 +353,7 @@ async def _send_feval_request_to_matlab(self, http_client, fname, nargout, *args ) else: self.logger.error( - f'Error during execution of FEval request in MATLAB:\n{feval_response["messageFaults"][0]["message"]}' + f"Error during execution of FEval request in MATLAB:\n{feval_response['messageFaults'][0]['message']}" ) error_message = "Failed to execute. Please try again." raise Exception(error_message) diff --git a/src/jupyter_matlab_kernel/mwi_exceptions.py b/src/jupyter_matlab_kernel/mwi_exceptions.py index 58671158..da45bc46 100644 --- a/src/jupyter_matlab_kernel/mwi_exceptions.py +++ b/src/jupyter_matlab_kernel/mwi_exceptions.py @@ -1,4 +1,4 @@ -# Copyright 2024 The MathWorks, Inc. +# Copyright 2024-2025 The MathWorks, Inc. # Custom Exceptions used in MATLAB Kernel @@ -38,5 +38,5 @@ class MATLABConnectionError(Exception): def __init__(self, message=None): if message is None: - message = 'Error connecting to MATLAB. Check the status of MATLAB by clicking the "Open MATLAB" button. Retry after ensuring MATLAB is running successfully' + message = 'Error connecting to MATLAB. Check the status of MATLAB by clicking the "Open MATLAB" button. Retry after ensuring MATLAB is running successfully.' super().__init__(message) diff --git a/src/jupyter_matlab_labextension/src/plugins/matlabToolbarButton.ts b/src/jupyter_matlab_labextension/src/plugins/matlabToolbarButton.ts index abefac94..d0cc0dfd 100644 --- a/src/jupyter_matlab_labextension/src/plugins/matlabToolbarButton.ts +++ b/src/jupyter_matlab_labextension/src/plugins/matlabToolbarButton.ts @@ -12,18 +12,77 @@ import { PageConfig } from '@jupyterlab/coreutils'; import { DocumentRegistry } from '@jupyterlab/docregistry'; import { INotebookModel, NotebookPanel } from '@jupyterlab/notebook'; -import { IDisposable } from '@lumino/disposable'; +import { DisposableDelegate } from '@lumino/disposable'; import { matlabIcon } from '../icons'; +function createMATLABToolbarButton (targetUrl: string): ToolbarButton { + return new ToolbarButton({ + className: 'openMATLABButton matlab-toolbar-button-spaced', + icon: matlabIcon, + label: 'Open MATLAB', + tooltip: 'Open MATLAB', + onClick: (): void => { + window.open(targetUrl, '_blank'); + } + }); +} + /** Wait until the kernel has loaded, then check if it is a MATLAB kernel. */ -export const insertButton = async ( - panel: NotebookPanel, - matlabToolbarButton: ToolbarButton -): Promise => { - await panel.sessionContext.ready; - if (panel.sessionContext.kernelDisplayName === 'MATLAB Kernel') { - panel.toolbar.insertItem(10, 'matlabToolbarButton', matlabToolbarButton); +export const insertButton = async (panel: NotebookPanel): Promise => { + try { + await panel.sessionContext.ready; + let targetUrl = ''; + let matlabToolbarButton: ToolbarButton | null = null; + + // Function to update the target URL based on kernel ID + const updateTargetUrl = (): void => { + // Check if the kernel is a MATLAB Kernel + if (panel.sessionContext.kernelDisplayName === 'MATLAB Kernel') { + let kernelId = ''; + + // Check that session and kernel exist and then retrieve kernel ID + if (panel.sessionContext.session && panel.sessionContext.session.kernel) { + kernelId = panel.sessionContext.session.kernel.id; + } + + if (kernelId !== '') { + targetUrl = PageConfig.getBaseUrl() + 'matlab/' + kernelId + '/'; + + // Create the button if it doesn't exist yet + if (!matlabToolbarButton) { + matlabToolbarButton = createMATLABToolbarButton(targetUrl); + panel.toolbar.insertItem(10, 'matlabToolbarButton', matlabToolbarButton); + } else { + // Update the button's onClick handler + matlabToolbarButton.onClick = () => { + window.open(targetUrl, '_blank'); + }; + } + } + } + }; + + // Create Open MATLAB toolbar button + updateTargetUrl(); + + // Listen for kernel changes + panel.sessionContext.kernelChanged.connect(() => { + updateTargetUrl(); + }); + + // Create a disposable that will clean up the listener + return new DisposableDelegate(() => { + if (matlabToolbarButton) { + matlabToolbarButton.dispose(); + } + panel.sessionContext.kernelChanged.disconnect(() => { + updateTargetUrl(); + }); + }); + } catch (error) { + console.error('Failed to insert MATLAB toolbar button: ', error); + return new DisposableDelegate(() => {}); } }; @@ -32,21 +91,13 @@ implements DocumentRegistry.IWidgetExtension { createNew ( panel: NotebookPanel, context: DocumentRegistry.IContext - ): IDisposable { + ): DisposableDelegate { /** Create the toolbar button to open MATLAB in a browser. */ - const matlabToolbarButton = new ToolbarButton({ - className: 'openMATLABButton', - icon: matlabIcon, - label: 'Open MATLAB', - tooltip: 'Open MATLAB', - onClick: (): void => { - const baseUrl = PageConfig.getBaseUrl(); - // "_blank" is the option to open in a new browser tab - window.open(baseUrl + 'matlab', '_blank'); - } + insertButton(panel).catch(error => { + console.error('Error inserting MATLAB toolbar button:', error); }); - insertButton(panel, matlabToolbarButton); - return matlabToolbarButton; + // Return a dummy disposable immediately + return new DisposableDelegate(() => {}); } } diff --git a/src/jupyter_matlab_labextension/src/tests/matlabToolbarButton.test.ts b/src/jupyter_matlab_labextension/src/tests/matlabToolbarButton.test.ts index 97fff9c9..c2167ac3 100644 --- a/src/jupyter_matlab_labextension/src/tests/matlabToolbarButton.test.ts +++ b/src/jupyter_matlab_labextension/src/tests/matlabToolbarButton.test.ts @@ -9,6 +9,7 @@ import { import { NotebookPanel, INotebookModel } from '@jupyterlab/notebook'; import { JupyterFrontEnd } from '@jupyterlab/application'; import { DocumentRegistry } from '@jupyterlab/docregistry'; +import { Signal } from '@lumino/signaling'; jest.mock('../icons', () => ({ matlabIcon: { @@ -17,9 +18,6 @@ jest.mock('../icons', () => ({ } })); -// Get the mocked matlabIcon -const { matlabIcon } = jest.requireMock('../icons'); - // Mock JupyterLab dependencies jest.mock('@jupyterlab/apputils', () => ({ ToolbarButton: jest.fn().mockImplementation((options: any) => ({ @@ -38,36 +36,34 @@ jest.mock('@jupyterlab/coreutils', () => ({ const originalWindowOpen = window.open; window.open = jest.fn(); -// Mock for NotebookPanel -const createMockNotebookPanel = (kernelDisplayName = 'MATLAB Kernel') => ({ - sessionContext: { - ready: Promise.resolve(), - kernelDisplayName, - session: null, - initialize: jest.fn(), - isReady: true, - isTerminating: false, - // Add other required methods as - dispose: jest.fn() - }, - toolbar: { - insertItem: jest.fn(), - names: [] - // addItem: jest.fn(), - // insertAfter: jest.fn(), - // insertBefore: jest.fn() - } -}); - -// Mock for ToolbarButton -const createMockToolbarButton = () => ({ - className: 'openMATLABButton', - icon: matlabIcon, - label: 'Open MATLAB', - tooltip: 'Open MATLAB', - onClick: expect.any(Function), - dispose: jest.fn() -}); +// Mock for NotebookPanel with kernel change signal +const createMockNotebookPanel = (kernelDisplayName = 'MATLAB Kernel', kernelId = '12345') => { + const kernelChangedSignal = new Signal({}); + + return { + sessionContext: { + ready: Promise.resolve(), + kernelDisplayName, + session: kernelId + ? { + kernel: { + id: kernelId + } + } + : null, + kernelChanged: kernelChangedSignal, + initialize: jest.fn(), + isReady: true, + isTerminating: false, + // Add other required methods as + dispose: jest.fn() + }, + toolbar: { + insertItem: jest.fn(), + names: [] + } + }; +}; // Mock for JupyterFrontEnd const createMockJupyterFrontEnd = () => ({ @@ -91,34 +87,84 @@ describe('matlab_browser_button', () => { }); describe('insertButton', () => { - test('should insert button when kernel is MATLAB Kernel', async () => { + test('should insert button when kernel is MATLAB Kernel with valid kernel ID', async () => { // Arrange - const panel = createMockNotebookPanel('MATLAB Kernel'); - const button = createMockToolbarButton(); + const panel = createMockNotebookPanel('MATLAB Kernel', 'test-kernel-123'); // Act - await insertButton(panel as unknown as NotebookPanel, button as any); + await insertButton(panel as unknown as NotebookPanel); // Assert expect(panel.toolbar!.insertItem).toHaveBeenCalledWith( 10, 'matlabToolbarButton', - button + expect.objectContaining({ + className: 'openMATLABButton matlab-toolbar-button-spaced', + label: 'Open MATLAB' + }) ); }); test('should not insert button when kernel is not MATLAB Kernel', async () => { // Arrange const panel = createMockNotebookPanel('Python 3'); - const button = createMockToolbarButton(); // Act - await insertButton(panel as unknown as NotebookPanel, button as any); + await insertButton(panel as unknown as NotebookPanel); // Assert expect(panel.toolbar!.insertItem).not.toHaveBeenCalled(); }); + test('should not insert button when kernel ID is empty', async () => { + const panel = createMockNotebookPanel('MATLAB Kernel', ''); + + await insertButton(panel as unknown as NotebookPanel); + + expect(panel.toolbar.insertItem).not.toHaveBeenCalled(); + }); + + test('should not insert button when session is null', async () => { + const panel = createMockNotebookPanel('MATLAB Kernel'); + panel.sessionContext.session = null; + + await insertButton(panel as unknown as NotebookPanel); + + expect(panel.toolbar.insertItem).not.toHaveBeenCalled(); + }); + + test('should not insert button when kernel is null', async () => { + const panel = createMockNotebookPanel('MATLAB Kernel'); + panel.sessionContext.session = { kernel: null as any }; + + await insertButton(panel as unknown as NotebookPanel); + + expect(panel.toolbar.insertItem).not.toHaveBeenCalled(); + }); + + test('should construct correct target URL with kernel ID', async () => { + const ToolbarButtonMock = jest.requireMock('@jupyterlab/apputils').ToolbarButton; + let capturedOnClick: () => void = () => {}; + + ToolbarButtonMock.mockImplementationOnce((options: any) => { + capturedOnClick = options.onClick; + return { + ...options, + dispose: jest.fn() + }; + }); + + const panel = createMockNotebookPanel('MATLAB Kernel', 'kernel-abc-123'); + await insertButton(panel as unknown as NotebookPanel); + + capturedOnClick(); + + expect(window.open).toHaveBeenCalledWith( + 'http://localhost:8888/matlab/kernel-abc-123/', + '_blank' + ); + }); + test('should wait for session context to be ready before checking kernel', async () => { // Arrange const readyPromise = new Promise((resolve) => @@ -127,16 +173,21 @@ describe('matlab_browser_button', () => { const panel = { sessionContext: { ready: readyPromise, - kernelDisplayName: 'MATLAB Kernel' + kernelDisplayName: 'MATLAB Kernel', + session: { + kernel: { + id: 'test-kernel' + } + }, + kernelChanged: new Signal({}) }, toolbar: { insertItem: jest.fn() } }; - const button = createMockToolbarButton(); // Act - const insertPromise = insertButton(panel as any, button as any); + const insertPromise = insertButton(panel as any); // Assert - insertItem should not be called before ready resolves expect(panel.toolbar.insertItem).not.toHaveBeenCalled(); @@ -145,11 +196,50 @@ describe('matlab_browser_button', () => { await insertPromise; // Now insertItem should have been called - expect(panel.toolbar.insertItem).toHaveBeenCalledWith( - 10, - 'matlabToolbarButton', - button + expect(panel.toolbar.insertItem).toHaveBeenCalled(); + }); + + test('should update button onClick when kernel changes', async () => { + const ToolbarButtonMock = jest.requireMock('@jupyterlab/apputils').ToolbarButton; + const mockButton = { + onClick: jest.fn(), + dispose: jest.fn() + }; + + ToolbarButtonMock.mockReturnValue(mockButton); + + const panel = createMockNotebookPanel('MATLAB Kernel', 'kernel-1'); + await insertButton(panel as unknown as NotebookPanel); + + // Simulate kernel change + panel.sessionContext.session = { + kernel: { id: 'kernel-2' } + }; + panel.sessionContext.kernelChanged.emit({}); + + // Wait for async operations + await new Promise(resolve => setTimeout(resolve, 0)); + + expect(mockButton.onClick).toBeDefined(); + }); + + test('should handle errors gracefully', async () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + const panel = { + sessionContext: { + ready: Promise.reject(new Error('Session failed')) + } + }; + + const result = await insertButton(panel as any); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Failed to insert MATLAB toolbar button: ', + expect.any(Error) ); + expect(result.dispose).toBeDefined(); + + consoleErrorSpy.mockRestore(); }); }); @@ -160,25 +250,10 @@ describe('matlab_browser_button', () => { beforeEach(() => { extension = new MatlabToolbarButtonExtension(); - panel = createMockNotebookPanel(); + panel = createMockNotebookPanel('MATLAB Kernel', 'test-kernel'); context = {}; }); - test('should create a toolbar button with correct properties', () => { - // Act - const result = extension.createNew(panel, context); - - // Assert - expect(result).toEqual( - expect.objectContaining({ - className: 'openMATLABButton', - icon: matlabIcon, - label: 'Open MATLAB', - tooltip: 'Open MATLAB' - }) - ); - }); - test('should return a disposable object', () => { // Act const result = extension.createNew(panel, context); @@ -188,35 +263,20 @@ describe('matlab_browser_button', () => { expect(typeof result.dispose).toBe('function'); }); - test('button onClick should open MATLAB in a new tab', () => { - // Arrange - const ToolbarButtonMock = jest.requireMock( - '@jupyterlab/apputils' - ).ToolbarButton; - let capturedOnClick: () => void = () => {}; // Initialize with empty function - - // Capture the onClick handler when ToolbarButton is constructed - ToolbarButtonMock.mockImplementationOnce((options: any) => { - capturedOnClick = options.onClick; - return { - ...options, - dispose: jest.fn() - }; - }); + test('should call insertButton when createNew is invoked', () => { + const matlabButtonModule = require('../plugins/matlabToolbarButton'); + const spy = jest + .spyOn(matlabButtonModule, 'insertButton') + .mockResolvedValue({ dispose: jest.fn() }); - // Act extension.createNew( - panel as unknown as NotebookPanel, - context as unknown as DocumentRegistry.IContext + panel as unknown as NotebookPanel, + context as unknown as DocumentRegistry.IContext ); - // Manually call the onClick handler - capturedOnClick(); - // Assert - expect(window.open).toHaveBeenCalledWith( - 'http://localhost:8888/matlab', - '_blank' - ); + expect(spy).toHaveBeenCalledWith(panel); + + spy.mockRestore(); }); test('should call insertButton with panel and button', () => { @@ -228,13 +288,13 @@ describe('matlab_browser_button', () => { .mockImplementation(() => Promise.resolve()); // Act - const button = extension.createNew( - panel as unknown as NotebookPanel, - context as unknown as DocumentRegistry.IContext + extension.createNew( + panel as unknown as NotebookPanel, + context as unknown as DocumentRegistry.IContext ); // Assert - expect(spy).toHaveBeenCalledWith(panel, button); + expect(spy).toHaveBeenCalledWith(panel); // Cleanup spy.mockRestore(); diff --git a/src/jupyter_matlab_labextension/style/base.css b/src/jupyter_matlab_labextension/style/base.css index e11f4577..de8fe6aa 100644 --- a/src/jupyter_matlab_labextension/style/base.css +++ b/src/jupyter_matlab_labextension/style/base.css @@ -1,5 +1,11 @@ +/* # Copyright 2024-2025 The MathWorks, Inc. */ /* See the JupyterLab Developer Guide for useful CSS Patterns: https://jupyterlab.readthedocs.io/en/stable/developer/css.html */ + +/* Adds spacing between toolbar icon and label */ +.matlab-toolbar-button-spaced .jp-ToolbarButtonComponent-label { + margin-left: 4px; +} \ No newline at end of file diff --git a/tests/integration/utils/integration_test_utils.py b/tests/integration/utils/integration_test_utils.py index 40a1be08..9f37daf2 100644 --- a/tests/integration/utils/integration_test_utils.py +++ b/tests/integration/utils/integration_test_utils.py @@ -1,4 +1,4 @@ -# Copyright 2023-2024 The MathWorks, Inc. +# Copyright 2023-2025 The MathWorks, Inc. # Utility functions for integration testing of jupyter-matlab-proxy import asyncio @@ -24,7 +24,7 @@ def perform_basic_checks(): # Check if MATLAB is in the system path assert matlab_path is not None, "MATLAB is not in system path" - # Check if MATLAB verison is >= R2020b + # Check if MATLAB version is >= R2020b assert ( matlab_proxy.settings.get_matlab_version(matlab_path) >= "R2020b" ), "MATLAB version should be R2020b or later" @@ -83,35 +83,33 @@ async def wait_matlab_proxy_ready(matlab_proxy_url): from jupyter_matlab_kernel.mwi_comm_helpers import MWICommHelper - is_matlab_licensed = False - matlab_status = "down" start_time = time.time() loop = asyncio.get_event_loop() - matlab_proxy = MWICommHelper("", matlab_proxy_url, loop, loop, {}) - await matlab_proxy.connect() + comm_helper = MWICommHelper("", matlab_proxy_url, loop, loop, {}) + await comm_helper.connect() + matlab_proxy_status = await comm_helper.fetch_matlab_proxy_status() # Poll for matlab-proxy to be up - while matlab_status in ["down", "starting"] and ( - time.time() - start_time < MATLAB_STARTUP_TIMEOUT + while ( + matlab_proxy_status + and matlab_proxy_status.matlab_status in ["down", "starting"] + and (time.time() - start_time < MATLAB_STARTUP_TIMEOUT) + and not matlab_proxy_status.matlab_proxy_has_error ): time.sleep(1) try: - ( - is_matlab_licensed, - matlab_status, - _, - ) = await matlab_proxy.fetch_matlab_proxy_status() + matlab_proxy_status = await comm_helper.fetch_matlab_proxy_status() except Exception: # The network connection can be flaky while the # matlab-proxy server is booting. There can also be some # intermediate connection errors pass - assert is_matlab_licensed is True, "MATLAB is not licensed" + assert matlab_proxy_status.is_matlab_licensed is True, "MATLAB is not licensed" assert ( - matlab_status == "up" - ), f"matlab-proxy process did not start successfully\nMATLAB Status is '{matlab_status}'" - await matlab_proxy.disconnect() + matlab_proxy_status.matlab_status == "up" + ), f"matlab-proxy process did not start successfully\nMATLAB Status is '{matlab_proxy_status.matlab_status}'" + await comm_helper.disconnect() def get_random_free_port() -> str: @@ -184,13 +182,14 @@ def license_matlab_proxy(matlab_proxy_url): status_info, "Verify if Licensing is successful. This might fail if incorrect credentials are provided", ).to_be_visible(timeout=60000) - except: + except Exception as e: # Grab screenshots log_dir = "./" file_name = "licensing-screenshot-failed.png" file_path = os.path.join(log_dir, file_name) os.makedirs(log_dir, exist_ok=True) page.screenshot(path=file_path) + print("Exception: %s", str(e)) finally: browser.close() diff --git a/tests/unit/jupyter_matlab_kernel/magics/test_matlab.py b/tests/unit/jupyter_matlab_kernel/magics/test_matlab.py new file mode 100644 index 00000000..e40efa1c --- /dev/null +++ b/tests/unit/jupyter_matlab_kernel/magics/test_matlab.py @@ -0,0 +1,202 @@ +# Copyright 2025 The MathWorks, Inc. + +import pytest + +from jupyter_matlab_kernel.magics.help import help +from jupyter_matlab_kernel.magics.matlab import ( + CMD_INFO, + CMD_NEW_SESSION, + DEDICATED_SESSION_CONFIRMATION_MSG, + EXISTING_NEW_SESSION_ERROR, + get_kernel_info, + handle_new_matlab_session, + matlab, +) +from jupyter_matlab_kernel.mwi_exceptions import MagicError + + +def test_help_magic(): + magic_object = help([matlab.__name__]) + before_cell_executor = magic_object.before_cell_execute() + output = next(before_cell_executor) + expected_output = matlab.info_about_magic + assert expected_output in output["value"][0] + + +@pytest.mark.parametrize( + "parameters", + [ + pytest.param([], id="atleast one argument is required"), + pytest.param( + ["invalid"], + id="Invalid argument should throw exception", + ), + pytest.param( + [CMD_INFO, CMD_NEW_SESSION], + id="more than one parameter should throw exception", + ), + ], +) +def test_matlab_magic_exceptions(parameters): + magic_object = matlab(parameters) + before_cell_executor = magic_object.before_cell_execute() + with pytest.raises(MagicError): + next(before_cell_executor) + + +@pytest.mark.parametrize( + "parameters, parameter_pos, cursor_pos, expected_output", + [ + pytest.param( + ["n"], + 1, + 1, + {"new_session"}, + id="n as parameter with parameter and cursor position as 1", + ), + pytest.param( + [""], + 1, + 1, + {"new_session", "info"}, + id="no parameter with parameter and cursor position as 1", + ), + pytest.param( + ["in"], + 1, + 2, + {"info"}, + id="i as parameter with parameter position as 1 and cursor position as 2", + ), + pytest.param( + ["i"], + 2, + 1, + set([]), + id="t as parameter with parameter position as 2 and cursor position as 1", + ), + pytest.param( + ["magic"], + 1, + 4, + set([]), + id="magic as parameter with parameter position as 1 and cursor position as 4", + ), + ], +) +def test_do_complete_in_matlab_magic( + parameters, parameter_pos, cursor_pos, expected_output +): + magic_object = matlab() + output = magic_object.do_complete(parameters, parameter_pos, cursor_pos) + assert expected_output.issubset(set(output)) + + +async def test_new_session_in_matlab_magic_while_already_in_new_session(mocker): + """ + Test that an appropriate message is displayed when trying to create a new session while already in a new session. + + This test verifies that when a %%matlab magic command with new_session option is executed + while MATLAB is already assigned to the kernel in a new session, an appropriate error message + is displayed indicating that the notebook is already linked to a new MATLAB session. + """ + mock_kernel = mocker.MagicMock() + mock_kernel.is_matlab_assigned = True + mock_kernel.is_shared_matlab = False + output = [] + async for result in handle_new_matlab_session(mock_kernel): + output.append(result) + + assert output is not None + assert EXISTING_NEW_SESSION_ERROR in output[0]["value"][0] + + +async def test_new_session_in_matlab_magic_while_already_in_shared_session(mocker): + """ + Test that an exception is raised when trying to switch from shared session to new session. + + This test verifies that when a %%matlab magic command with new_session option is executed + while MATLAB is already assigned to the kernel, an appropriate exception is raised + with a message indicating that the notebook is already linked to a MATLAB session. + """ + mock_kernel = mocker.MagicMock() + mock_kernel.is_matlab_assigned = True + mock_kernel.is_shared_matlab = True + with pytest.raises(Exception) as excinfo: + async for _ in handle_new_matlab_session(mock_kernel): + pass + + assert "linked to Default MATLAB session" in str(excinfo.value) + + +async def test_handle_new_matlab_session_success(mocker): + """ + Test that MATLAB proxy is started correctly when using MATLAB magic command. + + This test verifies that when a %%matlab magic command with new_session option is executed, + the kernel properly starts the MATLAB proxy, assigns MATLAB to the kernel + (is_matlab_assigned=True), and sets the shared MATLAB flag to False. + """ + mock_kernel = mocker.AsyncMock() + mock_kernel.is_matlab_assigned = False + output = [] + async for result in handle_new_matlab_session(mock_kernel): + output.append(result) + + assert output is not None + mock_kernel.start_matlab_proxy_and_comm_helper.assert_called_once() + mock_kernel.perform_startup_checks.assert_called_once() + mock_kernel.cleanup_matlab_proxy.assert_not_called() + assert mock_kernel.is_matlab_assigned is True + assert mock_kernel.is_shared_matlab is False + assert DEDICATED_SESSION_CONFIRMATION_MSG in output[0]["value"][0] + + +async def test_handle_new_matlab_session_raises_exception(mocker): + """ + Test that exceptions during MATLAB magic command execution are handled properly. + + This test verifies that when an exception occurs during the startup of the MATLAB proxy + (triggered by a %%matlab magic command), the kernel properly handles the error and + maintains the expected state (is_matlab_assigned=False, is_shared_matlab=True). + """ + mock_kernel = mocker.AsyncMock() + mock_kernel.is_matlab_assigned = False + output = [] + with pytest.raises(Exception): + async for result in handle_new_matlab_session(mock_kernel): + output.append(result) + + assert output is not None + mock_kernel.start_matlab_proxy_and_comm_helper.assert_called_once() + mock_kernel.perform_startup_checks.side_effect = Exception( + "MATLAB Connection Error" + ) + mock_kernel.cleanup_matlab_proxy.assert_called_once() + assert mock_kernel.is_matlab_assigned is False + assert mock_kernel.is_shared_matlab is True + + +@pytest.mark.parametrize( + "shared_matlab_status, expected_output", + [ + (True, "MATLAB Shared With Other Notebooks: True"), + (False, "MATLAB Shared With Other Notebooks: False"), + ], + ids=["Shared MATLAB", "Non-shared MATLAB"], +) +async def test_get_kernel_info_in_matlab_magic( + shared_matlab_status, expected_output, mocker +): + mock_kernel = mocker.MagicMock() + mock_kernel._get_kernel_info.return_value = { + "is_shared_matlab": shared_matlab_status, + "matlab_version": "R2025b", + "matlab_root_path": "/path/to/matlab", + "licensing_mode": "existing_license", + } + output = [] + async for result in get_kernel_info(mock_kernel): + output.append(result) + assert output is not None + assert expected_output in output[0]["value"][0] diff --git a/tests/unit/jupyter_matlab_kernel/test_kernel.py b/tests/unit/jupyter_matlab_kernel/test_kernel.py index 18ef551b..d0591ad6 100644 --- a/tests/unit/jupyter_matlab_kernel/test_kernel.py +++ b/tests/unit/jupyter_matlab_kernel/test_kernel.py @@ -6,10 +6,8 @@ from jupyter_server import serverapp from mocks.mock_jupyter_server import MockJupyterServerFixture -from jupyter_matlab_kernel.jsp_kernel import ( - MATLABKernelUsingJSP, - start_matlab_proxy, -) +from jupyter_matlab_kernel.jsp_kernel import start_matlab_proxy +from jupyter_matlab_kernel.mpm_kernel import MATLABKernelUsingMPM from jupyter_matlab_kernel.mwi_exceptions import MATLABConnectionError @@ -98,19 +96,24 @@ async def test_matlab_not_licensed_non_jupyter(mocker): exception (MATLABConnectionError) is raised when performing startup checks. """ # Mock the kernel's jupyter_base_url attribute to simulate a non-Jupyter environment - kernel = mocker.MagicMock(spec=MATLABKernelUsingJSP) + kernel = mocker.MagicMock(spec=MATLABKernelUsingMPM) kernel.jupyter_base_url = None kernel.startup_error = None + kernel.matlab_proxy_base_url = "/test" + + matlab_status_mock = mocker.Mock() + matlab_status_mock.is_matlab_licensed = False + matlab_status_mock.matlab_status = "down" + matlab_status_mock.matlab_proxy_has_error = False + kernel.mwi_comm_helper = mocker.Mock() kernel.mwi_comm_helper.fetch_matlab_proxy_status = mocker.AsyncMock( - return_value=(False, "down", False) + return_value=matlab_status_mock ) # Mock the perform_startup_checks method to actually call the implementation - async def mock_perform_startup_checks(*args, **kwargs): - return await MATLABKernelUsingJSP.perform_startup_checks( - kernel, *args, **kwargs - ) + async def mock_perform_startup_checks(): + return await MATLABKernelUsingMPM.perform_startup_checks(kernel) kernel.perform_startup_checks.side_effect = mock_perform_startup_checks diff --git a/tests/unit/jupyter_matlab_kernel/test_mpm_kernel.py b/tests/unit/jupyter_matlab_kernel/test_mpm_kernel.py index 1bf01d1b..f797d376 100644 --- a/tests/unit/jupyter_matlab_kernel/test_mpm_kernel.py +++ b/tests/unit/jupyter_matlab_kernel/test_mpm_kernel.py @@ -26,7 +26,6 @@ def mpm_kernel_instance(mocker) -> MATLABKernelUsingMPM: return MATLABKernelUsingMPM() -@pytest.mark.asyncio async def test_initialize_matlab_proxy_with_mpm_success(mocker, mpm_kernel_instance): mpm_lib_start_matlab_proxy_response = { "absolute_url": "dummyURL", @@ -166,3 +165,27 @@ async def test_do_shutdown_exception(mocker, mpm_kernel_instance): mpm_kernel_instance.mpm_auth_token, ) assert not mpm_kernel_instance.is_matlab_assigned + + +async def test_matlab_proxy_assignment_on_executing_matlab_command( + mocker, mpm_kernel_instance +): + """ + Test that MATLAB proxy is assigned when executing a MATLAB command. + + This test verifies that when a regular MATLAB command is executed while MATLAB + is not yet assigned, the kernel properly starts the MATLAB proxy and assigns + MATLAB to the kernel (is_matlab_assigned=True). + """ + code = "why" + mpm_kernel_instance.is_matlab_assigned = False + # Patch kernel instance to mock start_matlab_proxy_and_comm_helper method + mock_start_matlab_proxy = mocker.patch.object( + mpm_kernel_instance, + "start_matlab_proxy_and_comm_helper", + autospec=True, + ) + + await mpm_kernel_instance.do_execute(code, silent=True) + mock_start_matlab_proxy.assert_called_once() + assert mpm_kernel_instance.is_matlab_assigned is True diff --git a/tests/unit/jupyter_matlab_kernel/test_mwi_comm_helpers.py b/tests/unit/jupyter_matlab_kernel/test_mwi_comm_helpers.py index 4b7ee6f4..786dbd47 100644 --- a/tests/unit/jupyter_matlab_kernel/test_mwi_comm_helpers.py +++ b/tests/unit/jupyter_matlab_kernel/test_mwi_comm_helpers.py @@ -23,7 +23,7 @@ @pytest.fixture -async def matlab_proxy_fixture(): +async def comm_helper_fixture(): url = "http://localhost" headers = {} kernel_id = "" @@ -36,7 +36,7 @@ async def matlab_proxy_fixture(): # Testing fetch_matlab_proxy_status async def test_fetch_matlab_proxy_status_unauth_request( - monkeypatch, matlab_proxy_fixture + monkeypatch, comm_helper_fixture ): """ This test checks that fetch_matlab_proxy_status throws an exception @@ -48,7 +48,7 @@ async def mock_get(*args, **kwargs): monkeypatch.setattr(aiohttp.ClientSession, "get", mock_get) with pytest.raises(aiohttp.client_exceptions.ClientError) as exceptionInfo: - await matlab_proxy_fixture.fetch_matlab_proxy_status() + await comm_helper_fixture.fetch_matlab_proxy_status() assert MockUnauthorisedRequestResponse().exception_msg in str(exceptionInfo.value) @@ -63,7 +63,7 @@ async def mock_get(*args, **kwargs): ], ) async def test_fetch_matlab_proxy_status( - input_lic_type, expected_license_status, monkeypatch, matlab_proxy_fixture + input_lic_type, expected_license_status, monkeypatch, comm_helper_fixture ): """ This test checks that fetch_matlab_proxy_status returns the correct @@ -77,17 +77,35 @@ async def mock_get(*args, **kwargs): monkeypatch.setattr(aiohttp.ClientSession, "get", mock_get) - ( - is_matlab_licensed, - matlab_status, - matlab_proxy_has_error, - ) = await matlab_proxy_fixture.fetch_matlab_proxy_status() - assert is_matlab_licensed == expected_license_status - assert matlab_status == "up" - assert matlab_proxy_has_error is False + matlab_proxy_status = await comm_helper_fixture.fetch_matlab_proxy_status() + assert matlab_proxy_status.is_matlab_licensed == expected_license_status + assert matlab_proxy_status.matlab_status == "up" + assert matlab_proxy_status.matlab_proxy_has_error is False -async def test_interrupt_request_bad_request(monkeypatch, matlab_proxy_fixture): +async def test_fetch_matlab_root_path(mocker, comm_helper_fixture): + """ + This test checks that fetch_matlab_root_path returns the correct matlab root path. + """ + mock_response = mocker.AsyncMock() + mock_response.status = http.HTTPStatus.OK + mock_response.json = mocker.AsyncMock( + return_value={ + "matlab": { + "rootPath": "test_root_path", + "version": "test_version", + } + }, + ) + mocker.patch( + "aiohttp.ClientSession.get", new=mocker.AsyncMock(return_value=mock_response) + ) + + matlab_root_path = await comm_helper_fixture.fetch_matlab_root_path() + assert matlab_root_path == "test_root_path" + + +async def test_interrupt_request_bad_request(monkeypatch, comm_helper_fixture): """ This test checks that send_interrupt_request_to_matlab raises an exception if the response to the HTTP post is not valid. @@ -101,12 +119,12 @@ async def mock_post(*args, **kwargs): monkeypatch.setattr(aiohttp.ClientSession, "post", mock_post) with pytest.raises(aiohttp.client_exceptions.ClientError) as exceptionInfo: - await matlab_proxy_fixture.send_interrupt_request_to_matlab() + await comm_helper_fixture.send_interrupt_request_to_matlab() assert mock_exception_message in str(exceptionInfo.value) # Testing send_execution_request_to_matlab -async def test_execution_request_bad_request(monkeypatch, matlab_proxy_fixture): +async def test_execution_request_bad_request(monkeypatch, comm_helper_fixture): """ This test checks that send_execution_request_to_matlab throws an exception if the response to the HTTP request is invalid. @@ -120,12 +138,12 @@ async def mock_post(*args, **kwargs): code = "placeholder for code" with pytest.raises(aiohttp.client_exceptions.ClientError) as exceptionInfo: - await matlab_proxy_fixture.send_execution_request_to_matlab(code) + await comm_helper_fixture.send_execution_request_to_matlab(code) assert mock_exception_message in str(exceptionInfo.value) async def test_execution_request_invalid_feval_response( - monkeypatch, matlab_proxy_fixture + monkeypatch, comm_helper_fixture ): """ This test checks that send_execution_request_to_matlab raises an exception @@ -150,11 +168,11 @@ async def mock_post(*args, **kwargs): code = "placeholder for code" with pytest.raises(MATLABConnectionError) as exceptionInfo: - await matlab_proxy_fixture.send_execution_request_to_matlab(code) + await comm_helper_fixture.send_execution_request_to_matlab(code) assert str(exceptionInfo.value) == str(MATLABConnectionError()) -async def test_execution_interrupt(monkeypatch, matlab_proxy_fixture): +async def test_execution_interrupt(monkeypatch, comm_helper_fixture): """ This test checks that send_execution_request_to_matlab raises an exception if the matlab command appears to have been interupted. @@ -190,11 +208,11 @@ async def mock_post(*args, **kwargs): code = "placeholder for code" with pytest.raises(Exception) as exceptionInfo: - await matlab_proxy_fixture.send_execution_request_to_matlab(code) + await comm_helper_fixture.send_execution_request_to_matlab(code) assert "Operation may have interrupted by user" in str(exceptionInfo.value) -async def test_execution_success(monkeypatch, matlab_proxy_fixture): +async def test_execution_success(monkeypatch, comm_helper_fixture): """ This test checks that send_execution_request_to_matlab returns the correct information from a valid response from MATLAB. @@ -225,7 +243,7 @@ async def mock_post(*args, **kwargs): code = "placeholder for code" try: - outputs = await matlab_proxy_fixture.send_execution_request_to_matlab(code) + outputs = await comm_helper_fixture.send_execution_request_to_matlab(code) except Exception: pytest.fail("Unexpected failured in execution request") @@ -233,7 +251,7 @@ async def mock_post(*args, **kwargs): # Testing send_eval_request_to_matlab -async def test_send_eval_request_to_matlab_success(monkeypatch, matlab_proxy_fixture): +async def test_send_eval_request_to_matlab_success(monkeypatch, comm_helper_fixture): """Test that send_eval_request_to_matlab returns eval response correctly.""" # Arrange @@ -245,7 +263,7 @@ async def mock_post(*args, **kwargs): mcode = "x = 1 + 1" # Act - result = await matlab_proxy_fixture.send_eval_request_to_matlab(mcode) + result = await comm_helper_fixture.send_eval_request_to_matlab(mcode) # Assert # Verify the eval response is returned as-is @@ -253,9 +271,7 @@ async def mock_post(*args, **kwargs): assert result == expected_response -async def test_send_eval_request_to_matlab_with_error( - monkeypatch, matlab_proxy_fixture -): +async def test_send_eval_request_to_matlab_with_error(monkeypatch, comm_helper_fixture): """Test that send_eval_request_to_matlab returns error response correctly.""" # Arrange @@ -271,7 +287,7 @@ async def mock_post(*args, **kwargs): mcode = "invalid_syntax" # Act - result = await matlab_proxy_fixture.send_eval_request_to_matlab(mcode) + result = await comm_helper_fixture.send_eval_request_to_matlab(mcode) # Assert # Verify the error response is returned as-is @@ -284,7 +300,7 @@ async def mock_post(*args, **kwargs): async def test_send_eval_request_to_matlab_bad_request( - monkeypatch, matlab_proxy_fixture + monkeypatch, comm_helper_fixture ): """Test that send_eval_request_to_matlab raises exception for bad HTTP request.""" # Arrange @@ -299,14 +315,14 @@ async def mock_post(*args, **kwargs): # Act with pytest.raises(aiohttp.client_exceptions.ClientError) as exceptionInfo: - await matlab_proxy_fixture.send_eval_request_to_matlab(mcode) + await comm_helper_fixture.send_eval_request_to_matlab(mcode) # Assert assert mock_exception_message in str(exceptionInfo.value) async def test_send_eval_request_to_matlab_missing_eval_response( - monkeypatch, matlab_proxy_fixture + monkeypatch, comm_helper_fixture ): """Test that send_eval_request_to_matlab raises MATLABConnectionError for missing EvalResponse.""" @@ -318,11 +334,11 @@ async def mock_post(*args, **kwargs): mcode = "x = 1 + 1" with pytest.raises(MATLABConnectionError): - await matlab_proxy_fixture.send_eval_request_to_matlab(mcode) + await comm_helper_fixture.send_eval_request_to_matlab(mcode) # Testing _read_eval_response_from_file -async def test_read_eval_response_from_file_success_with_file(matlab_proxy_fixture): +async def test_read_eval_response_from_file_success_with_file(comm_helper_fixture): """Test _read_eval_response_from_file with successful response and file.""" # Arrange # Create a temporary file with test data @@ -341,7 +357,7 @@ async def test_read_eval_response_from_file_success_with_file(matlab_proxy_fixtu } # Act - result = await matlab_proxy_fixture._read_eval_response_from_file(eval_response) + result = await comm_helper_fixture._read_eval_response_from_file(eval_response) # Assert # Verify the result @@ -356,7 +372,7 @@ async def test_read_eval_response_from_file_success_with_file(matlab_proxy_fixtu os.remove(temp_file_path) -async def test_read_eval_response_from_file_success_without_file(matlab_proxy_fixture): +async def test_read_eval_response_from_file_success_without_file(comm_helper_fixture): """Test _read_eval_response_from_file with successful response but no file.""" # Arrange eval_response = { @@ -366,7 +382,7 @@ async def test_read_eval_response_from_file_success_without_file(matlab_proxy_fi } # Act - result = await matlab_proxy_fixture._read_eval_response_from_file(eval_response) + result = await comm_helper_fixture._read_eval_response_from_file(eval_response) # Assert # Verify empty result returns empty list @@ -374,7 +390,7 @@ async def test_read_eval_response_from_file_success_without_file(matlab_proxy_fi async def test_read_eval_response_from_file_error_with_message_faults( - matlab_proxy_fixture, + comm_helper_fixture, ): """Test _read_eval_response_from_file with error response containing message faults.""" # Arrange @@ -389,11 +405,11 @@ async def test_read_eval_response_from_file_error_with_message_faults( Exception, match="Failed to execute. Operation may have been interrupted by user.", ): - await matlab_proxy_fixture._read_eval_response_from_file(eval_response) + await comm_helper_fixture._read_eval_response_from_file(eval_response) async def test_read_eval_response_from_file_error_without_message_faults( - matlab_proxy_fixture, + comm_helper_fixture, ): """Test _read_eval_response_from_file with error response without message faults.""" @@ -404,11 +420,11 @@ async def test_read_eval_response_from_file_error_without_message_faults( } with pytest.raises(Exception, match="Custom error message"): - await matlab_proxy_fixture._read_eval_response_from_file(eval_response) + await comm_helper_fixture._read_eval_response_from_file(eval_response) async def test_read_eval_response_from_file_handles_file_deletion_error( - matlab_proxy_fixture, monkeypatch + comm_helper_fixture, monkeypatch ): """Test _read_eval_response_from_file handles file deletion errors gracefully.""" @@ -438,7 +454,7 @@ def mock_remove(path): } # Should not raise exception even if file deletion fails - result = await matlab_proxy_fixture._read_eval_response_from_file(eval_response) + result = await comm_helper_fixture._read_eval_response_from_file(eval_response) # Verify the result is still correct assert result == test_data @@ -450,7 +466,7 @@ def mock_remove(path): async def test_read_eval_response_from_file_with_empty_file_content( - matlab_proxy_fixture, + comm_helper_fixture, ): """Test _read_eval_response_from_file with empty file content.""" @@ -466,7 +482,7 @@ async def test_read_eval_response_from_file_with_empty_file_content( "messageFaults": [], } - result = await matlab_proxy_fixture._read_eval_response_from_file(eval_response) + result = await comm_helper_fixture._read_eval_response_from_file(eval_response) # Verify empty content returns empty list assert result == [] @@ -481,7 +497,7 @@ async def test_read_eval_response_from_file_with_empty_file_content( async def test_read_eval_response_from_file_with_whitespace_only_content( - matlab_proxy_fixture, + comm_helper_fixture, ): """Test _read_eval_response_from_file with whitespace-only file content.""" @@ -497,7 +513,7 @@ async def test_read_eval_response_from_file_with_whitespace_only_content( "messageFaults": [], } - result = await matlab_proxy_fixture._read_eval_response_from_file(eval_response) + result = await comm_helper_fixture._read_eval_response_from_file(eval_response) # Verify whitespace-only content returns empty list assert result == [] From 426901a1e428cb0951814c5ae3bf687f16615dae Mon Sep 17 00:00:00 2001 From: Prabhakar Kumar Date: Thu, 11 Dec 2025 18:49:49 +0530 Subject: [PATCH 15/15] Update to v0.18.0 --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 40813ca2..2b6370d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "jupyter-matlab-proxy" -version = "0.17.5" +version = "0.18.0" description = "MATLAB Integration for Jupyter" readme = "README.md" license = { file = "LICENSE.md" } @@ -43,7 +43,7 @@ dependencies = [ "ipykernel>=6.0.3", "jupyter-client", "jupyter-server-proxy>=4.1.0", - "matlab-proxy>=0.26.0", + "matlab-proxy>=0.30.0", "psutil", "requests", ]