diff --git a/frontend/webEditor/.gitignore b/frontend/webEditor/.gitignore index 76add878..f06235c4 100644 --- a/frontend/webEditor/.gitignore +++ b/frontend/webEditor/.gitignore @@ -1,2 +1,2 @@ node_modules -dist \ No newline at end of file +dist diff --git a/frontend/webEditor/.husky/.gitignore b/frontend/webEditor/.husky/.gitignore deleted file mode 100644 index 31354ec1..00000000 --- a/frontend/webEditor/.husky/.gitignore +++ /dev/null @@ -1 +0,0 @@ -_ diff --git a/frontend/webEditor/.husky/pre-commit b/frontend/webEditor/.husky/pre-commit old mode 100755 new mode 100644 index 0ad91c2c..516f6fc2 --- a/frontend/webEditor/.husky/pre-commit +++ b/frontend/webEditor/.husky/pre-commit @@ -2,8 +2,8 @@ set -e # Only run when WebEditor files are staged (optional guard) -if git diff --cached --name-only --diff-filter=ACMRT | grep -q '^Frontend/WebEditor/'; then +if git diff --cached --name-only --diff-filter=ACMRT | grep -q '^frontend/webEditor/'; then REPO_ROOT="$(git rev-parse --show-toplevel)" - cd "$REPO_ROOT/Frontend/WebEditor" + cd "$REPO_ROOT/frontend/webEditor" npx lint-staged -fi +fi \ No newline at end of file diff --git a/frontend/webEditor/.vscode/launch.json b/frontend/webEditor/.vscode/launch.json deleted file mode 100644 index d4733d5d..00000000 --- a/frontend/webEditor/.vscode/launch.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Launch vite and debug in MS Edge", - "request": "launch", - "type": "node", - "cwd": "${workspaceFolder}", - "runtimeExecutable": "npm", - "runtimeArgs": ["run-script", "dev"], - "serverReadyAction": { - "action": "debugWithEdge", - "pattern": "Local:\\s+http://localhost:([0-9]+)", - "uriFormat": "http://localhost:%s" - } - }, - { - "name": "Launch vite and debug in Chrome", - "request": "launch", - "type": "node", - "cwd": "${workspaceFolder}", - "runtimeExecutable": "npm", - "runtimeArgs": ["run-script", "dev"], - "serverReadyAction": { - "action": "debugWithChrome", - "pattern": "Local:\\s+http://localhost:([0-9]+)", - "uriFormat": "http://localhost:%s" - } - } - ] -} diff --git a/frontend/webEditor/.vscode/settings.json b/frontend/webEditor/.vscode/settings.json deleted file mode 100644 index ed1ac12d..00000000 --- a/frontend/webEditor/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - // It's annoying to get spelling infos about wWords for components that are used in this project - // So these are added here to the dictionary. - "cSpell.words": ["sprotty", "inversify", "codicon", "zorder"] -} diff --git a/frontend/webEditor/package-lock.json b/frontend/webEditor/package-lock.json index 164edf26..c0163abf 100644 --- a/frontend/webEditor/package-lock.json +++ b/frontend/webEditor/package-lock.json @@ -1,37 +1,37 @@ { "name": "data-flow-analysis-web-editor", - "version": "0.1.0", - "lockfileVersion": 2, + "version": "0.0.0", + "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "data-flow-analysis-web-editor", - "version": "0.1.0", + "version": "0.0.0", "devDependencies": { - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.35.0", + "@eslint/eslintrc": "^3.3.3", + "@eslint/js": "^9.39.1", "@fortawesome/fontawesome-free": "^7.0.0", - "@vscode/codicons": "^0.0.39", - "eslint": "^9.32.0", + "@vscode/codicons": "^0.0.43", + "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "husky": "^9.1.7", "inversify": "^6.2.2", - "lint-staged": "^16.1.6", + "lint-staged": "^16.2.7", "monaco-editor": "^0.52.2", - "prettier": "^3.6.2", + "prettier": "^3.7.4", "reflect-metadata": "^0.2.2", "sprotty": "^1.4.0", "sprotty-elk": "^1.4.0", "sprotty-protocol": "^1.4.0", "typescript": "^5.8.3", - "typescript-eslint": "^8.44.0", - "vite": "^7.1.7" + "typescript-eslint": "^8.48.1", + "vite": "^7.2.6" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", - "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", "cpu": [ "ppc64" ], @@ -46,9 +46,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", - "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", "cpu": [ "arm" ], @@ -63,9 +63,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", - "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", "cpu": [ "arm64" ], @@ -80,9 +80,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", - "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", "cpu": [ "x64" ], @@ -97,9 +97,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", - "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", "cpu": [ "arm64" ], @@ -114,9 +114,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", - "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", "cpu": [ "x64" ], @@ -131,9 +131,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", - "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", "cpu": [ "arm64" ], @@ -148,9 +148,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", - "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", "cpu": [ "x64" ], @@ -165,9 +165,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", - "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", "cpu": [ "arm" ], @@ -182,9 +182,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", - "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", "cpu": [ "arm64" ], @@ -199,9 +199,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", - "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", "cpu": [ "ia32" ], @@ -216,9 +216,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", - "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", "cpu": [ "loong64" ], @@ -233,9 +233,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", - "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", "cpu": [ "mips64el" ], @@ -250,9 +250,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", - "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", "cpu": [ "ppc64" ], @@ -267,9 +267,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", - "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", "cpu": [ "riscv64" ], @@ -284,9 +284,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", - "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", "cpu": [ "s390x" ], @@ -301,9 +301,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", - "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", "cpu": [ "x64" ], @@ -318,9 +318,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", - "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", "cpu": [ "arm64" ], @@ -335,9 +335,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", - "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", "cpu": [ "x64" ], @@ -352,9 +352,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", - "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", "cpu": [ "arm64" ], @@ -369,9 +369,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", - "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", "cpu": [ "x64" ], @@ -385,10 +385,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", - "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", "cpu": [ "x64" ], @@ -403,9 +420,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", - "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", "cpu": [ "arm64" ], @@ -420,9 +437,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", - "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", "cpu": [ "ia32" ], @@ -437,9 +454,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", - "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", "cpu": [ "x64" ], @@ -454,9 +471,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -477,6 +494,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -489,17 +507,19 @@ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -508,19 +528,24 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -529,9 +554,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -541,7 +566,7 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", + "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, @@ -553,10 +578,11 @@ } }, "node_modules/@eslint/js": { - "version": "9.35.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", - "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -565,21 +591,23 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", - "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.1", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { @@ -587,10 +615,11 @@ } }, "node_modules/@fortawesome/fontawesome-free": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.0.0.tgz", - "integrity": "sha512-X48nISrSOa89zu2VMljC4XaRf8NmgTwQBVHfS2Nu5G00ZwM31oOVrAtGxZF3b6wDYf9lJsf/Eq4cCSFKIkOWPQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.1.0.tgz", + "integrity": "sha512-+WxNld5ZCJHvPQCr/GnzCTVREyStrAJjisUPtUxG5ngDA8TMlPnKp6dddlTpai4+1GNmltAeuk1hJEkBohwZYA==", "dev": true, + "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", "engines": { "node": ">=6" } @@ -600,41 +629,31 @@ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=12.22" }, @@ -644,10 +663,11 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -660,13 +680,15 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/@inversifyjs/common/-/common-1.4.0.tgz", "integrity": "sha512-qfRJ/3iOlCL/VfJq8+4o5X4oA14cZSBbpAmHsYj8EsIit1xDndoOl0xKOyglKtQD4u4gdNVxMHx4RWARk/I4QA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@inversifyjs/core": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@inversifyjs/core/-/core-1.3.5.tgz", "integrity": "sha512-B4MFXabhNTAmrfgB+yeD6wd/GIvmvWC6IQ8Rh/j2C3Ix69kmqwz9pr8Jt3E+Nho9aEHOQCZaGmrALgtqRd+oEQ==", "dev": true, + "license": "MIT", "dependencies": { "@inversifyjs/common": "1.4.0", "@inversifyjs/reflect-metadata-utils": "0.2.4" @@ -677,370 +699,314 @@ "resolved": "https://registry.npmjs.org/@inversifyjs/reflect-metadata-utils/-/reflect-metadata-utils-0.2.4.tgz", "integrity": "sha512-u95rV3lKfG+NT2Uy/5vNzoDujos8vN8O18SSA5UyhxsGYd4GLQn/eUsGXfOsfa7m34eKrDelTKRUX1m/BcNX5w==", "dev": true, + "license": "MIT", "peerDependencies": { "reflect-metadata": "0.2.2" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", - "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true, - "optional": true, - "peer": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", - "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", + "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", - "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", + "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", - "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", + "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", - "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", + "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", - "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", + "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", - "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", + "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", - "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", + "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", - "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", + "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", - "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", + "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", - "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", + "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", - "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", + "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", - "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", + "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", - "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", + "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", - "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", + "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", - "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", + "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", - "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", + "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", - "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", + "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", + "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", - "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", + "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", - "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", + "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", + "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", - "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", + "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1050,7 +1016,8 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", @@ -1060,16 +1027,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.44.0.tgz", - "integrity": "sha512-EGDAOGX+uwwekcS0iyxVDmRV9HX6FLSM5kzrAToLTsr9OWCIKG/y3lQheCq18yZ5Xh78rRKJiEpP0ZaCs4ryOQ==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz", + "integrity": "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.44.0", - "@typescript-eslint/type-utils": "8.44.0", - "@typescript-eslint/utils": "8.44.0", - "@typescript-eslint/visitor-keys": "8.44.0", + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/type-utils": "8.48.1", + "@typescript-eslint/utils": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1083,7 +1051,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.44.0", + "@typescript-eslint/parser": "^8.48.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -1093,20 +1061,22 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.44.0.tgz", - "integrity": "sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz", + "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.44.0", - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/typescript-estree": "8.44.0", - "@typescript-eslint/visitor-keys": "8.44.0", + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", "debug": "^4.3.4" }, "engines": { @@ -1122,13 +1092,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.44.0.tgz", - "integrity": "sha512-ZeaGNraRsq10GuEohKTo4295Z/SuGcSq2LzfGlqiuEvfArzo/VRrT0ZaJsVPuKZ55lVbNk8U6FcL+ZMH8CoyVA==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz", + "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.44.0", - "@typescript-eslint/types": "^8.44.0", + "@typescript-eslint/tsconfig-utils": "^8.48.1", + "@typescript-eslint/types": "^8.48.1", "debug": "^4.3.4" }, "engines": { @@ -1143,13 +1114,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.44.0.tgz", - "integrity": "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz", + "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/visitor-keys": "8.44.0" + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1160,10 +1132,11 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.44.0.tgz", - "integrity": "sha512-x5Y0+AuEPqAInc6yd0n5DAcvtoQ/vyaGwuX5HE9n6qAefk1GaedqrLQF8kQGylLUb9pnZyLf+iEiL9fr8APDtQ==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz", + "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1176,14 +1149,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.44.0.tgz", - "integrity": "sha512-9cwsoSxJ8Sak67Be/hD2RNt/fsqmWnNE1iHohG8lxqLSNY8xNfyY7wloo5zpW3Nu9hxVgURevqfcH6vvKCt6yg==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz", + "integrity": "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/typescript-estree": "8.44.0", - "@typescript-eslint/utils": "8.44.0", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/utils": "8.48.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1200,10 +1174,11 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.44.0.tgz", - "integrity": "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", + "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1213,20 +1188,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.44.0.tgz", - "integrity": "sha512-lqNj6SgnGcQZwL4/SBJ3xdPEfcBuhCG8zdcwCPgYcmiPLgokiNDKlbPzCwEwu7m279J/lBYWtDYL+87OEfn8Jw==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz", + "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.44.0", - "@typescript-eslint/tsconfig-utils": "8.44.0", - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/visitor-keys": "8.44.0", + "@typescript-eslint/project-service": "8.48.1", + "@typescript-eslint/tsconfig-utils": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", + "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "engines": { @@ -1245,6 +1220,7 @@ "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" } @@ -1254,6 +1230,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -1265,15 +1242,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.44.0.tgz", - "integrity": "sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz", + "integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.44.0", - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/typescript-estree": "8.44.0" + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1288,12 +1266,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.44.0.tgz", - "integrity": "sha512-zaz9u8EJ4GBmnehlrpoKvj/E3dNbuQ7q0ucyZImm3cLqJ8INTc970B1qEqDX/Rzq65r3TvVTN7kHWPBoyW7DWw==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz", + "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.44.0", + "@typescript-eslint/types": "8.48.1", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -1305,10 +1284,11 @@ } }, "node_modules/@vscode/codicons": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.39.tgz", - "integrity": "sha512-gO09UZrOBONyzI8LWPRsCahnmUR16hkRQCJOSJzX8L4dC5aa6YGP4nS+gh5oSekMlM8LFJXMAgqBMGGiktdRJw==", - "dev": true + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.43.tgz", + "integrity": "sha512-8sf8WOBoZkyUi8ogCm5ycHJJGhwOEG3E9b64+JIx+m6bCExdkc30VwCwr94cXUU1opmRD0CTCWLcN46I8WLJIg==", + "dev": true, + "license": "CC-BY-4.0" }, "node_modules/acorn": { "version": "8.15.0", @@ -1328,6 +1308,7 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -1337,6 +1318,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1349,10 +1331,11 @@ } }, "node_modules/ansi-escapes": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", - "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", + "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", "dev": true, + "license": "MIT", "dependencies": { "environment": "^1.0.0" }, @@ -1364,10 +1347,11 @@ } }, "node_modules/ansi-regex": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", - "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -1376,12 +1360,16 @@ } }, "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=12" + "node": ">=8" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -1391,25 +1379,29 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "dev": true, + "license": "Python-2.0" }, "node_modules/autocompleter": { "version": "9.3.2", "resolved": "https://registry.npmjs.org/autocompleter/-/autocompleter-9.3.2.tgz", "integrity": "sha512-rLbf2TLGOD7y+gOS36ksrZdIsvoHa2KXc2A7503w+NBRPrcF73zzFeYBxEcV/iMPjaBH3jFhNIYObZ7zt1fkCQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "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" @@ -1428,30 +1420,28 @@ "node": ">=8" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/chalk": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", - "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": ">=10" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" @@ -1462,6 +1452,7 @@ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, + "license": "MIT", "dependencies": { "restore-cursor": "^5.0.0" }, @@ -1473,16 +1464,17 @@ } }, "node_modules/cli-truncate": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", - "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", + "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", "dev": true, + "license": "MIT", "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^7.0.0" + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -1493,6 +1485,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -1504,27 +1497,32 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", "dev": true, - "optional": true, - "peer": true + "license": "MIT", + "engines": { + "node": ">=20" + } }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -1542,9 +1540,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -1563,25 +1561,29 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/elkjs": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.8.2.tgz", "integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==", - "dev": true + "dev": true, + "license": "EPL-2.0" }, "node_modules/emoji-regex": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", - "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", - "dev": true + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -1590,9 +1592,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", - "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1603,31 +1605,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.0", - "@esbuild/android-arm": "0.25.0", - "@esbuild/android-arm64": "0.25.0", - "@esbuild/android-x64": "0.25.0", - "@esbuild/darwin-arm64": "0.25.0", - "@esbuild/darwin-x64": "0.25.0", - "@esbuild/freebsd-arm64": "0.25.0", - "@esbuild/freebsd-x64": "0.25.0", - "@esbuild/linux-arm": "0.25.0", - "@esbuild/linux-arm64": "0.25.0", - "@esbuild/linux-ia32": "0.25.0", - "@esbuild/linux-loong64": "0.25.0", - "@esbuild/linux-mips64el": "0.25.0", - "@esbuild/linux-ppc64": "0.25.0", - "@esbuild/linux-riscv64": "0.25.0", - "@esbuild/linux-s390x": "0.25.0", - "@esbuild/linux-x64": "0.25.0", - "@esbuild/netbsd-arm64": "0.25.0", - "@esbuild/netbsd-x64": "0.25.0", - "@esbuild/openbsd-arm64": "0.25.0", - "@esbuild/openbsd-x64": "0.25.0", - "@esbuild/sunos-x64": "0.25.0", - "@esbuild/win32-arm64": "0.25.0", - "@esbuild/win32-ia32": "0.25.0", - "@esbuild/win32-x64": "0.25.0" + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" } }, "node_modules/escape-string-regexp": { @@ -1635,6 +1638,7 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -1643,24 +1647,24 @@ } }, "node_modules/eslint": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", - "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.32.0", - "@eslint/plugin-kit": "^0.3.4", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", @@ -1707,6 +1711,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, + "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -1747,49 +1752,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/@eslint/js": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", - "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -1813,6 +1775,7 @@ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -1838,6 +1801,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -1847,6 +1811,7 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } @@ -1855,68 +1820,36 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } + "license": "MIT" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } + "license": "MIT" }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, + "license": "MIT", "dependencies": { "flat-cache": "^4.0.0" }, @@ -1928,7 +1861,8 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fill-range": { "version": "7.1.1", @@ -1948,6 +1882,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -1964,6 +1899,7 @@ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" @@ -1976,7 +1912,8 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", @@ -1984,6 +1921,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1993,10 +1931,11 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.1.tgz", - "integrity": "sha512-R1QfovbPsKmosqTnPoRFiJ7CF9MLRgb53ChvMZm+r4p76/+8yKDy17qLL2PKInORy2RkZZekuK0efYgmzTkXyQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -2009,6 +1948,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -2021,6 +1961,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -2032,13 +1973,15 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2064,6 +2007,7 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -2073,6 +2017,7 @@ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -2089,6 +2034,7 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.19" } @@ -2112,17 +2058,22 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2133,6 +2084,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -2158,10 +2110,11 @@ "license": "ISC" }, "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" }, @@ -2173,25 +2126,29 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -2201,6 +2158,7 @@ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -2209,32 +2167,17 @@ "node": ">= 0.8.0" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, "node_modules/lint-staged": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.1.6.tgz", - "integrity": "sha512-U4kuulU3CKIytlkLlaHcGgKscNfJPNTiDF2avIUGFCv7K95/DCYQ7Ra62ydeRWmgQGg9zJYw2dzdbztwJlqrow==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz", + "integrity": "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==", "dev": true, + "license": "MIT", "dependencies": { - "chalk": "^5.6.0", - "commander": "^14.0.0", - "debug": "^4.4.1", - "lilconfig": "^3.1.3", - "listr2": "^9.0.3", + "commander": "^14.0.2", + "listr2": "^9.0.5", "micromatch": "^4.0.8", - "nano-spawn": "^1.0.2", + "nano-spawn": "^2.0.0", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.8.1" @@ -2249,23 +2192,14 @@ "url": "https://opencollective.com/lint-staged" } }, - "node_modules/lint-staged/node_modules/commander": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", - "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, "node_modules/listr2": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.3.tgz", - "integrity": "sha512-0aeh5HHHgmq1KRdMMDHfhMWQmIT/m7nRDTlxlFqni2Sp0had9baqsjJRvDGdlvgd6NmPE0nPloOipiQJGFtTHQ==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", "dev": true, + "license": "MIT", "dependencies": { - "cli-truncate": "^4.0.0", + "cli-truncate": "^5.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", @@ -2281,6 +2215,7 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -2295,13 +2230,15 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", "dev": true, + "license": "MIT", "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", @@ -2316,46 +2253,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", - "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -2375,6 +2272,7 @@ "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -2387,6 +2285,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2409,9 +2308,9 @@ "license": "MIT" }, "node_modules/nano-spawn": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.2.tgz", - "integrity": "sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", + "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==", "dev": true, "license": "MIT", "engines": { @@ -2444,13 +2343,15 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, + "license": "MIT", "dependencies": { "mimic-function": "^5.0.0" }, @@ -2466,6 +2367,7 @@ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, + "license": "MIT", "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -2483,6 +2385,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -2498,6 +2401,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -2513,6 +2417,7 @@ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -2525,6 +2430,7 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2564,6 +2470,7 @@ "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", "dev": true, + "license": "MIT", "bin": { "pidtree": "bin/pidtree.js" }, @@ -2605,15 +2512,17 @@ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } }, "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, + "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, @@ -2629,30 +2538,11 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -2665,6 +2555,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -2674,6 +2565,7 @@ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, + "license": "MIT", "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" @@ -2685,27 +2577,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/rollup": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", - "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", + "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "1.0.8" }, @@ -2717,57 +2601,37 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.46.2", - "@rollup/rollup-android-arm64": "4.46.2", - "@rollup/rollup-darwin-arm64": "4.46.2", - "@rollup/rollup-darwin-x64": "4.46.2", - "@rollup/rollup-freebsd-arm64": "4.46.2", - "@rollup/rollup-freebsd-x64": "4.46.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", - "@rollup/rollup-linux-arm-musleabihf": "4.46.2", - "@rollup/rollup-linux-arm64-gnu": "4.46.2", - "@rollup/rollup-linux-arm64-musl": "4.46.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", - "@rollup/rollup-linux-ppc64-gnu": "4.46.2", - "@rollup/rollup-linux-riscv64-gnu": "4.46.2", - "@rollup/rollup-linux-riscv64-musl": "4.46.2", - "@rollup/rollup-linux-s390x-gnu": "4.46.2", - "@rollup/rollup-linux-x64-gnu": "4.46.2", - "@rollup/rollup-linux-x64-musl": "4.46.2", - "@rollup/rollup-win32-arm64-msvc": "4.46.2", - "@rollup/rollup-win32-ia32-msvc": "4.46.2", - "@rollup/rollup-win32-x64-msvc": "4.46.2", + "@rollup/rollup-android-arm-eabi": "4.52.4", + "@rollup/rollup-android-arm64": "4.52.4", + "@rollup/rollup-darwin-arm64": "4.52.4", + "@rollup/rollup-darwin-x64": "4.52.4", + "@rollup/rollup-freebsd-arm64": "4.52.4", + "@rollup/rollup-freebsd-x64": "4.52.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", + "@rollup/rollup-linux-arm-musleabihf": "4.52.4", + "@rollup/rollup-linux-arm64-gnu": "4.52.4", + "@rollup/rollup-linux-arm64-musl": "4.52.4", + "@rollup/rollup-linux-loong64-gnu": "4.52.4", + "@rollup/rollup-linux-ppc64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-musl": "4.52.4", + "@rollup/rollup-linux-s390x-gnu": "4.52.4", + "@rollup/rollup-linux-x64-gnu": "4.52.4", + "@rollup/rollup-linux-x64-musl": "4.52.4", + "@rollup/rollup-openharmony-arm64": "4.52.4", + "@rollup/rollup-win32-arm64-msvc": "4.52.4", + "@rollup/rollup-win32-ia32-msvc": "4.52.4", + "@rollup/rollup-win32-x64-gnu": "4.52.4", + "@rollup/rollup-win32-x64-msvc": "4.52.4", "fsevents": "~2.3.2" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -2803,6 +2667,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, + "license": "ISC", "engines": { "node": ">=14" }, @@ -2811,41 +2676,45 @@ } }, "node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/snabbdom": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.5.1.tgz", "integrity": "sha512-wHMNIOjkm/YNE5EM3RCbr/+DVgPg6AqQAX1eOxO46zYNvCXjKP5Y865tqQj3EXnaMBjkxmQA5jFuDpDK/dbfiA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.3.0" } }, - "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, - "optional": true, - "peer": 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", @@ -2856,23 +2725,12 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/sprotty": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/sprotty/-/sprotty-1.4.0.tgz", "integrity": "sha512-QGZZQAM2pOa1QxJUG05Ox76RJOKuvKloT1nCkvs6SD5w/HfkcL0mjq1Om1+fb5NAalDzurrJL6agKUReST3TFw==", "dev": true, + "license": "(EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0)", "dependencies": { "autocompleter": "^9.1.2", "file-saver": "^2.0.5", @@ -2887,6 +2745,7 @@ "resolved": "https://registry.npmjs.org/sprotty-elk/-/sprotty-elk-1.4.0.tgz", "integrity": "sha512-ewSoKgqmgR3lw0EQpjYOrlzpPofCB7UyXr5k9vfpF2ho5HEswGcpoPzLtqojB5UM0TlBYnm1S59ekN+RMhY4ng==", "dev": true, + "license": "(EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0)", "dependencies": { "elkjs": "^0.8.2", "sprotty-protocol": "^1.4.0" @@ -2907,32 +2766,34 @@ "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.6.19" } }, "node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", + "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -2948,6 +2809,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -2960,6 +2822,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -2967,31 +2830,12 @@ "node": ">=8" } }, - "node_modules/terser": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.1.tgz", - "integrity": "sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, + "license": "MIT", "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" @@ -3008,6 +2852,7 @@ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12.0.0" }, @@ -3025,6 +2870,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -3036,7 +2882,8 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -3056,6 +2903,7 @@ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18.12" }, @@ -3068,6 +2916,7 @@ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -3076,9 +2925,9 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3090,15 +2939,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.44.0.tgz", - "integrity": "sha512-ib7mCkYuIzYonCq9XWF5XNw+fkj2zg629PSa9KNIQ47RXFF763S5BIX4wqz1+FLPogTZoiw8KmCiRPRa8bL3qw==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.48.1.tgz", + "integrity": "sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.44.0", - "@typescript-eslint/parser": "8.44.0", - "@typescript-eslint/typescript-estree": "8.44.0", - "@typescript-eslint/utils": "8.44.0" + "@typescript-eslint/eslint-plugin": "8.48.1", + "@typescript-eslint/parser": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/utils": "8.48.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3117,15 +2967,17 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, "node_modules/vite": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz", - "integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==", + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz", + "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -3200,6 +3052,7 @@ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12.0.0" }, @@ -3217,6 +3070,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -3245,15 +3099,17 @@ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", @@ -3266,2037 +3122,62 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "dev": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - }, - "dependencies": { - "@esbuild/aix-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", - "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", - "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", - "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", - "dev": true, - "optional": true - }, - "@esbuild/android-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", - "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", - "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", - "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", - "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", - "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", - "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", - "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", - "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-loong64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", - "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", - "dev": true, - "optional": true - }, - "@esbuild/linux-mips64el": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", - "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", - "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", - "dev": true, - "optional": true - }, - "@esbuild/linux-riscv64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", - "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-s390x": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", - "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", - "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", - "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", - "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", - "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", - "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", - "dev": true, - "optional": true - }, - "@esbuild/sunos-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", - "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", - "dev": true, - "optional": true - }, - "@esbuild/win32-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", - "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", - "dev": true, - "optional": true - }, - "@esbuild/win32-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", - "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", - "dev": true, - "optional": true - }, - "@esbuild/win32-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", - "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", - "dev": true, - "optional": true - }, - "@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.4.3" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true - } - } - }, - "@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true - }, - "@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", - "dev": true, - "requires": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - } - }, - "@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", - "dev": true - }, - "@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.15" - } - }, - "@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "9.35.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", - "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", - "dev": true - }, - "@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true - }, - "@eslint/plugin-kit": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", - "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", - "dev": true, - "requires": { - "@eslint/core": "^0.15.1", - "levn": "^0.4.1" - } - }, - "@fortawesome/fontawesome-free": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.0.0.tgz", - "integrity": "sha512-X48nISrSOa89zu2VMljC4XaRf8NmgTwQBVHfS2Nu5G00ZwM31oOVrAtGxZF3b6wDYf9lJsf/Eq4cCSFKIkOWPQ==", - "dev": true - }, - "@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true - }, - "@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "requires": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "dependencies": { - "@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true - } - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true - }, - "@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", - "dev": true - }, - "@inversifyjs/common": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@inversifyjs/common/-/common-1.4.0.tgz", - "integrity": "sha512-qfRJ/3iOlCL/VfJq8+4o5X4oA14cZSBbpAmHsYj8EsIit1xDndoOl0xKOyglKtQD4u4gdNVxMHx4RWARk/I4QA==", - "dev": true - }, - "@inversifyjs/core": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@inversifyjs/core/-/core-1.3.5.tgz", - "integrity": "sha512-B4MFXabhNTAmrfgB+yeD6wd/GIvmvWC6IQ8Rh/j2C3Ix69kmqwz9pr8Jt3E+Nho9aEHOQCZaGmrALgtqRd+oEQ==", - "dev": true, - "requires": { - "@inversifyjs/common": "1.4.0", - "@inversifyjs/reflect-metadata-utils": "0.2.4" - } - }, - "@inversifyjs/reflect-metadata-utils": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@inversifyjs/reflect-metadata-utils/-/reflect-metadata-utils-0.2.4.tgz", - "integrity": "sha512-u95rV3lKfG+NT2Uy/5vNzoDujos8vN8O18SSA5UyhxsGYd4GLQn/eUsGXfOsfa7m34eKrDelTKRUX1m/BcNX5w==", - "dev": true, - "requires": {} - }, - "@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true, - "optional": true, - "peer": true - }, - "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, - "optional": true, - "peer": true - }, - "@jridgewell/source-map": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", - "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true, - "optional": true, - "peer": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@rollup/rollup-android-arm-eabi": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", - "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-android-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", - "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-darwin-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", - "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-darwin-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", - "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-freebsd-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", - "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", - "dev": true, - "optional": true - }, - "@rollup/rollup-freebsd-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", - "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", - "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm-musleabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", - "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", - "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", - "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", - "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-ppc64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", - "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-riscv64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", - "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-riscv64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", - "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-s390x-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", - "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-x64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", - "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-x64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", - "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-win32-arm64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", - "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", - "dev": true, - "optional": true - }, - "@rollup/rollup-win32-ia32-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", - "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-win32-x64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", - "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", - "dev": true, - "optional": true - }, - "@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true - }, - "@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 - }, - "@typescript-eslint/eslint-plugin": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.44.0.tgz", - "integrity": "sha512-EGDAOGX+uwwekcS0iyxVDmRV9HX6FLSM5kzrAToLTsr9OWCIKG/y3lQheCq18yZ5Xh78rRKJiEpP0ZaCs4ryOQ==", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.44.0", - "@typescript-eslint/type-utils": "8.44.0", - "@typescript-eslint/utils": "8.44.0", - "@typescript-eslint/visitor-keys": "8.44.0", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" - }, - "dependencies": { - "ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true - } - } - }, - "@typescript-eslint/parser": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.44.0.tgz", - "integrity": "sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "8.44.0", - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/typescript-estree": "8.44.0", - "@typescript-eslint/visitor-keys": "8.44.0", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/project-service": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.44.0.tgz", - "integrity": "sha512-ZeaGNraRsq10GuEohKTo4295Z/SuGcSq2LzfGlqiuEvfArzo/VRrT0ZaJsVPuKZ55lVbNk8U6FcL+ZMH8CoyVA==", - "dev": true, - "requires": { - "@typescript-eslint/tsconfig-utils": "^8.44.0", - "@typescript-eslint/types": "^8.44.0", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.44.0.tgz", - "integrity": "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA==", - "dev": true, - "requires": { - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/visitor-keys": "8.44.0" - } - }, - "@typescript-eslint/tsconfig-utils": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.44.0.tgz", - "integrity": "sha512-x5Y0+AuEPqAInc6yd0n5DAcvtoQ/vyaGwuX5HE9n6qAefk1GaedqrLQF8kQGylLUb9pnZyLf+iEiL9fr8APDtQ==", - "dev": true, - "requires": {} - }, - "@typescript-eslint/type-utils": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.44.0.tgz", - "integrity": "sha512-9cwsoSxJ8Sak67Be/hD2RNt/fsqmWnNE1iHohG8lxqLSNY8xNfyY7wloo5zpW3Nu9hxVgURevqfcH6vvKCt6yg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/typescript-estree": "8.44.0", - "@typescript-eslint/utils": "8.44.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - } - }, - "@typescript-eslint/types": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.44.0.tgz", - "integrity": "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.44.0.tgz", - "integrity": "sha512-lqNj6SgnGcQZwL4/SBJ3xdPEfcBuhCG8zdcwCPgYcmiPLgokiNDKlbPzCwEwu7m279J/lBYWtDYL+87OEfn8Jw==", - "dev": true, - "requires": { - "@typescript-eslint/project-service": "8.44.0", - "@typescript-eslint/tsconfig-utils": "8.44.0", - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/visitor-keys": "8.44.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - } - } - }, - "@typescript-eslint/utils": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.44.0.tgz", - "integrity": "sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.44.0", - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/typescript-estree": "8.44.0" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.44.0.tgz", - "integrity": "sha512-zaz9u8EJ4GBmnehlrpoKvj/E3dNbuQ7q0ucyZImm3cLqJ8INTc970B1qEqDX/Rzq65r3TvVTN7kHWPBoyW7DWw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "8.44.0", - "eslint-visitor-keys": "^4.2.1" - } - }, - "@vscode/codicons": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.39.tgz", - "integrity": "sha512-gO09UZrOBONyzI8LWPRsCahnmUR16hkRQCJOSJzX8L4dC5aa6YGP4nS+gh5oSekMlM8LFJXMAgqBMGGiktdRJw==", - "dev": true - }, - "acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-escapes": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", - "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", - "dev": true, - "requires": { - "environment": "^1.0.0" - } - }, - "ansi-regex": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", - "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", - "dev": true - }, - "ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "autocompleter": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/autocompleter/-/autocompleter-9.3.2.tgz", - "integrity": "sha512-rLbf2TLGOD7y+gOS36ksrZdIsvoHa2KXc2A7503w+NBRPrcF73zzFeYBxEcV/iMPjaBH3jFhNIYObZ7zt1fkCQ==", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "requires": { - "fill-range": "^7.1.1" - } - }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "optional": true, - "peer": true - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "chalk": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", - "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", - "dev": true - }, - "cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "requires": { - "restore-cursor": "^5.0.0" - } - }, - "cli-truncate": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", - "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", - "dev": true, - "requires": { - "slice-ansi": "^5.0.0", - "string-width": "^7.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true - }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "optional": true, - "peer": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "requires": { - "ms": "^2.1.3" - } - }, - "deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "elkjs": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.8.2.tgz", - "integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==", - "dev": true - }, - "emoji-regex": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", - "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", - "dev": true - }, - "environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true - }, - "esbuild": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", - "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", - "dev": true, - "requires": { - "@esbuild/aix-ppc64": "0.25.0", - "@esbuild/android-arm": "0.25.0", - "@esbuild/android-arm64": "0.25.0", - "@esbuild/android-x64": "0.25.0", - "@esbuild/darwin-arm64": "0.25.0", - "@esbuild/darwin-x64": "0.25.0", - "@esbuild/freebsd-arm64": "0.25.0", - "@esbuild/freebsd-x64": "0.25.0", - "@esbuild/linux-arm": "0.25.0", - "@esbuild/linux-arm64": "0.25.0", - "@esbuild/linux-ia32": "0.25.0", - "@esbuild/linux-loong64": "0.25.0", - "@esbuild/linux-mips64el": "0.25.0", - "@esbuild/linux-ppc64": "0.25.0", - "@esbuild/linux-riscv64": "0.25.0", - "@esbuild/linux-s390x": "0.25.0", - "@esbuild/linux-x64": "0.25.0", - "@esbuild/netbsd-arm64": "0.25.0", - "@esbuild/netbsd-x64": "0.25.0", - "@esbuild/openbsd-arm64": "0.25.0", - "@esbuild/openbsd-x64": "0.25.0", - "@esbuild/sunos-x64": "0.25.0", - "@esbuild/win32-arm64": "0.25.0", - "@esbuild/win32-ia32": "0.25.0", - "@esbuild/win32-x64": "0.25.0" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "eslint": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", - "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.32.0", - "@eslint/plugin-kit": "^0.3.4", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "dependencies": { - "@eslint/js": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", - "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - } - } - }, - "eslint-config-prettier": { - "version": "10.1.8", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", - "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", - "dev": true, - "requires": {} - }, - "eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true - }, - "espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "requires": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - } - }, - "esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "requires": { - "flat-cache": "^4.0.0" - } - }, - "file-saver": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", - "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", - "dev": true - }, - "fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "requires": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - } - }, - "flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true - }, - "fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "optional": true - }, - "get-east-asian-width": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.1.tgz", - "integrity": "sha512-R1QfovbPsKmosqTnPoRFiJ7CF9MLRgb53ChvMZm+r4p76/+8yKDy17qLL2PKInORy2RkZZekuK0efYgmzTkXyQ==", - "dev": true - }, - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true - }, - "graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "husky": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", - "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", - "dev": true - }, - "ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true - }, - "import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true - }, - "inversify": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/inversify/-/inversify-6.2.2.tgz", - "integrity": "sha512-KB836KHbZ9WrUnB8ax5MtadOwnqQYa+ZJO3KWbPFgcr4RIEnHM621VaqFZzOZd9+U7ln6upt9n0wJei7x2BNqw==", - "dev": true, - "requires": { - "@inversifyjs/common": "1.4.0", - "@inversifyjs/core": "1.3.5" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "requires": { - "json-buffer": "3.0.1" - } - }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true - }, - "lint-staged": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.1.6.tgz", - "integrity": "sha512-U4kuulU3CKIytlkLlaHcGgKscNfJPNTiDF2avIUGFCv7K95/DCYQ7Ra62ydeRWmgQGg9zJYw2dzdbztwJlqrow==", - "dev": true, - "requires": { - "chalk": "^5.6.0", - "commander": "^14.0.0", - "debug": "^4.4.1", - "lilconfig": "^3.1.3", - "listr2": "^9.0.3", - "micromatch": "^4.0.8", - "nano-spawn": "^1.0.2", - "pidtree": "^0.6.0", - "string-argv": "^0.3.2", - "yaml": "^2.8.1" - }, - "dependencies": { - "commander": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", - "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", - "dev": true - } - } - }, - "listr2": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.3.tgz", - "integrity": "sha512-0aeh5HHHgmq1KRdMMDHfhMWQmIT/m7nRDTlxlFqni2Sp0had9baqsjJRvDGdlvgd6NmPE0nPloOipiQJGFtTHQ==", - "dev": true, - "requires": { - "cli-truncate": "^4.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" - } - }, - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", - "dev": true, - "requires": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, - "requires": { - "get-east-asian-width": "^1.3.1" - } - }, - "slice-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", - "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", - "dev": true, - "requires": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - } - } - } - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "requires": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - } - }, - "mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "monaco-editor": { - "version": "0.52.2", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", - "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", - "dev": true - }, - "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 - }, - "nano-spawn": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.2.tgz", - "integrity": "sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==", - "dev": true - }, - "nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "requires": { - "mimic-function": "^5.0.0" - } - }, - "optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - } - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true - }, - "postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "requires": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - } - }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, - "prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", - "dev": true - }, - "punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true - }, - "reflect-metadata": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "requires": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - } - }, - "reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true - }, - "rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true - }, - "rollup": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", - "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", - "dev": true, - "requires": { - "@rollup/rollup-android-arm-eabi": "4.46.2", - "@rollup/rollup-android-arm64": "4.46.2", - "@rollup/rollup-darwin-arm64": "4.46.2", - "@rollup/rollup-darwin-x64": "4.46.2", - "@rollup/rollup-freebsd-arm64": "4.46.2", - "@rollup/rollup-freebsd-x64": "4.46.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", - "@rollup/rollup-linux-arm-musleabihf": "4.46.2", - "@rollup/rollup-linux-arm64-gnu": "4.46.2", - "@rollup/rollup-linux-arm64-musl": "4.46.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", - "@rollup/rollup-linux-ppc64-gnu": "4.46.2", - "@rollup/rollup-linux-riscv64-gnu": "4.46.2", - "@rollup/rollup-linux-riscv64-musl": "4.46.2", - "@rollup/rollup-linux-s390x-gnu": "4.46.2", - "@rollup/rollup-linux-x64-gnu": "4.46.2", - "@rollup/rollup-linux-x64-musl": "4.46.2", - "@rollup/rollup-win32-arm64-msvc": "4.46.2", - "@rollup/rollup-win32-ia32-msvc": "4.46.2", - "@rollup/rollup-win32-x64-msvc": "4.46.2", - "@types/estree": "1.0.8", - "fsevents": "~2.3.2" - } - }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true - }, - "slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "dev": true, - "requires": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - } - }, - "snabbdom": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.5.1.tgz", - "integrity": "sha512-wHMNIOjkm/YNE5EM3RCbr/+DVgPg6AqQAX1eOxO46zYNvCXjKP5Y865tqQj3EXnaMBjkxmQA5jFuDpDK/dbfiA==", - "dev": true - }, - "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, - "optional": true, - "peer": true - }, - "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 - }, - "source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "sprotty": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/sprotty/-/sprotty-1.4.0.tgz", - "integrity": "sha512-QGZZQAM2pOa1QxJUG05Ox76RJOKuvKloT1nCkvs6SD5w/HfkcL0mjq1Om1+fb5NAalDzurrJL6agKUReST3TFw==", - "dev": true, - "requires": { - "autocompleter": "^9.1.2", - "file-saver": "^2.0.5", - "inversify": "^6.1.3", - "snabbdom": "~3.5.1", - "sprotty-protocol": "^1.4.0", - "tinyqueue": "^2.0.3" - } - }, - "sprotty-elk": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/sprotty-elk/-/sprotty-elk-1.4.0.tgz", - "integrity": "sha512-ewSoKgqmgR3lw0EQpjYOrlzpPofCB7UyXr5k9vfpF2ho5HEswGcpoPzLtqojB5UM0TlBYnm1S59ekN+RMhY4ng==", - "dev": true, - "requires": { - "elkjs": "^0.8.2", - "inversify": "^6.1.3", - "sprotty-protocol": "^1.4.0" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "sprotty-protocol": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/sprotty-protocol/-/sprotty-protocol-1.4.0.tgz", - "integrity": "sha512-+AAskW3Mzcq5UhMnummp4wwJ1dYdgT7/utmWoHtjfrK7JTJq9G/VWWlHnTnQGzHHyma03Loy2AozToXoArQuAQ==", - "dev": true - }, - "string-argv": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "dev": true - }, - "string-width": { + "node_modules/wrap-ansi/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" - } - }, - "strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "terser": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.1.tgz", - "integrity": "sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - } - }, - "tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "requires": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" }, - "dependencies": { - "fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "requires": {} - }, - "picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true - } - } - }, - "tinyqueue": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", - "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "requires": {} - }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true - }, - "typescript-eslint": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.44.0.tgz", - "integrity": "sha512-ib7mCkYuIzYonCq9XWF5XNw+fkj2zg629PSa9KNIQ47RXFF763S5BIX4wqz1+FLPogTZoiw8KmCiRPRa8bL3qw==", - "dev": true, - "requires": { - "@typescript-eslint/eslint-plugin": "8.44.0", - "@typescript-eslint/parser": "8.44.0", - "@typescript-eslint/typescript-estree": "8.44.0", - "@typescript-eslint/utils": "8.44.0" - } - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "vite": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz", - "integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==", - "dev": true, - "requires": { - "esbuild": "^0.25.0", - "fdir": "^6.5.0", - "fsevents": "~2.3.3", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "engines": { + "node": ">=18" }, - "dependencies": { - "fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "requires": {} - }, - "picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true - } - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true - }, - "wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", - "dev": true, - "requires": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "yaml": { + "node_modules/yaml": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "dev": true + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } }, - "yocto-queue": { + "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/frontend/webEditor/package.json b/frontend/webEditor/package.json index 8f3beb39..5e118773 100644 --- a/frontend/webEditor/package.json +++ b/frontend/webEditor/package.json @@ -1,30 +1,30 @@ { "name": "data-flow-analysis-web-editor", - "version": "0.1.0", + "version": "0.0.0", "private": true, "repository": { "type": "git", - "url": "https://github.com/DataFlowAnalysis/WebEditor.git" + "url": "https://github.com/DataFlowAnalysis/OnlineEditor.git" }, "devDependencies": { - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.35.0", + "@eslint/eslintrc": "^3.3.3", + "@eslint/js": "^9.39.1", "@fortawesome/fontawesome-free": "^7.0.0", - "@vscode/codicons": "^0.0.39", - "eslint": "^9.32.0", + "@vscode/codicons": "^0.0.43", + "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "husky": "^9.1.7", "inversify": "^6.2.2", - "lint-staged": "^16.1.6", + "lint-staged": "^16.2.7", "monaco-editor": "^0.52.2", - "prettier": "^3.6.2", + "prettier": "^3.7.4", "reflect-metadata": "^0.2.2", "sprotty": "^1.4.0", "sprotty-elk": "^1.4.0", "sprotty-protocol": "^1.4.0", "typescript": "^5.8.3", - "typescript-eslint": "^8.44.0", - "vite": "^7.1.7" + "typescript-eslint": "^8.48.1", + "vite": "^7.2.6" }, "scripts": { "dev": "vite", diff --git a/frontend/webEditor/src/common/commonStyling.css b/frontend/webEditor/src/accordionUiExtension/accordion.css similarity index 66% rename from frontend/webEditor/src/common/commonStyling.css rename to frontend/webEditor/src/accordionUiExtension/accordion.css index b121db8b..6446547b 100644 --- a/frontend/webEditor/src/common/commonStyling.css +++ b/frontend/webEditor/src/accordionUiExtension/accordion.css @@ -1,29 +1,3 @@ -.ui-float { - position: absolute; - border-radius: 10px; - background-color: var(--color-primary); -} - -/* Styling for keyboard symbols. - Copied from the example at https://developer.mozilla.org/en-US/docs/Web/HTML/Element/kbd - with adapted colors */ -kbd { - background-color: var(--color-primary); - color: var(--color-foreground); - - border-radius: 3px; - border: 1px solid var(--color-foreground); - box-shadow: - 0 1px 1px var(--color-foreground), - 0 2px 0 0 var(--color-background) inset; - display: inline-block; - font-size: 0.85em; - font-weight: 700; - line-height: 1; - padding: 2px 4px; - white-space: nowrap; -} - /* accordion */ .accordion-content { display: grid; @@ -47,7 +21,7 @@ kbd { transition: grid-template-rows 300ms ease, /* ease-out animation: https://cubic-bezier.com/#0,.7,.4,1 */ /* mirrored version of the curve above */ - grid-template-columns 300ms cubic-bezier(0, 0.7, 0.4, 1), + grid-template-columns 300ms cubic-bezier(0, 0.7, 0.4, 1), padding-top 300ms ease; /* space between accordion button and the content, otherwise they would be directly next to each other without any spacing */ @@ -68,26 +42,26 @@ kbd { -webkit-user-select: none; user-select: none; - /* Default orientation of the arrow: pointing down */ - --arrow-scale: 1; + /* Default orientation of the chevron: pointing down */ + --chevron-scale: 1; } -.accordion-button.flip-arrow { - /* Default orientation of the arrow: pointing up */ - --arrow-scale: -1; +.accordion-button.flip-chevron { + /* Default orientation of the chevron: pointing up */ + --chevron-scale: -1; } -.accordion-button.cevron-right { +.accordion-button.chevron-right { /* space for the icon */ padding-right: 2em; } -.accordion-button.cevron-left { +.accordion-button.chevron-left { /* space for the icon */ padding-left: 2em; } -.accordion-button.cevron-right::after { +.accordion-button.chevron-right::after { content: ""; background-image: url("@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg"); right: 1em; @@ -103,10 +77,10 @@ kbd { vertical-align: text-top; transition: transform 500ms ease; - transform: scaleY(var(--arrow-scale)); + transform: scaleY(var(--chevron-scale)); } -.accordion-button.cevron-left::before { +.accordion-button.chevron-left::before { content: ""; background-image: url("@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg"); left: 1em; @@ -122,15 +96,15 @@ kbd { vertical-align: text-top; transition: transform 500ms ease; - transform: scaleY(var(--arrow-scale)); + transform: scaleY(var(--chevron-scale)); } .accordion-state:checked ~ label .accordion-button::after { - /* flip arrow in y direction */ - transform: scaleY(calc(var(--arrow-scale) * -1)); + /* flip chevron in y direction */ + transform: scaleY(calc(var(--chevron-scale) * -1)); } .accordion-state:checked ~ label .accordion-button::before { - /* flip arrow in y direction */ - transform: scaleY(calc(var(--arrow-scale) * -1)); + /* flip chevron in y direction */ + transform: scaleY(calc(var(--chevron-scale) * -1)); } diff --git a/frontend/webEditor/src/accordionUiExtension/index.ts b/frontend/webEditor/src/accordionUiExtension/index.ts new file mode 100644 index 00000000..8319974c --- /dev/null +++ b/frontend/webEditor/src/accordionUiExtension/index.ts @@ -0,0 +1,63 @@ +import { AbstractUIExtension } from "sprotty"; +import { injectable } from "inversify"; +import "./accordion.css"; + +/** + * Base class for an expandable accordion floating element + */ +@injectable() +export abstract class AccordionUiExtension extends AbstractUIExtension { + constructor( + private chevronPosition: "left" | "right", + private chevronOrientation: "up" | "down", + ) { + super(); + } + + protected initializeContents(containerElement: HTMLElement): void { + containerElement.classList.add("ui-float"); + + // create hidden checkbox used for toggling + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + const checkboxId = this.id() + "-checkbox"; + checkbox.id = checkboxId; + checkbox.classList.add("accordion-state"); + checkbox.hidden = true; + + // create clickable label for the checkbox + const label = document.createElement("label"); + label.htmlFor = checkboxId; + // create header inside label + const header = document.createElement("div"); + header.classList.add(`chevron-${this.chevronPosition}`, "accordion-button"); + if (this.chevronOrientation === "up") { + header.classList.add("flip-chevron"); + } + this.initializeHeaderContent(header); + label.appendChild(header); + + // create content holder and initialize it + const accordionContent = document.createElement("div"); + accordionContent.classList.add("accordion-content"); + const contentHolder = document.createElement("div"); + this.initializeHidableContent(contentHolder); + accordionContent.appendChild(contentHolder); + + containerElement.appendChild(checkbox); + containerElement.appendChild(label); + containerElement.appendChild(accordionContent); + } + + /** + * Initializes the hidable content of the accordion element + * @param contentElement The containing element of the content + */ + protected abstract initializeHidableContent(contentElement: HTMLElement): void; + + /** + * Initializes the header of the accordion element + * @param contentElement The containing element of the header + */ + protected abstract initializeHeaderContent(headerElement: HTMLElement): void; +} diff --git a/frontend/webEditor/src/annotation/DFDNodeAnnotation.ts b/frontend/webEditor/src/annotation/DFDNodeAnnotation.ts new file mode 100644 index 00000000..dc8f7f9c --- /dev/null +++ b/frontend/webEditor/src/annotation/DFDNodeAnnotation.ts @@ -0,0 +1,6 @@ +export interface DfdNodeAnnotation { + message: string; + color?: string; + icon?: string; + tfg?: number; +} diff --git a/frontend/webEditor/src/assets/commonStyling.css b/frontend/webEditor/src/assets/commonStyling.css new file mode 100644 index 00000000..48d7cb3c --- /dev/null +++ b/frontend/webEditor/src/assets/commonStyling.css @@ -0,0 +1,25 @@ +.ui-float { + position: absolute; + border-radius: 10px; + background-color: var(--color-primary); +} + +/* Styling for keyboard symbols. + Copied from the example at https://developer.mozilla.org/en-US/docs/Web/HTML/Element/kbd + with adapted colors */ +kbd { + background-color: var(--color-primary); + color: var(--color-foreground); + + border-radius: 3px; + border: 1px solid var(--color-foreground); + box-shadow: + 0 1px 1px var(--color-foreground), + 0 2px 0 0 var(--color-background) inset; + display: inline-block; + font-size: 0.85em; + font-weight: 700; + line-height: 1; + padding: 2px 4px; + white-space: nowrap; +} diff --git a/frontend/webEditor/src/page.css b/frontend/webEditor/src/assets/page.css similarity index 79% rename from frontend/webEditor/src/page.css rename to frontend/webEditor/src/assets/page.css index fd00b8a3..6ac44b3d 100644 --- a/frontend/webEditor/src/page.css +++ b/frontend/webEditor/src/assets/page.css @@ -15,11 +15,20 @@ body { and absolute top/left/bottom/right values. */ #sprotty { position: relative; + height: 100vh; + width: 100vw; + /*temporary*/ + font-family: + Helvetica Neue, + Helvetica, + Arial, + sans-serif; + padding: 0; } svg.sprotty-graph { width: 100%; - height: 100vh; + height: 100%; outline: none; } diff --git a/frontend/webEditor/src/theme.css b/frontend/webEditor/src/assets/theme.css similarity index 100% rename from frontend/webEditor/src/theme.css rename to frontend/webEditor/src/assets/theme.css diff --git a/frontend/webEditor/src/assignment/AssignmentEditUi.ts b/frontend/webEditor/src/assignment/AssignmentEditUi.ts new file mode 100644 index 00000000..a0c5570c --- /dev/null +++ b/frontend/webEditor/src/assignment/AssignmentEditUi.ts @@ -0,0 +1,254 @@ +import { inject, injectable } from "inversify"; +import { AbstractUIExtension, getAbsoluteClientBounds, SModelRootImpl, TYPES, ViewerOptions } from "sprotty"; +import { DfdOutputPortImpl } from "../diagram/ports/DfdOutputPort"; +import { DOMHelper } from "sprotty/lib/base/views/dom-helper"; +import { DfdNodeImpl } from "../diagram/nodes/common"; +import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; +import { ASSIGNMENT_LANGUAGE_ID, assignmentLanguageMonarchDefinition, AssignmentLanguageTreeBuilder } from "./language"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { DfdCompletionItemProvider } from "../languages/autocomplete"; +import { LanguageTreeNode, tokenize } from "../languages/tokenize"; +import { Word } from "../languages/words"; +import { Theme, ThemeManager } from "../settings/Theme"; +import { SETTINGS } from "../settings/Settings"; +import { verify } from "../languages/verify"; +import "./assignmentEditUi.css"; +import { EditorModeController } from "../settings/editorMode"; +import { matchesKeystroke } from "sprotty/lib/utils/keyboard"; + +@injectable() +export class AssignmentEditUi extends AbstractUIExtension { + public static readonly ID = "assignment-edit-ui"; + + private port?: DfdOutputPortImpl; + private tree?: LanguageTreeNode[]; + private editorContainer: HTMLDivElement = document.createElement("div") as HTMLDivElement; + private validationLabel: HTMLDivElement = document.createElement("div") as HTMLDivElement; + private unavailableInputsLabel: HTMLDivElement = document.createElement("div") as HTMLDivElement; + private editor?: monaco.editor.IStandaloneCodeEditor; + + constructor( + @inject(LabelTypeRegistry) private readonly labelTypeRegistry: LabelTypeRegistry, + @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController, + @inject(SETTINGS.Theme) private readonly themeManager: ThemeManager, + @inject(TYPES.ViewerOptions) private viewerOptions: ViewerOptions, + @inject(TYPES.DOMHelper) private domHelper: DOMHelper, + ) { + super(); + + editorModeController.registerListener(() => { + this.editor?.updateOptions({ + readOnly: this.editorModeController.isReadOnly(), + }); + }); + } + + id(): string { + return AssignmentEditUi.ID; + } + + containerClass(): string { + return AssignmentEditUi.ID; + } + + protected initializeContents(containerElement: HTMLElement): void { + containerElement.classList.add("ui-float"); + + containerElement.appendChild(this.unavailableInputsLabel); + this.unavailableInputsLabel.classList.add("unavailable-inputs"); + containerElement.appendChild(this.editorContainer); + this.editorContainer.classList.add("monaco-container"); + containerElement.appendChild(this.validationLabel); + this.validationLabel.classList.add("validation-label"); + + const keyboardShortcutLabel = document.createElement("div"); + keyboardShortcutLabel.innerHTML = "Press CTRL+Space for autocompletion"; + containerElement.appendChild(keyboardShortcutLabel); + + monaco.languages.register({ id: ASSIGNMENT_LANGUAGE_ID }); + monaco.languages.setMonarchTokensProvider(ASSIGNMENT_LANGUAGE_ID, assignmentLanguageMonarchDefinition); + + const monacoTheme = this.themeManager.getTheme() === Theme.DARK ? "vs-dark" : "vs"; + this.editor = monaco.editor.create(this.editorContainer, { + minimap: { + // takes too much space, not useful for our use case + enabled: false, + }, + lineNumbersMinChars: 3, // default is 5, which we'll never need. Save a bit of space. + folding: false, // Not supported by our language definition + wordBasedSuggestions: "off", // Does not really work for our use case + scrollBeyondLastLine: false, // Not needed + theme: monacoTheme, + language: ASSIGNMENT_LANGUAGE_ID, + readOnly: this.editorModeController.isReadOnly(), + }); + + this.editor.onDidChangeModelContent(() => { + this.validate(); + }); + + this.editor.onDidContentSizeChange(() => { + this.resizeEditor(); + }); + + this.labelTypeRegistry?.onUpdate(() => { + // The update handler for the refactoring might be after our handler. + // Delay update to the next event loop tick to ensure the refactoring is done. + setTimeout(() => { + if (this.editor && this.port) { + this.editor?.setValue(this.port?.getBehavior()); + } + }, 0); + }); + + // Hide/"close this window" when pressing escape. + containerElement.addEventListener("keydown", (event) => { + if (matchesKeystroke(event, "Escape")) { + this.hide(); + } + }); + } + + protected onBeforeShow( + containerElement: HTMLElement, + root: Readonly, + ...contextElementIds: string[] + ): void { + // Loads data for the port that shall be edited, which is defined by the context element id. + if (contextElementIds.length !== 1) { + throw new Error( + "Expected exactly one context element id which should be the port that shall be shown in the UI.", + ); + } + this.setPort(root.index.getById(contextElementIds[0]) as DfdOutputPortImpl, containerElement); + + this.checkForUnavailableInputs(); + + this.resizeEditor(); + + this.editor?.focus(); + } + + private setPort(port: DfdOutputPortImpl, containerElement: HTMLElement) { + this.port = port; + + const bounds = getAbsoluteClientBounds(this.port, this.domHelper, this.viewerOptions); + containerElement.style.left = `${bounds.x}px`; + containerElement.style.top = `${bounds.y}px`; + + this.tree = AssignmentLanguageTreeBuilder.buildTree(port, this.labelTypeRegistry); + monaco.languages.registerCompletionItemProvider( + ASSIGNMENT_LANGUAGE_ID, + new DfdCompletionItemProvider(this.tree), + ); + if (!this.editor) { + throw new Error("Expected editor to be initialized"); + } + + this.editor.setValue(port.getBehavior()); + } + + private checkForUnavailableInputs() { + if (!this.port) { + throw new Error("Expected Assignment Edit Ui to be assigned to a port"); + } + + const parent = this.port.parent; + if (!(parent instanceof DfdNodeImpl)) { + throw new Error("Expected parent to be a DfdNodeImpl."); + } + + const availableInputNames = parent.getAvailableInputs(); + const countUnavailableDueToMissingName = availableInputNames.filter((name) => name === undefined).length; + + if (countUnavailableDueToMissingName > 0) { + const unavailableInputsText = + countUnavailableDueToMissingName > 1 + ? `There are ${countUnavailableDueToMissingName} inputs that don't have a named edge and cannot be used` + : `There is ${countUnavailableDueToMissingName} input that doesn't have a named edge and cannot be used`; + + this.unavailableInputsLabel.innerText = unavailableInputsText; + this.unavailableInputsLabel.style.display = "block"; + } else { + this.unavailableInputsLabel.innerText = ""; + this.unavailableInputsLabel.style.display = "none"; + } + } + + private resizeEditor(): void { + // Resize editor to fit content. + // Has ranges for height and width to prevent the editor from getting too small or too large. + if (!this.editor) { + return; + } + + // For the height we can use the content height from the editor. + const height = this.editor.getContentHeight(); + + // For the width we cannot really do this. + // Monaco needs about 500ms to figure out the correct width when initially showing the editor. + // In the mean time the width will be too small and after the update + // the window size will jump visibly. + // So for the width we use this calculation to approximate the width. + const maxLineLength = this.editor + .getValue() + .split("\n") + .reduce((max, line) => Math.max(max, line.length), 0); + const width = 100 + maxLineLength * 8; + + const clamp = (value: number, range: readonly [number, number]) => + Math.min(range[1], Math.max(range[0], value)); + + const heightRange = [100, 350] as const; + const widthRange = [275, 650] as const; + + const cHeight = clamp(height, heightRange); + const cWidth = clamp(width, widthRange); + + this.editor.layout({ height: cHeight, width: cWidth }); + } + + private validate() { + if (!this.editor || !this.tree) { + return; + } + + const model = this.editor?.getModel(); + if (!model) { + return; + } + + const content = model.getLinesContent(); + this.port?.setBehavior(content.join("\n")); + const marker: monaco.editor.IMarkerData[] = []; + const emptyContent = content.length == 0 || (content.length == 1 && content[0] === ""); + // empty content gets accepted as valid as it represents no constraints + if (!emptyContent) { + const errors = verify(tokenize(content), this.tree); + marker.push( + ...errors.map((e) => ({ + severity: monaco.MarkerSeverity.Error, + startLineNumber: e.line, + startColumn: e.startColumn, + endLineNumber: e.line, + endColumn: e.endColumn, + message: e.message, + })), + ); + } + + if (marker.length == 0) { + this.validationLabel.innerText = "Assignments are valid"; + this.validationLabel.classList.remove("validation-error"); + this.validationLabel.classList.add("validation-success"); + } else { + this.validationLabel.innerText = `Assignments are invalid: ${marker.length} error${ + marker.length === 1 ? "" : "s" + }.`; + this.validationLabel.classList.remove("validation-success"); + this.validationLabel.classList.add("validation-error"); + } + + monaco.editor.setModelMarkers(model, "constraint", marker); + } +} diff --git a/frontend/webEditor/src/assignment/assignmentEditUi.css b/frontend/webEditor/src/assignment/assignmentEditUi.css new file mode 100644 index 00000000..52466f57 --- /dev/null +++ b/frontend/webEditor/src/assignment/assignmentEditUi.css @@ -0,0 +1,22 @@ +.assignment-edit-ui { + position: absolute; + padding: 10px; + + -webkit-user-select: none; + user-select: none; + + background: var(--color-primary); + + div.unavailable-inputs { + /* spacing between editor and this text */ + padding-bottom: 5px; + } + + div.validation-label.validation-error { + color: var(--color-error); + } + + div.validation-label.validation-success { + color: var(--color-valid); + } +} diff --git a/frontend/webEditor/src/assignment/clickListener.ts b/frontend/webEditor/src/assignment/clickListener.ts new file mode 100644 index 00000000..2d021048 --- /dev/null +++ b/frontend/webEditor/src/assignment/clickListener.ts @@ -0,0 +1,53 @@ +import { injectable } from "inversify"; +import { MouseListener, SModelElementImpl, SetUIExtensionVisibilityAction } from "sprotty"; +import { Action } from "sprotty-protocol"; +import { DfdOutputPortImpl } from "../diagram/ports/DfdOutputPort"; +import { AssignmentEditUi } from "./AssignmentEditUi"; + +/** + * Detects when a dfd output port is double clicked and shows the OutputPortEditUI + * with the clicked port as context element. + */ +@injectable() +export class OutputPortEditUIMouseListener extends MouseListener { + private editUIVisible = false; + + mouseDown(target: SModelElementImpl): (Action | Promise)[] { + if (this.editUIVisible) { + // The user has clicked somewhere on the sprotty diagram (not the port edit UI) + // while the UI was open. In this case we hide the UI. + // This may not be exactly accurate because the UI can close itself when + // the change was saved but in those cases editUIVisible is still true. + // However hiding it one more time here for those cases is not a problem. + // Because it is already hidden, nothing will happen and after one click + // editUIVisible will be false again. + this.editUIVisible = false; + return [ + SetUIExtensionVisibilityAction.create({ + extensionId: AssignmentEditUi.ID, + visible: false, + contextElementsId: [target.id], + }), + ]; + } + + return []; + } + + doubleClick(target: SModelElementImpl): (Action | Promise)[] { + if (target instanceof DfdOutputPortImpl) { + // The user has double clicked on a dfd output port + // => show the OutputPortEditUI for this port. + this.editUIVisible = true; + return [ + SetUIExtensionVisibilityAction.create({ + extensionId: AssignmentEditUi.ID, + visible: true, + contextElementsId: [target.id], + }), + ]; + } + + return []; + } +} diff --git a/frontend/webEditor/src/assignment/di.config.ts b/frontend/webEditor/src/assignment/di.config.ts new file mode 100644 index 00000000..854923bd --- /dev/null +++ b/frontend/webEditor/src/assignment/di.config.ts @@ -0,0 +1,11 @@ +import { ContainerModule } from "inversify"; +import { AssignmentEditUi } from "./AssignmentEditUi"; +import { TYPES } from "sprotty"; +import { OutputPortEditUIMouseListener } from "./clickListener"; + +export const assignmentModule = new ContainerModule((bind) => { + bind(AssignmentEditUi).toSelf().inSingletonScope(); + bind(TYPES.IUIExtension).toService(AssignmentEditUi); + + bind(TYPES.MouseListener).to(OutputPortEditUIMouseListener).inSingletonScope(); +}); diff --git a/frontend/webEditor/src/assignment/language.ts b/frontend/webEditor/src/assignment/language.ts new file mode 100644 index 00000000..90128c4c --- /dev/null +++ b/frontend/webEditor/src/assignment/language.ts @@ -0,0 +1,289 @@ +import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; +import { DfdOutputPortImpl } from "../diagram/ports/DfdOutputPort"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { LanguageTreeNode } from "../languages/tokenize"; +import { ConstantWord, ListWord, Word } from "../languages/words"; +import { DfdNodeImpl } from "../diagram/nodes/common"; +import { WordCompletion } from "../languages/autocomplete"; +import { ReplacementData } from "../languages/replace"; + +export const ASSIGNMENT_LANGUAGE_ID = "dfd-assignment-language"; + +const startOfLineKeywords = ["forward", "assign", "set", "unset"]; +const statementKeywords = [...startOfLineKeywords, "if", "from"]; +const constantsKeywords = ["TRUE", "FALSE"]; +export const assignmentLanguageMonarchDefinition: monaco.languages.IMonarchLanguage = { + keywords: [...statementKeywords, ...constantsKeywords], + + operators: ["=", "||", "&&", "!"], + + symbols: /[=>[] { + return [ + buildSetOrUnsetStatement(labelTypeRegistry, "set"), + buildSetOrUnsetStatement(labelTypeRegistry, "unset"), + buildForwardStatement(port), + buildAssignStatement(labelTypeRegistry, port), + ]; + } + + function buildSetOrUnsetStatement(labelTypeRegistry: LabelTypeRegistry, keyword: string): LanguageTreeNode { + const labelNode: LanguageTreeNode = { + word: new ListWord(new LabelWord(labelTypeRegistry)), + children: [], + }; + return { + word: new ConstantWord(keyword), + children: [labelNode], + }; + } + + function buildForwardStatement(port: DfdOutputPortImpl): LanguageTreeNode { + const inputNode: LanguageTreeNode = { + word: new ListWord(new InputWord(port)), + children: [], + }; + return { + word: new ConstantWord("forward"), + children: [inputNode], + }; + } + + function buildAssignStatement( + labelTypeRegistry: LabelTypeRegistry, + port: DfdOutputPortImpl, + ): LanguageTreeNode { + const fromNode: LanguageTreeNode = { + word: new ConstantWord("from"), + children: [ + { + word: new ListWord(new InputWord(port)), + children: [], + }, + ], + }; + const ifNode: LanguageTreeNode = { + word: new ConstantWord("if"), + children: buildCondition(labelTypeRegistry, fromNode, port), + }; + return { + word: new ConstantWord("assign"), + children: [ + { + word: new LabelWord(labelTypeRegistry), + children: [ifNode], + }, + ], + }; + } + + function buildCondition( + labelTypeRegistry: LabelTypeRegistry, + nextNode: LanguageTreeNode, + port: DfdOutputPortImpl, + ) { + const connectors: LanguageTreeNode[] = ["&&", "||"].map((o) => ({ + word: new ConstantWord(o), + children: [], + })); + + const expressors: LanguageTreeNode[] = [ + new ConstantWord("TRUE"), + new ConstantWord("FALSE"), + new InputLabelWord(labelTypeRegistry, port), + ].map((e) => ({ + word: e, + children: [...connectors, nextNode], + canBeFinal: true, + })); + + connectors.forEach((c) => { + c.children = expressors; + }); + return expressors; + } +} + +abstract class InputAwareWord { + constructor(private port: DfdOutputPortImpl) {} + + protected getAvailableInputs(): string[] { + const parent = this.port.parent; + if (parent instanceof DfdNodeImpl) { + return parent.getAvailableInputs().filter((input) => input !== undefined) as string[]; + } + return []; + } +} + +class LabelWord implements Word { + constructor(private readonly labelTypeRegistry: LabelTypeRegistry) {} + + completionOptions(word: string): WordCompletion[] { + const parts = word.split("."); + + if (parts.length == 1) { + return this.labelTypeRegistry.getLabelTypes().map((l) => ({ + insertText: l.name, + kind: monaco.languages.CompletionItemKind.Class, + })); + } else if (parts.length == 2) { + const type = this.labelTypeRegistry.getLabelTypes().find((l) => l.name === parts[0]); + if (!type) { + return []; + } + + return type.values.map((l) => ({ + insertText: l.text, + kind: monaco.languages.CompletionItemKind.Enum, + startOffset: parts[0].length + 1, + })); + } + + return []; + } + + verify(word: string): string[] { + const parts = word.split("."); + + if (parts.length > 2) { + return ["Expected at most 2 parts in characteristic selector"]; + } + + const type = this.labelTypeRegistry.getLabelTypes().find((l) => l.name === parts[0]); + if (!type) { + return ['Unknown label type "' + parts[0] + '"']; + } + + if (parts.length < 2) { + return ["Expected characteristic to have value"]; + } + + if (parts[1].startsWith("$") && parts[1].length >= 2) { + return []; + } + + const label = type.values.find((l) => l.text === parts[1]); + if (!label) { + return ['Unknown label value "' + parts[1] + '" for type "' + parts[0] + '"']; + } + + return []; + } + + replace(text: string, replacement: ReplacementData) { + if (replacement.type == "label" && text == replacement.old) { + return replacement.replacement; + } + return text; + } +} + +class InputWord extends InputAwareWord implements Word { + completionOptions(): WordCompletion[] { + const inputs = this.getAvailableInputs(); + return inputs.map((input) => ({ + insertText: input, + kind: monaco.languages.CompletionItemKind.Variable, + })); + } + + verify(word: string): string[] { + const availableInputs = this.getAvailableInputs(); + if (availableInputs.includes(word)) { + return []; + } + return [`Unknown input "${word}"`]; + } +} + +class InputLabelWord implements Word { + private inputWord: InputWord; + private labelWord: LabelWord; + + constructor(labelTypeRegistry: LabelTypeRegistry, port: DfdOutputPortImpl) { + this.inputWord = new InputWord(port); + this.labelWord = new LabelWord(labelTypeRegistry); + } + + completionOptions(word: string): WordCompletion[] { + const parts = this.getParts(word); + if (parts[1] === undefined) { + return this.inputWord.completionOptions().map((c) => ({ + ...c, + insertText: c.insertText, + })); + } else if (parts.length >= 2) { + return this.labelWord.completionOptions(parts[1]).map((c) => ({ + ...c, + insertText: c.insertText, + startOffset: (c.startOffset ?? 0) + parts[0].length + 1, // +1 for the dot + })); + } + return []; + } + + verify(word: string): string[] { + const parts = this.getParts(word); + const inputErrors = this.inputWord.verify(parts[0]); + if (inputErrors.length > 0) { + return inputErrors; + } + if (parts[1] === undefined) { + return ["Expected input and label separated by a dot"]; + } + const labelErrors = this.labelWord.verify(parts[1]); + return [...inputErrors, ...labelErrors]; + } + + replaceWord(text: string, replacement: ReplacementData) { + const [input, label] = this.getParts(text); + if (replacement.type == "label" && label === replacement.old) { + return input + "." + replacement.replacement; + } + return text; + } + + private getParts(text: string): [string, string] | [string, undefined] { + if (text.includes(".")) { + const index = text.indexOf("."); + const input = text.substring(0, index); + const label = text.substring(index + 1); + return [input, label]; + } + return [text, undefined]; + } +} diff --git a/frontend/webEditor/src/features/commandPalette/commandPalette.css b/frontend/webEditor/src/commandPalette/commandPalette.css similarity index 100% rename from frontend/webEditor/src/features/commandPalette/commandPalette.css rename to frontend/webEditor/src/commandPalette/commandPalette.css diff --git a/frontend/webEditor/src/features/commandPalette/commandPalette.ts b/frontend/webEditor/src/commandPalette/commandPalette.ts similarity index 97% rename from frontend/webEditor/src/features/commandPalette/commandPalette.ts rename to frontend/webEditor/src/commandPalette/commandPalette.ts index cee2490a..eb2dc822 100644 --- a/frontend/webEditor/src/features/commandPalette/commandPalette.ts +++ b/frontend/webEditor/src/commandPalette/commandPalette.ts @@ -1,11 +1,12 @@ import { injectable } from "inversify"; import { CommandPalette, LabeledAction, SModelRootImpl } from "sprotty"; import { matchesKeystroke } from "sprotty/lib/utils/keyboard"; -import { FolderAction } from "./commandPaletteProvider"; import "./commandPalette.css"; +import "sprotty/css/command-palette.css"; +import { FolderAction } from "./commandPaletteProvider"; @injectable() -export class CustomCommandPalette extends CommandPalette { +export class WebEditorCommandPalette extends CommandPalette { static readonly ID = "command-palette"; protected suggestionElement?: HTMLElement; @@ -134,10 +135,10 @@ export class CustomCommandPalette extends CommandPalette { } id(): string { - return CustomCommandPalette.ID; + return WebEditorCommandPalette.ID; } containerClass(): string { - return CustomCommandPalette.ID; + return WebEditorCommandPalette.ID; } protected processKeyStrokeInInput(event: KeyboardEvent) { diff --git a/frontend/webEditor/src/features/commandPalette/commandPaletteProvider.ts b/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts similarity index 60% rename from frontend/webEditor/src/features/commandPalette/commandPaletteProvider.ts rename to frontend/webEditor/src/commandPalette/commandPaletteProvider.ts index 9553fbd6..ad0e53e2 100644 --- a/frontend/webEditor/src/features/commandPalette/commandPaletteProvider.ts +++ b/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts @@ -1,52 +1,45 @@ -import { inject, injectable } from "inversify"; +import { injectable } from "inversify"; import { ICommandPaletteActionProvider, LabeledAction, SModelRootImpl, CommitModelAction } from "sprotty"; -import { LoadDiagramAction } from "../serialize/load"; -import { createDefaultFitToScreenAction } from "../../utils"; -import { SaveDiagramAction } from "../serialize/save"; import { LoadDefaultDiagramAction } from "../serialize/loadDefaultDiagram"; -import { LayoutModelAction } from "../autoLayout/command"; - -import "@vscode/codicons/dist/codicon.css"; -import "sprotty/css/command-palette.css"; -import { SaveDFDandDDAction } from "../serialize/saveDFDandDD"; -import { LoadDFDandDDAction } from "../serialize/loadDFDandDD"; -import { LoadPalladioAction } from "../serialize/loadPalladio"; -import { SaveImageAction } from "../serialize/image"; -import { SettingsManager } from "../settingsMenu/SettingsManager"; +import { LoadJsonFileAction } from "../serialize/loadJsonFile"; import { Action } from "sprotty-protocol"; -import { LayoutMethod } from "../settingsMenu/LayoutMethod"; +import { LoadDfdAndDdFileAction } from "../serialize/loadDfdAndDdFile"; +import { LoadPalladioFileAction } from "../serialize/loadPalladioFile"; +import { DefaultFitToScreenAction } from "../fitToScreen/action"; +import { LayoutMethod } from "../layout/layoutMethod"; +import { LayoutModelAction } from "../layout/command"; +import { SaveJsonFileAction } from "../serialize/saveJsonFile"; +import { SaveDfdAndDdFileAction } from "../serialize/saveDfdAndDdFile"; /** * Provides possible actions for the command palette. */ @injectable() -export class ServerCommandPaletteActionProvider implements ICommandPaletteActionProvider { - constructor(@inject(SettingsManager) protected readonly settings: SettingsManager) {} - +export class WebEditorCommandPaletteActionProvider implements ICommandPaletteActionProvider { async getActions(root: Readonly): Promise<(LabeledAction | FolderAction)[]> { - const fitToScreenAction = createDefaultFitToScreenAction(root); + const fitToScreenAction = DefaultFitToScreenAction.create(root); const commitAction = CommitModelAction.create(); return [ new FolderAction( "Load", [ - new LabeledAction("Load diagram from JSON", [LoadDiagramAction.create(), commitAction], "json"), - new LabeledAction("Load DFD and DD", [LoadDFDandDDAction.create(), commitAction], "coffee"), - new LabeledAction("Load Palladio", [LoadPalladioAction.create(), commitAction], "fa-puzzle-piece"), + new LabeledAction("Load diagram from JSON", [LoadJsonFileAction.create(), commitAction], "json"), + new LabeledAction("Load DFD and DD", [LoadDfdAndDdFileAction.create(), commitAction], "coffee"), + new LabeledAction( + "Load Palladio", + [LoadPalladioFileAction.create(), commitAction], + "fa-puzzle-piece", + ), ], "go-to-file", ), new FolderAction( "Save", [ - new LabeledAction("Save diagram as JSON", [SaveDiagramAction.create()], "json"), - new LabeledAction( - "Save diagram as DFD and DD", - [SaveDFDandDDAction.create(), commitAction], - "coffee", - ), - new LabeledAction("Save viewport as image", [SaveImageAction.create()], "device-camera"), + new LabeledAction("Save diagram as JSON", [SaveJsonFileAction.create()], "json"), + new LabeledAction("Save diagram as DFD and DD", [SaveDfdAndDdFileAction.create()], "coffee"), + //new LabeledAction("Save viewport as image", [SaveImageAction.create()], "device-camera"), ], "save", ), diff --git a/frontend/webEditor/src/commandPalette/di.config.ts b/frontend/webEditor/src/commandPalette/di.config.ts new file mode 100644 index 00000000..e8f536bd --- /dev/null +++ b/frontend/webEditor/src/commandPalette/di.config.ts @@ -0,0 +1,11 @@ +import { ContainerModule } from "inversify"; +import { CommandPalette, TYPES } from "sprotty"; +import { WebEditorCommandPaletteActionProvider } from "./commandPaletteProvider"; +import { WebEditorCommandPalette } from "./commandPalette"; + +export const commandPaletteModule = new ContainerModule((bind, _, __, rebind) => { + rebind(CommandPalette).to(WebEditorCommandPalette).inSingletonScope(); + + bind(WebEditorCommandPaletteActionProvider).toSelf().inSingletonScope(); + bind(TYPES.ICommandPaletteActionProvider).toService(WebEditorCommandPaletteActionProvider); +}); diff --git a/frontend/webEditor/src/common/commandPalette.ts b/frontend/webEditor/src/common/commandPalette.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/frontend/webEditor/src/common/customCommandStack.ts b/frontend/webEditor/src/common/customCommandStack.ts deleted file mode 100644 index fd5de380..00000000 --- a/frontend/webEditor/src/common/customCommandStack.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { - BringToFrontCommand, - CenterCommand, - CommandStack, - FitToScreenCommand, - HiddenCommand, - ICommand, - SelectCommand, - SetViewportCommand, -} from "sprotty"; - -/** - * Custom command stack implementations that only pushes - * commands that modify the diagram to the undo stack. - * Commands like selections, viewport moves etc. are filtered out - * and not pushed to the undo stack. Because of this they will not - * be undone when the user presses Ctrl+Z. - * - * This is done because the commands like selections clutter up - * the stack and the user has to undo many commands without - * really knowing what they are undoing when the selections/viewport moves - * are small. - */ -export class DiagramModificationCommandStack extends CommandStack { - protected override isPushToUndoStack(command: ICommand): boolean { - return !( - command instanceof HiddenCommand || - command instanceof SelectCommand || - command instanceof SetViewportCommand || - command instanceof BringToFrontCommand || - command instanceof FitToScreenCommand || - command instanceof CenterCommand - ); - } -} diff --git a/frontend/webEditor/src/common/deleteKeyListener.ts b/frontend/webEditor/src/common/deleteKeyListener.ts deleted file mode 100644 index 015fe387..00000000 --- a/frontend/webEditor/src/common/deleteKeyListener.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { - CommitModelAction, - KeyListener, - SModelElementImpl, - isDeletable, - isSelectable, - SConnectableElementImpl, - SChildElementImpl, -} from "sprotty"; -import { Action, DeleteElementAction } from "sprotty-protocol"; -import { matchesKeystroke } from "sprotty/lib/utils/keyboard"; - -/** - * Custom sprotty key listener that deletes all selected elements when the user presses the delete key. - */ -export class DeleteKeyListener extends KeyListener { - override keyDown(element: SModelElementImpl, event: KeyboardEvent): Action[] { - if (matchesKeystroke(event, "Delete")) { - return this.deleteSelectedElements(element); - } - return []; - } - - private deleteSelectedElements(element: SModelElementImpl): Action[] { - const index = element.root.index; - const selectedElements = Array.from( - index - .all() - .filter((e) => isDeletable(e) && isSelectable(e) && e.selected) - .filter((e) => e.id !== e.root.id), // Deleting the model root would be a bad idea - ); - - const deleteElementIds = selectedElements.flatMap((e) => { - const ids = [e.id]; - - if (e instanceof SConnectableElementImpl) { - // This element can be connected to other elements, so we need to delete all edges connected to it as well. - // Otherwise the edges would be left dangling in the graph. - ids.push(...this.getEdgeIdsOfElement(e)); - } - if (e instanceof SChildElementImpl) { - // Add all children and their edges to the list of elements to delete - // This is needed when the edges are not connected to the element itself but to a port of the element. - e.children.forEach((child) => { - ids.push(child.id); - if (child instanceof SConnectableElementImpl) { - ids.push(...this.getEdgeIdsOfElement(child)); - } - }); - } - - return ids; - }); - - if (deleteElementIds.length > 0) { - const uniqueIds = [...new Set(deleteElementIds)]; - - return [DeleteElementAction.create(uniqueIds), CommitModelAction.create()]; - } else { - return []; - } - } - - private getEdgeIdsOfElement(element: SConnectableElementImpl): string[] { - return [...element.incomingEdges.map((e) => e.id), ...element.outgoingEdges.map((e) => e.id)]; - } -} diff --git a/frontend/webEditor/src/common/di.config.ts b/frontend/webEditor/src/common/di.config.ts deleted file mode 100644 index 58706e39..00000000 --- a/frontend/webEditor/src/common/di.config.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { ContainerModule } from "inversify"; -import { - CenterGridSnapper, - CenterKeyboardListener, - ConsoleLogger, - CreateElementCommand, - LocalModelSource, - LogLevel, - TYPES, - configureCommand, - configureViewerOptions, -} from "sprotty"; -import { HelpUI } from "./helpUi"; -import { DeleteKeyListener } from "./deleteKeyListener"; -import { EDITOR_TYPES } from "../utils"; -import { DynamicChildrenProcessor } from "../features/dfdElements/dynamicChildren"; -import { FitToScreenKeyListener as CenterDiagramKeyListener } from "./fitToScreenKeyListener"; -import { DiagramModificationCommandStack } from "./customCommandStack"; - -import "./commonStyling.css"; -import { LoadingIndicator } from "./loadingIndicator"; - -export const commonModule = new ContainerModule((bind, unbind, isBound, rebind) => { - bind(DeleteKeyListener).toSelf().inSingletonScope(); - bind(TYPES.KeyListener).toService(DeleteKeyListener); - bind(CenterDiagramKeyListener).toSelf().inSingletonScope(); - rebind(CenterKeyboardListener).toService(CenterDiagramKeyListener); - - bind(HelpUI).toSelf().inSingletonScope(); - bind(TYPES.IUIExtension).toService(HelpUI); - bind(EDITOR_TYPES.DefaultUIElement).toService(HelpUI); - - bind(LoadingIndicator).toSelf().inSingletonScope(); - bind(TYPES.IUIExtension).toService(LoadingIndicator); - bind(EDITOR_TYPES.DefaultUIElement).toService(LoadingIndicator); - - bind(DynamicChildrenProcessor).toSelf().inSingletonScope(); - - unbind(TYPES.ICommandStack); - bind(TYPES.ICommandStack).to(DiagramModificationCommandStack).inSingletonScope(); - - // Sprotty configuration - bind(TYPES.ModelSource).to(LocalModelSource).inSingletonScope(); - rebind(TYPES.ILogger).to(ConsoleLogger).inSingletonScope(); - rebind(TYPES.LogLevel).toConstantValue(LogLevel.log); - bind(TYPES.ISnapper).to(CenterGridSnapper); - - // For some reason the CreateElementAction and Command exist but in no sprotty module is the command registered, so we need to do this here. - const context = { bind, unbind, isBound, rebind }; - configureCommand(context, CreateElementCommand); - - // Configure zoom limits - // Without these you could zoom in/out to infinity by accident resulting in your diagram being "gone". - // You can still get back to the diagram using the fit to screen action but these zoom limits prevents this from happening in the most cases. - configureViewerOptions(context, { - zoomLimits: { min: 0.05, max: 20 }, - }); -}); diff --git a/frontend/webEditor/src/common/fitToScreenKeyListener.ts b/frontend/webEditor/src/common/fitToScreenKeyListener.ts deleted file mode 100644 index ce46cae7..00000000 --- a/frontend/webEditor/src/common/fitToScreenKeyListener.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { KeyListener, SModelElementImpl } from "sprotty"; -import { Action, CenterAction } from "sprotty-protocol"; -import { matchesKeystroke } from "sprotty/lib/utils/keyboard"; -import { createDefaultFitToScreenAction } from "../utils"; - -/** - * Key listener that fits the diagram to the screen when pressing Ctrl+Shift+F - * and centers the diagram when pressing Ctrl+Shift+C. - * - * Custom version of the CenterKeyboardListener from sprotty because that one - * does not allow setting a padding. - */ -export class FitToScreenKeyListener extends KeyListener { - override keyDown(element: SModelElementImpl, event: KeyboardEvent): Action[] { - if (matchesKeystroke(event, "KeyC", "ctrlCmd", "shift")) { - return [CenterAction.create([])]; - } - - if (matchesKeystroke(event, "KeyF", "ctrlCmd", "shift")) { - return [createDefaultFitToScreenAction(element.root)]; - } - - return []; - } -} diff --git a/frontend/webEditor/src/common/helpUi.css b/frontend/webEditor/src/common/helpUi.css deleted file mode 100644 index 65782c88..00000000 --- a/frontend/webEditor/src/common/helpUi.css +++ /dev/null @@ -1,16 +0,0 @@ -div.help-ui { - left: 20px; - bottom: 20px; - padding: 10px 10px; -} - -#help-ui-accordion-label .accordion-button::before { - content: ""; - background-image: url("@fortawesome/fontawesome-free/svgs/regular/circle-question.svg"); - display: inline-block; - filter: invert(var(--dark-mode)); - height: 16px; - width: 16px; - background-size: 16px 16px; - vertical-align: text-top; -} diff --git a/frontend/webEditor/src/common/helpUi.ts b/frontend/webEditor/src/common/helpUi.ts deleted file mode 100644 index 839b0125..00000000 --- a/frontend/webEditor/src/common/helpUi.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { AbstractUIExtension } from "sprotty"; -import { injectable } from "inversify"; - -import "./helpUi.css"; - -@injectable() -export class HelpUI extends AbstractUIExtension { - static readonly ID = "help-ui"; - - id(): string { - return HelpUI.ID; - } - - containerClass(): string { - return HelpUI.ID; - } - - protected initializeContents(containerElement: HTMLElement): void { - containerElement.classList.add("ui-float"); - containerElement.innerHTML = ` - - -
-
-

CTRL+Space: Command Palette

-

CTRL+Z: Undo

-

CTRL+Shift+Z: Redo

-

Del: Delete selected elements

-

T: Toggle Label Type Edit UI

-

CTRL+O: Load diagram from json

-

CTRL+Shift+O: Open default diagram

-

CTRL+S: Save diagram to json

-

CTRL+L: Automatically layout diagram

-

CTRL+Shift+F: Fit diagram to screen

-

CTRL+C: Copy selected elements

-

CTRL+V: Paste previously copied elements

-

Esc: Disable current creation tool

-

Toggle Creation Tool: Refer to key in the tool palette

-
-
- `; - - // Set `help-enabled` class on body element when keyboard shortcut overview is open. - const checkbox = containerElement.querySelector("#accordion-state-help") as HTMLInputElement; - const bodyElement = document.querySelector("body") as HTMLBodyElement; - checkbox.addEventListener("change", () => { - if (checkbox.checked) { - bodyElement.classList.add("help-enabled"); - } else { - bodyElement.classList.remove("help-enabled"); - } - }); - } -} diff --git a/frontend/webEditor/src/commonModule.ts b/frontend/webEditor/src/commonModule.ts new file mode 100644 index 00000000..d0dde0ad --- /dev/null +++ b/frontend/webEditor/src/commonModule.ts @@ -0,0 +1,12 @@ +import { ContainerModule } from "inversify"; +import { TYPES, LocalModelSource, ConsoleLogger, LogLevel, configureViewerOptions } from "sprotty"; + +export const commonModule = new ContainerModule((bind, unbind, isBound, rebind) => { + bind(TYPES.ModelSource).to(LocalModelSource).inSingletonScope(); + rebind(TYPES.ILogger).to(ConsoleLogger).inSingletonScope(); + rebind(TYPES.LogLevel).toConstantValue(LogLevel.log); + const context = { bind, unbind, isBound, rebind }; + configureViewerOptions(context, { + zoomLimits: { min: 0.05, max: 20 }, + }); +}); diff --git a/frontend/webEditor/src/constraint/Constraint.ts b/frontend/webEditor/src/constraint/Constraint.ts new file mode 100644 index 00000000..64deeb2f --- /dev/null +++ b/frontend/webEditor/src/constraint/Constraint.ts @@ -0,0 +1,4 @@ +export interface Constraint { + name: string; + constraint: string; +} diff --git a/frontend/webEditor/src/features/constraintMenu/ConstraintMenu.ts b/frontend/webEditor/src/constraint/ConstraintMenu.ts similarity index 77% rename from frontend/webEditor/src/features/constraintMenu/ConstraintMenu.ts rename to frontend/webEditor/src/constraint/ConstraintMenu.ts index 6e35e682..bed9d06f 100644 --- a/frontend/webEditor/src/features/constraintMenu/ConstraintMenu.ts +++ b/frontend/webEditor/src/constraint/ConstraintMenu.ts @@ -1,6 +1,6 @@ -import { inject, injectable, optional } from "inversify"; +import { inject, injectable } from "inversify"; import "./constraintMenu.css"; -import { AbstractUIExtension, IActionDispatcher, LocalModelSource, TYPES } from "sprotty"; +import { IActionDispatcher, LocalModelSource, TYPES } from "sprotty"; import { ConstraintRegistry } from "./constraintRegistry"; // Enable hover feature that is used to show validation errors. @@ -8,45 +8,44 @@ import { ConstraintRegistry } from "./constraintRegistry"; import "monaco-editor/esm/vs/editor/contrib/hover/browser/hoverContribution"; import "monaco-editor/esm/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.js"; import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; -import { - constraintDslLanguageMonarchDefinition, - DSL_LANGUAGE_ID, - MonacoEditorConstraintDslCompletionProvider, -} from "./DslLanguage"; -import { AutoCompleteTree } from "./AutoCompletion"; -import { TreeBuilder } from "./DslLanguage"; -import { LabelTypeRegistry } from "../labels/labelTypeRegistry"; -import { EditorModeController } from "../editorMode/editorModeController"; -import { Switchable, ThemeManager } from "../settingsMenu/themeManager"; -import { AnalyzeDiagramAction } from "../serialize/analyze"; -import { ChooseConstraintAction } from "./actions"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { SETTINGS } from "../settings/Settings"; +import { EditorModeController } from "../settings/editorMode"; +import { AccordionUiExtension } from "../accordionUiExtension"; +import { LanguageTreeNode, tokenize } from "../languages/tokenize"; +import { Word } from "../languages/words"; +import { constraintDslLanguageMonarchDefinition, ConstraintDslTreeBuilder, DSL_LANGUAGE_ID } from "./language"; +import { verify } from "../languages/verify"; +import { DfdCompletionItemProvider } from "../languages/autocomplete"; +import { AnalyzeAction } from "../serialize/analyze"; +import { ApplyableTheme, Theme, ThemeManager, ThemeSwitchable } from "../settings/Theme"; +import { SelectConstraintsAction } from "./selection"; @injectable() -export class ConstraintMenu extends AbstractUIExtension implements Switchable { +export class ConstraintMenu extends AccordionUiExtension implements ThemeSwitchable { static readonly ID = "constraint-menu"; private editorContainer: HTMLDivElement = document.createElement("div") as HTMLDivElement; private validationLabel: HTMLDivElement = document.createElement("div") as HTMLDivElement; private editor?: monaco.editor.IStandaloneCodeEditor; - private tree: AutoCompleteTree; - private forceReadOnly: boolean; private optionsMenu?: HTMLDivElement; private ignoreCheckboxChange = false; + private readonly tree: LanguageTreeNode[]; constructor( @inject(ConstraintRegistry) private readonly constraintRegistry: ConstraintRegistry, @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, @inject(TYPES.ModelSource) modelSource: LocalModelSource, @inject(TYPES.IActionDispatcher) private readonly dispatcher: IActionDispatcher, - @inject(EditorModeController) - @optional() - editorModeController?: EditorModeController, + @inject(SETTINGS.Mode) + private readonly editorModeController: EditorModeController, + @inject(SETTINGS.Theme) private readonly themeManager: ThemeManager, ) { - super(); + super("left", "up"); this.constraintRegistry = constraintRegistry; - this.tree = new AutoCompleteTree(TreeBuilder.buildTree(modelSource, labelTypeRegistry)); - this.forceReadOnly = editorModeController?.getCurrentMode() !== "edit"; - editorModeController?.onModeChange(() => { - this.forceReadOnly = editorModeController!.isReadOnly(); + editorModeController.registerListener(() => { + this.editor?.updateOptions({ + readOnly: editorModeController.isReadOnly(), + }); }); constraintRegistry.onUpdate(() => { if (this.editor) { @@ -57,6 +56,8 @@ export class ConstraintMenu extends AbstractUIExtension implements Switchable { } } }); + + this.tree = ConstraintDslTreeBuilder.buildTree(modelSource, labelTypeRegistry); } id(): string { @@ -65,28 +66,23 @@ export class ConstraintMenu extends AbstractUIExtension implements Switchable { containerClass(): string { return ConstraintMenu.ID; } - protected initializeContents(containerElement: HTMLElement): void { - containerElement.classList.add("ui-float"); - containerElement.innerHTML = ` - - - `; - - const title = containerElement.querySelector("#constraint-menu-expand-title") as HTMLElement; - title.appendChild(this.buildOptionsButton()); - - const accordionContent = document.createElement("div"); - accordionContent.classList.add("accordion-content"); + + protected initializeHeaderContent(headerElement: HTMLElement): void { + headerElement.id = "constraint-menu-expand-title"; + headerElement.innerText = "Constraints"; + headerElement.appendChild(this.buildOptionsButton()); + } + + protected initializeHidableContent(contentElement: HTMLElement): void { const contentDiv = document.createElement("div"); contentDiv.id = "constraint-menu-content"; - accordionContent.appendChild(contentDiv); contentDiv.appendChild(this.buildConstraintInputWrapper()); + contentElement.appendChild(contentDiv); + } + + protected initializeContents(containerElement: HTMLElement): void { + super.initializeContents(containerElement); containerElement.appendChild(this.buildRunButton()); - containerElement.appendChild(accordionContent); } private buildConstraintInputWrapper(): HTMLElement { @@ -103,12 +99,9 @@ export class ConstraintMenu extends AbstractUIExtension implements Switchable { monaco.languages.register({ id: DSL_LANGUAGE_ID }); monaco.languages.setMonarchTokensProvider(DSL_LANGUAGE_ID, constraintDslLanguageMonarchDefinition); - monaco.languages.registerCompletionItemProvider( - DSL_LANGUAGE_ID, - new MonacoEditorConstraintDslCompletionProvider(this.tree), - ); + monaco.languages.registerCompletionItemProvider(DSL_LANGUAGE_ID, new DfdCompletionItemProvider(this.tree)); - const monacoTheme = ThemeManager.useDarkMode ? "vs-dark" : "vs"; + const monacoTheme = this.themeManager.getTheme() === Theme.DARK ? "vs-dark" : "vs"; this.editor = monaco.editor.create(this.editorContainer, { minimap: { // takes too much space, not useful for our use case @@ -128,12 +121,12 @@ export class ConstraintMenu extends AbstractUIExtension implements Switchable { alwaysConsumeMouseWheel: false, }, lineNumbers: "on", - readOnly: this.forceReadOnly, + readOnly: this.editorModeController.isReadOnly(), }); - this.editor?.setValue(this.constraintRegistry.getConstraintsAsText() || ""); + this.editor.setValue(this.constraintRegistry.getConstraintsAsText() || ""); - this.editor?.onDidChangeModelContent(() => { + this.editor.onDidChangeModelContent(() => { if (!this.editor) { return; } @@ -150,7 +143,7 @@ export class ConstraintMenu extends AbstractUIExtension implements Switchable { const emptyContent = content.length == 0 || (content.length == 1 && content[0] === ""); // empty content gets accepted as valid as it represents no constraints if (!emptyContent) { - const errors = this.tree.verify(content); + const errors = verify(tokenize(content), this.tree); marker.push( ...errors.map((e) => ({ severity: monaco.MarkerSeverity.Error, @@ -181,7 +174,10 @@ export class ConstraintMenu extends AbstractUIExtension implements Switchable { button.id = "run-button"; button.innerHTML = "Run"; button.onclick = () => { - this.dispatcher.dispatch(AnalyzeDiagramAction.create()); + this.dispatcher.dispatchAll([ + AnalyzeAction.create(), + SelectConstraintsAction.create(this.constraintRegistry.getConstraintList().map((c) => c.name)), + ]); }; wrapper.appendChild(button); @@ -226,8 +222,8 @@ export class ConstraintMenu extends AbstractUIExtension implements Switchable { e.layout({ height: cHeight, width: cWidth }); } - switchTheme(useDark: boolean): void { - this.editor?.updateOptions({ theme: useDark ? "vs-dark" : "vs" }); + switchTheme(theme: ApplyableTheme): void { + this.editor?.updateOptions({ theme: theme == Theme.DARK ? "vs-dark" : "vs" }); } private buildOptionsButton(): HTMLElement { @@ -241,7 +237,7 @@ export class ConstraintMenu extends AbstractUIExtension implements Switchable { /** show or hide the menu, generate checkboxes on the fly */ private toggleOptionsMenu(): void { - if (this.optionsMenu) { + if (this.optionsMenu !== undefined) { this.optionsMenu.remove(); this.optionsMenu = undefined; return; @@ -273,13 +269,13 @@ export class ConstraintMenu extends AbstractUIExtension implements Switchable { if (cb !== allCb) cb.checked = true; }); this.dispatcher.dispatch( - ChooseConstraintAction.create(this.constraintRegistry.getConstraintList().map((c) => c.name)), + SelectConstraintsAction.create(this.constraintRegistry.getConstraintList().map((c) => c.name)), ); } else { this.optionsMenu.querySelectorAll("input[type=checkbox]").forEach((cb) => { if (cb !== allCb) cb.checked = false; }); - this.dispatcher.dispatch(ChooseConstraintAction.create([])); + this.dispatcher.dispatch(SelectConstraintsAction.create([])); } } finally { this.ignoreCheckboxChange = false; @@ -312,7 +308,7 @@ export class ConstraintMenu extends AbstractUIExtension implements Switchable { allCb.checked = individualCheckboxes.every((cb) => cb.checked); - this.dispatcher.dispatch(ChooseConstraintAction.create(selected)); + this.dispatcher.dispatch(SelectConstraintsAction.create(selected)); }; label.appendChild(cb); diff --git a/frontend/webEditor/src/features/constraintMenu/constraintMenu.css b/frontend/webEditor/src/constraint/constraintMenu.css similarity index 100% rename from frontend/webEditor/src/features/constraintMenu/constraintMenu.css rename to frontend/webEditor/src/constraint/constraintMenu.css diff --git a/frontend/webEditor/src/features/constraintMenu/constraintRegistry.ts b/frontend/webEditor/src/constraint/constraintRegistry.ts similarity index 98% rename from frontend/webEditor/src/features/constraintMenu/constraintRegistry.ts rename to frontend/webEditor/src/constraint/constraintRegistry.ts index 3b773635..2324c318 100644 --- a/frontend/webEditor/src/features/constraintMenu/constraintRegistry.ts +++ b/frontend/webEditor/src/constraint/constraintRegistry.ts @@ -13,6 +13,7 @@ export class ConstraintRegistry { public setConstraints(constraints: string[]): void { this.constraints = this.splitIntoConstraintTexts(constraints).map((c) => this.mapToConstraint(c)); + this.constraintListChanged(); } public setConstraintsFromArray(constraints: Constraint[]): void { diff --git a/frontend/webEditor/src/constraint/di.config.ts b/frontend/webEditor/src/constraint/di.config.ts new file mode 100644 index 00000000..4f0d664d --- /dev/null +++ b/frontend/webEditor/src/constraint/di.config.ts @@ -0,0 +1,20 @@ +import { ContainerModule } from "inversify"; +import { configureCommand, TYPES } from "sprotty"; +import { EDITOR_TYPES } from "../editorTypes"; +import { ConstraintMenu } from "./ConstraintMenu"; +import { ConstraintRegistry } from "./constraintRegistry"; +import { ThemeSwitchable } from "../settings/Theme"; +import { TFGManager } from "./tfgManager"; +import { SelectConstraintsCommand } from "./selection"; + +export const constraintModule = new ContainerModule((bind, _, isBound) => { + bind(ConstraintRegistry).toSelf().inSingletonScope(); + + bind(ConstraintMenu).toSelf().inSingletonScope(); + bind(TYPES.IUIExtension).toService(ConstraintMenu); + bind(EDITOR_TYPES.DefaultUIElement).toService(ConstraintMenu); + bind(ThemeSwitchable).toService(ConstraintMenu); + + bind(TFGManager).toSelf().inSingletonScope(); + configureCommand({ bind, isBound }, SelectConstraintsCommand); +}); diff --git a/frontend/webEditor/src/features/constraintMenu/DslLanguage.ts b/frontend/webEditor/src/constraint/language.ts similarity index 81% rename from frontend/webEditor/src/features/constraintMenu/DslLanguage.ts rename to frontend/webEditor/src/constraint/language.ts index 73d15d60..38591e26 100644 --- a/frontend/webEditor/src/features/constraintMenu/DslLanguage.ts +++ b/frontend/webEditor/src/constraint/language.ts @@ -1,43 +1,14 @@ -import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; -import { - AbstractWord, - AnyWord, - AutoCompleteNode, - AutoCompleteTree, - ConstantWord, - NegatableWord, - WordCompletion, -} from "./AutoCompletion"; -import { LabelTypeRegistry } from "../labels/labelTypeRegistry"; -import { SModelRoot } from "sprotty-protocol"; -import { ArrowEdge } from "../dfdElements/edges"; import { LocalModelSource } from "sprotty"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { AnyWord, ConstantWord, NegatableWord, Word } from "../languages/words"; +import { LanguageTreeNode } from "../languages/tokenize"; +import { SModelRoot } from "sprotty-protocol"; +import { ArrowEdge } from "../diagram/edges/ArrowEdge"; +import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; +import { WordCompletion } from "../languages/autocomplete"; +import { ReplacementData } from "../languages/replace"; -export const DSL_LANGUAGE_ID = "constraint-dsl"; - -export class MonacoEditorConstraintDslCompletionProvider implements monaco.languages.CompletionItemProvider { - constructor(private tree: AutoCompleteTree) {} - - triggerCharacters = [".", "(", " ", ","]; - - provideCompletionItems( - model: monaco.editor.ITextModel, - position: monaco.Position, - ): monaco.languages.ProviderResult { - const allLines = model.getLinesContent(); - const includedLines: string[] = []; - for (let i = 0; i < position.lineNumber - 1; i++) { - includedLines.push(allLines[i]); - } - const currentLine = allLines[position.lineNumber - 1].substring(0, position.column - 1); - includedLines.push(currentLine); - - const r = this.tree.getCompletion(includedLines); - return { - suggestions: r, - }; - } -} +export const DSL_LANGUAGE_ID = "dfd-constraint"; export const constraintDslLanguageMonarchDefinition: monaco.languages.IMonarchLanguage = { keywords: ["data", "vertex", "neverFlows", "to", "where", "named", "present", "empty", "type"], @@ -82,10 +53,13 @@ export const constraintDslLanguageMonarchDefinition: monaco.languages.IMonarchLa }, }; -export namespace TreeBuilder { - export function buildTree(modelSource: LocalModelSource, labelTypeRegistry: LabelTypeRegistry): AutoCompleteNode[] { +export namespace ConstraintDslTreeBuilder { + export function buildTree( + modelSource: LocalModelSource, + labelTypeRegistry: LabelTypeRegistry, + ): LanguageTreeNode[] { const conditions = getConditionalSelectors(); - const conditionalSelector: AutoCompleteNode = { + const conditionalSelector: LanguageTreeNode = { word: new ConstantWord("where"), children: conditions, }; @@ -97,18 +71,18 @@ export namespace TreeBuilder { n.children.push(conditionalSelector); }); }); - const nodeDestinationSelector: AutoCompleteNode = { + const nodeDestinationSelector: LanguageTreeNode = { word: new ConstantWord("vertex"), children: destinationSelectors, }; - const neverFlows: AutoCompleteNode = { + const neverFlows: LanguageTreeNode = { word: new ConstantWord("neverFlows"), children: [nodeDestinationSelector, conditionalSelector], canBeFinal: true, }; - const dataSourceSelector: AutoCompleteNode = { + const dataSourceSelector: LanguageTreeNode = { word: new ConstantWord("data"), children: [], }; @@ -120,7 +94,7 @@ export namespace TreeBuilder { n.children.push(neverFlows); }); }); - const nodeSourceSelector: AutoCompleteNode = { + const nodeSourceSelector: LanguageTreeNode = { word: new ConstantWord("vertex"), children: nodeSelectors, }; @@ -134,12 +108,12 @@ export namespace TreeBuilder { }); dataSourceSelector.children = dataSelectors; - const nameNode: AutoCompleteNode = { + const nameNode: LanguageTreeNode = { word: new NameWord(), children: [nodeSourceSelector, dataSourceSelector], }; - const startNode: AutoCompleteNode = { + const startNode: LanguageTreeNode = { word: new ConstantWord("-"), children: [nameNode], }; @@ -147,11 +121,11 @@ export namespace TreeBuilder { return [startNode]; } - function getLeaves(node: AutoCompleteNode): AutoCompleteNode[] { + function getLeaves(node: LanguageTreeNode): LanguageTreeNode[] { if (node.children.length == 0) { return [node]; } - let result: AutoCompleteNode[] = []; + let result: LanguageTreeNode[] = []; for (const n of node.children) { result = result.concat(getLeaves(n)); } @@ -161,8 +135,8 @@ export namespace TreeBuilder { function getAbstractSelectors( modelSource: LocalModelSource, labelTypeRegistry: LabelTypeRegistry, - ): AutoCompleteNode[] { - const vertexTypeSelector: AutoCompleteNode = { + ): LanguageTreeNode[] { + const vertexTypeSelector: LanguageTreeNode = { word: new ConstantWord("type"), children: [ new NegatableWord(new ConstantWord("EXTERNAL")), @@ -190,8 +164,8 @@ export namespace TreeBuilder { return [vertexTypeSelector, characteristicsSelector, dataCharacteristicListSelector, variableNameSelector]; } - function getConditionalSelectors(): AutoCompleteNode[] { - const variableConditionalSelector: AutoCompleteNode = { + function getConditionalSelectors(): LanguageTreeNode[] { + const variableConditionalSelector: LanguageTreeNode = { word: new ConstantWord("present"), children: [ { @@ -201,7 +175,7 @@ export namespace TreeBuilder { ], }; - const emptySetOperationSelector: AutoCompleteNode = { + const emptySetOperationSelector: LanguageTreeNode = { word: new ConstantWord("empty"), children: [ { @@ -214,7 +188,7 @@ export namespace TreeBuilder { return [variableConditionalSelector, emptySetOperationSelector]; } - class IntersectionWord implements AbstractWord { + class IntersectionWord implements Word { private constraintVariableReference: ConstraintVariableReference; constructor() { @@ -241,7 +215,7 @@ export namespace TreeBuilder { } return this.constraintVariableReference.completionOptions(); } - verifyWord(word: string): string[] { + verify(word: string): string[] { if (!word.startsWith("intersection(")) { return ['Expected keyword "intersection"']; } @@ -249,13 +223,13 @@ export namespace TreeBuilder { if (attributes.length > 2) { return ['Expected at most 2 attributes in "intersection"']; } - return attributes.flatMap((a) => this.constraintVariableReference.verifyWord(a)); + return attributes.flatMap((a) => this.constraintVariableReference.verify(a)); } } class ConstraintVariableReference extends AnyWord {} - class CharacteristicSelectorData implements AbstractWord { + class CharacteristicSelectorData implements Word { constructor(private readonly labelTypeRegistry: LabelTypeRegistry) {} completionOptions(word: string): WordCompletion[] { @@ -288,7 +262,7 @@ export namespace TreeBuilder { return []; } - verifyWord(word: string): string[] { + verify(word: string): string[] { const parts = word.split("."); if (parts.length > 2) { @@ -315,9 +289,16 @@ export namespace TreeBuilder { return []; } + + replace(text: string, replacement: ReplacementData) { + if (replacement.type == "label" && text == replacement.old) { + return replacement.replacement; + } + return text; + } } - class NameWord implements AbstractWord { + class NameWord implements Word { completionOptions(word: string): WordCompletion[] { if (word.length === 0) { return []; @@ -330,7 +311,7 @@ export namespace TreeBuilder { ]; } - verifyWord(word: string): string[] { + verify(word: string): string[] { const name = word.split(":")[0]; if (name.length === 0) { return ["Expected a name"]; @@ -342,7 +323,7 @@ export namespace TreeBuilder { } } - class VariableName implements AbstractWord { + class VariableName implements Word { constructor(private readonly modelSource: LocalModelSource) {} completionOptions(): WordCompletion[] { @@ -351,7 +332,7 @@ export namespace TreeBuilder { kind: monaco.languages.CompletionItemKind.Variable, })); } - verifyWord(word: string): string[] { + verify(word: string): string[] { if (this.getAllPortNames().includes(word)) { return []; } @@ -381,7 +362,7 @@ export namespace TreeBuilder { } } - class CharacteristicSelectorDataList implements AbstractWord { + class CharacteristicSelectorDataList implements Word { private characteristicSelectorData: CharacteristicSelectorData; constructor(labelTypeRegistry: LabelTypeRegistry) { @@ -394,10 +375,10 @@ export namespace TreeBuilder { return this.characteristicSelectorData.completionOptions(last); } - verifyWord(word: string): string[] { + verify(word: string): string[] { const parts = word.split(","); for (let i = 0; i < parts.length; i++) { - const r = this.characteristicSelectorData.verifyWord(parts[i]); + const r = this.characteristicSelectorData.verify(parts[i]); if (r.length > 0) { return r; } @@ -405,5 +386,13 @@ export namespace TreeBuilder { return []; } + + replace(text: string, replacement: ReplacementData) { + if (!this.characteristicSelectorData.replace) { + return text; + } + const parts = text.split(","); + return parts.map((p) => this.characteristicSelectorData.replace(p, replacement)).join(","); + } } } diff --git a/frontend/webEditor/src/constraint/selection.ts b/frontend/webEditor/src/constraint/selection.ts new file mode 100644 index 00000000..03718085 --- /dev/null +++ b/frontend/webEditor/src/constraint/selection.ts @@ -0,0 +1,107 @@ +import { Command, CommandExecutionContext, CommandReturn, SModelRootImpl, TYPES } from "sprotty"; +import { TFGManager } from "./tfgManager"; +import { ConstraintRegistry } from "./constraintRegistry"; +import { Action, getBasicType } from "sprotty-protocol"; +import { DfdNodeImpl } from "../diagram/nodes/common"; +import { inject } from "inversify"; + +function selectConstraints( + selectedConstraintNames: string[], + root: SModelRootImpl, + constraintRegistry: ConstraintRegistry, + tfgManager: TFGManager, +) { + tfgManager.clearTfgs(); + constraintRegistry.setSelectedConstraints(selectedConstraintNames); + + const nodes = root.children.filter((node) => getBasicType(node) === "node") as DfdNodeImpl[]; + if (selectedConstraintNames.length === 0) { + nodes.forEach((node) => { + node.setColor("var(--color-primary)"); + }); + return root; + } + + nodes.forEach((node) => { + const annotations = node.annotations!; + let wasAdjusted = false; + if (constraintRegistry.selectedContainsAllConstraints()) { + annotations.forEach((annotation) => { + if (annotation.message.startsWith("Constraint")) { + wasAdjusted = true; + node.setColor(annotation.color!); + } + }); + } + selectedConstraintNames.forEach((name) => { + annotations.forEach((annotation) => { + if (annotation.message.startsWith("Constraint ") && annotation.message.split(" ")[1] === name) { + node.setColor(annotation.color!); + wasAdjusted = true; + tfgManager.addTfg(annotation.tfg!); + } + }); + }); + if (!wasAdjusted) node.setColor("var(--color-primary)"); + }); + + nodes.forEach((node) => { + const inTFG = node.annotations!.filter((annotation) => tfgManager.getSelectedTfgs().has(annotation.tfg!)); + if (inTFG.length > 0) node.setColor("var(--color-highlighted)", false); + }); + + return root; +} + +interface SelectConstraintsAction extends Action { + selectedConstraintNames: string[]; +} + +export namespace SelectConstraintsAction { + export const KIND = "select-constraints"; + export function create(selectedConstraintNames: string[]): SelectConstraintsAction { + return { + kind: KIND, + selectedConstraintNames, + }; + } +} + +export class SelectConstraintsCommand extends Command { + static readonly KIND = SelectConstraintsAction.KIND; + private oldConstraintSelection?: string[]; + + constructor( + @inject(TYPES.Action) private readonly action: SelectConstraintsAction, + @inject(ConstraintRegistry) private readonly constraintRegistry: ConstraintRegistry, + @inject(TFGManager) private readonly tfgManager: TFGManager, + ) { + super(); + } + + execute(context: CommandExecutionContext): CommandReturn { + this.oldConstraintSelection = this.constraintRegistry.getSelectedConstraints(); + return selectConstraints( + this.action.selectedConstraintNames, + context.root, + this.constraintRegistry, + this.tfgManager, + ); + } + undo(context: CommandExecutionContext): CommandReturn { + return selectConstraints( + this.oldConstraintSelection ?? [], + context.root, + this.constraintRegistry, + this.tfgManager, + ); + } + redo(context: CommandExecutionContext): CommandReturn { + return selectConstraints( + this.action.selectedConstraintNames, + context.root, + this.constraintRegistry, + this.tfgManager, + ); + } +} diff --git a/frontend/webEditor/src/features/settingsMenu/annotationManager.ts b/frontend/webEditor/src/constraint/tfgManager.ts similarity index 71% rename from frontend/webEditor/src/features/settingsMenu/annotationManager.ts rename to frontend/webEditor/src/constraint/tfgManager.ts index 064304b9..46bcd1e9 100644 --- a/frontend/webEditor/src/features/settingsMenu/annotationManager.ts +++ b/frontend/webEditor/src/constraint/tfgManager.ts @@ -1,13 +1,7 @@ import { injectable } from "inversify"; -export enum Mode { - INCOMING = "Incoming Labels", - OUTGOING = "Outgoing Labels", - ALL = "All Labels", -} - @injectable() -export class AnnnotationsManager { +export class TFGManager { private selectedTfgs = new Set(); public getSelectedTfgs(): Set { diff --git a/frontend/webEditor/src/features/dfdElements/di.config.ts b/frontend/webEditor/src/diagram/di.config.ts similarity index 57% rename from frontend/webEditor/src/features/dfdElements/di.config.ts rename to frontend/webEditor/src/diagram/di.config.ts index 86ef126a..358fc9fa 100644 --- a/frontend/webEditor/src/features/dfdElements/di.config.ts +++ b/frontend/webEditor/src/diagram/di.config.ts @@ -1,71 +1,81 @@ import { ContainerModule } from "inversify"; import { + configureActionHandler, + configureCommand, + configureModelElement, + EditLabelAction, + EditLabelActionHandler, + editLabelFeature, SGraphImpl, SGraphView, SLabelImpl, - configureModelElement, - editLabelFeature, - withEditLabelFeature, SLabelView, SRoutingHandleImpl, TYPES, - configureCommand, + withEditLabelFeature, } from "sprotty"; -import { FunctionNodeImpl, FunctionNodeView, IONodeImpl, IONodeView, StorageNodeImpl, StorageNodeView } from "./nodes"; -import { ArrowEdgeImpl, ArrowEdgeView, CustomRoutingHandleView } from "./edges"; -import { DfdInputPortImpl, DfdInputPortView, DfdOutputPortImpl, DfdOutputPortView } from "./ports"; -import { FilledBackgroundLabelView, DfdPositionalLabelView } from "./labels"; -import { AlwaysSnapPortsMoveMouseListener, ReSnapPortsAfterLabelChangeCommand, PortAwareSnapper } from "./portSnapper"; -import { OutputPortEditUIMouseListener, OutputPortEditUI, SetDfdOutputPortBehaviorCommand } from "./outputPortEditUi"; -import { DfdEditLabelValidator, DfdEditLabelValidatorDecorator } from "./editLabelValidator"; -import { DfdNodeAnnotationUI, DfdNodeAnnotationUIMouseListener } from "./nodeAnnotationUi"; -import { DFDBehaviorRefactorer, RefactorInputNameInDFDBehaviorCommand } from "./behaviorRefactorer"; - -import "./elementStyles.css"; -import { SWITCHABLE } from "../settingsMenu/themeManager"; +import { ArrowEdgeImpl, ArrowEdgeView, CustomRoutingHandleView } from "./edges/ArrowEdge"; +import { DfdInputPortImpl, DfdInputPortView } from "./ports/DfdInputPort"; +import { DfdOutputPortImpl, DfdOutputPortView } from "./ports/DfdOutputPort"; +import { StorageNodeImpl, StorageNodeView } from "./nodes/DfdStorageNode"; +import { FunctionNodeImpl, FunctionNodeView } from "./nodes/DfdFunctionNode"; +import { IONodeImpl, IONodeView } from "./nodes/DfdIONode"; +import "./style.css"; +import { DfdPositionalLabelView } from "./labels/DfdPositionalLabel"; +import { DfdNodeLabelRenderer } from "./nodes/DfdNodeLabels"; +import { FilledBackgroundLabelView } from "./labels/FilledBackgroundLabel"; +import { DfdEditLabelValidatorDecorator } from "./labels/EditLabelDecorator"; +import { DfdEditLabelValidator } from "./labels/EditLabelValidator"; +import { NoScrollEditLabelUI } from "./labels/NoScrollEditLabelUI"; +import { + PortAwareSnapper, + AlwaysSnapPortsMoveMouseListener, + ReSnapPortsAfterLabelChangeCommand, +} from "./ports/portSnapper"; +import { DfdNodeAnnotationUI, DfdNodeAnnotationUIMouseListener } from "./nodes/annotation"; -export const dfdElementsModule = new ContainerModule((bind, unbind, isBound, rebind) => { +export const diagramModule = new ContainerModule((bind, unbind, isBound, rebind) => { const context = { bind, unbind, isBound, rebind }; - rebind(TYPES.ISnapper).to(PortAwareSnapper).inSingletonScope(); - bind(TYPES.MouseListener).to(AlwaysSnapPortsMoveMouseListener).inSingletonScope(); - configureCommand(context, ReSnapPortsAfterLabelChangeCommand); - - bind(OutputPortEditUI).toSelf().inSingletonScope(); - bind(TYPES.IUIExtension).toService(OutputPortEditUI); - bind(SWITCHABLE).toService(OutputPortEditUI); - - bind(TYPES.MouseListener).to(OutputPortEditUIMouseListener).inSingletonScope(); - configureCommand(context, SetDfdOutputPortBehaviorCommand); - bind(TYPES.IEditLabelValidator).to(DfdEditLabelValidator).inSingletonScope(); bind(TYPES.IEditLabelValidationDecorator).to(DfdEditLabelValidatorDecorator).inSingletonScope(); + configureActionHandler(context, EditLabelAction.KIND, EditLabelActionHandler); + bind(NoScrollEditLabelUI).toSelf().inSingletonScope(); + bind(TYPES.IUIExtension).toService(NoScrollEditLabelUI); + + bind(TYPES.ISnapper).to(PortAwareSnapper).inSingletonScope(); + bind(TYPES.MouseListener).to(AlwaysSnapPortsMoveMouseListener).inSingletonScope(); + configureCommand(context, ReSnapPortsAfterLabelChangeCommand); + bind(DfdNodeAnnotationUI).toSelf().inSingletonScope(); + bind(TYPES.IUIExtension).toService(DfdNodeAnnotationUI); bind(DfdNodeAnnotationUIMouseListener).toSelf().inSingletonScope(); bind(TYPES.MouseListener).toService(DfdNodeAnnotationUIMouseListener); - bind(TYPES.IUIExtension).to(DfdNodeAnnotationUI).inSingletonScope(); - - bind(DFDBehaviorRefactorer).toSelf().inSingletonScope(); - configureCommand(context, RefactorInputNameInDFDBehaviorCommand); configureModelElement(context, "graph", SGraphImpl, SGraphView); + configureModelElement(context, "node:storage", StorageNodeImpl, StorageNodeView); configureModelElement(context, "node:function", FunctionNodeImpl, FunctionNodeView); configureModelElement(context, "node:input-output", IONodeImpl, IONodeView); + configureModelElement(context, "edge:arrow", ArrowEdgeImpl, ArrowEdgeView, { enable: [withEditLabelFeature], }); + configureModelElement(context, "routing-point", SRoutingHandleImpl, CustomRoutingHandleView); + configureModelElement(context, "volatile-routing-point", SRoutingHandleImpl, CustomRoutingHandleView); + + configureModelElement(context, "port:dfd-input", DfdInputPortImpl, DfdInputPortView); + configureModelElement(context, "port:dfd-output", DfdOutputPortImpl, DfdOutputPortView); + configureModelElement(context, "label", SLabelImpl, SLabelView, { enable: [editLabelFeature], }); - configureModelElement(context, "label:filled-background", SLabelImpl, FilledBackgroundLabelView, { + configureModelElement(context, "label:positional", SLabelImpl, DfdPositionalLabelView, { enable: [editLabelFeature], }); - configureModelElement(context, "label:positional", SLabelImpl, DfdPositionalLabelView, { + configureModelElement(context, "label:filled-background", SLabelImpl, FilledBackgroundLabelView, { enable: [editLabelFeature], }); - configureModelElement(context, "port:dfd-input", DfdInputPortImpl, DfdInputPortView); - configureModelElement(context, "port:dfd-output", DfdOutputPortImpl, DfdOutputPortView); - configureModelElement(context, "routing-point", SRoutingHandleImpl, CustomRoutingHandleView); - configureModelElement(context, "volatile-routing-point", SRoutingHandleImpl, CustomRoutingHandleView); + + bind(DfdNodeLabelRenderer).toSelf().inSingletonScope(); }); diff --git a/frontend/webEditor/src/features/dfdElements/edges.tsx b/frontend/webEditor/src/diagram/edges/ArrowEdge.tsx similarity index 71% rename from frontend/webEditor/src/features/dfdElements/edges.tsx rename to frontend/webEditor/src/diagram/edges/ArrowEdge.tsx index 5e363601..ef2c23d5 100644 --- a/frontend/webEditor/src/features/dfdElements/edges.tsx +++ b/frontend/webEditor/src/diagram/edges/ArrowEdge.tsx @@ -12,38 +12,16 @@ import { SRoutingHandleView, } from "sprotty"; import { VNode } from "snabbdom"; -import { Point, angleOfPoint, toDegrees, SEdge, SLabel } from "sprotty-protocol"; -import { DynamicChildrenEdge } from "./dynamicChildren"; -import { SettingsManager } from "../settingsMenu/SettingsManager"; +import { Point, angleOfPoint, toDegrees, SEdge } from "sprotty-protocol"; +import { HideEdgeNames, SETTINGS } from "../../settings/Settings"; export interface ArrowEdge extends SEdge { text?: string; } -export class ArrowEdgeImpl extends DynamicChildrenEdge implements WithEditableLabel { +export class ArrowEdgeImpl extends SEdgeImpl implements WithEditableLabel { text?: string; - setChildren(schema: ArrowEdge): void { - schema.children = [ - { - type: "label:filled-background", - text: schema.text ?? "", - id: schema.id + "-label", - edgePlacement: { - position: 0.5, - side: "on", - rotate: false, - }, - } as SLabel, - ]; - } - - removeChildren(schema: ArrowEdge): void { - const label = schema.children?.find((element) => element.type.startsWith("label")) as SLabel | undefined; - schema.text = label?.text ?? ""; - schema.children = []; - } - get editableLabel() { const label = this.children.find((element) => element.type.startsWith("label")); if (label && isEditableLabel(label)) { @@ -56,48 +34,10 @@ export class ArrowEdgeImpl extends DynamicChildrenEdge implements WithEditableLa @injectable() export class ArrowEdgeView extends PolylineEdgeViewWithGapsOnIntersections { - constructor(@inject(SettingsManager) protected readonly settings: SettingsManager) { + constructor(@inject(SETTINGS.HideEdgeNames) private readonly hideEdgeNames: HideEdgeNames) { super(); } - override render(edge: Readonly, context: RenderingContext, args?: IViewArgs): VNode | undefined { - // In the default implementation children of the edge are always rendered, because they - // may be visible when the rest of the edge is not. - // We only have the edge label as an children which only must be rendered when the rest of the edge is visible. - // So as an optimization for big diagrams we don't render the label when the rest of the edge is not visible either. - // Otherwise all these labels would be added to the DOM, making it slow.. - const route = this.edgeRouterRegistry.route(edge, args); - if (!this.isVisible(edge, route, context)) { - return undefined; - } - - return this.superRender(edge, context, args); - } - - superRender(edge: Readonly, context: RenderingContext, args?: IViewArgs): VNode | undefined { - const route = this.edgeRouterRegistry.route(edge, args); - if (route.length === 0) { - return this.renderDanglingEdge("Cannot compute route", edge, context); - } - if (!this.isVisible(edge, route, context)) { - if (edge.children.length === 0) { - return undefined; - } - // The children of an edge are not necessarily inside the bounding box of the route, - // so we need to render a group to ensure the children have a chance to be rendered. - return {this.settings.hideEdgeLabels ? [] : context.renderChildren(edge, { route })}; - } - - return ( - - {this.renderLine(edge, route)} - {this.renderAdditionals(edge, route, context)} - {this.renderJunctionPoints(edge, route, context, args)} - {this.settings.hideEdgeLabels ? [] : context.renderChildren(edge, { route })} - - ); - } - /** * Renders an arrow at the end of the edge. */ @@ -124,7 +64,7 @@ export class ArrowEdgeView extends PolylineEdgeViewWithGapsOnIntersections { * In contrast to the default implementation that we override here, * this implementation makes the edge line 10px shorter at the end to make space for the arrow without any overlap. */ - protected renderLine(edge: SEdgeImpl, segments: Point[]): VNode { + override renderLine(edge: SEdgeImpl, segments: Point[]): VNode { const firstPoint = segments[0]; let path = `M ${firstPoint.x},${firstPoint.y}`; for (let i = 1; i < segments.length; i++) { @@ -153,6 +93,30 @@ export class ArrowEdgeView extends PolylineEdgeViewWithGapsOnIntersections { ); } + + override render(edge: Readonly, context: RenderingContext, args?: IViewArgs): VNode | undefined { + // In the default implementation children of the edge are always rendered, because they + // may be visible when the rest of the edge is not. + // We only have the edge label as an children which only must be rendered when the rest of the edge is visible. + // So as an optimization for big diagrams we don't render the label when the rest of the edge is not visible either. + // Otherwise all these labels would be added to the DOM, making it slow.. + const route = this.edgeRouterRegistry.route(edge, args); + if (!this.isVisible(edge, route, context)) { + return undefined; + } + if (route.length === 0) { + return this.renderDanglingEdge("Cannot compute route", edge, context); + } + + return ( + + {this.renderLine(edge, route)} + {this.renderAdditionals(edge, route, context)} + {this.renderJunctionPoints(edge, route, context, args)} + {this.hideEdgeNames.get() ? undefined : context.renderChildren(edge, { route })} + + ); + } } /** diff --git a/frontend/webEditor/src/features/dfdElements/labels.tsx b/frontend/webEditor/src/diagram/labels/DfdPositionalLabel.tsx similarity index 57% rename from frontend/webEditor/src/features/dfdElements/labels.tsx rename to frontend/webEditor/src/diagram/labels/DfdPositionalLabel.tsx index f7dec3ca..1c4ebe92 100644 --- a/frontend/webEditor/src/features/dfdElements/labels.tsx +++ b/frontend/webEditor/src/diagram/labels/DfdPositionalLabel.tsx @@ -4,7 +4,6 @@ import { IViewArgs, SLabelImpl, SNodeImpl, ShapeView, RenderingContext, svg } fr import { VNode } from "snabbdom"; import { injectable } from "inversify"; import { Point } from "sprotty-protocol"; -import { calculateTextSize } from "../../utils"; export interface DfdPositionalLabelArgs extends IViewArgs { xPosition: number; @@ -34,29 +33,3 @@ export class DfdPositionalLabelView extends ShapeView { ); } } - -/** - * A sprotty label view that renders the label text with a filled background behind it. - * This is used to make the element behind the label invisible. - */ -@injectable() -export class FilledBackgroundLabelView extends ShapeView { - static readonly PADDING = 5; - - render(label: Readonly, context: RenderingContext): VNode | undefined { - if (!this.isVisible(label, context)) { - return undefined; - } - - const size = calculateTextSize(label.text); - const width = size.width + FilledBackgroundLabelView.PADDING; - const height = size.height + FilledBackgroundLabelView.PADDING; - - return ( - - {label.text ? : undefined} - {label.text} - - ); - } -} diff --git a/frontend/webEditor/src/diagram/labels/EditLabelDecorator.ts b/frontend/webEditor/src/diagram/labels/EditLabelDecorator.ts new file mode 100644 index 00000000..ab8aaddc --- /dev/null +++ b/frontend/webEditor/src/diagram/labels/EditLabelDecorator.ts @@ -0,0 +1,38 @@ +import { injectable } from "inversify"; +import { IEditLabelValidationDecorator, EditLabelValidationResult } from "sprotty"; +import "./editLabelDecorator.css"; + +/** + * Renders the validation result of an dfd edge label to the label edit ui. + */ +@injectable() +export class DfdEditLabelValidatorDecorator implements IEditLabelValidationDecorator { + private readonly cssClass = "label-validation-results"; + + decorate(input: HTMLInputElement | HTMLTextAreaElement, validationResult: EditLabelValidationResult): void { + const containerElement = input.parentElement; + if (!containerElement) { + return; + } + + // Only display something when there is a validation error or warning + if (validationResult.severity !== "ok") { + const span = document.createElement("span"); + span.innerText = validationResult.message ?? validationResult.severity; + span.classList.add(this.cssClass); + + // Place validation notice right under the input field + span.style.top = `${input.clientHeight}px`; + // Rest is styled in the corresponding css file, as it is not dynamic + + containerElement.appendChild(span); + } + } + + dispose(input: HTMLInputElement | HTMLTextAreaElement): void { + const containerElement = input.parentElement; + if (containerElement) { + containerElement.querySelector(`span.${this.cssClass}`)?.remove(); + } + } +} diff --git a/frontend/webEditor/src/features/dfdElements/editLabelValidator.ts b/frontend/webEditor/src/diagram/labels/EditLabelValidator.ts similarity index 60% rename from frontend/webEditor/src/features/dfdElements/editLabelValidator.ts rename to frontend/webEditor/src/diagram/labels/EditLabelValidator.ts index 2ef9dece..7751f118 100644 --- a/frontend/webEditor/src/features/dfdElements/editLabelValidator.ts +++ b/frontend/webEditor/src/diagram/labels/EditLabelValidator.ts @@ -1,17 +1,14 @@ import { injectable } from "inversify"; import { - EditLabelValidationResult, - EditableLabel, - IEditLabelValidationDecorator, IEditLabelValidator, + EditableLabel, + SModelElementImpl, + EditLabelValidationResult, SChildElementImpl, SEdgeImpl, - SModelElementImpl, } from "sprotty"; -import { DfdInputPortImpl } from "./ports"; -import { DfdNodeImpl } from "./nodes"; - -import "./editLabelValidator.css"; +import { DfdNodeImpl } from "../nodes/common"; +import { DfdInputPortImpl } from "../ports/DfdInputPort"; /** * Validator for the label of an dfd edge. @@ -65,38 +62,3 @@ export class DfdEditLabelValidator implements IEditLabelValidator { return { severity: "ok" }; } } - -/** - * Renders the validation result of an dfd edge label to the label edit ui. - */ -@injectable() -export class DfdEditLabelValidatorDecorator implements IEditLabelValidationDecorator { - private readonly cssClass = "label-validation-results"; - - decorate(input: HTMLInputElement | HTMLTextAreaElement, validationResult: EditLabelValidationResult): void { - const containerElement = input.parentElement; - if (!containerElement) { - return; - } - - // Only display something when there is a validation error or warning - if (validationResult.severity !== "ok") { - const span = document.createElement("span"); - span.innerText = validationResult.message ?? validationResult.severity; - span.classList.add(this.cssClass); - - // Place validation notice right under the input field - span.style.top = `${input.clientHeight}px`; - // Rest is styled in the corresponding css file, as it is not dynamic - - containerElement.appendChild(span); - } - } - - dispose(input: HTMLInputElement | HTMLTextAreaElement): void { - const containerElement = input.parentElement; - if (containerElement) { - containerElement.querySelector(`span.${this.cssClass}`)?.remove(); - } - } -} diff --git a/frontend/webEditor/src/diagram/labels/FilledBackgroundLabel.tsx b/frontend/webEditor/src/diagram/labels/FilledBackgroundLabel.tsx new file mode 100644 index 00000000..e8cc209f --- /dev/null +++ b/frontend/webEditor/src/diagram/labels/FilledBackgroundLabel.tsx @@ -0,0 +1,32 @@ +/** @jsx svg */ +import { injectable } from "inversify"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { svg, ShapeView, SLabelImpl, RenderingContext } from "sprotty"; +import { calculateTextSize } from "../../utils/TextSize"; +import { VNode } from "snabbdom"; + +/** + * A sprotty label view that renders the label text with a filled background behind it. + * This is used to make the element behind the label invisible. + */ +@injectable() +export class FilledBackgroundLabelView extends ShapeView { + static readonly PADDING = 5; + + render(label: Readonly, context: RenderingContext): VNode | undefined { + if (!this.isVisible(label, context)) { + return undefined; + } + + const size = calculateTextSize(label.text); + const width = size.width + FilledBackgroundLabelView.PADDING; + const height = size.height + FilledBackgroundLabelView.PADDING; + + return ( + + {label.text ? : undefined} + {label.text} + + ); + } +} diff --git a/frontend/webEditor/src/common/labelEditNoScroll.ts b/frontend/webEditor/src/diagram/labels/NoScrollEditLabelUI.ts similarity index 56% rename from frontend/webEditor/src/common/labelEditNoScroll.ts rename to frontend/webEditor/src/diagram/labels/NoScrollEditLabelUI.ts index 89ad85d1..444e4065 100644 --- a/frontend/webEditor/src/common/labelEditNoScroll.ts +++ b/frontend/webEditor/src/diagram/labels/NoScrollEditLabelUI.ts @@ -1,12 +1,4 @@ -import { ContainerModule } from "inversify"; -import { - EditLabelAction, - EditLabelActionHandler, - EditLabelUI, - SModelRootImpl, - TYPES, - configureActionHandler, -} from "sprotty"; +import { EditLabelUI, SModelRootImpl } from "sprotty"; // For our use-case the sprotty container is at (0, 0) and fills the whole screen. // Scrolling is disabled using CSS which disallows scrolling from the user. @@ -19,7 +11,7 @@ import { // scroll the page back to the page origin at (0, 0) if it has been moved due to the // focus event. -class NoScrollEditLabelUI extends EditLabelUI { +export class NoScrollEditLabelUI extends EditLabelUI { protected override onBeforeShow( containerElement: HTMLElement, root: Readonly, @@ -33,13 +25,3 @@ class NoScrollEditLabelUI extends EditLabelUI { } } } - -export const noScrollLabelEditUiModule = new ContainerModule((bind, _unbind, isBound) => { - // Provide the same stuff as the labelEditUiModule from sprotty but use our own EditLabelUI - // instead of the default one. - // When using this module the original sprotty labelEditUiModule must not be loaded aswell. - const context = { bind, isBound }; - configureActionHandler(context, EditLabelAction.KIND, EditLabelActionHandler); - bind(NoScrollEditLabelUI).toSelf().inSingletonScope(); - bind(TYPES.IUIExtension).toService(NoScrollEditLabelUI); -}); diff --git a/frontend/webEditor/src/features/dfdElements/editLabelValidator.css b/frontend/webEditor/src/diagram/labels/editLabelDecorator.css similarity index 100% rename from frontend/webEditor/src/features/dfdElements/editLabelValidator.css rename to frontend/webEditor/src/diagram/labels/editLabelDecorator.css diff --git a/frontend/webEditor/src/diagram/nodes/DfdFunctionNode.tsx b/frontend/webEditor/src/diagram/nodes/DfdFunctionNode.tsx new file mode 100644 index 00000000..bcca50ed --- /dev/null +++ b/frontend/webEditor/src/diagram/nodes/DfdFunctionNode.tsx @@ -0,0 +1,51 @@ +/** @jsx svg */ +import { inject, injectable } from "inversify"; +import { DfdNodeImpl } from "./common"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { ShapeView, RenderingContext, svg } from "sprotty"; +import { VNode } from "snabbdom"; +import { DfdPositionalLabelArgs } from "../labels/DfdPositionalLabel"; +import { DfdNodeLabelRenderer } from "./DfdNodeLabels"; + +export class FunctionNodeImpl extends DfdNodeImpl { + static readonly TEXT_HEIGHT = 28; + static readonly SEPARATOR_NO_LABEL_PADDING = 4; + static readonly SEPARATOR_LABEL_PADDING = 4; + static readonly LABEL_START_HEIGHT = this.TEXT_HEIGHT + this.SEPARATOR_LABEL_PADDING; + static readonly BORDER_RADIUS = 5; + + protected noLabelHeight(): number { + return FunctionNodeImpl.LABEL_START_HEIGHT + FunctionNodeImpl.SEPARATOR_NO_LABEL_PADDING; + } + protected labelStartHeight(): number { + return FunctionNodeImpl.LABEL_START_HEIGHT; + } +} + +@injectable() +export class FunctionNodeView extends ShapeView { + constructor(@inject(DfdNodeLabelRenderer) private readonly labelRenderer: DfdNodeLabelRenderer) { + super(); + } + + render(node: Readonly, context: RenderingContext): VNode | undefined { + if (!this.isVisible(node, context)) { + return undefined; + } + + const { width, height } = node.bounds; + const r = FunctionNodeImpl.BORDER_RADIUS; + + return ( + + + + {context.renderChildren(node, { + xPosition: width / 2, + yPosition: FunctionNodeImpl.TEXT_HEIGHT / 2, + } as DfdPositionalLabelArgs)} + {this.labelRenderer.renderNodeLabels(node, FunctionNodeImpl.LABEL_START_HEIGHT)} + + ); + } +} diff --git a/frontend/webEditor/src/diagram/nodes/DfdIONode.tsx b/frontend/webEditor/src/diagram/nodes/DfdIONode.tsx new file mode 100644 index 00000000..65bb9894 --- /dev/null +++ b/frontend/webEditor/src/diagram/nodes/DfdIONode.tsx @@ -0,0 +1,47 @@ +/** @jsx svg */ +import { inject, injectable } from "inversify"; +import { DfdNodeImpl } from "./common"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { ShapeView, svg, RenderingContext } from "sprotty"; +import { VNode } from "snabbdom"; +import { DfdPositionalLabelArgs } from "../labels/DfdPositionalLabel"; +import { DfdNodeLabelRenderer } from "./DfdNodeLabels"; + +@injectable() +export class IONodeImpl extends DfdNodeImpl { + static readonly TEXT_HEIGHT = 32; + static readonly LABEL_START_HEIGHT = 28; + + protected noLabelHeight(): number { + return IONodeImpl.TEXT_HEIGHT; + } + protected labelStartHeight(): number { + return IONodeImpl.LABEL_START_HEIGHT; + } +} + +@injectable() +export class IONodeView extends ShapeView { + constructor(@inject(DfdNodeLabelRenderer) private readonly labelRenderer: DfdNodeLabelRenderer) { + super(); + } + + render(node: Readonly, context: RenderingContext): VNode | undefined { + if (!this.isVisible(node, context)) { + return undefined; + } + + const { width, height } = node.bounds; + + return ( + + + {context.renderChildren(node, { + xPosition: width / 2, + yPosition: IONodeImpl.TEXT_HEIGHT / 2, + } as DfdPositionalLabelArgs)} + {this.labelRenderer.renderNodeLabels(node, IONodeImpl.LABEL_START_HEIGHT)} + + ); + } +} diff --git a/frontend/webEditor/src/features/labels/labelRenderer.tsx b/frontend/webEditor/src/diagram/nodes/DfdNodeLabels.tsx similarity index 65% rename from frontend/webEditor/src/features/labels/labelRenderer.tsx rename to frontend/webEditor/src/diagram/nodes/DfdNodeLabels.tsx index aa0281bd..fef034de 100644 --- a/frontend/webEditor/src/features/labels/labelRenderer.tsx +++ b/frontend/webEditor/src/diagram/nodes/DfdNodeLabels.tsx @@ -1,13 +1,13 @@ /** @jsx svg */ -import { injectable, inject, optional } from "inversify"; -import { VNode } from "snabbdom"; // eslint-disable-next-line @typescript-eslint/no-unused-vars -import { IActionDispatcher, SNodeImpl, TYPES, svg } from "sprotty"; -import { calculateTextSize } from "../../utils"; -import { LabelAssignment, LabelTypeRegistry, globalLabelTypeRegistry } from "./labelTypeRegistry"; -import { DeleteLabelAssignmentAction } from "./commands"; -import { ContainsDfdLabels } from "./elementFeature"; -import { SettingsManager } from "../settingsMenu/SettingsManager"; +import { IActionDispatcher, SNodeImpl, svg, TYPES } from "sprotty"; +import { LabelAssignment, LabelType, LabelTypeValue } from "../../labels/LabelType"; +import { inject, injectable } from "inversify"; +import { LabelTypeRegistry } from "../../labels/LabelTypeRegistry"; +import { calculateTextSize } from "../../utils/TextSize"; +import { VNode } from "snabbdom"; +import { ContainsDfdLabels } from "../../labels/feature"; +import { RemoveLabelAssignmentAction } from "../../labels/assignmentCommand"; @injectable() export class DfdNodeLabelRenderer { @@ -18,38 +18,47 @@ export class DfdNodeLabelRenderer { constructor( @inject(TYPES.IActionDispatcher) private readonly actionDispatcher: IActionDispatcher, - @inject(SettingsManager) private readonly settingsManager: SettingsManager, - @inject(LabelTypeRegistry) @optional() private readonly labelTypeRegistry?: LabelTypeRegistry, + @inject(LabelTypeRegistry) private readonly labelTypeRegistry: LabelTypeRegistry, ) {} + private getLabel(label: LabelAssignment): { type: LabelType; value: LabelTypeValue } | undefined { + const labelType = this.labelTypeRegistry.getLabelType(label.labelTypeId); + const labelTypeValue = labelType?.values.find((value) => value.id === label.labelTypeValueId); + if (!labelType || !labelTypeValue) { + return undefined; + } + return { + type: labelType, + value: labelTypeValue, + }; + } + /** * Gets the label type of the assignment and builds the text to display. * From this text the width of the label is calculated using the corresponding font size and padding. * @returns a tuple containing the text and the width of the label in pixel */ - static computeLabelContent(label: LabelAssignment): [string, number] { - const labelType = globalLabelTypeRegistry.getLabelType(label.labelTypeId); - const labelTypeValue = labelType?.values.find((value) => value.id === label.labelTypeValueId); - if (!labelType || !labelTypeValue) { + computeLabelContent(labelAssignment: LabelAssignment): [string, number] { + const label = this.getLabel(labelAssignment); + if (!label) { return ["", 0]; } - const text = `${labelType.name}: ${labelTypeValue.text}`; + const text = `${label.type.name}: ${label.value.text}`; const width = calculateTextSize(text, "5pt sans-serif").width + DfdNodeLabelRenderer.LABEL_TEXT_PADDING; return [text, width]; } renderSingleNodeLabel(node: ContainsDfdLabels & SNodeImpl, label: LabelAssignment, x: number, y: number): VNode { - const [text, width] = DfdNodeLabelRenderer.computeLabelContent(label); + const [text, width] = this.computeLabelContent(label); const xLeft = x - width / 2; const xRight = x + width / 2; const height = DfdNodeLabelRenderer.LABEL_HEIGHT; const radius = height / 2; const deleteLabelHandler = () => { - const action = DeleteLabelAssignmentAction.create(label, node); - this.actionDispatcher.dispatch(action); + this.actionDispatcher.dispatch(RemoveLabelAssignmentAction.create(label, node)); }; return ( @@ -80,24 +89,18 @@ export class DfdNodeLabelRenderer { */ private sortLabels(labels: LabelAssignment[]): void { labels.sort((a, b) => { - const labelTypeA = this.labelTypeRegistry?.getLabelType(a.labelTypeId); - const labelTypeB = this.labelTypeRegistry?.getLabelType(b.labelTypeId); + const labelTypeA = this.getLabel(a); + const labelTypeB = this.getLabel(b); if (!labelTypeA || !labelTypeB) { return 0; } - if (labelTypeA.name < labelTypeB.name) { + if (labelTypeA.type.name < labelTypeB.type.name) { return -1; - } else if (labelTypeA.name > labelTypeB.name) { + } else if (labelTypeA.type.name > labelTypeB.type.name) { return 1; } else { - const labelTypeValueA = labelTypeA.values.find((value) => value.id === a.labelTypeValueId); - const labelTypeValueB = labelTypeB.values.find((value) => value.id === b.labelTypeValueId); - if (!labelTypeValueA || !labelTypeValueB) { - return 0; - } - - return labelTypeValueA.text.localeCompare(labelTypeValueB.text); + return labelTypeA.value.text.localeCompare(labelTypeB.value.text); } }); } @@ -108,9 +111,6 @@ export class DfdNodeLabelRenderer { xOffset = 0, labelSpacing = DfdNodeLabelRenderer.LABEL_SPACING_HEIGHT, ): VNode | undefined { - if (this.settingsManager.simplifyNodeNames) { - return undefined; - } this.sortLabels(node.labels); return ( diff --git a/frontend/webEditor/src/diagram/nodes/DfdStorageNode.tsx b/frontend/webEditor/src/diagram/nodes/DfdStorageNode.tsx new file mode 100644 index 00000000..cbd895d5 --- /dev/null +++ b/frontend/webEditor/src/diagram/nodes/DfdStorageNode.tsx @@ -0,0 +1,54 @@ +/** @jsx svg */ +import { injectable, inject } from "inversify"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { svg, RenderingContext, ShapeView } from "sprotty"; +import { DfdNodeImpl } from "./common"; +import { VNode } from "snabbdom"; +import { DfdPositionalLabelArgs } from "../labels/DfdPositionalLabel"; +import { DfdNodeLabelRenderer } from "./DfdNodeLabels"; + +@injectable() +export class StorageNodeImpl extends DfdNodeImpl { + static readonly TEXT_HEIGHT = 32; + static readonly LABEL_START_HEIGHT = 28; + static readonly LEFT_PADDING = 10; + + protected noLabelHeight(): number { + return StorageNodeImpl.TEXT_HEIGHT; + } + protected labelStartHeight(): number { + return StorageNodeImpl.LABEL_START_HEIGHT; + } + + protected override calculateWidth(): number { + return super.calculateWidth() + StorageNodeImpl.LEFT_PADDING; + } +} + +@injectable() +export class StorageNodeView extends ShapeView { + constructor(@inject(DfdNodeLabelRenderer) private readonly labelRenderer: DfdNodeLabelRenderer) { + super(); + } + + render(node: Readonly, context: RenderingContext): VNode | undefined { + if (!this.isVisible(node, context)) { + return undefined; + } + + const { width, height } = node.bounds; + const leftPadding = StorageNodeImpl.LEFT_PADDING / 2; + + return ( + + + + {context.renderChildren(node, { + xPosition: width / 2 + leftPadding, + yPosition: StorageNodeImpl.TEXT_HEIGHT / 2, + } as DfdPositionalLabelArgs)} + {this.labelRenderer.renderNodeLabels(node, StorageNodeImpl.LABEL_START_HEIGHT, leftPadding)} + + ); + } +} diff --git a/frontend/webEditor/src/features/dfdElements/nodeAnnotationUi.ts b/frontend/webEditor/src/diagram/nodes/annotation.ts similarity index 73% rename from frontend/webEditor/src/features/dfdElements/nodeAnnotationUi.ts rename to frontend/webEditor/src/diagram/nodes/annotation.ts index b8bc0976..a0d5e002 100644 --- a/frontend/webEditor/src/features/dfdElements/nodeAnnotationUi.ts +++ b/frontend/webEditor/src/diagram/nodes/annotation.ts @@ -1,24 +1,23 @@ import { inject, injectable } from "inversify"; import { - AbstractUIExtension, - IActionDispatcher, MouseListener, - SChildElementImpl, + TYPES, + IActionDispatcher, SModelElementImpl, - SModelRootImpl, + SChildElementImpl, SetUIExtensionVisibilityAction, - TYPES, + AbstractUIExtension, + SModelRootImpl, } from "sprotty"; import { Action } from "sprotty-protocol"; -import { DfdNodeImpl } from "./nodes"; - -import "@fortawesome/fontawesome-free/css/all.min.css"; +import { DfdNodeImpl } from "./common"; import "./nodeAnnotationUi.css"; -import { SettingsManager } from "../settingsMenu/SettingsManager"; -import { Mode } from "../settingsMenu/annotationManager"; +import { ShownLabels, ShownLabelsValue } from "../../settings/ShownLabels"; +import { SETTINGS } from "../../settings/Settings"; export class DfdNodeAnnotationUIMouseListener extends MouseListener { private stillTimeout: number | undefined; + private lastTarget?: DfdNodeImpl; private lastPosition = { x: 0, y: 0 }; constructor(@inject(TYPES.IActionDispatcher) private readonly actionDispatcher: IActionDispatcher) { @@ -32,32 +31,34 @@ export class DfdNodeAnnotationUIMouseListener extends MouseListener { clearTimeout(this.stillTimeout); this.stillTimeout = undefined; } + return this.hidePopup(); + } + this.lastPosition = { x: event.clientX, y: event.clientY }; + + if (dfdNode === this.lastTarget) { return []; } - if (this.lastPosition.x !== event.clientX || this.lastPosition.y !== event.clientY) { - this.lastPosition = { x: event.clientX, y: event.clientY }; - // Mouse has moved, so we reset the timeout - if (this.stillTimeout) { - clearTimeout(this.stillTimeout); + this.stillTimeout = setTimeout(() => { + // When the mouse has not moved for 500ms, we show the popup + this.stillTimeout = undefined; + + if (dfdNode.opacity !== 1) { + // Only show when opacity is 1. + // The opacity is not 1 when the node is currently being created but has not been + // placed yet. + // In this case we don't want to show the popup + // and interfere with the creation process. + return; } - this.stillTimeout = setTimeout(() => { - // When the mouse has not moved for 500ms, we show the popup - this.stillTimeout = undefined; - if (dfdNode.opacity !== 1) { - // Only show when opacity is 1. - // The opacity is not 1 when the node is currently being created but has not been - // placed yet. - // In this case we don't want to show the popup - // and interfere with the creation process. - return; - } + this.showPopup(dfdNode); + }, 500); - this.showPopup(dfdNode); - }, 500); + if (this.lastTarget !== dfdNode) { + this.lastTarget = dfdNode; + return this.hidePopup(); } - return []; } @@ -86,6 +87,10 @@ export class DfdNodeAnnotationUIMouseListener extends MouseListener { ); } + private hidePopup() { + return [SetUIExtensionVisibilityAction.create({ extensionId: DfdNodeAnnotationUI.ID, visible: false })]; + } + public getMousePosition(): { x: number; y: number } { return this.lastPosition; } @@ -100,7 +105,7 @@ export class DfdNodeAnnotationUI extends AbstractUIExtension { constructor( @inject(DfdNodeAnnotationUIMouseListener) private readonly mouseListener: DfdNodeAnnotationUIMouseListener, - @inject(SettingsManager) private settings: SettingsManager, + @inject(SETTINGS.ShownLabels) private shownLabels: ShownLabelsValue, ) { super(); } @@ -116,24 +121,6 @@ export class DfdNodeAnnotationUI extends AbstractUIExtension { protected override initializeContents(containerElement: HTMLElement): void { containerElement.classList.add("ui-float"); containerElement.appendChild(this.annotationParagraph); - - document.addEventListener("mousemove", (event) => { - if (containerElement.style.visibility === "hidden") { - // Not visible anyway, no need to do the check - return; - } - - // If mouse not in popup => hide - const rect = containerElement.getBoundingClientRect(); - if ( - event.clientX < rect.left || - event.clientX > rect.right || - event.clientY < rect.top || - event.clientY > rect.bottom - ) { - this.hide(); - } - }); } protected override onBeforeShow( @@ -188,12 +175,14 @@ export class DfdNodeAnnotationUI extends AbstractUIExtension { this.annotationParagraph.innerHTML = ""; - const mode = this.settings.getCurrentLabelMode(); + const mode = this.shownLabels.get(); node.annotations.forEach((a) => { if ( - ((mode === Mode.INCOMING || mode === Mode.ALL) && a.message.trim().startsWith("Incoming")) || - ((mode === Mode.OUTGOING || mode === Mode.ALL) && a.message.trim().startsWith("Propagated")) || + ((mode === ShownLabels.INCOMING || mode === ShownLabels.ALL) && + a.message.trim().startsWith("Incoming")) || + ((mode === ShownLabels.OUTGOING || mode === ShownLabels.ALL) && + a.message.trim().startsWith("Propagated")) || a.message.startsWith("Constraint") ) { const line = document.createElement("div"); diff --git a/frontend/webEditor/src/diagram/nodes/common.ts b/frontend/webEditor/src/diagram/nodes/common.ts new file mode 100644 index 00000000..b49ba37c --- /dev/null +++ b/frontend/webEditor/src/diagram/nodes/common.ts @@ -0,0 +1,135 @@ +import { Bounds, SNode, SPort } from "sprotty-protocol"; +import { DfdNodeAnnotation } from "../../annotation/DFDNodeAnnotation"; +import { LabelAssignment } from "../../labels/LabelType"; +import { isEditableLabel, SNodeImpl, WithEditableLabel, withEditLabelFeature } from "sprotty"; +import { calculateTextSize } from "../../utils/TextSize"; +import { ArrowEdgeImpl } from "../edges/ArrowEdge"; +import { VNodeStyle } from "snabbdom"; +import { DfdInputPortImpl } from "../ports/DfdInputPort"; +import { inject } from "inversify"; +import { DfdNodeLabelRenderer } from "./DfdNodeLabels"; +import { containsDfdLabelFeature } from "../../labels/feature"; + +export interface DfdNode extends SNode { + text: string; + labels: LabelAssignment[]; + ports: SPort[]; + annotations?: DfdNodeAnnotation[]; +} + +export abstract class DfdNodeImpl extends SNodeImpl implements WithEditableLabel { + static readonly DEFAULT_FEATURES = [...SNodeImpl.DEFAULT_FEATURES, withEditLabelFeature, containsDfdLabelFeature]; + static readonly DEFAULT_WIDTH = 50; + static readonly WIDTH_PADDING = 12; + static readonly NODE_COLOR = "var(--color-primary)"; + static readonly HIGHLIGHTED_COLOR = "var(--color-highlighted)"; + @inject(DfdNodeLabelRenderer) private readonly dfdNodeLabelRenderer?: DfdNodeLabelRenderer; + text: string = ""; + color?: string; + labels: LabelAssignment[] = []; + ports: SPort[] = []; + hideLabels: boolean = false; + minimumWidth: number = DfdNodeImpl.DEFAULT_WIDTH; + annotations: DfdNodeAnnotation[] = []; + + constructor() { + super(); + } + + get editableLabel() { + const label = this.children.find((element) => element.type === "label:positional"); + if (label && isEditableLabel(label)) { + return label; + } + + return undefined; + } + + protected calculateWidth(): number { + if (this.hideLabels) { + return this.minimumWidth + DfdNodeImpl.WIDTH_PADDING; + } + const textWidth = calculateTextSize(this.text).width; + const labelWidths = this.labels.map( + (labelAssignment) => this.dfdNodeLabelRenderer?.computeLabelContent(labelAssignment)[1] ?? 0, + ); + + const neededWidth = Math.max(...labelWidths, textWidth, DfdNodeImpl.DEFAULT_WIDTH); + return neededWidth + DfdNodeImpl.WIDTH_PADDING; + } + + protected calculateHeight(): number { + const hasLabels = this.labels.length > 0; + if (hasLabels && !this.hideLabels) { + return ( + this.labelStartHeight() + + this.labels.length * DfdNodeLabelRenderer.LABEL_SPACING_HEIGHT + + DfdNodeLabelRenderer.LABEL_SPACE_BETWEEN + ); + } else { + return this.noLabelHeight(); + } + } + + protected abstract noLabelHeight(): number; + protected abstract labelStartHeight(): number; + + override get bounds(): Bounds { + return { + x: this.position.x, + y: this.position.y, + width: this.calculateWidth(), + height: this.calculateHeight(), + }; + } + + /** + * Gets the names of all available input ports. + * @returns a list of the names of all available input ports. + * Can include undefined if a port has no named edges connected to it. + */ + getAvailableInputs(): (string | undefined)[] { + return this.children + .filter((child) => child instanceof DfdInputPortImpl) + .map((child) => child as DfdInputPortImpl) + .map((child) => child.getName()); + } + + /** + * Gets the text of all dfd edges that are connected to the input ports of this node. + * Applies the passed filter to the edges. + * If a edge has no label, the empty string is returned. + */ + getEdgeTexts(edgePredicate: (e: ArrowEdgeImpl) => boolean): string[] { + const inputPorts = this.children + .filter((child) => child instanceof DfdInputPortImpl) + .map((child) => child as DfdInputPortImpl); + + return inputPorts + .flatMap((port) => port.incomingEdges) + .filter((edge) => edge instanceof ArrowEdgeImpl) + .map((edge) => edge as ArrowEdgeImpl) + .filter(edgePredicate) + .map((edge) => edge.editableLabel?.text ?? ""); + } + + /** + * Generates the per-node inline style object for the view. + * Contains the opacity and the color of the node that may be set by the annotation (if any). + */ + geViewStyleObject(): VNodeStyle { + const style: VNodeStyle = { + opacity: this.opacity.toString(), + }; + + style["--border"] = "#FFFFFF"; + + if (this.color) style["--color"] = this.color; + + return style; + } + + public setColor(color: string, override: boolean = true) { + if (override || this.color === DfdNodeImpl.NODE_COLOR) this.color = color; + } +} diff --git a/frontend/webEditor/src/features/dfdElements/nodeAnnotationUi.css b/frontend/webEditor/src/diagram/nodes/nodeAnnotationUi.css similarity index 100% rename from frontend/webEditor/src/features/dfdElements/nodeAnnotationUi.css rename to frontend/webEditor/src/diagram/nodes/nodeAnnotationUi.css diff --git a/frontend/webEditor/src/diagram/ports/DfdInputPort.tsx b/frontend/webEditor/src/diagram/ports/DfdInputPort.tsx new file mode 100644 index 00000000..e342ecd9 --- /dev/null +++ b/frontend/webEditor/src/diagram/ports/DfdInputPort.tsx @@ -0,0 +1,63 @@ +/** @jsx svg */ +import { injectable } from "inversify"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { svg, SRoutableElementImpl, ShapeView, SPortImpl, RenderingContext } from "sprotty"; +import { SPort } from "sprotty-protocol"; +import { ArrowEdgeImpl } from "../edges/ArrowEdge"; +import { DfdPortImpl } from "./common"; +import { VNode } from "snabbdom"; + +export type DfdInputPort = SPort; + +@injectable() +export class DfdInputPortImpl extends DfdPortImpl { + /** + * Builds the name of the input port from the names of the incoming dfd edges. + * @returns either the concatenated names of the incoming edges or undefined if there are no named incoming edges. + */ + getName(): string | undefined { + const edgeNames: string[] = []; + + this.incomingEdges.forEach((edge) => { + if (edge instanceof ArrowEdgeImpl) { + const name = edge.editableLabel?.text; + if (name) { + edgeNames.push(name); + } + } else { + return undefined; + } + }); + + if (edgeNames.length === 0) { + return undefined; + } else { + return edgeNames.sort().join("|"); + } + } + + canConnect(_routable: SRoutableElementImpl, role: "source" | "target"): boolean { + // Only allow edges into this port + return role === "target"; + } +} + +export class DfdInputPortView extends ShapeView { + render(node: Readonly, context: RenderingContext): VNode | undefined { + if (!this.isVisible(node, context)) { + return undefined; + } + + const { width, height } = node.bounds; + + return ( + + + + I + + {context.renderChildren(node)} + + ); + } +} diff --git a/frontend/webEditor/src/diagram/ports/DfdOutputPort.tsx b/frontend/webEditor/src/diagram/ports/DfdOutputPort.tsx new file mode 100644 index 00000000..32c24814 --- /dev/null +++ b/frontend/webEditor/src/diagram/ports/DfdOutputPort.tsx @@ -0,0 +1,99 @@ +/** @jsx svg */ +import { inject, injectable } from "inversify"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { svg, isEditableLabel, SRoutableElementImpl, ShapeView, RenderingContext } from "sprotty"; +import { SPort } from "sprotty-protocol"; +import { DfdPortImpl } from "./common"; +import { VNode, VNodeStyle } from "snabbdom"; +import { LabelTypeRegistry } from "../../labels/LabelTypeRegistry"; +import { LanguageTreeNode, tokenize } from "../../languages/tokenize"; +import { verify, VerifyWord } from "../../languages/verify"; +import { AssignmentLanguageTreeBuilder } from "../../assignment/language"; + +export interface DfdOutputPort extends SPort { + behavior: string; +} + +@injectable() +export class DfdOutputPortImpl extends DfdPortImpl { + private behavior: string = ""; + private validBehavior: boolean = true; + private tree?: LanguageTreeNode[]; + @inject(LabelTypeRegistry) private labelTypeRegistry?: LabelTypeRegistry; + + constructor() { + super(); + } + + get editableLabel() { + const label = this.children.find((element) => element.type === "label:invisible"); + if (label && isEditableLabel(label)) { + return label; + } + + return undefined; + } + + canConnect(_routable: SRoutableElementImpl, role: "source" | "target"): boolean { + // Only allow edges from this port outwards + return role === "source"; + } + + /** + * Generates the per-node inline style object for the view. + */ + geViewStyleObject(): VNodeStyle { + const style: VNodeStyle = { + opacity: this.opacity.toString(), + }; + + if (!this.validBehavior) { + style["--port-border"] = "#ff0000"; + style["--port-color"] = "#ff6961"; + } + + return style; + } + + public setBehavior(value: string) { + this.behavior = value; + if (value === "") { + this.validBehavior = true; + return; + } + + if (!this.tree) { + if (!this.labelTypeRegistry) { + return; + } + this.tree = AssignmentLanguageTreeBuilder.buildTree(this, this.labelTypeRegistry); + } + const errors = verify(tokenize(this.behavior.split("\n")), this.tree); + this.validBehavior = errors.length === 0; + } + + public getBehavior() { + return this.behavior; + } +} + +@injectable() +export class DfdOutputPortView extends ShapeView { + render(node: Readonly, context: RenderingContext): VNode | undefined { + if (!this.isVisible(node, context)) { + return undefined; + } + + const { width, height } = node.bounds; + + return ( + + + + O + + {context.renderChildren(node)} + + ); + } +} diff --git a/frontend/webEditor/src/diagram/ports/common.ts b/frontend/webEditor/src/diagram/ports/common.ts new file mode 100644 index 00000000..69c9db23 --- /dev/null +++ b/frontend/webEditor/src/diagram/ports/common.ts @@ -0,0 +1,18 @@ +import { deletableFeature, moveFeature, SPortImpl } from "sprotty"; +import { Bounds } from "sprotty-protocol"; + +export const defaultPortFeatures = [...SPortImpl.DEFAULT_FEATURES, moveFeature, deletableFeature]; +const portSize = 7; + +export abstract class DfdPortImpl extends SPortImpl { + static readonly DEFAULT_FEATURES = defaultPortFeatures; + + override get bounds(): Bounds { + return { + x: this.position.x, + y: this.position.y, + width: portSize, + height: portSize, + }; + } +} diff --git a/frontend/webEditor/src/features/dfdElements/portSnapper.ts b/frontend/webEditor/src/diagram/ports/portSnapper.ts similarity index 100% rename from frontend/webEditor/src/features/dfdElements/portSnapper.ts rename to frontend/webEditor/src/diagram/ports/portSnapper.ts diff --git a/frontend/webEditor/src/diagram/style.css b/frontend/webEditor/src/diagram/style.css new file mode 100644 index 00000000..ca699f5c --- /dev/null +++ b/frontend/webEditor/src/diagram/style.css @@ -0,0 +1,115 @@ +/* sprotty-* classes are automatically added by sprotty and the other ones + are added in the definition inside nodes.tsx, edge.tsx and ports.tsx */ + +/* Nodes */ + +.sprotty-node { + rect, + line, + circle { + /* stroke color defaults to be the foreground color of the theme. + Alternatively it can be overwritten by setting the --color variable + As a inline style attribute for the specific node. + Used as a highlighter to mark nodes with errors. + This is essentially a "optional parameter" to this css rule. + See https://stackoverflow.com/questions/17893823/how-to-pass-parameters-to-css-classes */ + stroke: var(--color-foreground); + stroke-width: 1; + /* Background fill of the node. + When --color is unset this is just --color-primary. + If this node is annotated and --color is set, it will be included in the color mix. */ + fill: color-mix(in srgb, var(--color-primary), var(--color, transparent) 40%); + } + + .node-label text { + font-size: 5pt; + } + .node-label rect, + .node-label .label-delete circle { + fill: var(--color-primary); + stroke: var(--color-foreground); + stroke-width: 0.5; + } + + .node-label .label-delete text { + fill: var(--color-foreground); + font-size: 5px; + } +} +/* Edges */ + +.sprotty-edge { + stroke: var(--color-foreground); + fill: none; + stroke-width: 1; + + /* On top of the actual edge path we draw a transparent path with a larger stroke width. + This makes it easier to select the edge with the mouse. */ + .sprotty-edge path.select-path { + stroke: transparent; + /* make the "invisible hitbox" 8 pixels wide. This is the same width as the arrow head */ + stroke-width: 8; + } + + .arrow { + fill: var(--color-foreground); + stroke: none; + } + + .label-background rect { + fill: var(--color-background); + stroke-width: 0; + } +} + +.sprotty-edge > .sprotty-routing-handle { + fill: var(--color-foreground); + stroke: none; +} + +/* Ports */ +.sprotty-port { + rect { + stroke: var(--port-border, var(--color-foreground)); + fill: color-mix(in srgb, var(--port-color, var(--color-primary)), var(--color-background) 25%); + stroke-width: 0.5; + } + .port-text { + font-size: 4pt; + } +} + +/* All nodes/misc */ + +.sprotty-node.selected circle, +.sprotty-node.selected rect, +.sprotty-node.selected line, +.sprotty-edge.selected { + stroke-width: 2; +} + +.sprotty-port.selected rect { + stroke-width: 1; +} + +text { + stroke-width: 0; + fill: var(--color-foreground); + font-family: "Arial", sans-serif; + font-size: 11pt; + text-anchor: middle; + dominant-baseline: central; + + -webkit-user-select: none; + user-select: none; +} + +/* elements with the sprotty-missing class use a node type that has not been registered. + Because of this sprotty does not know what to do with them and renders their content and specifies them as missing. + To make these errors very visible we make them red here. + Ideally a user should never see this. */ +.sprotty-missing { + stroke-width: 1; + stroke: var(--color-error); + fill: var(--color-error); +} diff --git a/frontend/webEditor/src/editModeOverwrites/di.config.ts b/frontend/webEditor/src/editModeOverwrites/di.config.ts new file mode 100644 index 00000000..d0e882c0 --- /dev/null +++ b/frontend/webEditor/src/editModeOverwrites/di.config.ts @@ -0,0 +1,8 @@ +import { ContainerModule } from "inversify"; +import { DeleteElementCommand, EditLabelMouseListener } from "sprotty"; +import { EditorModeAwareDeleteElementCommand, EditorModeAwareEditLabelMouseListener } from "./overwrites"; + +export const editorModeOverwritesModule = new ContainerModule((_, __, ___, rebind) => { + rebind(EditLabelMouseListener).to(EditorModeAwareEditLabelMouseListener); + rebind(DeleteElementCommand).to(EditorModeAwareDeleteElementCommand); +}); diff --git a/frontend/webEditor/src/features/editorMode/sprottyHooks.ts b/frontend/webEditor/src/editModeOverwrites/overwrites.ts similarity index 89% rename from frontend/webEditor/src/features/editorMode/sprottyHooks.ts rename to frontend/webEditor/src/editModeOverwrites/overwrites.ts index 59b070e4..8e01069f 100644 --- a/frontend/webEditor/src/features/editorMode/sprottyHooks.ts +++ b/frontend/webEditor/src/editModeOverwrites/overwrites.ts @@ -6,13 +6,14 @@ import { EditLabelMouseListener, SModelElementImpl, } from "sprotty"; -import { EditorModeController } from "./editorModeController"; import { Action } from "sprotty-protocol"; +import { SETTINGS } from "../settings/Settings"; +import { EditorModeController } from "../settings/editorMode"; @injectable() export class EditorModeAwareEditLabelMouseListener extends EditLabelMouseListener { constructor( - @inject(EditorModeController) + @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController, ) { super(); @@ -29,7 +30,7 @@ export class EditorModeAwareEditLabelMouseListener extends EditLabelMouseListene @injectable() export class EditorModeAwareDeleteElementCommand extends DeleteElementCommand { - @inject(EditorModeController) + @inject(SETTINGS.Mode) private readonly editorModeController?: EditorModeController; execute(context: CommandExecutionContext): CommandReturn { diff --git a/frontend/webEditor/src/editorTypes.ts b/frontend/webEditor/src/editorTypes.ts new file mode 100644 index 00000000..45eac078 --- /dev/null +++ b/frontend/webEditor/src/editorTypes.ts @@ -0,0 +1,10 @@ +/** + * Type identifiers for use with inversify. + */ +export const EDITOR_TYPES = { + // Enableable and disableable tools that can be used to create new elements. + CreationTool: Symbol("CreationTool"), + // All IUIExtension instances that are bound to this symbol will + // be loaded and enabled at editor startup. + DefaultUIElement: Symbol("DefaultUIElement"), +}; diff --git a/frontend/webEditor/src/favicon.ico b/frontend/webEditor/src/favicon.ico deleted file mode 100644 index 168d55ed..00000000 Binary files a/frontend/webEditor/src/favicon.ico and /dev/null differ diff --git a/frontend/webEditor/src/features/commandPalette/di.config.ts b/frontend/webEditor/src/features/commandPalette/di.config.ts deleted file mode 100644 index 93aaddce..00000000 --- a/frontend/webEditor/src/features/commandPalette/di.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ContainerModule } from "inversify"; -import { CommandPalette, TYPES } from "sprotty"; -import { ServerCommandPaletteActionProvider } from "./commandPaletteProvider"; -import { CustomCommandPalette } from "./commandPalette"; -import "./commandPalette.css"; - -export const commandPaletteModule = new ContainerModule((bind, _, __, rebind) => { - rebind(CommandPalette).to(CustomCommandPalette).inSingletonScope(); - - bind(ServerCommandPaletteActionProvider).toSelf().inSingletonScope(); - bind(TYPES.ICommandPaletteActionProvider).toService(ServerCommandPaletteActionProvider); -}); diff --git a/frontend/webEditor/src/features/constraintMenu/AutoCompletion.ts b/frontend/webEditor/src/features/constraintMenu/AutoCompletion.ts deleted file mode 100644 index 6877bac0..00000000 --- a/frontend/webEditor/src/features/constraintMenu/AutoCompletion.ts +++ /dev/null @@ -1,300 +0,0 @@ -import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; - -export interface RequiredCompletionParts { - kind: monaco.languages.CompletionItemKind; - insertText: string; - startOffset?: number; -} - -export interface ValidationError { - message: string; - line: number; - startColumn: number; - endColumn: number; -} - -export interface Token { - text: string; - line: number; - column: number; - whiteSpaceAfter?: string; -} - -export type WordCompletion = RequiredCompletionParts & Partial; - -export interface AbstractWord { - /** - * Calculates the completion options for the given word - * @param word Can be taken into account for returning completion options - * @returns Array of completion options. Can contain all options from @link{monaco.languages.CompletionItem} - */ - completionOptions(word: string): WordCompletion[]; - - /** - * Verifies if the given word is valid - * An empty array means that the word is valid - * The strings in the array are error messages - * @param word The word to verify - * @returns Array of all error messages - */ - verifyWord(word: string): string[]; -} - -export class ConstantWord implements AbstractWord { - constructor(protected word: string) {} - - verifyWord(word: string): string[] { - if (word == this.word) { - return []; - } else { - return [`Expected keyword "${this.word}"`]; - } - } - - completionOptions(): WordCompletion[] { - return [ - { - insertText: this.word, - kind: monaco.languages.CompletionItemKind.Keyword, - }, - ]; - } -} - -export class AnyWord implements AbstractWord { - completionOptions(): WordCompletion[] { - return []; - } - verifyWord(word: string): string[] { - if (word.length > 0) { - return []; - } else { - return ["Expected a word"]; - } - } -} - -export class NegatableWord implements AbstractWord { - constructor(protected word: AbstractWord) {} - - verifyWord(word: string): string[] { - if (word.startsWith("!")) { - return this.word.verifyWord(word.substring(1)); - } - return this.word.verifyWord(word); - } - - completionOptions(part: string): WordCompletion[] { - if (part.startsWith("!")) { - const options = this.word.completionOptions(part.substring(1)); - return options.map((o) => ({ - ...o, - startOffset: (o.startOffset ?? 0) + 1, - })); - } - return this.word.completionOptions(part); - } -} - -export class AutoCompleteTree { - constructor(protected roots: AutoCompleteNode[]) {} - - protected tokenize(text: string[]): Token[] { - if (!text || text.length == 0) { - return []; - } - - const tokens: Token[] = []; - for (const [lineNumber, line] of text.entries()) { - const lineTokens = line.split(/(\s+)/); - let column = 0; - for (let i = 0; i < lineTokens.length; i += 2) { - const token = lineTokens[i]; - if (token.length > 0) { - tokens.push({ - text: token, - line: lineNumber + 1, - column: column + 1, - whiteSpaceAfter: lineTokens[i + 1], - }); - } - column += token.length; - column += lineTokens[i + 1] ? lineTokens[i + 1].length : 0; // Add whitespace length - } - } - - return tokens; - } - - /** - * Checks the set content for errors - * @returns An array of errors. An empty array means that the content is valid - */ - public verify(lines: string[]): ValidationError[] { - const tokens = this.tokenize(lines); - return this.verifyNode(this.roots, tokens, 0, false, true); - } - - private verifyNode( - nodes: AutoCompleteNode[], - tokens: Token[], - index: number, - comesFromFinal: boolean, - skipStartCheck = false, - ): ValidationError[] { - if (index >= tokens.length) { - if (nodes.length == 0 || comesFromFinal) { - return []; - } else { - return [ - { - message: "Unexpected end of line", - line: tokens[index - 1].line, - startColumn: tokens[index - 1].column + tokens[index - 1].text.length - 1, - endColumn: tokens[index - 1].column + tokens[index - 1].text.length, - }, - ]; - } - } - if (!skipStartCheck && tokens[index].column == 1) { - const matchesAnyRoot = this.roots.some((r) => r.word.verifyWord(tokens[index].text).length === 0); - if (matchesAnyRoot) { - return this.verifyNode(this.roots, tokens, index, false, true); - } - } - - const foundErrors: ValidationError[] = []; - let childErrors: ValidationError[] = []; - for (const n of nodes) { - const v = n.word.verifyWord(tokens[index].text); - if (v.length > 0) { - foundErrors.push({ - message: v[0], - startColumn: tokens[index].column, - endColumn: tokens[index].column + tokens[index].text.length, - line: tokens[index].line, - }); - continue; - } - - const childResult = this.verifyNode(n.children, tokens, index + 1, n.canBeFinal || false); - if (childResult.length == 0) { - return []; - } else { - childErrors = childErrors.concat(childResult); - } - } - if (childErrors.length > 0) { - return deduplicateErrors(childErrors); - } - return deduplicateErrors(foundErrors); - } - - /** - * Calculates the completion options for the current content - */ - public getCompletion(lines: string[]): monaco.languages.CompletionItem[] { - const tokens = this.tokenize(lines); - const endsWithWhitespace = - (lines.length > 0 && lines[lines.length - 1].charAt(lines[lines.length - 1].length - 1).match(/\s/)) || - lines[lines.length - 1].length == 0; - if (endsWithWhitespace) { - tokens.push({ - text: "", - line: lines.length, - column: lines[lines.length - 1].length + 1, - }); - } - - let result: WordCompletion[] = []; - if (tokens.length == 0) { - for (const r of this.roots) { - result = result.concat(r.word.completionOptions("")); - } - } else { - result = this.completeNode(this.roots, tokens, 0); - } - return this.transformResults(result, tokens); - } - - private completeNode( - nodes: AutoCompleteNode[], - tokens: Token[], - index: number, - cameFromFinal = false, - skipStartCheck = false, - ): WordCompletion[] { - // check for new start - if (!skipStartCheck && tokens[index].column == 1) { - const matchesAnyRoot = this.roots.some((n) => n.word.verifyWord(tokens[index].text).length === 0); - if (matchesAnyRoot) { - return this.completeNode(this.roots, tokens, index, cameFromFinal, true); - } else if (cameFromFinal || nodes.length == 0) { - return this.completeNode([...this.roots, ...nodes], tokens, index, cameFromFinal, true); - } - } - - let result: WordCompletion[] = []; - if (index == tokens.length - 1) { - for (const node of nodes) { - result = result.concat(node.word.completionOptions(tokens[index].text)); - } - return result; - } - for (const n of nodes) { - if (n.word.verifyWord(tokens[index].text).length > 0) { - continue; - } - result = result.concat(this.completeNode(n.children, tokens, index + 1, n.canBeFinal || false)); - } - return result; - } - - private transformResults(comp: WordCompletion[], tokens: Token[]): monaco.languages.CompletionItem[] { - const result: monaco.languages.CompletionItem[] = []; - const filtered = comp.filter( - (c, idx) => comp.findIndex((c2) => c2.insertText === c.insertText && c2.kind === c.kind) === idx, - ); - for (const c of filtered) { - const r = this.transformResult(c, tokens); - result.push(r); - } - return result; - } - - private transformResult(comp: WordCompletion, tokens: Token[]): monaco.languages.CompletionItem { - const wordStart = tokens.length == 0 ? 1 : tokens[tokens.length - 1].column; - const lineNumber = tokens.length == 0 ? 1 : tokens[tokens.length - 1].line; - return { - insertText: comp.insertText, - kind: comp.kind, - label: comp.label ?? comp.insertText, - insertTextRules: comp.insertTextRules, - range: new monaco.Range( - lineNumber, - wordStart + (comp.startOffset ?? 0), - lineNumber, - wordStart + (comp.startOffset ?? 0) + comp.insertText.length, - ), - }; - } -} - -function deduplicateErrors(errors: ValidationError[]): ValidationError[] { - const seen = new Set(); - return errors.filter((error) => { - const key = `${error.line}-${error.startColumn}-${error.endColumn}-${error.message}`; - if (seen.has(key)) { - return false; - } - seen.add(key); - return true; - }); -} - -export interface AutoCompleteNode { - word: W; - children: AutoCompleteNode[]; - canBeFinal?: boolean; - viewAsLeaf?: boolean; -} diff --git a/frontend/webEditor/src/features/constraintMenu/actions.ts b/frontend/webEditor/src/features/constraintMenu/actions.ts deleted file mode 100644 index f69f1104..00000000 --- a/frontend/webEditor/src/features/constraintMenu/actions.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Action } from "sprotty-protocol"; - -export interface ChooseConstraintAction extends Action { - kind: typeof ChooseConstraintAction.KIND; - names: string[]; -} - -export namespace ChooseConstraintAction { - export const KIND = "choose-constraint"; - - export function create(names: string[]): ChooseConstraintAction { - return { kind: KIND, names }; - } -} diff --git a/frontend/webEditor/src/features/constraintMenu/commands.ts b/frontend/webEditor/src/features/constraintMenu/commands.ts deleted file mode 100644 index d131ca6d..00000000 --- a/frontend/webEditor/src/features/constraintMenu/commands.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { inject, injectable } from "inversify"; -import { Command, CommandExecutionContext, CommandReturn, TYPES } from "sprotty"; -import { DfdNodeImpl } from "../dfdElements/nodes"; -import { ChooseConstraintAction } from "./actions"; -import { getBasicType } from "sprotty-protocol"; -import { AnnnotationsManager } from "../settingsMenu/annotationManager"; -import { ConstraintRegistry } from "./constraintRegistry"; - -@injectable() -export class ChooseConstraintCommand extends Command { - static readonly KIND = ChooseConstraintAction.KIND; - - constructor( - @inject(TYPES.Action) private action: ChooseConstraintAction, - @inject(AnnnotationsManager) private annnotationsManager: AnnnotationsManager, - @inject(ConstraintRegistry) private constraintRegistry: ConstraintRegistry, - ) { - super(); - } - - execute(context: CommandExecutionContext): CommandReturn { - this.annnotationsManager.clearTfgs(); - const names = this.action.names; - this.constraintRegistry.setSelectedConstraints(names); - - const nodes = context.root.children.filter((node) => getBasicType(node) === "node") as DfdNodeImpl[]; - if (names.length === 0) { - nodes.forEach((node) => { - node.setColor("var(--color-primary)"); - }); - return context.root; - } - - nodes.forEach((node) => { - const annotations = node.annotations!; - let wasAdjusted = false; - if (this.constraintRegistry.selectedContainsAllConstraints()) { - annotations.forEach((annotation) => { - if (annotation.message.startsWith("Constraint")) { - wasAdjusted = true; - node.setColor(annotation.color!); - } - }); - } - names.forEach((name) => { - annotations.forEach((annotation) => { - if (annotation.message.startsWith("Constraint ") && annotation.message.split(" ")[1] === name) { - node.setColor(annotation.color!); - wasAdjusted = true; - this.annnotationsManager.addTfg(annotation.tfg!); - } - }); - }); - if (!wasAdjusted) node.setColor("var(--color-primary)"); - }); - - nodes.forEach((node) => { - const inTFG = node.annotations!.filter((annotation) => - this.annnotationsManager.getSelectedTfgs().has(annotation.tfg!), - ); - if (inTFG.length > 0) node.setColor("var(--color-highlighted)", false); - }); - - return context.root; - } - - undo(context: CommandExecutionContext): CommandReturn { - return context.root; - } - redo(context: CommandExecutionContext): CommandReturn { - return context.root; - } -} diff --git a/frontend/webEditor/src/features/constraintMenu/di.config.ts b/frontend/webEditor/src/features/constraintMenu/di.config.ts deleted file mode 100644 index 9a35f761..00000000 --- a/frontend/webEditor/src/features/constraintMenu/di.config.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ContainerModule } from "inversify"; -import { EDITOR_TYPES } from "../../utils"; -import { ConstraintMenu } from "./ConstraintMenu"; -import { configureCommand, TYPES } from "sprotty"; -import { ConstraintRegistry } from "./constraintRegistry"; -import { SWITCHABLE } from "../settingsMenu/themeManager"; -import { ChooseConstraintCommand } from "./commands"; - -// This module contains an UI extension that adds a tool palette to the editor. -// This tool palette allows the user to create new nodes and edges. -// Additionally it contains the tools that are used to create the nodes and edges. - -export const constraintMenuModule = new ContainerModule((bind, unbind, isBound, rebind) => { - bind(ConstraintRegistry).toSelf().inSingletonScope(); - - bind(ConstraintMenu).toSelf().inSingletonScope(); - bind(TYPES.IUIExtension).toService(ConstraintMenu); - bind(EDITOR_TYPES.DefaultUIElement).toService(ConstraintMenu); - bind(SWITCHABLE).toService(ConstraintMenu); - - const context = { bind, unbind, isBound, rebind }; - configureCommand(context, ChooseConstraintCommand); -}); diff --git a/frontend/webEditor/src/features/copyPaste/di.config.ts b/frontend/webEditor/src/features/copyPaste/di.config.ts deleted file mode 100644 index 21fca863..00000000 --- a/frontend/webEditor/src/features/copyPaste/di.config.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ContainerModule } from "inversify"; -import { TYPES, configureCommand } from "sprotty"; -import { CopyPasteKeyListener } from "./keyListener"; -import { PasteElementsCommand } from "./pasteCommand"; - -/** - * This feature allows the user to copy and paste elements. - * When ctrl+c is pressed, all selected elements are copied into an internal array. - * When ctrl+v is pressed, all elements in the internal array are pasted with an fixed offset. - * Nodes are copied with their ports and edges are copied if source and target were copied as well. - */ -export const copyPasteModule = new ContainerModule((bind, unbind, isBound, rebind) => { - const context = { bind, unbind, isBound, rebind }; - bind(TYPES.KeyListener).to(CopyPasteKeyListener).inSingletonScope(); - configureCommand(context, PasteElementsCommand); -}); diff --git a/frontend/webEditor/src/features/copyPaste/keyListener.ts b/frontend/webEditor/src/features/copyPaste/keyListener.ts deleted file mode 100644 index 1d70f51d..00000000 --- a/frontend/webEditor/src/features/copyPaste/keyListener.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { inject, injectable } from "inversify"; -import { PasteElementsAction } from "./pasteCommand"; -import { - CommitModelAction, - KeyListener, - MousePositionTracker, - SModelElementImpl, - SModelRootImpl, - isSelected, -} from "sprotty"; -import { Action } from "sprotty-protocol"; -import { matchesKeystroke } from "sprotty/lib/utils/keyboard"; - -/** - * This class is responsible for listening to ctrl+c and ctrl+v events. - * On copy the selected elements are copied into an internal array. - * On paste the {@link PasteElementsAction} is executed to paste the elements. - * This is done inside a command, so that it can be undone/redone. - */ -@injectable() -export class CopyPasteKeyListener implements KeyListener { - private copyElements: SModelElementImpl[] = []; - - constructor(@inject(MousePositionTracker) private readonly mousePositionTracker: MousePositionTracker) {} - - keyUp(): Action[] { - return []; - } - - keyDown(element: SModelElementImpl, event: KeyboardEvent): Action[] { - if (matchesKeystroke(event, "KeyC", "ctrl")) { - return this.copy(element.root); - } else if (matchesKeystroke(event, "KeyV", "ctrl")) { - return this.paste(); - } - - return []; - } - - /** - * Copy all selected elements into the "clipboard" (the internal element array) - */ - private copy(root: SModelRootImpl): Action[] { - this.copyElements = []; // Clear the clipboard - - // Find selected elements - root.index - .all() - .filter((element) => isSelected(element)) - .forEach((e) => this.copyElements.push(e)); - - return []; - } - - /** - * Pastes elements by creating new elements and copying the properties of the copied elements. - * This is done inside a command, so that it can be undone/redone. - */ - private paste(): Action[] { - const targetPosition = this.mousePositionTracker.lastPositionOnDiagram ?? { x: 0, y: 0 }; - return [PasteElementsAction.create(this.copyElements, targetPosition), CommitModelAction.create()]; - } -} diff --git a/frontend/webEditor/src/features/copyPaste/pasteCommand.ts b/frontend/webEditor/src/features/copyPaste/pasteCommand.ts deleted file mode 100644 index 846af490..00000000 --- a/frontend/webEditor/src/features/copyPaste/pasteCommand.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { inject, injectable, optional } from "inversify"; -import { - Command, - CommandExecutionContext, - CommandReturn, - SChildElementImpl, - SEdgeImpl, - SModelElementImpl, - SNodeImpl, - TYPES, - isSelectable, -} from "sprotty"; -import { DynamicChildrenProcessor } from "../dfdElements/dynamicChildren"; -import { generateRandomSprottyId } from "../../utils"; -import { DfdNode, DfdNodeImpl } from "../dfdElements/nodes"; -import { Action, Point, SEdge, SModelElement } from "sprotty-protocol"; -import { LoadDiagramCommand } from "../serialize/load"; -import { EditorModeController } from "../editorMode/editorModeController"; - -export interface PasteElementsAction extends Action { - kind: typeof PasteElementsAction.KIND; - copyElements: SModelElementImpl[]; - targetPosition: Point; -} -export namespace PasteElementsAction { - export const KIND = "paste-clipboard-elements"; - export function create(copyElements: SModelElementImpl[], targetPosition: Point): PasteElementsAction { - return { - kind: KIND, - copyElements, - targetPosition, - }; - } -} - -/** - * This command is used to paste elements that were copied by the CopyPasteFeature. - * It creates new elements and copies the properties of the copied elements. - * This is done inside a command, so that it can be undone/redone. - */ -@injectable() -export class PasteElementsCommand extends Command { - public static readonly KIND = PasteElementsAction.KIND; - - @inject(DynamicChildrenProcessor) - private dynamicChildrenProcessor: DynamicChildrenProcessor = new DynamicChildrenProcessor(); - @inject(EditorModeController) - @optional() - private editorModeController?: EditorModeController; - - private newElements: SChildElementImpl[] = []; - // This maps the element id of the copy source element to the - // id that the newly created copy target element has. - private copyElementIdMapping: Record = {}; - - constructor(@inject(TYPES.Action) private readonly action: PasteElementsAction) { - super(); - } - - /** - * Selects the newly created copy and deselects the copy source. - */ - private setSelection(context: CommandExecutionContext, selection: "old" | "new"): void { - Object.entries(this.copyElementIdMapping).forEach(([oldId, newId]) => { - const oldElement = context.root.index.getById(oldId); - const newElement = context.root.index.getById(newId); - - if (oldElement && isSelectable(oldElement)) { - oldElement.selected = selection === "old"; - } - if (newElement && isSelectable(newElement)) { - newElement.selected = selection === "new"; - } - }); - } - - /** - * Calculates the offset between the copy source elements and the set paste target position. - * Does this by finding the top left position of the copy source elements and subtracting it from the target position. - * - * @returns The offset between the top left position of the copy source elements and the target position. - */ - private computeElementOffset(): Point { - const sourcePosition = { x: Infinity, y: Infinity }; - - this.action.copyElements.forEach((element) => { - if (!(element instanceof SNodeImpl)) { - return; - } - - if (element.position.x < sourcePosition.x) { - sourcePosition.x = element.position.x; - } - if (element.position.y < sourcePosition.y) { - sourcePosition.y = element.position.y; - } - }); - - if (sourcePosition.x === Infinity || sourcePosition.y === Infinity) { - return { x: 0, y: 0 }; - } - - // Compute delta between top left position of copy source elements and the target position - return Point.subtract(this.action.targetPosition, sourcePosition); - } - - execute(context: CommandExecutionContext): CommandReturn { - if (this.editorModeController?.isReadOnly()) { - return context.root; - } - - // Step 1: copy nodes and their ports - const positionOffset = this.computeElementOffset(); - this.action.copyElements.forEach((element) => { - if (!(element instanceof SNodeImpl)) { - return; - } - - // createSchema only does a shallow copy, so we need to do an additional deep copy here because - // we want to support copying elements with objects and arrays in them. - const schema = JSON.parse(JSON.stringify(context.modelFactory.createSchema(element))) as SModelElement; - // Remove json artifacts - LoadDiagramCommand.preprocessModelSchema(schema); - - schema.id = generateRandomSprottyId(); - this.copyElementIdMapping[element.id] = schema.id; - if ("position" in schema) { - schema.position = Point.add(element.position, positionOffset); - } - - // Regenerate dynamic sub elements - this.dynamicChildrenProcessor.processGraphChildren(schema, "remove"); - - if (element instanceof DfdNodeImpl) { - // Special case for DfdNodes: copy ports and give the nodes new ids. - (schema as DfdNode).ports.forEach((port) => { - const oldPortId = port.id; - port.id = generateRandomSprottyId(); - this.copyElementIdMapping[oldPortId] = port.id; - }); - } - - this.dynamicChildrenProcessor.processGraphChildren(schema, "set"); - - const newElement = context.modelFactory.createElement(schema); - this.newElements.push(newElement); - }); - - // Step 2: copy edges - // If the source and target element of an edge are copied, the edge can be copied as well. - // If only one of them is copied, the edge is not copied. - this.action.copyElements.forEach((element) => { - if (!(element instanceof SEdgeImpl)) { - return; - } - - const newSourceId = this.copyElementIdMapping[element.sourceId]; - const newTargetId = this.copyElementIdMapping[element.targetId]; - - if (!newSourceId || !newTargetId) { - // Not both source and target are copied, ignore this edge - return; - } - - const schema = JSON.parse(JSON.stringify(context.modelFactory.createSchema(element))) as SEdge; - LoadDiagramCommand.preprocessModelSchema(schema); - - schema.id = generateRandomSprottyId(); - this.copyElementIdMapping[element.id] = schema.id; - - schema.sourceId = newSourceId; - schema.targetId = newTargetId; - - // Regenerate dynamic sub elements (the edge label) - this.dynamicChildrenProcessor.processGraphChildren(schema, "remove"); - this.dynamicChildrenProcessor.processGraphChildren(schema, "set"); - - const newElement = context.modelFactory.createElement(schema); - this.newElements.push(newElement); - }); - - // Step 3: add new elements to the model and select them - this.newElements.forEach((element) => { - context.root.add(element); - }); - this.setSelection(context, "new"); - - return context.root; - } - - undo(context: CommandExecutionContext): CommandReturn { - if (this.editorModeController?.isReadOnly()) { - return context.root; - } - - // Remove elements from the model - this.newElements.forEach((element) => { - context.root.remove(element); - }); - // Select the old elements - this.setSelection(context, "old"); - - return context.root; - } - - redo(context: CommandExecutionContext): CommandReturn { - if (this.editorModeController?.isReadOnly()) { - return context.root; - } - - this.newElements.forEach((element) => { - context.root.add(element); - }); - this.setSelection(context, "new"); - - return context.root; - } -} diff --git a/frontend/webEditor/src/features/dfdElements/AssignmentLanguage.ts b/frontend/webEditor/src/features/dfdElements/AssignmentLanguage.ts deleted file mode 100644 index 6751570d..00000000 --- a/frontend/webEditor/src/features/dfdElements/AssignmentLanguage.ts +++ /dev/null @@ -1,477 +0,0 @@ -import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; -import { - AbstractWord, - AutoCompleteNode, - AutoCompleteTree, - ConstantWord, - Token, - WordCompletion, -} from "../constraintMenu/AutoCompletion"; -import { SModelElementImpl, SParentElementImpl, SPortImpl } from "sprotty"; -import { LabelTypeRegistry } from "../labels/labelTypeRegistry"; -import { DfdNodeImpl } from "./nodes"; - -export class MonacoEditorAssignmentLanguageCompletionProvider implements monaco.languages.CompletionItemProvider { - constructor(private tree: AutoCompleteTree) {} - - triggerCharacters = [".", ";", " ", ",", "("]; - - provideCompletionItems( - model: monaco.editor.ITextModel, - position: monaco.Position, - ): monaco.languages.ProviderResult { - const allLines = model.getLinesContent(); - const includedLines: string[] = []; - for (let i = 0; i < position.lineNumber - 1; i++) { - includedLines.push(allLines[i]); - } - const currentLine = allLines[position.lineNumber - 1].substring(0, position.column - 1); - includedLines.push(currentLine); - - const r = this.tree.getCompletion(includedLines); - return { - suggestions: r, - }; - } -} - -const startOfLineKeywords = ["forward", "assign", "set", "unset"]; -const statementKeywords = [...startOfLineKeywords, "if", "from"]; -const constantsKeywords = ["TRUE", "FALSE"]; -export const assignemntLanguageMonarchDefinition: monaco.languages.IMonarchLanguage = { - keywords: [...statementKeywords, ...constantsKeywords], - - operators: ["=", "||", "&&", "!"], - - symbols: /[=>[]) { - super(roots); - } - - public replace(lines: string[], replacement: ReplacementData): string[] { - const tokens = this.tokenize(lines); - const replaced = this.replaceToken(this.roots, tokens, 0, replacement); - const newLines: string[] = []; - let currentLine = ""; - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i]; - const newText = replaced[i]; - currentLine += newText; - currentLine += token.whiteSpaceAfter || ""; - if (i == tokens.length - 1 || tokens[i + 1].line !== token.line) { - newLines.push(currentLine); - currentLine = ""; - } - } - return newLines; - } - - private replaceToken( - nodes: AutoCompleteNode[], - tokens: Token[], - index: number, - replacement: ReplacementData, - skipStartCheck = false, - ): string[] { - if (index >= tokens.length) { - return []; - } - // check for new start - if (!skipStartCheck && tokens[index].column == 1) { - const matchesAnyRoot = this.roots.some((n) => n.word.verifyWord(tokens[index].text).length === 0); - if (matchesAnyRoot) { - return this.replaceToken(this.roots, tokens, index, replacement, true); - } - } - let text = tokens[index].text; - for (const n of nodes) { - if ((n.word as ReplaceableAbstractWord).replaceWord) { - text = (n.word as ReplaceableAbstractWord).replaceWord(text, replacement); - } - } - return [ - text, - ...this.replaceToken( - nodes.flatMap((n) => n.children), - tokens, - index + 1, - replacement, - ), - ]; - } -} - -export namespace TreeBuilder { - export function buildTree( - labelTypeRegistry: LabelTypeRegistry, - port?: SPortImpl, - ): AutoCompleteNode[] { - return [ - buildSetOrUnsetStatement(labelTypeRegistry, "set"), - buildSetOrUnsetStatement(labelTypeRegistry, "unset"), - buildForwardStatement(port), - buildAssignStatement(labelTypeRegistry, port), - ]; - } - - function buildSetOrUnsetStatement( - labelTypeRegistry: LabelTypeRegistry, - keyword: string, - ): AutoCompleteNode { - const labelNode: AutoCompleteNode = { - word: new LabelListWord(labelTypeRegistry), - children: [], - }; - return { - word: new ConstantWord(keyword), - children: [labelNode], - }; - } - - function buildForwardStatement(port?: SPortImpl) { - const inputNode: AutoCompleteNode = { - word: new InputListWord(port), - children: [], - }; - return { - word: new ConstantWord("forward"), - children: [inputNode], - }; - } - - function buildAssignStatement( - labelTypeRegistry: LabelTypeRegistry, - port?: SPortImpl, - ): AutoCompleteNode { - const fromNode: AutoCompleteNode = { - word: new ConstantWord("from"), - children: [ - { - word: new InputListWord(port), - children: [], - }, - ], - }; - const ifNode: AutoCompleteNode = { - word: new ConstantWord("if"), - children: buildCondition(labelTypeRegistry, fromNode, port), - }; - return { - word: new ConstantWord("assign"), - children: [ - { - word: new LabelWord(labelTypeRegistry), - children: [ifNode], - }, - ], - }; - } - - function buildCondition(labelTypeRegistry: LabelTypeRegistry, nextNode: AutoCompleteNode, port?: SPortImpl) { - const connectors: AutoCompleteNode[] = ["&&", "||"].map((o) => ({ - word: new ConstantWord(o), - children: [], - })); - - const expressors: AutoCompleteNode[] = [ - new ConstantWord("TRUE"), - new ConstantWord("FALSE"), - new InputLabelWord(labelTypeRegistry, port), - ].map((e) => ({ - word: e, - children: [...connectors, nextNode], - canBeFinal: true, - })); - - connectors.forEach((c) => { - c.children = expressors; - }); - return expressors; - } -} - -abstract class InputAwareWord { - constructor(private port?: SPortImpl) {} - - protected getAvailableInputs(): string[] { - const parent = this.port?.parent; - if (parent && parent instanceof DfdNodeImpl) { - return parent.getAvailableInputs().filter((input) => input !== undefined) as string[]; - } - return []; - } - - private getSelectedPorts(node: SModelElementImpl): SPortImpl[] { - if (node instanceof SPortImpl && node.selected) { - return [node]; - } - if (node instanceof SParentElementImpl) { - return node.children.flatMap((child) => this.getSelectedPorts(child)); - } - return []; - } -} - -class LabelWord implements ReplaceableAbstractWord { - constructor(private readonly labelTypeRegistry: LabelTypeRegistry) {} - - completionOptions(word: string): WordCompletion[] { - const parts = word.split("."); - - if (parts.length == 1) { - return this.labelTypeRegistry.getLabelTypes().map((l) => ({ - insertText: l.name, - kind: monaco.languages.CompletionItemKind.Class, - })); - } else if (parts.length == 2) { - const type = this.labelTypeRegistry.getLabelTypes().find((l) => l.name === parts[0]); - if (!type) { - return []; - } - - return type.values.map((l) => ({ - insertText: l.text, - kind: monaco.languages.CompletionItemKind.Enum, - startOffset: parts[0].length + 1, - })); - } - - return []; - } - - verifyWord(word: string): string[] { - const parts = word.split("."); - - if (parts.length > 2) { - return ["Expected at most 2 parts in characteristic selector"]; - } - - const type = this.labelTypeRegistry.getLabelTypes().find((l) => l.name === parts[0]); - if (!type) { - return ['Unknown label type "' + parts[0] + '"']; - } - - if (parts.length < 2) { - return ["Expected characteristic to have value"]; - } - - if (parts[1].startsWith("$") && parts[1].length >= 2) { - return []; - } - - const label = type.values.find((l) => l.text === parts[1]); - if (!label) { - return ['Unknown label value "' + parts[1] + '" for type "' + parts[0] + '"']; - } - - return []; - } - - replaceWord(text: string, replacement: ReplacementData) { - if (replacement.type == "Label" && text == replacement.old) { - return replacement.replacement; - } - return text; - } -} - -class LabelListWord implements ReplaceableAbstractWord { - labelWord: LabelWord; - - constructor(labelTypeRegistry: LabelTypeRegistry) { - this.labelWord = new LabelWord(labelTypeRegistry); - } - - completionOptions(word: string): WordCompletion[] { - const parts = word.split(","); - const lastPart = parts[parts.length - 1]; - const prefixLength = parts.slice(0, -1).reduce((acc, part) => acc + part.length + 1, 0); // +1 for the commas - return this.labelWord.completionOptions(lastPart).map((c) => ({ - ...c, - startOffset: prefixLength + (c.startOffset ?? 0), - })); - } - - verifyWord(word: string): string[] { - const parts = word.split(","); - const errors: string[] = []; - for (const part of parts) { - errors.push(...this.labelWord.verifyWord(part)); - } - return errors; - } - - replaceWord(text: string, replacement: ReplacementData) { - const parts = text.split(","); - const newParts = parts.map((part) => this.labelWord.replaceWord(part, replacement)); - return newParts.join(","); - } -} - -class InputWord extends InputAwareWord implements ReplaceableAbstractWord { - completionOptions(): WordCompletion[] { - const inputs = this.getAvailableInputs(); - return inputs.map((input) => ({ - insertText: input, - kind: monaco.languages.CompletionItemKind.Variable, - })); - } - - verifyWord(word: string): string[] { - const availableInputs = this.getAvailableInputs(); - if (availableInputs.includes(word)) { - return []; - } - return [`Unknown input "${word}"`]; - } - - replaceWord(text: string, replacement: ReplacementData) { - if (replacement.type == "Input" && text == replacement.old) { - return replacement.replacement; - } - return text; - } -} - -class InputListWord implements ReplaceableAbstractWord { - private inputWord: InputWord; - constructor(port?: SPortImpl) { - this.inputWord = new InputWord(port); - } - - completionOptions(word: string): WordCompletion[] { - const parts = word.split(","); - // remove last one as we are completing that one - if (parts.length > 1) { - parts.pop(); - } - const startOffset = parts.reduce((acc, part) => acc + part.length + 1, 0); // +1 for the commas - return this.inputWord - .completionOptions() - .filter((c) => !parts.includes(c.insertText)) - .map((c) => ({ - ...c, - startOffset: startOffset + (c.startOffset ?? 0), - })); - } - - verifyWord(word: string): string[] { - const parts = word.split(","); - const errors: string[] = []; - for (const part of parts) { - errors.push(...this.inputWord.verifyWord(part)); - } - return errors; - } - - replaceWord(text: string, replacement: ReplacementData) { - const parts = text.split(","); - const newParts = parts.map((part) => this.inputWord.replaceWord(part, replacement)); - return newParts.join(","); - } -} - -class InputLabelWord implements ReplaceableAbstractWord { - private inputWord: InputWord; - private labelWord: LabelWord; - - constructor(labelTypeRegistry: LabelTypeRegistry, port?: SPortImpl) { - this.inputWord = new InputWord(port); - this.labelWord = new LabelWord(labelTypeRegistry); - } - - completionOptions(word: string): WordCompletion[] { - const parts = this.getParts(word); - if (parts[1] === undefined) { - return this.inputWord.completionOptions().map((c) => ({ - ...c, - insertText: c.insertText, - })); - } else if (parts.length >= 2) { - return this.labelWord.completionOptions(parts[1]).map((c) => ({ - ...c, - insertText: c.insertText, - startOffset: (c.startOffset ?? 0) + parts[0].length + 1, // +1 for the dot - })); - } - return []; - } - - verifyWord(word: string): string[] { - const parts = this.getParts(word); - const inputErrors = this.inputWord.verifyWord(parts[0]); - if (inputErrors.length > 0) { - return inputErrors; - } - if (parts[1] === undefined) { - return ["Expected input and label separated by a dot"]; - } - const labelErrors = this.labelWord.verifyWord(parts[1]); - return [...inputErrors, ...labelErrors]; - } - - replaceWord(text: string, replacement: ReplacementData) { - const [input, label] = this.getParts(text); - if (replacement.type == "Input" && input === replacement.old) { - return replacement.replacement + (label ? "." + label : ""); - } else if (replacement.type == "Label" && label === replacement.old) { - return input + "." + replacement.replacement; - } - return text; - } - - private getParts(text: string): [string, string] | [string, undefined] { - if (text.includes(".")) { - const index = text.indexOf("."); - const input = text.substring(0, index); - const label = text.substring(index + 1); - return [input, label]; - } - return [text, undefined]; - } -} diff --git a/frontend/webEditor/src/features/dfdElements/behaviorRefactorer.ts b/frontend/webEditor/src/features/dfdElements/behaviorRefactorer.ts deleted file mode 100644 index 049e3719..00000000 --- a/frontend/webEditor/src/features/dfdElements/behaviorRefactorer.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { inject, injectable } from "inversify"; -import { LabelType, LabelTypeRegistry } from "../labels/labelTypeRegistry"; -import { - Command, - CommandExecutionContext, - CommandReturn, - ICommandStack, - ILogger, - ModelSource, - SEdgeImpl, - SLabelImpl, - SModelElementImpl, - SParentElementImpl, - TYPES, -} from "sprotty"; -import { DfdInputPortImpl, DfdOutputPortImpl } from "./ports"; -import { ApplyLabelEditAction } from "sprotty-protocol"; -import { DfdNodeImpl } from "./nodes"; -import { ReplaceAutoCompleteTree, TreeBuilder } from "./AssignmentLanguage"; - -interface LabelChange { - oldLabel: string; - newLabel: string; -} - -/** - * This class listens to changes in the label type registry and updates the behavior of the DFD elements accordingly. - * When a label type/value is renamed, the behavior of the DFD elements is updated to reflect the new name. - * Also provides a method to refactor the behavior of a DFD element when the name of an input is changed. - */ -@injectable() -export class DFDBehaviorRefactorer { - private previousLabelTypes: LabelType[] = []; - - constructor( - @inject(LabelTypeRegistry) private readonly registry: LabelTypeRegistry, - @inject(TYPES.ILogger) private readonly logger: ILogger, - @inject(TYPES.ICommandStack) private readonly commandStack: ICommandStack, - ) { - if (this.registry) { - this.previousLabelTypes = structuredClone(this.registry.getLabelTypes()); - this.registry?.onUpdate(() => { - this.handleLabelUpdate().catch((error) => - this.logger.error(this, "Error while processing label type registry update", error), - ); - }); - } - } - - private async handleLabelUpdate(): Promise { - this.logger.log(this, "Handling label type registry update"); - const currentLabelTypes = this.registry.getLabelTypes() ?? []; - - const changedLabels: LabelChange[] = []; - for (const newLabel of currentLabelTypes) { - const oldLabel = this.previousLabelTypes.find((label) => label.id === newLabel.id); - if (!oldLabel) { - continue; - } - if (oldLabel.name !== newLabel.name) { - for (const newValue of newLabel.values) { - const oldValue = oldLabel.values.find((value) => value.id === newValue.id); - if (!oldValue) { - continue; - } - changedLabels.push({ - oldLabel: `${oldLabel.name}.${oldValue.text}`, - newLabel: `${newLabel.name}.${newValue.text}`, - }); - } - } - for (const newValue of newLabel.values) { - const oldValue = oldLabel.values.find((value) => value.id === newValue.id); - if (!oldValue) { - continue; - } - if (oldValue.text !== newValue.text) { - changedLabels.push({ - oldLabel: `${newLabel.name}.${oldValue.text}`, - newLabel: `${newLabel.name}.${newValue.text}`, - }); - } - } - } - - this.logger.log(this, "Changed labels", changedLabels); - - const model = await this.commandStack.executeAll([]); - this.traverseDfdOutputPorts(model, (port) => { - const tree = new ReplaceAutoCompleteTree(TreeBuilder.buildTree(this.registry, port)); - this.renameLabelsForPort(port, changedLabels, tree); - }); - - this.previousLabelTypes = structuredClone(currentLabelTypes); - } - - private renameLabelsForPort(port: DfdOutputPortImpl, labelChanges: LabelChange[], tree: ReplaceAutoCompleteTree) { - let lines = port.getBehavior().split(/\n/); - for (const change of labelChanges) { - lines = tree.replace(lines, { old: change.oldLabel, replacement: change.newLabel, type: "Label" }); - } - port.setBehavior(lines.join("\n")); - } - - private traverseDfdOutputPorts(element: SModelElementImpl, cb: (port: DfdOutputPortImpl) => void) { - if (element instanceof DfdOutputPortImpl) { - cb(element); - } - - if (element instanceof SParentElementImpl) { - element.children.forEach((child) => this.traverseDfdOutputPorts(child, cb)); - } - } - - processInputLabelRename( - label: SLabelImpl, - port: DfdInputPortImpl, - oldLabelText: string, - newLabelText: string, - ): Map { - label.text = oldLabelText; - const oldInputName = port.getName(); - label.text = newLabelText; - const newInputName = port.getName(); - - const behaviorChanges: Map = new Map(); - const node = port.parent; - if (!(node instanceof DfdNodeImpl) || !oldInputName || !newInputName) { - return behaviorChanges; - } - - const tree = new ReplaceAutoCompleteTree(TreeBuilder.buildTree(this.registry, port)); - - node.children.forEach((child) => { - if (!(child instanceof DfdOutputPortImpl)) { - return; - } - - behaviorChanges.set(child.id, this.processInputRenameForPort(child, oldInputName, newInputName, tree)); - }); - - return behaviorChanges; - } - - private processInputRenameForPort( - port: DfdOutputPortImpl, - oldInputName: string, - newInputName: string, - tree: ReplaceAutoCompleteTree, - ): string { - const lines = port.getBehavior().split("\n"); - const newLines = tree.replace(lines, { old: oldInputName, replacement: newInputName, type: "Input" }); - return newLines.join("\n"); - } -} - -/** - * A command that refactors the behavior of DFD output ports when the name of an input is changed. - * Designed to be added as a command handler for the ApplyLabelEditAction to automatically - * detect all edit of labels on a edge element. - * When a label is changed, the old and new input name of the dfd input port that the edge - * is pointing to is used to update the behavior of all dfd output ports that are connected to the same node. - */ -export class RefactorInputNameInDFDBehaviorCommand extends Command { - static readonly KIND = ApplyLabelEditAction.KIND; - - constructor( - @inject(TYPES.Action) protected readonly action: ApplyLabelEditAction, - @inject(TYPES.ModelSource) protected readonly modelSource: ModelSource, - @inject(DFDBehaviorRefactorer) protected readonly refactorer: DFDBehaviorRefactorer, - ) { - super(); - } - - private oldBehaviors: Map = new Map(); - private newBehaviors: Map = new Map(); - - execute(context: CommandExecutionContext): CommandReturn { - // This command will be executed after the ApplyLabelEditCommand. - // Therefore the label will already be changed in the model. - // To get the old value we get the label from the model source, - // which still has the old value because the model commit will be done after this command. - const modelBeforeChange = context.modelFactory.createRoot(this.modelSource.model); - const labelBeforeChange = modelBeforeChange.index.getById(this.action.labelId); - if (!(labelBeforeChange instanceof SLabelImpl)) { - // should not happen - return context.root; - } - - const oldInputName = labelBeforeChange.text; - const newInputName = this.action.text; - const edge = labelBeforeChange.parent; - if (!(edge instanceof SEdgeImpl)) { - // should not happen - return context.root; - } - - const port = edge.target; - if (!(port instanceof DfdInputPortImpl)) { - // Edge does not point to a dfd port, but maybe some node directly. - // Cannot be used in behaviors in this case so we don't need to refactor anything. - return context.root; - } - - const behaviorChanges: Map = this.refactorer.processInputLabelRename( - labelBeforeChange, - port, - oldInputName, - newInputName, - ); - behaviorChanges.forEach((updatedBehavior, id) => { - const port = context.root.index.getById(id); - if (port instanceof DfdOutputPortImpl) { - this.oldBehaviors.set(id, port.getBehavior()); - this.newBehaviors.set(id, updatedBehavior); - port.setBehavior(updatedBehavior); - } - }); - - return context.root; - } - - undo(context: CommandExecutionContext): CommandReturn { - this.oldBehaviors.forEach((oldBehavior, id) => { - const port = context.root.index.getById(id); - if (port instanceof DfdOutputPortImpl) { - port.setBehavior(oldBehavior); - } - }); - - return context.root; - } - - redo(context: CommandExecutionContext): CommandReturn { - this.newBehaviors.forEach((newBehavior, id) => { - const port = context.root.index.getById(id); - if (port instanceof DfdOutputPortImpl) { - port.setBehavior(newBehavior); - } - }); - - return context.root; - } -} diff --git a/frontend/webEditor/src/features/dfdElements/dynamicChildren.ts b/frontend/webEditor/src/features/dfdElements/dynamicChildren.ts deleted file mode 100644 index 0752b8a3..00000000 --- a/frontend/webEditor/src/features/dfdElements/dynamicChildren.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { injectable, multiInject } from "inversify"; -import { SModelElementRegistration, SNodeImpl, SEdgeImpl, TYPES } from "sprotty"; -import { SModelElement, SEdge, SNode } from "sprotty-protocol"; - -// This file contains helpers to dynamically specify the children of a sprotty element. -// Element children are generally used for e.g. labels in sprotty or other sub elements. -// You could embed everything into one element but it is often easier to use children. -// E.g. for editable labels you would need to implement a custom label edit ui which is pretty complicated. - -// Normally, the children of a sprotty element are specified in the model. -// However this means that the children are saved together with the model. -// Imagine you want to change the children of a node at some point, e.g to add another text label, -// move it slightly or align the text differently. -// When you save the children you would need to migrate the previously saved models -// to the new children. - -// This is undesirable as the display of a node should not be hardcoded in the serialized model. -// To circumvent this, these helper classes were developed. -// The model is saved without children and the children are added dynamically by the runtime -// by each model element using the setChildren method. -// This sets the `children` array of the element and also loads data from the parent element into the children -// (e.g. texts for labels). -// When the model is saved, the removeChildren method is called to remove the children again. -// This method also needs to save the data of the children in the parent element, so it is properly saved. -// This ensures that the display of a node is a implementation detail and not encoded in the saved models. - -// Abstract classes that define the both abstract methods setChildren and removeChildren - -export abstract class DynamicChildrenNode extends SNodeImpl { - abstract setChildren(schema: SNode): void; - abstract removeChildren(schema: SNode): void; -} - -export abstract class DynamicChildrenEdge extends SEdgeImpl { - abstract setChildren(schema: SEdge): void; - abstract removeChildren(schema: SEdge): void; -} - -@injectable() -export class DynamicChildrenProcessor { - @multiInject(TYPES.SModelElementRegistration) - private readonly elementRegistrations: SModelElementRegistration[] = []; - - /** - * Recursively either adds or removes the children of a model graph. - * Recursively traverses the graph, gets the registration of the corresponding element type, - * checks whether it extends a DynamicChildren* abstract class and then calls the corresponding method. - */ - public processGraphChildren(graphElement: SModelElement | SEdge, action: "set" | "remove"): void { - // When removing children we need to remove them from children to parents to do it correctly. - // When setting children we need to do it the other way around to set the children - // of the elements that have been set by the parent first. - if (action === "remove") { - graphElement.children?.forEach((child) => this.processGraphChildren(child, action)); - } - - const registration = this.elementRegistrations.find((r) => r.type === graphElement.type); - if (registration) { - // If registration is undefined some element hasn't been registered but used, so this shouldn't happen - // if the model is valid. - - // Create a instance of the element. - // Ideally the *Children methods should be static, but static methods can't be abstract. - // So we need to create a instance we can then call the method on. - const impl = new registration.constr(); - if (impl instanceof DynamicChildrenNode) { - if (action === "set") { - impl.setChildren(graphElement); - } else { - impl.removeChildren(graphElement); - } - } - - // sourceId is only present in edges and ensures that the graphElement is an edge (to calm the type system) - if (impl instanceof DynamicChildrenEdge && "sourceId" in graphElement) { - if (action === "set") { - impl.setChildren(graphElement); - } else { - impl.removeChildren(graphElement); - } - } - } - - if (action === "set") { - graphElement.children?.forEach((child) => this.processGraphChildren(child, action)); - } - } -} diff --git a/frontend/webEditor/src/features/dfdElements/elementStyles.css b/frontend/webEditor/src/features/dfdElements/elementStyles.css deleted file mode 100644 index 91c2d742..00000000 --- a/frontend/webEditor/src/features/dfdElements/elementStyles.css +++ /dev/null @@ -1,117 +0,0 @@ -/* This file contains styling for the node views defined in nodes.tsx, edge.tsx and ports.tsx */ - -/* sprotty-* classes are automatically added by sprotty and the other ones - are added in the definition inside nodes.tsx, edge.tsx and ports.tsx */ - -/* Nodes */ - -.sprotty-node rect, -.sprotty-node line, -.sprotty-node circle { - /* stroke color defaults to be the foreground color of the theme. - Alternatively it can be overwritten by setting the --color variable - As a inline style attribute for the specific node. - Used as a highlighter to mark nodes with errors. - This is essentially a "optional parameter" to this css rule. - See https://stackoverflow.com/questions/17893823/how-to-pass-parameters-to-css-classes */ - stroke: var(--color-foreground); - stroke-width: 1; - /* Background fill of the node. - When --color is unset this is just --color-primary. - If this node is annotated and --color is set, it will be included in the color mix. */ - fill: color-mix(in srgb, var(--color-primary), var(--color, transparent) 40%); -} - -.sprotty-node .node-label text { - font-size: 5pt; -} - -.sprotty-node .node-label rect, -.sprotty-node .node-label .label-delete circle { - fill: var(--color-primary); - stroke: var(--color-foreground); - stroke-width: 0.5; -} - -.sprotty-node .node-label .label-delete text { - fill: var(--color-foreground); - font-size: 5px; -} - -/* Edges */ - -.sprotty-edge { - stroke: var(--color-foreground); - fill: none; - stroke-width: 1; -} - -/* On top of the actual edge path we draw a transparent path with a larger stroke width. - This makes it easier to select the edge with the mouse. */ -.sprotty-edge path.select-path { - stroke: transparent; - /* make the "invisible hitbox" 8 pixels wide. This is the same width as the arrow head */ - stroke-width: 8; -} - -.sprotty-edge .arrow { - fill: var(--color-foreground); - stroke: none; -} - -.sprotty-edge > .sprotty-routing-handle { - fill: var(--color-foreground); - stroke: none; -} - -.sprotty-edge .label-background rect { - fill: var(--color-background); - stroke-width: 0; -} - -/* Ports */ - -.sprotty-port rect { - stroke: var(--port-border, var(--color-foreground)); - fill: color-mix(in srgb, var(--port-color, var(--color-primary)), var(--color-background) 25%); - stroke-width: 0.5; -} - -.sprotty-port .port-text { - font-size: 4pt; -} - -/* All nodes/misc */ - -.sprotty-node.selected circle, -.sprotty-node.selected rect, -.sprotty-node.selected line, -.sprotty-edge.selected { - stroke-width: 2; -} - -.sprotty-port.selected rect { - stroke-width: 1; -} - -text { - stroke-width: 0; - fill: var(--color-foreground); - font-family: "Arial", sans-serif; - font-size: 11pt; - text-anchor: middle; - dominant-baseline: central; - - -webkit-user-select: none; - user-select: none; -} - -/* elements with the sprotty-missing class use a node type that has not been registered. - Because of this sprotty does not know what to do with them and renders their content and specifies them as missing. - To make these errors very visible we make them red here. - Ideally a user should never see this. */ -.sprotty-missing { - stroke-width: 1; - stroke: var(--color-error); - fill: var(--color-error); -} diff --git a/frontend/webEditor/src/features/dfdElements/nodes.tsx b/frontend/webEditor/src/features/dfdElements/nodes.tsx deleted file mode 100644 index 8bae434e..00000000 --- a/frontend/webEditor/src/features/dfdElements/nodes.tsx +++ /dev/null @@ -1,315 +0,0 @@ -/** @jsx svg */ -import { - SNodeImpl, - WithEditableLabel, - isEditableLabel, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - svg, - withEditLabelFeature, - RenderingContext, - ShapeView, -} from "sprotty"; -import { SNode, SLabel, Bounds, SModelElement, SPort } from "sprotty-protocol"; -import { inject, injectable, optional } from "inversify"; -import { VNode, VNodeStyle } from "snabbdom"; -import { LabelAssignment } from "../labels/labelTypeRegistry"; -import { DynamicChildrenNode } from "./dynamicChildren"; -import { containsDfdLabelFeature } from "../labels/elementFeature"; -import { calculateTextSize } from "../../utils"; -import { DfdNodeLabelRenderer } from "../labels/labelRenderer"; -import { DfdPositionalLabelArgs } from "./labels"; -import { DfdInputPortImpl } from "./ports"; -import { ArrowEdgeImpl } from "./edges"; - -export interface DfdNode extends SNode { - text: string; - labels: LabelAssignment[]; - ports: SPort[]; - annotations?: DfdNodeAnnotation[]; -} - -export interface DfdNodeAnnotation { - message: string; - color?: string; - icon?: string; - tfg?: number; -} - -export abstract class DfdNodeImpl extends DynamicChildrenNode implements WithEditableLabel { - static readonly DEFAULT_FEATURES = [...SNodeImpl.DEFAULT_FEATURES, withEditLabelFeature, containsDfdLabelFeature]; - static readonly DEFAULT_WIDTH = 50; - static readonly WIDTH_PADDING = 12; - static readonly NODE_COLOR = "var(--color-primary)"; - static readonly HIGHLIGHTED_COLOR = "var(--color-highlighted)"; - - text: string = ""; - color?: string; - labels: LabelAssignment[] = []; - ports: SPort[] = []; - hideLabels: boolean = false; - minimumWidth: number = DfdNodeImpl.DEFAULT_WIDTH; - annotations: DfdNodeAnnotation[] = []; - - override setChildren(schema: DfdNode): void { - const children: SModelElement[] = [ - { - type: "label:positional", - text: schema.text ?? "", - id: schema.id + "-label", - } as SLabel, - ]; - - schema.ports?.forEach((port) => { - // Remove wrongly serialized features Set. - // Refer to preprocessModelSchema in the load action for more information. - if ("features" in port) { - delete port.features; - } - - children.push(port); - }); - schema.children = children; - } - - override removeChildren(schema: DfdNode): void { - const label = schema.children?.find((element) => element.type === "label:positional") as SLabel | undefined; - const ports = schema.children?.filter((element) => element.type.startsWith("port")) ?? []; - - schema.text = label?.text ?? ""; - schema.ports = ports as SPort[]; - schema.children = []; - } - - get editableLabel() { - const label = this.children.find((element) => element.type === "label:positional"); - if (label && isEditableLabel(label)) { - return label; - } - - return undefined; - } - - protected calculateWidth(): number { - if (this.hideLabels) { - return this.minimumWidth + DfdNodeImpl.WIDTH_PADDING; - } - const textWidth = calculateTextSize(this.text).width; - const labelWidths = this.labels.map( - (labelAssignment) => DfdNodeLabelRenderer.computeLabelContent(labelAssignment)[1], - ); - - const neededWidth = Math.max(...labelWidths, textWidth, DfdNodeImpl.DEFAULT_WIDTH); - return neededWidth + DfdNodeImpl.WIDTH_PADDING; - } - - protected abstract calculateHeight(): number; - - override get bounds(): Bounds { - return { - x: this.position.x, - y: this.position.y, - width: this.calculateWidth(), - height: this.calculateHeight(), - }; - } - - /** - * Gets the names of all available input ports. - * @returns a list of the names of all available input ports. - * Can include undefined if a port has no named edges connected to it. - */ - getAvailableInputs(): (string | undefined)[] { - return this.children - .filter((child) => child instanceof DfdInputPortImpl) - .map((child) => child as DfdInputPortImpl) - .map((child) => child.getName()); - } - - /** - * Gets the text of all dfd edges that are connected to the input ports of this node. - * Applies the passed filter to the edges. - * If a edge has no label, the empty string is returned. - */ - getEdgeTexts(edgePredicate: (e: ArrowEdgeImpl) => boolean): string[] { - const inputPorts = this.children - .filter((child) => child instanceof DfdInputPortImpl) - .map((child) => child as DfdInputPortImpl); - - return inputPorts - .flatMap((port) => port.incomingEdges) - .filter((edge) => edge instanceof ArrowEdgeImpl) - .map((edge) => edge as ArrowEdgeImpl) - .filter(edgePredicate) - .map((edge) => edge.editableLabel?.text ?? ""); - } - - /** - * Generates the per-node inline style object for the view. - * Contains the opacity and the color of the node that may be set by the annotation (if any). - */ - geViewStyleObject(): VNodeStyle { - const style: VNodeStyle = { - opacity: this.opacity.toString(), - }; - - style["--border"] = "#FFFFFF"; - - if (this.color) style["--color"] = this.color; - - return style; - } - - public setColor(color: string, override: boolean = true) { - if (override || this.color === DfdNodeImpl.NODE_COLOR) this.color = color; - } -} - -@injectable() -export class StorageNodeImpl extends DfdNodeImpl { - protected override calculateHeight(): number { - const hasLabels = this.labels.length > 0; - if (hasLabels && !this.hideLabels) { - return ( - StorageNodeImpl.LABEL_START_HEIGHT + - this.labels.length * DfdNodeLabelRenderer.LABEL_SPACING_HEIGHT + - DfdNodeLabelRenderer.LABEL_SPACE_BETWEEN - ); - } else { - return StorageNodeImpl.TEXT_HEIGHT; - } - } - - protected override calculateWidth(): number { - return super.calculateWidth() + StorageNodeImpl.LEFT_PADDING; - } - - static readonly TEXT_HEIGHT = 32; - static readonly LABEL_START_HEIGHT = 28; - static readonly LEFT_PADDING = 10; -} - -@injectable() -export class StorageNodeView extends ShapeView { - constructor(@inject(DfdNodeLabelRenderer) @optional() private readonly labelRenderer?: DfdNodeLabelRenderer) { - super(); - } - - render(node: Readonly, context: RenderingContext): VNode | undefined { - if (!this.isVisible(node, context)) { - return undefined; - } - - const { width, height } = node.bounds; - const leftPadding = StorageNodeImpl.LEFT_PADDING / 2; - - return ( - - - - {context.renderChildren(node, { - xPosition: width / 2 + leftPadding, - yPosition: StorageNodeImpl.TEXT_HEIGHT / 2, - } as DfdPositionalLabelArgs)} - {this.labelRenderer?.renderNodeLabels(node, StorageNodeImpl.LABEL_START_HEIGHT, leftPadding)} - - ); - } -} - -export class FunctionNodeImpl extends DfdNodeImpl { - protected override calculateHeight(): number { - const hasLabels = this.labels.length > 0; - if (hasLabels && !this.hideLabels) { - return ( - // height for text - FunctionNodeImpl.LABEL_START_HEIGHT + - // height for the labels - this.labels.length * DfdNodeLabelRenderer.LABEL_SPACING_HEIGHT + - // Spacing between last label and the under edge of the node rectangle - DfdNodeLabelRenderer.LABEL_SPACE_BETWEEN - ); - } else { - return FunctionNodeImpl.LABEL_START_HEIGHT + FunctionNodeImpl.SEPARATOR_NO_LABEL_PADDING; - } - } - - static readonly TEXT_HEIGHT = 28; - static readonly SEPARATOR_NO_LABEL_PADDING = 4; - static readonly SEPARATOR_LABEL_PADDING = 4; - static readonly LABEL_START_HEIGHT = this.TEXT_HEIGHT + this.SEPARATOR_LABEL_PADDING; - static readonly BORDER_RADIUS = 5; -} - -@injectable() -export class FunctionNodeView extends ShapeView { - constructor(@inject(DfdNodeLabelRenderer) @optional() private readonly labelRenderer?: DfdNodeLabelRenderer) { - super(); - } - - render(node: Readonly, context: RenderingContext): VNode | undefined { - if (!this.isVisible(node, context)) { - return undefined; - } - - const { width, height } = node.bounds; - const r = FunctionNodeImpl.BORDER_RADIUS; - - return ( - - - - {context.renderChildren(node, { - xPosition: width / 2, - yPosition: FunctionNodeImpl.TEXT_HEIGHT / 2, - } as DfdPositionalLabelArgs)} - {this.labelRenderer?.renderNodeLabels(node, FunctionNodeImpl.LABEL_START_HEIGHT)} - - ); - } -} - -@injectable() -export class IONodeImpl extends DfdNodeImpl { - protected override calculateHeight(): number { - const hasLabels = this.labels.length > 0; - if (hasLabels && !this.hideLabels) { - return ( - IONodeImpl.LABEL_START_HEIGHT + - this.labels.length * DfdNodeLabelRenderer.LABEL_SPACING_HEIGHT + - DfdNodeLabelRenderer.LABEL_SPACE_BETWEEN - ); - } else { - return IONodeImpl.TEXT_HEIGHT; - } - } - - static readonly TEXT_HEIGHT = 32; - static readonly LABEL_START_HEIGHT = 28; -} - -@injectable() -export class IONodeView extends ShapeView { - constructor(@inject(DfdNodeLabelRenderer) @optional() private readonly labelRenderer?: DfdNodeLabelRenderer) { - super(); - } - - render(node: Readonly, context: RenderingContext): VNode | undefined { - if (!this.isVisible(node, context)) { - return undefined; - } - - const { width, height } = node.bounds; - - return ( - - - - {context.renderChildren(node, { - xPosition: width / 2, - yPosition: IONodeImpl.TEXT_HEIGHT / 2, - } as DfdPositionalLabelArgs)} - {this.labelRenderer?.renderNodeLabels(node, IONodeImpl.LABEL_START_HEIGHT)} - - ); - } -} diff --git a/frontend/webEditor/src/features/dfdElements/outputPortEditUi.css b/frontend/webEditor/src/features/dfdElements/outputPortEditUi.css deleted file mode 100644 index 90e50abf..00000000 --- a/frontend/webEditor/src/features/dfdElements/outputPortEditUi.css +++ /dev/null @@ -1,22 +0,0 @@ -.output-port-edit-ui { - position: absolute; - padding: 10px; - - -webkit-user-select: none; - user-select: none; - - background: var(--color-primary); -} - -.output-port-edit-ui div.unavailable-inputs { - /* spacing between editor and this text */ - padding-bottom: 5px; -} - -.output-port-edit-ui div.validation-label.validation-error { - color: var(--color-error); -} - -.output-port-edit-ui div.validation-label.validation-success { - color: var(--color-valid); -} diff --git a/frontend/webEditor/src/features/dfdElements/outputPortEditUi.ts b/frontend/webEditor/src/features/dfdElements/outputPortEditUi.ts deleted file mode 100644 index 59f8c359..00000000 --- a/frontend/webEditor/src/features/dfdElements/outputPortEditUi.ts +++ /dev/null @@ -1,483 +0,0 @@ -import { inject, injectable, optional } from "inversify"; -import { - AbstractUIExtension, - ActionDispatcher, - Command, - CommandExecutionContext, - CommandReturn, - CommitModelAction, - MouseListener, - MouseTool, - SModelElementImpl, - SModelRootImpl, - SetUIExtensionVisibilityAction, - TYPES, - ViewerOptions, - getAbsoluteClientBounds, -} from "sprotty"; -import { Action } from "sprotty-protocol"; -import { DOMHelper } from "sprotty/lib/base/views/dom-helper"; -import { matchesKeystroke } from "sprotty/lib/utils/keyboard"; -import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; -import { DfdOutputPortImpl } from "./ports"; -import { DfdNodeImpl } from "./nodes"; -import { LabelTypeRegistry } from "../labels/labelTypeRegistry"; -import { EditorModeController } from "../editorMode/editorModeController"; -import { DFDBehaviorRefactorer } from "./behaviorRefactorer"; - -// Enable hover feature that is used to show validation errors. -// Inline completions are enabled to allow autocompletion of keywords and inputs/label types/label values. -import "monaco-editor/esm/vs/editor/contrib/hover/browser/hoverContribution"; -import "monaco-editor/esm/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.js"; - -import "./outputPortEditUi.css"; -import { ThemeManager, Switchable } from "../settingsMenu/themeManager"; -import { - assignemntLanguageMonarchDefinition, - MonacoEditorAssignmentLanguageCompletionProvider, - ReplaceAutoCompleteTree, - TreeBuilder, -} from "./AssignmentLanguage"; - -/** - * Detects when a dfd output port is double clicked and shows the OutputPortEditUI - * with the clicked port as context element. - */ -@injectable() -export class OutputPortEditUIMouseListener extends MouseListener { - private editUIVisible = false; - - mouseDown(target: SModelElementImpl): (Action | Promise)[] { - if (this.editUIVisible) { - // The user has clicked somewhere on the sprotty diagram (not the port edit UI) - // while the UI was open. In this case we hide the UI. - // This may not be exactly accurate because the UI can close itself when - // the change was saved but in those cases editUIVisible is still true. - // However hiding it one more time here for those cases is not a problem. - // Because it is already hidden, nothing will happen and after one click - // editUIVisible will be false again. - this.editUIVisible = false; - return [ - SetUIExtensionVisibilityAction.create({ - extensionId: OutputPortEditUI.ID, - visible: false, - contextElementsId: [target.id], - }), - ]; - } - - return []; - } - - doubleClick(target: SModelElementImpl): (Action | Promise)[] { - if (target instanceof DfdOutputPortImpl) { - // The user has double clicked on a dfd output port - // => show the OutputPortEditUI for this port. - this.editUIVisible = true; - return [ - SetUIExtensionVisibilityAction.create({ - extensionId: OutputPortEditUI.ID, - visible: true, - contextElementsId: [target.id], - }), - ]; - } - - return []; - } -} - -/** - * UI that allows editing the behavior text of a dfd output port (DfdOutputPortImpl). - */ -@injectable() -export class OutputPortEditUI extends AbstractUIExtension implements Switchable { - static readonly ID = "output-port-edit-ui"; - - private unavailableInputsLabel: HTMLDivElement = document.createElement("div") as HTMLDivElement; - private editorContainer: HTMLDivElement = document.createElement("div") as HTMLDivElement; - private validationLabel: HTMLDivElement = document.createElement("div") as HTMLDivElement; - - private port: DfdOutputPortImpl | undefined; - private editor?: monaco.editor.IStandaloneCodeEditor; - private tree?: ReplaceAutoCompleteTree; - private completionProvider?: monaco.IDisposable; - - private static readonly DFD_LANGUAGE_NAME = "dfd-behavior"; - - constructor( - @inject(TYPES.IActionDispatcher) private actionDispatcher: ActionDispatcher, - @inject(TYPES.ViewerOptions) private viewerOptions: ViewerOptions, - @inject(TYPES.DOMHelper) private domHelper: DOMHelper, - @inject(MouseTool) private mouseTool: MouseTool, - // Load label type registry watcher that handles changes to the behavior of - // output ports when label types are changed. - // It has to be loaded somewhere for inversify to create it and start watching. - // Since this is thematically related to the output port edit UI, it is loaded here. - // @ts-expect-error TS6133: 'labelTypeRegistry' is declared but its value is never read. - @inject(DFDBehaviorRefactorer) private readonly _labelTypeChangeWatcher: DFDBehaviorRefactorer, - - @inject(LabelTypeRegistry) private readonly labelTypeRegistry: LabelTypeRegistry, - @inject(EditorModeController) - @optional() - private editorModeController?: EditorModeController, - ) { - super(); - } - - id(): string { - return OutputPortEditUI.ID; - } - - containerClass(): string { - // The container element gets this class name by the sprotty base class. - return this.id(); - } - - protected initializeContents(containerElement: HTMLElement): void { - containerElement.appendChild(this.unavailableInputsLabel); - containerElement.appendChild(this.editorContainer); - containerElement.appendChild(this.validationLabel); - const keyboardShortcutLabel = document.createElement("div"); - keyboardShortcutLabel.innerHTML = "Press CTRL+Space for autocompletion"; - containerElement.appendChild(keyboardShortcutLabel); - - containerElement.classList.add("ui-float"); - this.unavailableInputsLabel.classList.add("unavailable-inputs"); - this.editorContainer.classList.add("monaco-container"); - this.validationLabel.classList.add("validation-label"); - - // Initialize the monaco editor and setup the language for highlighting and autocomplete. - - monaco.languages.register({ id: OutputPortEditUI.DFD_LANGUAGE_NAME }); - monaco.languages.setMonarchTokensProvider( - OutputPortEditUI.DFD_LANGUAGE_NAME, - assignemntLanguageMonarchDefinition, - ); - this.registerCompletionProvider(); - - const monacoTheme = (ThemeManager?.useDarkMode ?? true) ? "vs-dark" : "vs"; - this.editor = monaco.editor.create(this.editorContainer, { - minimap: { - // takes too much space, not useful for our use case - enabled: false, - }, - lineNumbersMinChars: 3, // default is 5, which we'll never need. Save a bit of space. - folding: false, // Not supported by our language definition - wordBasedSuggestions: "off", // Does not really work for our use case - scrollBeyondLastLine: false, // Not needed - theme: monacoTheme, - language: OutputPortEditUI.DFD_LANGUAGE_NAME, - }); - - this.configureHandlers(containerElement); - } - - private resizeEditor(): void { - // Resize editor to fit content. - // Has ranges for height and width to prevent the editor from getting too small or too large. - const e = this.editor; - if (!e) { - return; - } - - // For the height we can use the content height from the editor. - const height = e.getContentHeight(); - - // For the width we cannot really do this. - // Monaco needs about 500ms to figure out the correct width when initially showing the editor. - // In the mean time the width will be too small and after the update - // the window size will jump visibly. - // So for the width we use this calculation to approximate the width. - const maxLineLength = e - .getValue() - .split("\n") - .reduce((max, line) => Math.max(max, line.length), 0); - const width = 100 + maxLineLength * 8; - - const clamp = (value: number, range: readonly [number, number]) => - Math.min(range[1], Math.max(range[0], value)); - - const heightRange = [100, 350] as const; - const widthRange = [275, 650] as const; - - const cHeight = clamp(height, heightRange); - const cWidth = clamp(width, widthRange); - - e.layout({ height: cHeight, width: cWidth }); - } - - private configureHandlers(containerElement: HTMLElement): void { - // If the user unfocuses the editor, save the changes. - this.editor?.onDidBlurEditorText(() => { - this.save(); - }); - - // Run behavior validation when the behavior text changes. - this.editor?.onDidChangeModelContent(() => { - this.validateBehavior(); - }); - - // When the content size of the editor changes, resize the editor accordingly. - this.editor?.onDidContentSizeChange(() => { - this.resizeEditor(); - }); - - // Hide/"close this window" when pressing escape. - containerElement.addEventListener("keydown", (event) => { - if (matchesKeystroke(event, "Escape")) { - this.hide(); - } - }); - - containerElement.addEventListener("mouseleave", () => { - // User might refactor some label type/value. - // Doing so will change the behavior text of all ports referencing the label type/value. - // Save the value so the user doesn't lose their work. - // After the change of the behavior text, it will be reloaded into here with the refactoring done. - this.save(); - }); - this.labelTypeRegistry?.onUpdate(() => { - // The update handler for the refactoring might be after our handler. - // Delay update to the next event loop tick to ensure the refactoring is done. - setTimeout(() => { - if (this.editor && this.port) { - this.editor?.setValue(this.port?.getBehavior()); - } - }, 0); - }); - - // Configure editor readonly depending on editor mode. - // Is set after opening the editor each time but the - // editor mode may change while the editor is open, making this handler necessary. - this.editorModeController?.onModeChange(() => { - this.editor?.updateOptions({ - readOnly: this.editorModeController?.isReadOnly() ?? false, - }); - }); - - // we allow aliasing here so it is available in the inner class, as this would refer to the inner class - // eslint-disable-next-line @typescript-eslint/no-this-alias - const portEditUi = this; - class ZoomMouseListener extends MouseListener { - wheel(): (Action | Promise)[] { - // Re-set position of the UI after next event loop tick. - // In the current event loop tick the scoll is still processed and the - // position of the port may change after the scroll processing, so we need to wait for that. - setTimeout(() => { - portEditUi.setPosition(containerElement); - }); - return []; - } - } - this.mouseTool.register(new ZoomMouseListener()); - } - - protected onBeforeShow( - containerElement: HTMLElement, - root: Readonly, - ...contextElementIds: string[] - ): void { - // Loads data for the port that shall be edited, which is defined by the context element id. - if (contextElementIds.length !== 1) { - throw new Error( - "Expected exactly one context element id which should be the port that shall be shown in the UI.", - ); - } - this.port = root.index.getById(contextElementIds[0]) as DfdOutputPortImpl; - this.setPosition(containerElement); - - const parent = this.port.parent; - if (!(parent instanceof DfdNodeImpl)) { - throw new Error("Expected parent to be a DfdNodeImpl."); - } - - const availableInputNames = parent.getAvailableInputs(); - const countUnavailableDueToMissingName = availableInputNames.filter((name) => name === undefined).length; - - if (countUnavailableDueToMissingName > 0) { - const unavailableInputsText = - countUnavailableDueToMissingName > 1 - ? `There are ${countUnavailableDueToMissingName} inputs that don't have a named edge and cannot be used` - : `There is ${countUnavailableDueToMissingName} input that doesn't have a named edge and cannot be used`; - - this.unavailableInputsLabel.innerText = unavailableInputsText; - this.unavailableInputsLabel.style.display = "block"; - } else { - this.unavailableInputsLabel.innerText = ""; - this.unavailableInputsLabel.style.display = "none"; - } - - // Load the current behavior text of the port into the text editor. - this.editor?.setValue(this.port.getBehavior()); - this.editor?.getModel()?.setEOL(monaco.editor.EndOfLineSequence.LF); - this.resizeEditor(); - - // Configure editor readonly depending on editor mode - this.editor?.updateOptions({ - readOnly: this.editorModeController?.isReadOnly() ?? false, - }); - - this.tree = new ReplaceAutoCompleteTree(TreeBuilder.buildTree(this.labelTypeRegistry, this.port)); - - // Validation of loaded behavior text. - this.validateBehavior(); - - this.registerCompletionProvider(); - - // Wait for the next event loop tick to focus the port edit UI. - // The user may have clicked more times before the show click was processed - // (showing the UI takes some time due to finding the element in the graph, etc.). - // There might still be some clicks in the event loop queue queue which would de-focus the port edit UI. - // Instead process them (fast as no UI is shown or similar slow tasks are done) and then focus the UI. - setTimeout(() => { - this.editor?.focus(); - }, 0); // 0ms => next event loop tick - } - - private registerCompletionProvider() { - if (!this.tree) { - return; - } - this.completionProvider?.dispose(); - this.completionProvider = monaco.languages.registerCompletionItemProvider( - OutputPortEditUI.DFD_LANGUAGE_NAME, - new MonacoEditorAssignmentLanguageCompletionProvider(this.tree), - ); - } - - /** - * Sets the position of the UI to the position of the port that is currently edited. - */ - private setPosition(containerElement: HTMLElement) { - if (!this.port) { - return; - } - - const bounds = getAbsoluteClientBounds(this.port, this.domHelper, this.viewerOptions); - containerElement.style.left = `${bounds.x}px`; - containerElement.style.top = `${bounds.y}px`; - } - - private validateBehavior(): void { - if (!this.port) { - return; - } - - if (!this.editor) { - return; - } - if (!this.tree) { - return; - } - - const model = this.editor?.getModel(); - if (!model) { - return; - } - - const content = model.getLinesContent(); - const marker: monaco.editor.IMarkerData[] = []; - const emptyContent = content.length == 0 || (content.length == 1 && content[0] === ""); - // empty content gets accepted as valid as it represents no constraints - if (!emptyContent) { - const errors = this.tree.verify(content); - marker.push( - ...errors.map((e) => ({ - severity: monaco.MarkerSeverity.Error, - startLineNumber: e.line, - startColumn: e.startColumn, - endLineNumber: e.line, - endColumn: e.endColumn, - message: e.message, - })), - ); - } - - if (marker.length == 0) { - this.validationLabel.innerText = "Assignments are valid"; - this.validationLabel.classList.remove("validation-error"); - this.validationLabel.classList.add("validation-success"); - } else { - this.validationLabel.innerText = `Assignments are invalid: ${marker.length} error${ - marker.length === 1 ? "" : "s" - }.`; - this.validationLabel.classList.remove("validation-success"); - this.validationLabel.classList.add("validation-error"); - } - - monaco.editor.setModelMarkers(model, "assignment", marker); - } - - /** - * Saves the current behavior text inside the editor to the port. - */ - private save(): void { - if (!this.port) { - throw new Error("Cannot save without set port."); - } - - const behaviorText = this.editor?.getValue() ?? ""; - this.actionDispatcher.dispatch(SetDfdOutputPortBehaviorAction.create(this.port.id, behaviorText)); - this.actionDispatcher.dispatch(CommitModelAction.create()); - } - - public getCurrentEditingPort(): DfdOutputPortImpl | undefined { - return this.port; - } - - switchTheme(useDark: boolean): void { - this.editor?.updateOptions({ theme: useDark ? "vs-dark" : "vs" }); - } -} - -/** - * Sets the behavior property of a dfd output port (DfdOutputPortImpl). - * This is used by the OutputPortEditUI but implemented as an action for undo/redo support. - */ -export interface SetDfdOutputPortBehaviorAction extends Action { - kind: typeof SetDfdOutputPortBehaviorAction.KIND; - portId: string; - behavior: string; -} -export namespace SetDfdOutputPortBehaviorAction { - export const KIND = "setDfdOutputPortBehavior"; - export function create(portId: string, behavior: string): SetDfdOutputPortBehaviorAction { - return { - kind: KIND, - portId, - behavior, - }; - } -} - -@injectable() -export class SetDfdOutputPortBehaviorCommand extends Command { - static readonly KIND = SetDfdOutputPortBehaviorAction.KIND; - - constructor(@inject(TYPES.Action) private action: SetDfdOutputPortBehaviorAction) { - super(); - } - - private oldBehavior: string | undefined; - - execute(context: CommandExecutionContext): CommandReturn { - const port = context.root.index.getById(this.action.portId) as DfdOutputPortImpl; - this.oldBehavior = port.getBehavior(); - port.setBehavior(this.action.behavior); - return context.root; - } - - undo(context: CommandExecutionContext): CommandReturn { - const port = context.root.index.getById(this.action.portId) as DfdOutputPortImpl; - if (this.oldBehavior) { - port.setBehavior(this.oldBehavior); - } - - return context.root; - } - - redo(context: CommandExecutionContext): CommandReturn { - return this.execute(context); - } -} diff --git a/frontend/webEditor/src/features/dfdElements/ports.tsx b/frontend/webEditor/src/features/dfdElements/ports.tsx deleted file mode 100644 index c666c44d..00000000 --- a/frontend/webEditor/src/features/dfdElements/ports.tsx +++ /dev/null @@ -1,178 +0,0 @@ -/** @jsx svg */ -import { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - svg, - ShapeView, - SPortImpl, - RenderingContext, - moveFeature, - deletableFeature, - withEditLabelFeature, - isEditableLabel, - SRoutableElementImpl, -} from "sprotty"; -import { Bounds, SPort } from "sprotty-protocol"; -import { injectable } from "inversify"; -import { VNode, VNodeStyle } from "snabbdom"; -import { ArrowEdgeImpl } from "./edges"; -import { AutoCompleteTree } from "../constraintMenu/AutoCompletion"; -import { TreeBuilder } from "./AssignmentLanguage"; -import { labelTypeRegistry } from "../.."; - -const defaultPortFeatures = [...SPortImpl.DEFAULT_FEATURES, moveFeature, deletableFeature]; -const portSize = 7; - -export type DfdInputPort = SPort; - -@injectable() -export class DfdInputPortImpl extends SPortImpl { - static readonly DEFAULT_FEATURES = defaultPortFeatures; - - override get bounds(): Bounds { - return { - x: this.position.x, - y: this.position.y, - width: portSize, - height: portSize, - }; - } - - /** - * Builds the name of the input port from the names of the incoming dfd edges. - * @returns either the concatenated names of the incoming edges or undefined if there are no named incoming edges. - */ - getName(): string | undefined { - const edgeNames: string[] = []; - - this.incomingEdges.forEach((edge) => { - if (edge instanceof ArrowEdgeImpl) { - const name = edge.editableLabel?.text; - if (name) { - edgeNames.push(name); - } - } else { - return undefined; - } - }); - - if (edgeNames.length === 0) { - return undefined; - } else { - return edgeNames.sort().join("|"); - } - } - - canConnect(_routable: SRoutableElementImpl, role: "source" | "target"): boolean { - // Only allow edges into this port - return role === "target"; - } -} - -export class DfdInputPortView extends ShapeView { - render(node: Readonly, context: RenderingContext): VNode | undefined { - if (!this.isVisible(node, context)) { - return undefined; - } - - const { width, height } = node.bounds; - - return ( - - - - I - - {context.renderChildren(node)} - - ); - } -} - -export interface DfdOutputPort extends SPort { - behavior: string; -} - -@injectable() -export class DfdOutputPortImpl extends SPortImpl { - static readonly DEFAULT_FEATURES = [...defaultPortFeatures, withEditLabelFeature]; - - private behavior: string = ""; - private validBehavior: boolean = true; - - override get bounds(): Bounds { - return { - x: this.position.x, - y: this.position.y, - width: portSize, - height: portSize, - }; - } - - get editableLabel() { - const label = this.children.find((element) => element.type === "label:invisible"); - if (label && isEditableLabel(label)) { - return label; - } - - return undefined; - } - - canConnect(_routable: SRoutableElementImpl, role: "source" | "target"): boolean { - // Only allow edges from this port outwards - return role === "source"; - } - - /** - * Generates the per-node inline style object for the view. - */ - geViewStyleObject(): VNodeStyle { - const style: VNodeStyle = { - opacity: this.opacity.toString(), - }; - if (!labelTypeRegistry) return style; - - if (!this.validBehavior) { - style["--port-border"] = "#ff0000"; - style["--port-color"] = "#ff6961"; - } - - return style; - } - - public setBehavior(value: string) { - this.behavior = value; - if (value === "") { - this.validBehavior = true; - return; - } - const errors = new AutoCompleteTree(TreeBuilder.buildTree(labelTypeRegistry, this)).verify( - this.behavior.split("\n"), - ); - this.validBehavior = errors.length === 0; - } - - public getBehavior() { - return this.behavior; - } -} - -@injectable() -export class DfdOutputPortView extends ShapeView { - render(node: Readonly, context: RenderingContext): VNode | undefined { - if (!this.isVisible(node, context)) { - return undefined; - } - - const { width, height } = node.bounds; - - return ( - - - - O - - {context.renderChildren(node)} - - ); - } -} diff --git a/frontend/webEditor/src/features/editorMode/command.ts b/frontend/webEditor/src/features/editorMode/command.ts deleted file mode 100644 index 4ea79a2b..00000000 --- a/frontend/webEditor/src/features/editorMode/command.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { inject } from "inversify"; -import { Command, TYPES, CommandExecutionContext, CommandReturn } from "sprotty"; -import { Action } from "sprotty-protocol"; -import { DfdNodeAnnotation, DfdNodeImpl } from "../dfdElements/nodes"; -import { EditorMode, EditorModeController } from "./editorModeController"; - -export interface ChangeEditorModeAction extends Action { - kind: typeof ChangeEditorModeAction.KIND; - newMode: EditorMode; -} -export namespace ChangeEditorModeAction { - export const KIND = "changeEditorMode"; - - export function create(newMode: EditorMode): ChangeEditorModeAction { - return { - kind: KIND, - newMode, - }; - } -} - -export class ChangeEditorModeCommand extends Command { - static readonly KIND = ChangeEditorModeAction.KIND; - - private oldMode?: EditorMode; - private oldNodeAnnotations: Map = new Map(); - - @inject(EditorModeController) - private readonly controller?: EditorModeController; - - constructor(@inject(TYPES.Action) private action: ChangeEditorModeAction) { - super(); - } - - execute(context: CommandExecutionContext): CommandReturn { - if (!this.controller) throw new Error("Missing injects"); - - this.oldMode = this.controller.getCurrentMode(); - this.controller.setMode(this.action.newMode); - this.postModeSwitch(context); - - return context.root; - } - - undo(context: CommandExecutionContext): CommandReturn { - if (!this.controller) throw new Error("Missing injects"); - - if (!this.oldMode) { - // This should never happen because execute() is called before undo() is called. - throw new Error("No old mode to restore"); - } - this.controller.setMode(this.oldMode); - this.undoPostModeSwitch(context); - - return context.root; - } - - redo(context: CommandExecutionContext): CommandReturn { - return this.execute(context); - } - - private postModeSwitch(context: CommandExecutionContext): void { - if (this.oldMode === "view" && this.action.newMode === "edit") { - // Remove annotations when enabling editing - - this.oldNodeAnnotations.clear(); - context.root.index.all().forEach((element) => { - if (element instanceof DfdNodeImpl && element.annotations) { - this.oldNodeAnnotations.set(element.id, element.annotations); - element.annotations = []; - } - }); - } - } - - private undoPostModeSwitch(context: CommandExecutionContext): void { - if (this.oldMode === "view" && this.action.newMode === "edit") { - // Restore annotations when disabling editing - this.oldNodeAnnotations.forEach((annotation, id) => { - const element = context.root.index.getById(id); - if (element instanceof DfdNodeImpl) { - element.annotations = annotation; - } - }); - } - } -} diff --git a/frontend/webEditor/src/features/editorMode/di.config.ts b/frontend/webEditor/src/features/editorMode/di.config.ts deleted file mode 100644 index 4920a487..00000000 --- a/frontend/webEditor/src/features/editorMode/di.config.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ContainerModule } from "inversify"; -import { DeleteElementCommand, EditLabelMouseListener, TYPES, configureCommand } from "sprotty"; -import { EditorModeController } from "./editorModeController"; -import { EditorModeSwitchUi } from "./modeSwitchUi"; -import { EDITOR_TYPES } from "../../utils"; -import { EditorModeAwareDeleteElementCommand, EditorModeAwareEditLabelMouseListener } from "./sprottyHooks"; -import { ChangeEditorModeCommand } from "./command"; - -export const editorModeModule = new ContainerModule((bind, unbind, isBound, rebind) => { - const context = { bind, unbind, isBound, rebind }; - - bind(EditorModeController).toSelf().inSingletonScope(); - bind(EditorModeSwitchUi).toSelf().inSingletonScope(); - bind(TYPES.IUIExtension).toService(EditorModeSwitchUi); - bind(EDITOR_TYPES.DefaultUIElement).toService(EditorModeSwitchUi); - - configureCommand(context, ChangeEditorModeCommand); - - // Sprotty hooks that hook into the edit label, move and edit module - // to intercept model modifications to prevent them when the editor is in a read-only mode. - rebind(EditLabelMouseListener).to(EditorModeAwareEditLabelMouseListener); - rebind(DeleteElementCommand).to(EditorModeAwareDeleteElementCommand); -}); diff --git a/frontend/webEditor/src/features/editorMode/editorModeController.ts b/frontend/webEditor/src/features/editorMode/editorModeController.ts deleted file mode 100644 index db4ac77a..00000000 --- a/frontend/webEditor/src/features/editorMode/editorModeController.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { injectable } from "inversify"; - -export type EditorMode = "edit" | "view"; - -/** - * Holds the current editor mode in a central place. - * Used to get the current mode in places where it is used. - * - * Changes to the mode should be done using the ChangeEditorModeCommand - * and not directly on this class when done interactively - * for undo/redo support and actions that are done to the model - * when the mode changes. - */ -@injectable() -export class EditorModeController { - private mode: EditorMode = "edit"; - private modeChangeCallbacks: ((mode: EditorMode) => void)[] = []; - - getCurrentMode(): EditorMode { - return this.mode; - } - - setMode(mode: EditorMode) { - this.mode = mode; - - this.modeChangeCallbacks.forEach((callback) => callback(mode)); - } - - setDefaultMode() { - this.mode = "edit"; - } - - onModeChange(callback: (mode: EditorMode) => void) { - this.modeChangeCallbacks.push(callback); - } - - isReadOnly(): boolean { - return this.mode !== "edit"; - } -} diff --git a/frontend/webEditor/src/features/editorMode/modeSwitchUi.css b/frontend/webEditor/src/features/editorMode/modeSwitchUi.css deleted file mode 100644 index c451b909..00000000 --- a/frontend/webEditor/src/features/editorMode/modeSwitchUi.css +++ /dev/null @@ -1,11 +0,0 @@ -.editor-mode-switcher { - /* Position the switcher in the top left corner */ - top: 40px; - padding: 8px; - left: 40px; - line-height: 1.5; - - /* Make text non-selectable */ - -webkit-user-select: none; /* Safari only supports user select using the -webkit prefix */ - user-select: none; -} diff --git a/frontend/webEditor/src/features/editorMode/modeSwitchUi.ts b/frontend/webEditor/src/features/editorMode/modeSwitchUi.ts deleted file mode 100644 index 26c976c4..00000000 --- a/frontend/webEditor/src/features/editorMode/modeSwitchUi.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { AbstractUIExtension } from "sprotty"; -import { EditorMode, EditorModeController } from "./editorModeController"; -import { inject, injectable } from "inversify"; - -import "./modeSwitchUi.css"; - -/** - * UI that shows the current editor mode (unless it is edit mode) - * with details about the mode. - */ -@injectable() -export class EditorModeSwitchUi extends AbstractUIExtension { - static readonly ID = "editor-mode-switcher"; - - constructor( - @inject(EditorModeController) - private readonly editorModeController: EditorModeController, - ) { - super(); - } - - id(): string { - return EditorModeSwitchUi.ID; - } - containerClass(): string { - return this.id(); - } - - protected initializeContents(containerElement: HTMLElement): void { - containerElement.classList.add("ui-float"); - containerElement.style.visibility = "hidden"; - this.editorModeController.onModeChange((mode) => this.reRender(mode)); - this.reRender(this.editorModeController.getCurrentMode()); - } - - private reRender(mode: EditorMode): void { - this.containerElement.innerHTML = ""; - switch (mode) { - case "edit": - this.containerElement.style.visibility = "hidden"; - break; - case "view": - this.containerElement.style.visibility = "visible"; - this.renderViewMode(); - break; - default: - throw new Error(`Unknown editor mode: ${mode}`); - } - } - - private renderViewMode(): void { - this.containerElement.innerHTML = ` - Currently viewing model in read only mode.
- Enabling editing will remove the annotations.
- `; - } -} diff --git a/frontend/webEditor/src/features/labels/commands.ts b/frontend/webEditor/src/features/labels/commands.ts deleted file mode 100644 index 1ea864af..00000000 --- a/frontend/webEditor/src/features/labels/commands.ts +++ /dev/null @@ -1,355 +0,0 @@ -import { Action } from "sprotty-protocol"; -import { - Command, - CommandExecutionContext, - CommandReturn, - ISnapper, - isSelected, - SChildElementImpl, - SModelElementImpl, - SNodeImpl, - SParentElementImpl, - TYPES, -} from "sprotty"; -import { injectable, inject, optional } from "inversify"; -import { ContainsDfdLabels, containsDfdLabels } from "./elementFeature"; -import { LabelAssignment, LabelTypeRegistry } from "./labelTypeRegistry"; -import { snapPortsOfNode } from "../dfdElements/portSnapper"; -import { EditorModeController } from "../editorMode/editorModeController"; - -interface LabelAction extends Action { - element?: ContainsDfdLabels & SNodeImpl; - labelAssignment: LabelAssignment; -} -abstract class LabelCommand extends Command { - @inject(EditorModeController) - @optional() - protected readonly editorModeController?: EditorModeController; - - protected elements?: SModelElementImpl[]; - - constructor( - @inject(TYPES.Action) protected action: LabelAction, - @inject(TYPES.ISnapper) protected snapper: ISnapper, - ) { - super(); - } - - protected fetchElements(context: CommandExecutionContext): SModelElementImpl[] { - if (this.editorModeController?.isReadOnly()) { - return []; - } - - const allElements = getAllElements(context.root.children); - const selectedElements = allElements.filter((element) => isSelected(element)); - - const selectionHasElement = - selectedElements.find((element) => element.id === this.action.element?.id) !== undefined; - if (selectionHasElement) { - return selectedElements; - } - return this.action.element ? [this.action.element] : selectedElements; - } - - protected addLabel(context: CommandExecutionContext) { - if (this.editorModeController?.isReadOnly()) { - return context.root; - } - - if (this.elements === undefined) { - this.elements = this.fetchElements(context); - } - - this.elements.forEach((element) => { - if (containsDfdLabels(element)) { - const hasBeenAdded = - element.labels.find((as) => { - return ( - as.labelTypeId === this.action.labelAssignment.labelTypeId && - as.labelTypeValueId === this.action.labelAssignment.labelTypeValueId - ); - }) !== undefined; - if (!hasBeenAdded) { - element.labels.push(this.action.labelAssignment); - if (element instanceof SNodeImpl) { - snapPortsOfNode(element, this.snapper); - } - } - } - }); - - return context.root; - } - - protected removeLabel(context: CommandExecutionContext) { - if (this.editorModeController?.isReadOnly()) { - return context.root; - } - - if (this.elements === undefined) { - this.elements = this.fetchElements(context); - } - - this.elements.forEach((element) => { - if (containsDfdLabels(element)) { - const labels = element.labels; - const idx = labels.findIndex( - (l) => - l.labelTypeId == this.action.labelAssignment.labelTypeId && - l.labelTypeValueId == this.action.labelAssignment.labelTypeValueId, - ); - if (idx >= 0) { - labels.splice(idx, 1); - if (element instanceof SNodeImpl) { - snapPortsOfNode(element, this.snapper); - } - } - } - }); - - return context.root; - } -} - -export interface AddLabelAssignmentAction extends LabelAction { - kind: typeof AddLabelAssignmentAction.TYPE; -} -export namespace AddLabelAssignmentAction { - export const TYPE = "add-label-assignment"; - export function create( - labelAssignment: LabelAssignment, - element?: ContainsDfdLabels & SNodeImpl, - ): AddLabelAssignmentAction { - return { - kind: TYPE, - element, - labelAssignment, - }; - } -} - -@injectable() -export class AddLabelAssignmentCommand extends LabelCommand { - public static readonly KIND = AddLabelAssignmentAction.TYPE; - - constructor(@inject(TYPES.Action) action: AddLabelAssignmentAction, @inject(TYPES.ISnapper) snapper: ISnapper) { - super(action, snapper); - } - - execute(context: CommandExecutionContext): CommandReturn { - return this.addLabel(context); - } - - undo(context: CommandExecutionContext): CommandReturn { - return this.removeLabel(context); - } - - redo(context: CommandExecutionContext): CommandReturn { - return this.execute(context); - } -} - -export interface DeleteLabelAssignmentAction extends LabelAction { - kind: typeof DeleteLabelAssignmentAction.TYPE; -} -export namespace DeleteLabelAssignmentAction { - export const TYPE = "delete-label-assignment"; - export function create( - labelAssignment: LabelAssignment, - element?: ContainsDfdLabels & SNodeImpl, - ): DeleteLabelAssignmentAction { - return { - kind: TYPE, - element, - labelAssignment, - }; - } -} - -@injectable() -export class DeleteLabelAssignmentCommand extends LabelCommand { - public static readonly KIND = DeleteLabelAssignmentAction.TYPE; - - constructor(@inject(TYPES.Action) action: DeleteLabelAssignmentAction, @inject(TYPES.ISnapper) snapper: ISnapper) { - super(action, snapper); - } - - execute(context: CommandExecutionContext): CommandReturn { - return this.removeLabel(context); - } - - undo(context: CommandExecutionContext): CommandReturn { - return this.addLabel(context); - } - - redo(context: CommandExecutionContext): CommandReturn { - return this.execute(context); - } -} - -/** - * Recursively traverses the sprotty diagram graph and removes all labels that match the given predicate. - * @param predicate a function deciding whether the label assignment should be kept - */ -function removeLabelsFromGraph( - element: SModelElementImpl | SParentElementImpl, - snapper: ISnapper, - predicate: (type: LabelAssignment) => boolean, -): void { - if (containsDfdLabels(element)) { - const filteredLabels = element.labels.filter(predicate); - if (filteredLabels.length !== element.labels.length) { - element.labels = filteredLabels; - if (containsDfdLabels(element) && element instanceof SNodeImpl) { - snapPortsOfNode(element, snapper); - } - } - } - - if ("children" in element) { - element.children.forEach((child) => removeLabelsFromGraph(child, snapper, predicate)); - } -} - -export interface DeleteLabelTypeValueAction extends Action { - kind: typeof DeleteLabelTypeValueAction.TYPE; - registry: LabelTypeRegistry; - labelTypeId: string; - labelTypeValueId: string; -} -export namespace DeleteLabelTypeValueAction { - export const TYPE = "delete-label-type-value"; - export function create( - registry: LabelTypeRegistry, - labelTypeId: string, - labelTypeValueId: string, - ): DeleteLabelTypeValueAction { - return { - kind: TYPE, - registry, - labelTypeId, - labelTypeValueId, - }; - } -} - -@injectable() -export class DeleteLabelTypeValueCommand extends Command { - public static readonly KIND = DeleteLabelTypeValueAction.TYPE; - - @inject(EditorModeController) - @optional() - private readonly editorModeController?: EditorModeController; - - constructor( - @inject(TYPES.Action) private action: DeleteLabelTypeValueAction, - @inject(TYPES.ISnapper) private snapper: ISnapper, - ) { - super(); - } - - execute(context: CommandExecutionContext): CommandReturn { - if (this.editorModeController?.isReadOnly()) { - return context.root; - } - - const labelType = this.action.registry.getLabelType(this.action.labelTypeId); - if (!labelType) { - return context.root; - } - - const labelTypeValue = labelType.values.find((value) => value.id === this.action.labelTypeValueId); - if (!labelTypeValue) { - return context.root; - } - - removeLabelsFromGraph(context.root, this.snapper, (label) => { - return ( - label.labelTypeId !== this.action.labelTypeId || label.labelTypeValueId !== this.action.labelTypeValueId - ); - }); - - const index = labelType.values.indexOf(labelTypeValue); - if (index > -1) { - labelType.values.splice(index, 1); - this.action.registry.labelTypeChanged(); - } - - return context.root; - } - - undo(context: CommandExecutionContext): CommandReturn { - return context.root; - } - - redo(context: CommandExecutionContext): CommandReturn { - return this.execute(context); - } -} - -export interface DeleteLabelTypeAction extends Action { - kind: typeof DeleteLabelTypeAction.TYPE; - registry: LabelTypeRegistry; - labelTypeId: string; -} -export namespace DeleteLabelTypeAction { - export const TYPE = "delete-label-type"; - export function create(registry: LabelTypeRegistry, labelTypeId: string): DeleteLabelTypeAction { - return { - kind: TYPE, - registry, - labelTypeId, - }; - } -} - -@injectable() -export class DeleteLabelTypeCommand extends Command { - public static readonly KIND = DeleteLabelTypeAction.TYPE; - - @inject(EditorModeController) - @optional() - private readonly editorModeController?: EditorModeController; - - constructor( - @inject(TYPES.Action) private action: DeleteLabelTypeAction, - @inject(TYPES.ISnapper) private snapper: ISnapper, - ) { - super(); - } - - execute(context: CommandExecutionContext): CommandReturn { - if (this.editorModeController?.isReadOnly()) { - return context.root; - } - - const labelType = this.action.registry.getLabelType(this.action.labelTypeId); - if (!labelType) { - return context.root; - } - - removeLabelsFromGraph(context.root, this.snapper, (label) => label.labelTypeId !== this.action.labelTypeId); - this.action.registry.unregisterLabelType(labelType); - - return context.root; - } - - undo(context: CommandExecutionContext): CommandReturn { - return context.root; - } - - redo(context: CommandExecutionContext): CommandReturn { - return this.execute(context); - } -} - -function getAllElements(elements: readonly SChildElementImpl[]): SModelElementImpl[] { - const elementsList: SModelElementImpl[] = []; - for (const element of elements) { - elementsList.push(element); - if ("children" in element) { - elementsList.push(...getAllElements(element.children)); - } - } - return elementsList; -} diff --git a/frontend/webEditor/src/features/labels/di.config.ts b/frontend/webEditor/src/features/labels/di.config.ts deleted file mode 100644 index a0f085e3..00000000 --- a/frontend/webEditor/src/features/labels/di.config.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ContainerModule } from "inversify"; -import { LabelTypeRegistry, globalLabelTypeRegistry } from "./labelTypeRegistry"; -import { DfdNodeLabelRenderer } from "./labelRenderer"; -import { EDITOR_TYPES } from "../../utils"; -import { DfdLabelMouseDropListener } from "./dropListener"; -import { LabelTypeEditorUI } from "./labelTypeEditor"; -import { TYPES, configureCommand } from "sprotty"; -import { - AddLabelAssignmentCommand, - DeleteLabelAssignmentCommand, - DeleteLabelTypeCommand, - DeleteLabelTypeValueCommand, -} from "./commands"; - -// This module contains the components required for the dfd node labels. -// This includes a registry for the label types, a UI to manage them, -// a renderer to display them inside nodes and commands to add/delete them to nodes. - -export const dfdLabelModule = new ContainerModule((bind, unbind, isBound, rebind) => { - bind(LabelTypeRegistry).toConstantValue(globalLabelTypeRegistry); - bind(DfdNodeLabelRenderer).toSelf().inSingletonScope(); - bind(DfdLabelMouseDropListener).toSelf().inSingletonScope(); - bind(TYPES.MouseListener).toService(DfdLabelMouseDropListener); - - bind(LabelTypeEditorUI).toSelf().inSingletonScope(); - bind(TYPES.KeyListener).toService(LabelTypeEditorUI); - bind(TYPES.IUIExtension).toService(LabelTypeEditorUI); - bind(EDITOR_TYPES.DefaultUIElement).to(LabelTypeEditorUI); - - const context = { bind, unbind, isBound, rebind }; - configureCommand(context, AddLabelAssignmentCommand); - configureCommand(context, DeleteLabelAssignmentCommand); - configureCommand(context, DeleteLabelTypeValueCommand); - configureCommand(context, DeleteLabelTypeCommand); -}); diff --git a/frontend/webEditor/src/features/labels/elementFeature.ts b/frontend/webEditor/src/features/labels/elementFeature.ts deleted file mode 100644 index df18c4f0..00000000 --- a/frontend/webEditor/src/features/labels/elementFeature.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { SChildElementImpl, SModelElementImpl, SParentElementImpl, SShapeElementImpl } from "sprotty"; -import { LabelAssignment } from "./labelTypeRegistry"; - -export const containsDfdLabelFeature = Symbol("dfd-label-feature"); - -export interface ContainsDfdLabels extends SModelElementImpl { - labels: LabelAssignment[]; -} - -export function containsDfdLabels(element: T): element is T & ContainsDfdLabels { - return element.features?.has(containsDfdLabelFeature) ?? false; -} - -// Traverses the graph upwards to find any element having the dfd label feature. -// This is needed because you may select/drop onto a child element of the node implementing and displaying dfd labels. -// If the element itself and no parent has the feature undefined is returned. -export function getParentWithDfdLabels( - element: SChildElementImpl | SParentElementImpl | SShapeElementImpl, -): (SModelElementImpl & ContainsDfdLabels) | undefined { - if (containsDfdLabels(element)) { - return element; - } - - if ("parent" in element) { - return getParentWithDfdLabels(element.parent); - } - - return undefined; -} diff --git a/frontend/webEditor/src/features/labels/labelTypeEditor.css b/frontend/webEditor/src/features/labels/labelTypeEditor.css deleted file mode 100644 index 19839b30..00000000 --- a/frontend/webEditor/src/features/labels/labelTypeEditor.css +++ /dev/null @@ -1,69 +0,0 @@ -/* General UI */ - -div.label-type-editor-ui { - padding: 10px; - top: 150px; - right: 40px; - /* limit height so that max width has still 40px to bottom edge of the parent element - 100% is the full sprotty viewer height, 150px the space above the element, - 40px is the space that should be left under the editor and 2*10px is the padding */ - max-height: calc(100vh - 150px - 40px - 2 * 10px); - overflow: auto; /* Show a scroll bar if content in the editor does not fully fit */ -} - -.label-type-editor-ui * { - color: var(--color-foreground); -} - -.label-type-editor-ui .codicon { - vertical-align: middle; -} - -.label-type-editor-ui hr { - height: 1px; - border: 0; - background-color: var(--color-foreground); -} - -.label-type-editor-ui button { - background-color: transparent; - border: none; - cursor: pointer; - padding: 0; -} - -/* Label Types */ - -.label-type { - padding-bottom: 5px; -} - -.label-type-value, -.label-type-value-add { - margin-left: 10px; -} - -.label-type-name { - font-size: 12pt; -} - -/* This is the input field for the label type name */ -.label-type-editor-ui input { - background-color: transparent; - outline: none; - border: none; -} - -/* Label Type value */ -.label-type-value input { - background-color: var(--color-background); - text-align: center; - border: 1px solid var(--color-foreground); - border-radius: 15px; - padding: 3px; - margin: 4px; -} - -#accordion-state-label-title { - padding-right: 5px; -} diff --git a/frontend/webEditor/src/features/labels/labelTypeEditor.ts b/frontend/webEditor/src/features/labels/labelTypeEditor.ts deleted file mode 100644 index b5232e2f..00000000 --- a/frontend/webEditor/src/features/labels/labelTypeEditor.ts +++ /dev/null @@ -1,383 +0,0 @@ -import { injectable, inject, optional } from "inversify"; -import { calculateTextSize, generateRandomSprottyId } from "../../utils"; -import { - AbstractUIExtension, - CommandStack, - CommitModelAction, - IActionDispatcher, - ISnapper, - KeyListener, - SModelElementImpl, - TYPES, -} from "sprotty"; -import { LabelAssignment, LabelType, LabelTypeRegistry, LabelTypeValue } from "./labelTypeRegistry"; -import { AddLabelAssignmentAction, DeleteLabelTypeAction, DeleteLabelTypeValueAction } from "./commands"; -import { LABEL_ASSIGNMENT_MIME_TYPE } from "./dropListener"; -import { Action } from "sprotty-protocol"; -import { snapPortsOfNode } from "../dfdElements/portSnapper"; -import { DfdNodeImpl } from "../dfdElements/nodes"; -import { matchesKeystroke } from "sprotty/lib/utils/keyboard"; -import { EditorModeController } from "../editorMode/editorModeController"; - -import "../../common/commonStyling.css"; -import "./labelTypeEditor.css"; - -@injectable() -export class LabelTypeEditorUI extends AbstractUIExtension implements KeyListener { - private accordionStateElement: HTMLInputElement = document.createElement("input"); - - constructor( - @inject(LabelTypeRegistry) private readonly labelTypeRegistry: LabelTypeRegistry, - @inject(TYPES.IActionDispatcher) private readonly actionDispatcher: IActionDispatcher, - @inject(TYPES.ICommandStack) private readonly commandStack: CommandStack, - @inject(TYPES.ISnapper) private readonly snapper: ISnapper, - @inject(EditorModeController) - @optional() - private readonly editorModeController: EditorModeController, - ) { - super(); - labelTypeRegistry.onUpdate(() => this.reRender()); - - this.accordionStateElement.type = "checkbox"; - this.accordionStateElement.id = "accordion-state-label-types"; - this.accordionStateElement.classList.add("accordion-state"); - this.accordionStateElement.hidden = true; - } - - static readonly ID = "label-type-editor-ui"; - - id(): string { - return LabelTypeEditorUI.ID; - } - - containerClass(): string { - return LabelTypeEditorUI.ID; - } - - private reRender(): void { - if (!this.containerElement) { - // The ui extension has not been initialized yet. - return; - } - - // Remove all children - this.containerElement.innerHTML = ""; - // Re-render - this.initializeContents(this.containerElement); - - // Re-render sprotty model viewport by dispatching some command. - // sprotty automatically triggers a re-render after any command is executed as it may change the model. - - // CommitModelAction is a great idea because that way we don't have to call it - // each time we do some operation on the model inside the UI, like when removing a label type, - // we also need to commit the removal from the model. - // We can just do it here and not worry about it in the buttons/change handlers inside the ui. - // All changes are propagated through the label type registry. - this.actionDispatcher.dispatch(CommitModelAction.create()); - } - - protected initializeContents(containerElement: HTMLElement): void { - containerElement.classList.add("ui-float"); - containerElement.innerHTML = ` - -
-
-
- `; - // Add input used by the label and the accordion-content div - // This element is not re-created on new renders and reused to save the expansion state of the accordion - // This is important because the ui is re-rendered on every change to the label type registry - containerElement.prepend(this.accordionStateElement); - - const innerContainerElement = containerElement.querySelector(".label-type-edit-ui-inner"); - if (!innerContainerElement) { - throw new Error("Could not find inner container element"); - } - - this.labelTypeRegistry.getLabelTypes().forEach((labelType, idx) => { - innerContainerElement.appendChild(this.renderLabelType(labelType)); - - if (idx < this.labelTypeRegistry.getLabelTypes().length - 1) { - // Add a horizontal line between label types - const horizontalLine = document.createElement("hr"); - innerContainerElement.appendChild(horizontalLine); - } - }); - - // Render add button for whole label type - const addButton = document.createElement("button"); - addButton.innerHTML = ' Label Type'; - addButton.onclick = () => { - if (this.editorModeController?.isReadOnly()) { - return; - } - - const labelType: LabelType = { - id: generateRandomSprottyId(), - name: "", - values: [ - { - id: generateRandomSprottyId(), - text: "Value", - }, - ], - }; - this.labelTypeRegistry.registerLabelType(labelType); - - // Select the text input element of the new label type to allow entering the name - const inputElement: HTMLElement | null = innerContainerElement.querySelector( - `.label-type-${labelType.id} input`, - ); - inputElement?.focus(); - }; - innerContainerElement.appendChild(addButton); - } - - private renderLabelType(labelType: LabelType): HTMLElement { - const labelTypeElement = document.createElement("div"); - labelTypeElement.classList.add("label-type"); - labelTypeElement.classList.add(`label-type-${labelType.id}`); - - const labelTypeNameInput = document.createElement("input"); - labelTypeNameInput.value = labelType.name; - labelTypeNameInput.placeholder = "Label Type Name"; - labelTypeNameInput.classList.add("label-type-name"); - - this.dynamicallySetInputSize(labelTypeNameInput); - - // Disallow spaces in label type names and changes when readonly - labelTypeNameInput.onbeforeinput = (event) => { - if (event.data?.includes(" ")) { - event.preventDefault(); - } - - if (this.editorModeController && this.editorModeController.isReadOnly()) { - event.preventDefault(); - } - }; - - labelTypeNameInput.onchange = () => { - const newLabelTypeName = labelTypeNameInput.value; - // Check for duplicate and don't change the name if it is a duplicate - if (this.labelTypeRegistry.getLabelTypes().some((type) => type.name === newLabelTypeName)) { - // Undo change in UI - labelTypeNameInput.value = labelType.name; - return; - } - - labelType.name = labelTypeNameInput.value; - this.labelTypeRegistry.labelTypeChanged(); - this.reSnapPorts(labelType.id); - }; - // Only allow alphanumerical characters - labelTypeNameInput.oninput = this.onInputOnlyAlphanumeric; - - labelTypeElement.appendChild(labelTypeNameInput); - - const deleteButton = document.createElement("button"); - deleteButton.innerHTML = ''; - deleteButton.onclick = () => { - this.actionDispatcher.dispatch(DeleteLabelTypeAction.create(this.labelTypeRegistry, labelType.id)); - }; - labelTypeElement.appendChild(deleteButton); - - labelType.values.forEach((possibleValue) => { - labelTypeElement.appendChild(this.renderLabelTypeValue(labelType, possibleValue)); - }); - - // Add + button - const addButton = document.createElement("button"); - addButton.classList.add("label-type-value-add"); - addButton.innerHTML = ' Value'; - addButton.onclick = () => { - if (this.editorModeController?.isReadOnly()) { - return; - } - - const labelValue: LabelTypeValue = { - id: generateRandomSprottyId(), - text: "", - }; - labelType.values.push(labelValue); - - // Insert label type last but before the button - const newValueElement = this.renderLabelTypeValue(labelType, labelValue); - labelTypeElement.insertBefore(newValueElement, labelTypeElement.lastChild); - - // Select the text input element of the new value to allow entering the value - newValueElement.querySelector("input")?.focus(); - }; - labelTypeElement.appendChild(addButton); - - return labelTypeElement; - } - - private renderLabelTypeValue(labelType: LabelType, labelTypeValue: LabelTypeValue): HTMLElement { - const valueElement = document.createElement("div"); - valueElement.classList.add("label-type-value"); - - const valueInput = document.createElement("input"); - valueInput.value = labelTypeValue.text; - valueInput.placeholder = "Value"; - this.dynamicallySetInputSize(valueInput); - - // Disallow spaces in label type values and changes when readonly - valueInput.onbeforeinput = (event) => { - if (event.data?.includes(" ")) { - event.preventDefault(); - } - - if (this.editorModeController && this.editorModeController.isReadOnly()) { - event.preventDefault(); - } - }; - - valueInput.onchange = () => { - const newValue = valueInput.value; - // Check for duplicate and don't change the value if it is a duplicate - if (labelType.values.some((value) => value.text === newValue)) { - // Undo change in UI - valueInput.value = labelTypeValue.text; - return; - } - - labelTypeValue.text = valueInput.value; - this.labelTypeRegistry.labelTypeChanged(); - this.reSnapPorts(labelType.id); - }; - // Only allow alphanumerical characters - valueInput.oninput = this.onInputOnlyAlphanumeric; - - // Allow dragging to create a label assignment - valueInput.draggable = true; - valueInput.ondragstart = (event) => { - const assignment: LabelAssignment = { - labelTypeId: labelType.id, - labelTypeValueId: labelTypeValue.id, - }; - const assignmentJson = JSON.stringify(assignment); - event.dataTransfer?.setData(LABEL_ASSIGNMENT_MIME_TYPE, assignmentJson); - }; - - valueInput.onclick = () => { - if (valueInput.getAttribute("clicked") === "true") { - return; - } - - valueInput.setAttribute("clicked", "true"); - setTimeout(() => { - if (valueInput.getAttribute("clicked") === "true") { - this.actionDispatcher.dispatch( - AddLabelAssignmentAction.create({ - labelTypeId: labelType.id, - labelTypeValueId: labelTypeValue.id, - }), - ); - valueInput.removeAttribute("clicked"); - } - }, 500); - }; - valueInput.ondblclick = () => { - valueInput.removeAttribute("clicked"); - valueInput.focus(); - }; - valueInput.onfocus = (event) => { - // we check for the single click here, since this gets triggered before the ondblclick event - if (valueInput.getAttribute("clicked") !== "true") { - event.preventDefault(); - // the blur needs to occur with a delay, as otherwise chromium browsers prevent the drag - setTimeout(() => { - valueInput.blur(); - }, 0); - } - }; - - valueElement.appendChild(valueInput); - - const deleteButton = document.createElement("button"); - deleteButton.innerHTML = ''; - deleteButton.onclick = () => { - this.actionDispatcher.dispatch( - DeleteLabelTypeValueAction.create(this.labelTypeRegistry, labelType.id, labelTypeValue.id), - ); - }; - valueElement.appendChild(deleteButton); - return valueElement; - } - - /** - * Sets and dynamically updates the size property of the passed input element. - * When the text is zero the width is set to the placeholder length to make place for it. - * When the text is changed the size gets updated with the keyup event. - * @param inputElement the html dom input element to set the size property for - */ - private dynamicallySetInputSize(inputElement: HTMLInputElement): void { - const handleResize = () => { - const displayText = inputElement.value || inputElement.placeholder; - const { width } = calculateTextSize(displayText, window.getComputedStyle(inputElement).font); - - // Values have higher padding for the rounded border - const widthPadding = inputElement.classList.contains("label-type-name") ? 2 : 8; - const finalWidth = width + widthPadding; - - inputElement.style.width = finalWidth + "px"; - }; - - inputElement.onkeyup = handleResize; - - // The inputElement is not added to the DOM yet, so we cannot set the size now. - // Wait for next JS tick, after which the element has been added to the DOM and we can set the initial size - setTimeout(handleResize, 0); - } - - keyDown(_element: SModelElementImpl, event: KeyboardEvent): Action[] { - // Toggle the accordion on press of T - if (matchesKeystroke(event, "KeyT")) { - this.accordionStateElement.checked = !this.accordionStateElement.checked; - } - - return []; - } - - keyUp(): Action[] { - return []; - } - - /** - * Re-snaps the ports of all nodes that have a label of the changed label type. - * When a label type or label type value is changed the size of nodes that - * use that label type could be changed to fit the longer/shorter label text - * more appropriately. Because of the node size change the ports may be out of - * place and need to be re-snapped. - * - * This is called only for changes made by the UI because label type deletions - * handle the resnapping in the corresponding commands and for addition - * it is not necessary because the label type or value cannot be used already. - */ - private async reSnapPorts(changedLabelTypeId: string): Promise { - const root = await this.commandStack.executeAll([]); - root.children.forEach((node) => { - if (!(node instanceof DfdNodeImpl)) { - return; - } - - // Only do the snapping if the node contains a label of the changed label type - if (node.labels.some((labelAs) => labelAs.labelTypeId === changedLabelTypeId)) { - snapPortsOfNode(node, this.snapper); - } - }); - - // Commit the model to trigger a re-render and save the changes into the model source - this.actionDispatcher.dispatch(CommitModelAction.create()); - } - - /** - * onInput handler for inputs that restricts the input to only alphanumeric characters and underscores - */ - private onInputOnlyAlphanumeric(event: Event): void { - const input = event.target as HTMLInputElement; - input.value = input.value.replace(/[^a-zA-Z0-9_]/g, ""); - } -} diff --git a/frontend/webEditor/src/features/labels/labelTypeRegistry.ts b/frontend/webEditor/src/features/labels/labelTypeRegistry.ts deleted file mode 100644 index 7f71d3f9..00000000 --- a/frontend/webEditor/src/features/labels/labelTypeRegistry.ts +++ /dev/null @@ -1,59 +0,0 @@ -export interface LabelType { - id: string; - name: string; - values: LabelTypeValue[]; -} - -export interface LabelTypeValue { - id: string; - text: string; -} - -export interface LabelAssignment { - labelTypeId: string; - labelTypeValueId: string; -} - -export class LabelTypeRegistry { - private labelTypes: LabelType[] = []; - private updateCallbacks: (() => void)[] = []; - - public registerLabelType(labelType: LabelType): void { - this.labelTypes.push(labelType); - this.updateCallbacks.forEach((cb) => cb()); - } - - public unregisterLabelType(labelType: LabelType): void { - this.labelTypes = this.labelTypes.filter((type) => type.id !== labelType.id); - this.updateCallbacks.forEach((cb) => cb()); - } - - public clearLabelTypes(): void { - this.labelTypes = []; - this.updateCallbacks.forEach((cb) => cb()); - } - - public labelTypeChanged(): void { - this.updateCallbacks.forEach((cb) => cb()); - } - - public onUpdate(callback: () => void): void { - this.updateCallbacks.push(callback); - } - - public getLabelTypes(): LabelType[] { - return this.labelTypes; - } - - public getLabelType(id: string): LabelType | undefined { - return this.labelTypes.find((type) => type.id === id); - } -} - -// Usually we would add the registry to a inversify container module and inject it using dependency injection where needed. -// Sadly some places where the registry is used are not inside a inversify container. -// An example for this are the node implementation classes that need the registry to compute the bounds of the node. -// These classes are not managed by inversify and therefore we cannot inject the registry there. -// To solve this we export this registry instance as a global variable for these situations. -// For all other situations where inversify can be used this exact same instance is available to be injected as well. -export const globalLabelTypeRegistry = new LabelTypeRegistry(); diff --git a/frontend/webEditor/src/features/serialize/analyze.ts b/frontend/webEditor/src/features/serialize/analyze.ts deleted file mode 100644 index 30c562cc..00000000 --- a/frontend/webEditor/src/features/serialize/analyze.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { inject, injectable, optional } from "inversify"; -import { Command, CommandExecutionContext, LocalModelSource, SModelRootImpl, TYPES } from "sprotty"; -import { Action } from "sprotty-protocol"; -import { LabelTypeRegistry } from "../labels/labelTypeRegistry"; -import { DynamicChildrenProcessor } from "../dfdElements/dynamicChildren"; -import { EditorModeController } from "../editorMode/editorModeController"; -import { sendMessage } from "./webSocketHandler"; -import { ConstraintRegistry } from "../constraintMenu/constraintRegistry"; -import { CURRENT_VERSION, SavedDiagram } from "./save"; -import { LoadingIndicator } from "../../common/loadingIndicator"; - -export interface AnalyzeDiagramAction extends Action { - kind: typeof AnalyzeDiagramAction.KIND; - suggestedFileName: string; -} -export namespace AnalyzeDiagramAction { - export const KIND = "analyze-diagram"; - - export function create(suggestedFileName?: string): AnalyzeDiagramAction { - return { - kind: KIND, - suggestedFileName: suggestedFileName ?? "diagram.json", - }; - } -} - -@injectable() -export class AnalyzeDiagramCommand extends Command { - static readonly KIND = AnalyzeDiagramAction.KIND; - @inject(TYPES.ModelSource) - private modelSource: LocalModelSource = new LocalModelSource(); - @inject(DynamicChildrenProcessor) - private dynamicChildrenProcessor: DynamicChildrenProcessor = new DynamicChildrenProcessor(); - @inject(LabelTypeRegistry) - @optional() - private labelTypeRegistry?: LabelTypeRegistry; - @inject(EditorModeController) - @optional() - private editorModeController?: EditorModeController; - @inject(ConstraintRegistry) - @optional() - private readonly constraintRegistry?: ConstraintRegistry; - @inject(LoadingIndicator) - @optional() - private loadingIndicator?: LoadingIndicator; - - constructor() { - super(); - } - - execute(context: CommandExecutionContext): SModelRootImpl { - this.loadingIndicator?.showIndicator("Analyzing diagram..."); - // Convert the model to JSON - // Do a copy because we're going to modify it - const modelCopy = JSON.parse(JSON.stringify(this.modelSource.model)); - // Remove element children that are implementation detail - this.dynamicChildrenProcessor.processGraphChildren(modelCopy, "remove"); - - // Export the diagram as a JSON data URL. - const diagram: SavedDiagram = { - model: modelCopy, - labelTypes: this.labelTypeRegistry?.getLabelTypes(), - constraints: this.constraintRegistry?.getConstraintList(), - mode: this.editorModeController?.getCurrentMode(), - version: CURRENT_VERSION, - }; - const diagramJson = JSON.stringify(diagram, undefined, 4); - sendMessage("Json:" + diagramJson); - return context.root; - } - - // Saving cannot be meaningfully undone/redone - - undo(context: CommandExecutionContext): SModelRootImpl { - return context.root; - } - - redo(context: CommandExecutionContext): SModelRootImpl { - return context.root; - } -} diff --git a/frontend/webEditor/src/features/serialize/di.config.ts b/frontend/webEditor/src/features/serialize/di.config.ts deleted file mode 100644 index 9ac28f36..00000000 --- a/frontend/webEditor/src/features/serialize/di.config.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { TYPES, configureCommand } from "sprotty"; -import { LoadDiagramCommand } from "./load"; -import { SaveDiagramCommand } from "./save"; -import { LoadDefaultDiagramCommand } from "./loadDefaultDiagram"; -import { ContainerModule } from "inversify"; -import { SerializeKeyListener } from "./keyListener"; -import { SerializeDropHandler } from "./dropListener"; -import { AnalyzeDiagramCommand } from "./analyze"; -import { LoadDFDandDDCommand } from "./loadDFDandDD"; -import { SaveDFDandDDCommand } from "./saveDFDandDD"; -import { LoadPalladioCommand } from "./loadPalladio"; -import { SaveImageCommand } from "./image"; - -export const serializeModule = new ContainerModule((bind, unbind, isBound, rebind) => { - const context = { bind, unbind, isBound, rebind }; - configureCommand(context, LoadDiagramCommand); - configureCommand(context, LoadDefaultDiagramCommand); - configureCommand(context, SaveDiagramCommand); - configureCommand(context, AnalyzeDiagramCommand); - configureCommand(context, LoadDFDandDDCommand); - configureCommand(context, SaveDFDandDDCommand); - configureCommand(context, LoadPalladioCommand); - configureCommand(context, SaveImageCommand); - - bind(TYPES.KeyListener).to(SerializeKeyListener).inSingletonScope(); - bind(TYPES.MouseListener).to(SerializeDropHandler).inSingletonScope(); -}); diff --git a/frontend/webEditor/src/features/serialize/dropListener.ts b/frontend/webEditor/src/features/serialize/dropListener.ts deleted file mode 100644 index 8db156e3..00000000 --- a/frontend/webEditor/src/features/serialize/dropListener.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { inject, injectable } from "inversify"; -import { ILogger, MouseListener, SModelElementImpl, TYPES } from "sprotty"; -import { LoadDiagramAction } from "./load"; -import { Action } from "sprotty-protocol"; - -@injectable() -export class SerializeDropHandler extends MouseListener { - constructor(@inject(TYPES.ILogger) private readonly logger: ILogger) { - super(); - } - - drop(_target: SModelElementImpl, ev: DragEvent): (Action | Promise)[] { - this.logger.log(this, "Drop event detected", ev); - - // Prevent default behavior which would open the file in the browser - ev.preventDefault(); - - const file = ev.dataTransfer?.files[0]; - if (!file) { - return []; - } - - if (file.type !== "application/json") { - alert("Diagram file must be in JSON format"); - return []; - } - - return [LoadDiagramAction.create(file)]; - } -} diff --git a/frontend/webEditor/src/features/serialize/image.ts b/frontend/webEditor/src/features/serialize/image.ts deleted file mode 100644 index 2f6a9256..00000000 --- a/frontend/webEditor/src/features/serialize/image.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { Command, CommandExecutionContext, CommandReturn } from "sprotty"; -// typescript does not recognize css files as modules -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-expect-error -import themeCss from "../../theme.css?raw"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-expect-error -import elementCss from "../dfdElements/elementStyles.css?raw"; -import { Action } from "sprotty-protocol"; -import { getModelFileName } from "../.."; - -export interface SaveImageAction extends Action { - kind: typeof SaveImageAction.KIND; -} -export namespace SaveImageAction { - export const KIND = "save-image"; - - export function create(): SaveImageAction { - return { - kind: KIND, - }; - } -} - -export class SaveImageCommand extends Command { - static readonly KIND = SaveImageAction.KIND; - execute(context: CommandExecutionContext): CommandReturn { - const root = document.getElementById("sprotty_root"); - if (!root) return context.root; - const firstChild = root.children[0]; - if (!firstChild) return context.root; - const innerSvg = firstChild.innerHTML; - /* The result svg will render (0,0) as the top left corner of the svg. - * We calculate the minimum translation of all children. - * We then offset the whole svg by this opposite of this amount. - */ - const minTranslate = { x: Infinity, y: Infinity }; - for (const child of firstChild.children) { - const childTranslate = this.getMinTranslate(child as HTMLElement); - minTranslate.x = Math.min(minTranslate.x, childTranslate.x); - minTranslate.y = Math.min(minTranslate.y, childTranslate.y); - } - const svg = `${innerSvg}`; - - const blob = new Blob([svg], { type: "image/svg+xml" }); - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = getModelFileName() + ".svg"; - link.click(); - - return context.root; - } - undo(context: CommandExecutionContext): CommandReturn { - return context.root; - } - redo(context: CommandExecutionContext): CommandReturn { - return context.root; - } - - /** - * Gets the minimum translation of an element relative to the svg. - * This is done by recursively getting the translation of all child elements - * @param e the element to get the translation from - * @param parentOffset Offset of the containing element - * @returns Minimum absolute offset of any child element relative to the svg - */ - private getMinTranslate( - e: HTMLElement, - parentOffset: { x: number; y: number } = { x: 0, y: 0 }, - ): { x: number; y: number } { - const myTranslate = this.getTranslate(e, parentOffset); - const minTranslate = myTranslate; - - const children = e.children; - for (const child of children) { - const childTranslate = this.getMinTranslate(child as HTMLElement, myTranslate); - minTranslate.x = Math.min(minTranslate.x, childTranslate.x); - minTranslate.y = Math.min(minTranslate.y, childTranslate.y); - } - return minTranslate; - } - - /** - * Calculates the absolute translation of an element relative to the svg. - * If the element has no translation, the offset of the parent is returned. - * @param e the element to get the translation from - * @param parentOffset Offset of the containing element - * @returns Offset of the child relative to the svg - */ - private getTranslate( - e: HTMLElement, - parentOffset: { x: number; y: number } = { x: 0, y: 0 }, - ): { x: number; y: number } { - const transform = e.getAttribute("transform"); - if (!transform) return parentOffset; - const translateMatch = transform.match(/translate\(([^)]+)\)/); - if (!translateMatch) return parentOffset; - const translate = translateMatch[1].match(/(-?[0-9.]+)(?:, | |,)(-?[0-9.]+)/); - if (!translate) return parentOffset; - const x = parseFloat(translate[1]); - const y = parseFloat(translate[2]); - const newX = x + parentOffset.x; - const newY = y + parentOffset.y; - return { x: newX, y: newY }; - } -} diff --git a/frontend/webEditor/src/features/serialize/keyListener.ts b/frontend/webEditor/src/features/serialize/keyListener.ts deleted file mode 100644 index f4e40a6f..00000000 --- a/frontend/webEditor/src/features/serialize/keyListener.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { injectable } from "inversify"; -import { CommitModelAction, KeyListener, SModelElementImpl } from "sprotty"; -import { Action } from "sprotty-protocol"; -import { matchesKeystroke } from "sprotty/lib/utils/keyboard"; -import { LoadDefaultDiagramAction } from "./loadDefaultDiagram"; -import { LoadDiagramAction } from "./load"; -import { SaveDiagramAction } from "./save"; -import { AnalyzeDiagramAction } from "./analyze"; - -@injectable() -export class SerializeKeyListener extends KeyListener { - keyDown(_element: SModelElementImpl, event: KeyboardEvent): Action[] { - if (matchesKeystroke(event, "KeyO", "ctrlCmd")) { - // Prevent the browser file open dialog from opening - event.preventDefault(); - - return [LoadDiagramAction.create(), CommitModelAction.create()]; - } else if (matchesKeystroke(event, "KeyO", "ctrlCmd", "shift")) { - event.preventDefault(); - return [LoadDefaultDiagramAction.create(), CommitModelAction.create()]; - } else if (matchesKeystroke(event, "KeyS", "ctrlCmd")) { - event.preventDefault(); - return [SaveDiagramAction.create()]; - } else if (matchesKeystroke(event, "KeyA", "ctrlCmd", "shift")) { - event.preventDefault(); - return [AnalyzeDiagramAction.create()]; - } - - return []; - } -} diff --git a/frontend/webEditor/src/features/serialize/load.ts b/frontend/webEditor/src/features/serialize/load.ts deleted file mode 100644 index 6eb6f1f3..00000000 --- a/frontend/webEditor/src/features/serialize/load.ts +++ /dev/null @@ -1,368 +0,0 @@ -import { - ActionDispatcher, - Command, - CommandExecutionContext, - EMPTY_ROOT, - ILogger, - NullLogger, - SModelRootImpl, - SNodeImpl, - TYPES, - isLocateable, -} from "sprotty"; -import { Action, SModelElement, SModelRoot } from "sprotty-protocol"; -import { DynamicChildrenProcessor } from "../dfdElements/dynamicChildren"; -import { inject, optional } from "inversify"; -import { createDefaultFitToScreenAction } from "../../utils"; -import { SavedDiagram } from "./save"; -import { LabelType, LabelTypeRegistry } from "../labels/labelTypeRegistry"; -import { LayoutModelAction } from "../autoLayout/command"; -import { EditorMode, EditorModeController } from "../editorMode/editorModeController"; -import { Constraint, ConstraintRegistry } from "../constraintMenu/constraintRegistry"; -import { LoadingIndicator } from "../../common/loadingIndicator"; -import { ChooseConstraintAction } from "../constraintMenu/actions"; -import { LayoutMethod } from "../settingsMenu/LayoutMethod"; - -export interface LoadDiagramAction extends Action { - kind: typeof LoadDiagramAction.KIND; - file: File | undefined; -} -export namespace LoadDiagramAction { - export const KIND = "load-diagram"; - - export function create(file?: File): LoadDiagramAction { - return { - kind: KIND, - file, - }; - } -} - -export class LoadDiagramCommand extends Command { - static readonly KIND = LoadDiagramAction.KIND; - - constructor(@inject(TYPES.Action) private readonly action: LoadDiagramAction) { - super(); - } - - // After loading a diagram, this command dispatches other actions like fit to screen - // and optional auto layouting. However when returning a new model in the execute method, - // the diagram is not directly updated. We need to wait for the - // InitializeCanvasBoundsCommand to be fired and finish before we can do things like fit to screen. - // Because of that we block the execution newly dispatched actions including - // the actions we dispatched after loading the diagram until - // the InitializeCanvasBoundsCommand has been processed. - // This works because the canvasBounds property is always removed before loading a diagram, - // requiring the InitializeCanvasBoundsCommand to be fired. - readonly blockUntil = LoadDiagramCommand.loadBlockUntilFn; - static readonly loadBlockUntilFn = (action: Action) => { - return action.kind === "initializeCanvasBounds"; - }; - - @inject(TYPES.ILogger) - private readonly logger: ILogger = new NullLogger(); - @inject(DynamicChildrenProcessor) - private readonly dynamicChildrenProcessor: DynamicChildrenProcessor = new DynamicChildrenProcessor(); - @inject(TYPES.IActionDispatcher) - private readonly actionDispatcher: ActionDispatcher = new ActionDispatcher(); - @inject(LabelTypeRegistry) - @optional() - private readonly labelTypeRegistry?: LabelTypeRegistry; - @inject(EditorModeController) - @optional() - private editorModeController?: EditorModeController; - @inject(ConstraintRegistry) - @optional() - private readonly constraintRegistry?: ConstraintRegistry; - @inject(LoadingIndicator) - @optional() - private readonly loadingIndicator?: LoadingIndicator; - - private oldRoot: SModelRootImpl | undefined; - private newRoot: SModelRootImpl | undefined; - private oldLabelTypes: LabelType[] | undefined; - private newLabelTypes: LabelType[] | undefined; - private oldEditorMode: EditorMode | undefined; - private newEditorMode: EditorMode | undefined; - private oldFileName: string | undefined; - private newFileName: string | undefined; - private oldConstrains: Constraint[] | undefined; - private newConstrains: Constraint[] | undefined; - - /** - * Gets the model file from the action or opens a file picker dialog if no file is provided. - * @returns A promise that resolves to the model file. - */ - private getModelFile(): Promise { - if (this.action.file) { - return Promise.resolve(this.action.file); - } - - // Open a file picker dialog if no file is provided in the action. - // The cleaner way to do this would be showOpenFilePicker(), - // but safari and firefox don't support it at the time of writing this code: - // https://developer.mozilla.org/en-US/docs/web/api/window/showOpenFilePicker#browser_compatibility - const input = document.createElement("input"); - input.type = "file"; - input.accept = "application/json"; - const fileLoadPromise = new Promise((resolve, reject) => { - // This event is fired when the user successfully submits the file picker dialog. - input.onchange = () => { - if (input.files && input.files.length > 0) { - const file = input.files[0]; - if (file.type !== "application/json") { - reject("Diagram file must be in JSON format"); - return; - } - - resolve(file); - } else { - reject("No file selected"); - } - }; - // The focus event is fired when the file picker dialog is closed. - // This includes cases where a file was selected and when the dialog was canceled and no file was selected. - // If a file was selected the change event above is fired after the focus event. - // So if a file was selected the focus event should be ignored and the promise is resolved in the onchange handler. - // If the file dialog was canceled undefined should be resolved by the focus handler. - // Because we don't know whether the change event will follow the focus event, - // we have a 300ms timeout before resolving the promise. - // If the promise was already resolved by the onchange handler, this won't do anything. - window.addEventListener( - "focus", - () => { - setTimeout(() => { - resolve(undefined); - }, 300); - }, - { once: true }, - ); - }); - input.click(); - - return fileLoadPromise; - } - - async execute(context: CommandExecutionContext): Promise { - this.loadingIndicator?.showIndicator("Loading model..."); - this.oldRoot = context.root; - try { - const file = await this.getModelFile(); - if (!file) { - // No file was selected, skip - this.loadingIndicator?.hideIndicator(); - return context.root; - } - - const newDiagram = await new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => { - const json = reader.result as string; - try { - const model = JSON.parse(json); - resolve(model); - } catch (error) { - reject(error); - } - }; - reader.onerror = () => { - reject(reader.error); - }; - reader.readAsText(file); - }); - - const newSchema = newDiagram?.model; - if (!newSchema) { - this.logger.info(this, "Model loading aborted"); - this.newRoot = this.oldRoot; - this.loadingIndicator?.hideIndicator(); - return this.oldRoot; - } - - // Load sprotty model - LoadDiagramCommand.preprocessModelSchema(newSchema); - this.dynamicChildrenProcessor.processGraphChildren(newSchema, "set"); - this.newRoot = context.modelFactory.createRoot(newSchema); - - this.logger.info(this, "Model loaded successfully"); - - if (this.labelTypeRegistry) { - // Load label types - this.oldLabelTypes = this.labelTypeRegistry.getLabelTypes(); - this.newLabelTypes = newDiagram?.labelTypes; - this.labelTypeRegistry.clearLabelTypes(); - if (newDiagram?.labelTypes) { - newDiagram.labelTypes.forEach((labelType) => { - this.labelTypeRegistry?.registerLabelType(labelType); - }); - - this.logger.info(this, "Label types loaded successfully"); - } - } - - if (this.editorModeController) { - // Load editor mode - this.oldEditorMode = this.editorModeController.getCurrentMode(); - this.newEditorMode = newDiagram?.mode; - if (newDiagram?.mode) { - this.editorModeController.setMode(newDiagram.mode); - } else { - this.editorModeController.setDefaultMode(); - } - - this.logger.info(this, "Editor mode loaded successfully"); - } - - if (this.constraintRegistry) { - // Load label types - this.oldConstrains = this.constraintRegistry.getConstraintList(); - this.newConstrains = newDiagram?.constraints; - this.constraintRegistry.clearConstraints(); - if (newDiagram?.constraints) { - this.constraintRegistry.setConstraintsFromArray(newDiagram.constraints); - - this.logger.info(this, "Constraints loaded successfully"); - } - } - - postLoadActions(this.newRoot, this.actionDispatcher); - - if (this.constraintRegistry) { - this.constraintRegistry.setAllConstraintsAsSelected(); - this.actionDispatcher.dispatch( - ChooseConstraintAction.create(this.constraintRegistry!.getConstraintList().map((c) => c.name)), - ); - } - - this.oldFileName = currentFileName; - this.newFileName = file.name; - setFileNameInPageTitle(file.name); - - this.loadingIndicator?.hideIndicator(); - return this.newRoot; - } catch (error) { - this.logger.error(this, "Error loading model", error); - alert("Error loading model: " + error); - this.newRoot = this.oldRoot; - - this.loadingIndicator?.hideIndicator(); - return this.oldRoot; - } - } - - /** - * Before a saved model schema can be loaded, it needs to be preprocessed. - * Currently this means that the features property is removed from all model elements recursively. - * Additionally the canvasBounds property is removed from the root element, because it may change - * depending on browser window. - * In the future this method may be extended to preprocess other properties. - * - * The feature property at runtime is a js Set with the relevant features. - * E.g. for the top graph this is the viewportFeature among others. - * When converting js Sets objects into json, the result is an empty js object. - * When loading the object is converted into an empty js Set and the features are lost. - * Because of this the editor won't work properly after loading a model. - * To prevent this, the features property is removed before loading the model. - * When the features property is missing it gets rebuilt on loading with the currently used features. - * - * @param modelSchema The model schema to preprocess - */ - public static preprocessModelSchema(modelSchema: SModelRoot): void { - // These properties are all not included in the root typing and if present are not loaded and handled correctly. So they are removed. - if ("features" in modelSchema) { - delete modelSchema["features"]; - } - if ("canvasBounds" in modelSchema) { - delete modelSchema["canvasBounds"]; - } - - if (modelSchema.children) { - modelSchema.children.forEach((child: SModelElement) => this.preprocessModelSchema(child)); - } - } - - undo(context: CommandExecutionContext): SModelRootImpl { - this.loadingIndicator?.showIndicator("Undoing load..."); - this.labelTypeRegistry?.clearLabelTypes(); - this.oldLabelTypes?.forEach((labelType) => this.labelTypeRegistry?.registerLabelType(labelType)); - if (this.oldEditorMode) { - this.editorModeController?.setMode(this.oldEditorMode); - } - this.constraintRegistry?.clearConstraints(); - if (this.oldConstrains) { - this.constraintRegistry?.setConstraintsFromArray(this.oldConstrains); - } - setFileNameInPageTitle(this.oldFileName); - - this.loadingIndicator?.hideIndicator(); - return this.oldRoot ?? context.modelFactory.createRoot(EMPTY_ROOT); - } - - redo(context: CommandExecutionContext): SModelRootImpl { - this.loadingIndicator?.showIndicator("Redoing load..."); - this.labelTypeRegistry?.clearLabelTypes(); - this.newLabelTypes?.forEach((labelType) => this.labelTypeRegistry?.registerLabelType(labelType)); - if (this.editorModeController) { - if (this.newEditorMode) { - this.editorModeController.setMode(this.newEditorMode); - } else { - this.editorModeController.setDefaultMode(); - } - } - this.constraintRegistry?.clearConstraints(); - if (this.newConstrains) { - this.constraintRegistry?.setConstraintsFromArray(this.newConstrains); - } - setFileNameInPageTitle(this.newFileName); - - this.loadingIndicator?.hideIndicator(); - return this.newRoot ?? this.oldRoot ?? context.modelFactory.createRoot(EMPTY_ROOT); - } -} - -/** - * Utility function to fit the diagram to the screen after loading a model inside a command. - * Captures all element ids and dispatches a FitToScreenAction. - * Also performs auto layouting if there are unpositioned nodes. - */ -export async function postLoadActions( - newRoot: SModelRootImpl | undefined, - actionDispatcher: ActionDispatcher, -): Promise { - if (!newRoot) { - return; - } - - // Layouting: - const containsUnPositionedNodes = newRoot.children - .filter((child) => child instanceof SNodeImpl) - .some((child) => isLocateable(child) && child.position.x === 0 && child.position.y === 0); - if (containsUnPositionedNodes) { - await actionDispatcher.dispatch(LayoutModelAction.create(LayoutMethod.LINES)); - } - - // fit to screen is done after auto layouting because that may change the bounds of the diagram - // requiring another fit to screen. - await actionDispatcher.dispatch(createDefaultFitToScreenAction(newRoot, false)); -} - -let initialPageTitle: string | undefined; -export let currentFileName: string | undefined; - -/** - * Sets the file name in the page title. - * If the given file name is undefined, no file name is displayed in the page title. - * The current file name is stored in the exported currentFileName variable. - */ -export function setFileNameInPageTitle(filename: string | undefined) { - if (!initialPageTitle) { - initialPageTitle = document.title; - } - - currentFileName = filename; - if (filename) { - document.title = `${filename} - ${initialPageTitle}`; - } else { - document.title = initialPageTitle; - } -} diff --git a/frontend/webEditor/src/features/serialize/loadDFDandDD.ts b/frontend/webEditor/src/features/serialize/loadDFDandDD.ts deleted file mode 100644 index 0ed41550..00000000 --- a/frontend/webEditor/src/features/serialize/loadDFDandDD.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { Command, CommandExecutionContext, ILogger, NullLogger, SModelRootImpl, TYPES } from "sprotty"; -import { Action } from "sprotty-protocol"; -import { sendMessage } from "./webSocketHandler"; -import { setModelFileName } from "../../index"; -import { setFileNameInPageTitle } from "./load"; -import { inject, optional } from "inversify"; -import { LoadingIndicator } from "../../common/loadingIndicator"; - -export interface LoadDFDandDDAction extends Action { - kind: typeof LoadDFDandDDAction.KIND; - file: File | undefined; -} -export namespace LoadDFDandDDAction { - export const KIND = "load-dfd"; - - export function create(file?: File): LoadDFDandDDAction { - return { - kind: KIND, - file, - }; - } -} - -export class LoadDFDandDDCommand extends Command { - static readonly KIND = LoadDFDandDDAction.KIND; - - @inject(TYPES.ILogger) - private readonly logger: ILogger = new NullLogger(); - @inject(LoadingIndicator) - @optional() - protected loadingIndicator?: LoadingIndicator; - - constructor() { - super(); - } - - /** - * Gets the model file from the action or opens a file picker dialog if no file is provided. - * @returns A promise that resolves to the model file. - */ - private getModelFiles(): Promise { - // Open a file picker dialog if no file is provided in the action. - // The cleaner way to do this would be showOpenFilePicker(), - // but safari and firefox don't support it at the time of writing this code: - // https://developer.mozilla.org/en-US/docs/web/api/window/showOpenFilePicker#browser_compatibility - const input = document.createElement("input"); - input.type = "file"; - input.accept = ".dataflowdiagram, .datadictionary"; - input.multiple = true; - - const fileLoadPromise = new Promise((resolve, reject) => { - // This event is fired when the user successfully submits the file picker dialog. - input.onchange = () => { - if (input.files && input.files.length === 2) { - const files = Array.from(input.files); - const dataflowFile = files.find((file) => file.name.endsWith(".dataflowdiagram")); - const dictionaryFile = files.find((file) => file.name.endsWith(".datadictionary")); - - if (dataflowFile && dictionaryFile) { - resolve([dataflowFile, dictionaryFile]); - } else { - reject("Please select one .dataflowdiagram file and one .datadictionary file."); - } - } else { - reject("You must select exactly two files: one .dataflowdiagram and one .datadictionary."); - } - }; - }); - input.click(); - - return fileLoadPromise; - } - - async execute(context: CommandExecutionContext): Promise { - this.loadingIndicator?.showIndicator("Loading DFD and DD files..."); - try { - const [dataflowFile, dictionaryFile] = (await this.getModelFiles()) ?? []; - - // Read the content of both files - const dataflowFileContent = await this.readFileContent(dataflowFile); - const dictionaryFileContent = await this.readFileContent(dictionaryFile); - - setModelFileName(dataflowFile.name.substring(0, dataflowFile.name.lastIndexOf("."))); - - // Send each file's content in separate WebSocket messages - sendMessage( - "DFD:" + - dataflowFileContent + - "\n:DD:\n" + - dictionaryFileContent, - ); - - setFileNameInPageTitle(dataflowFile.name); - return context.root; - } catch (error) { - this.logger.error(this, (error as Error).message); - this.loadingIndicator?.hideIndicator(); - return context.root; - } - } - - undo(context: CommandExecutionContext): SModelRootImpl { - return context.root; - } - - redo(context: CommandExecutionContext): SModelRootImpl { - return context.root; - } - - /** - * Utility function to read the content of a file as a string. - * @param file The file to read. - * @returns A promise that resolves to the file content as a string. - */ - private readFileContent(file: File): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result as string); - reader.onerror = () => reject(reader.error); - reader.readAsText(file); - }); - } - - getFileNameWithoutExtension(file: File): string { - const fileName = file.name; - return fileName.substring(0, fileName.lastIndexOf(".")) || fileName; - } -} diff --git a/frontend/webEditor/src/features/serialize/loadDefaultDiagram.ts b/frontend/webEditor/src/features/serialize/loadDefaultDiagram.ts deleted file mode 100644 index 88cf830a..00000000 --- a/frontend/webEditor/src/features/serialize/loadDefaultDiagram.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { inject, injectable, optional } from "inversify"; -import { - ActionDispatcher, - Command, - CommandExecutionContext, - CommandReturn, - EMPTY_ROOT, - ILogger, - NullLogger, - SModelRootImpl, - TYPES, -} from "sprotty"; -import { Action } from "sprotty-protocol"; -import { LabelType, LabelTypeRegistry } from "../labels/labelTypeRegistry"; -import { DynamicChildrenProcessor } from "../dfdElements/dynamicChildren"; -import { LoadDiagramCommand, currentFileName, postLoadActions, setFileNameInPageTitle } from "./load"; -import { SavedDiagram } from "./save"; -import { ConstraintRegistry } from "../constraintMenu/constraintRegistry"; -import { EditorMode, EditorModeController } from "../editorMode/editorModeController"; - -import defaultDiagramData from "./defaultDiagram.json"; -const defaultDiagram = defaultDiagramData as SavedDiagram; - -export interface LoadDefaultDiagramAction extends Action { - readonly kind: typeof LoadDefaultDiagramAction.KIND; -} -export namespace LoadDefaultDiagramAction { - export const KIND = "loadDefaultDiagram"; - - export function create(): LoadDefaultDiagramAction { - return { - kind: KIND, - }; - } -} - -@injectable() -export class LoadDefaultDiagramCommand extends Command { - readonly blockUntil = LoadDiagramCommand.loadBlockUntilFn; - - static readonly KIND = LoadDefaultDiagramAction.KIND; - @inject(TYPES.ILogger) - private readonly logger: ILogger = new NullLogger(); - @inject(DynamicChildrenProcessor) - private readonly dynamicChildrenProcessor: DynamicChildrenProcessor = new DynamicChildrenProcessor(); - @inject(TYPES.IActionDispatcher) - private readonly actionDispatcher: ActionDispatcher = new ActionDispatcher(); - @inject(LabelTypeRegistry) - @optional() - private readonly labelTypeRegistry?: LabelTypeRegistry; - @inject(EditorModeController) - @optional() - private editorModeController?: EditorModeController; - @inject(ConstraintRegistry) - @optional() - private readonly constraintRegistry?: ConstraintRegistry; - - private oldRoot: SModelRootImpl | undefined; - private newRoot: SModelRootImpl | undefined; - private oldLabelTypes: LabelType[] | undefined; - private oldEditorMode: EditorMode | undefined; - private oldFileName: string | undefined; - - execute(context: CommandExecutionContext): CommandReturn { - this.oldRoot = context.root; - - const graphCopy = JSON.parse(JSON.stringify(defaultDiagram.model)); - LoadDiagramCommand.preprocessModelSchema(graphCopy); - this.dynamicChildrenProcessor.processGraphChildren(graphCopy, "set"); - this.newRoot = context.modelFactory.createRoot(graphCopy); - - this.logger.info(this, "Default Model loaded successfully"); - - if (this.labelTypeRegistry) { - this.oldLabelTypes = this.labelTypeRegistry.getLabelTypes(); - this.labelTypeRegistry.clearLabelTypes(); - defaultDiagram.labelTypes?.forEach((labelType) => { - this.labelTypeRegistry?.registerLabelType(labelType); - }); - this.logger.info(this, "Default Label Types loaded successfully"); - } - - if (this.editorModeController) { - this.oldEditorMode = this.editorModeController.getCurrentMode(); - if (defaultDiagram.mode) { - this.editorModeController.setMode(defaultDiagram.mode); - } else { - this.editorModeController.setDefaultMode(); - } - - this.logger.info(this, "Default Editor Mode loaded successfully"); - } - - if (this.constraintRegistry) { - // Load label types - this.constraintRegistry.clearConstraints(); - if (defaultDiagram?.constraints) { - this.constraintRegistry.setConstraintsFromArray(defaultDiagram.constraints); - this.logger.info(this, "Constraints loaded successfully"); - } - } - - postLoadActions(this.newRoot, this.actionDispatcher); - - this.oldFileName = currentFileName; - setFileNameInPageTitle(undefined); - - return this.newRoot; - } - - undo(context: CommandExecutionContext): SModelRootImpl { - this.labelTypeRegistry?.clearLabelTypes(); - this.oldLabelTypes?.forEach((labelType) => this.labelTypeRegistry?.registerLabelType(labelType)); - if (this.oldEditorMode) { - this.editorModeController?.setMode(this.oldEditorMode); - } - setFileNameInPageTitle(this.oldFileName); - - return this.oldRoot ?? context.modelFactory.createRoot(EMPTY_ROOT); - } - - redo(context: CommandExecutionContext): SModelRootImpl { - this.labelTypeRegistry?.clearLabelTypes(); - defaultDiagram.labelTypes?.forEach((labelType) => { - this.labelTypeRegistry?.registerLabelType(labelType); - }); - if (this.editorModeController) { - if (defaultDiagram.mode) { - this.editorModeController.setMode(defaultDiagram.mode); - } else { - this.editorModeController.setDefaultMode(); - } - } - setFileNameInPageTitle(undefined); - - return this.newRoot ?? this.oldRoot ?? context.modelFactory.createRoot(EMPTY_ROOT); - } -} diff --git a/frontend/webEditor/src/features/serialize/loadPalladio.ts b/frontend/webEditor/src/features/serialize/loadPalladio.ts deleted file mode 100644 index 357af949..00000000 --- a/frontend/webEditor/src/features/serialize/loadPalladio.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { Command, CommandExecutionContext, ILogger, NullLogger, SModelRootImpl, TYPES } from "sprotty"; -import { Action } from "sprotty-protocol"; -import { setModelFileName } from "../../index"; -import { sendMessage } from "./webSocketHandler"; -import { inject, optional } from "inversify"; -import { LoadingIndicator } from "../../common/loadingIndicator"; - -export interface LoadPalladioAction extends Action { - kind: typeof LoadPalladioAction.KIND; - file: File | undefined; -} -export namespace LoadPalladioAction { - export const KIND = "load-pcm"; - - export function create(file?: File): LoadPalladioAction { - return { - kind: KIND, - file, - }; - } -} - -export class LoadPalladioCommand extends Command { - static readonly KIND = LoadPalladioAction.KIND; - - @inject(TYPES.ILogger) - private readonly logger: ILogger = new NullLogger(); - @inject(LoadingIndicator) - @optional() - protected loadingIndicator?: LoadingIndicator; - - constructor() { - super(); - } - - /** - * Gets the model file from the action or opens a file picker dialog if no file is provided. - * @returns A promise that resolves to the model file. - */ - private getModelFiles(): Promise { - // Open a file picker dialog if no file is provided in the action. - // The cleaner way to do this would be showOpenFilePicker(), - // but safari and firefox don't support it at the time of writing this code: - // https://developer.mozilla.org/en-US/docs/web/api/window/showOpenFilePicker#browser_compatibility - const input = document.createElement("input"); - input.type = "file"; - input.accept = - ".pddc, .allocation, .nodecharacteristics, .repository, .resourceenvironment, .system, .usagemodel"; - input.multiple = true; - - const fileLoadPromise = new Promise((resolve, reject) => { - // This event is fired when the user successfully submits the file picker dialog. - input.onchange = () => { - if (input.files && input.files.length === 7) { - const files = Array.from(input.files); - const requiredFiles = { - pddc: files.find((file) => file.name.endsWith(".pddc")), - allocation: files.find((file) => file.name.endsWith(".allocation")), - nodecharacteristics: files.find((file) => file.name.endsWith(".nodecharacteristics")), - repository: files.find((file) => file.name.endsWith(".repository")), - resourceenvironment: files.find((file) => file.name.endsWith(".resourceenvironment")), - system: files.find((file) => file.name.endsWith(".system")), - usagemodel: files.find((file) => file.name.endsWith(".usagemodel")), - }; - - // Check if each required file type is present - const allFilesPresent = Object.values(requiredFiles).every((file) => file !== undefined); - - if (allFilesPresent) { - resolve(Object.values(requiredFiles) as File[]); - } else { - reject( - "Please select one file of each required type: .pddc, .allocation, .nodecharacteristics, .repository, .resourceenvironment, .system, .usagemodel", - ); - } - } else { - reject("You must select exactly 7 files"); - } - }; - }); - input.click(); - - return fileLoadPromise; - } - - async execute(context: CommandExecutionContext): Promise { - this.loadingIndicator?.showIndicator("Loading model files..."); - try { - // Fetch all required files - const files = (await this.getModelFiles()) ?? []; // Ensure getModelFiles() returns exactly seven files - - // Read the content of each file and structure them - const fileContents = await Promise.all( - files.map(async (file) => ({ - name: file.name, // Full filename with extension - content: await this.readFileContent(file), - })), - ); - - // Construct the message format for WebSocket - const message = [...fileContents.map(({ name, content }) => `${name}:${content}`)].join("---FILE---"); - - // Send the structured message over WebSocket - sendMessage(message); - - // Set the model file name and page title based on one of the files (e.g., the first file) - setModelFileName(files[0].name.substring(0, files[0].name.lastIndexOf("."))); - - return context.root; - } catch (error) { - this.logger.error(this, (error as Error).message); - this.loadingIndicator?.hideIndicator(); - return context.root; - } - } - - undo(context: CommandExecutionContext): SModelRootImpl { - return context.root; - } - - redo(context: CommandExecutionContext): SModelRootImpl { - return context.root; - } - - /** - * Utility function to read the content of a file as a string. - * @param file The file to read. - * @returns A promise that resolves to the file content as a string. - */ - private readFileContent(file: File): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result as string); - reader.onerror = () => reject(reader.error); - reader.readAsText(file); - }); - } - - getFileNameWithoutExtension(file: File): string { - const fileName = file.name; - return fileName.substring(0, fileName.lastIndexOf(".")) || fileName; - } -} diff --git a/frontend/webEditor/src/features/serialize/save.ts b/frontend/webEditor/src/features/serialize/save.ts deleted file mode 100644 index e5523ee1..00000000 --- a/frontend/webEditor/src/features/serialize/save.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { inject, injectable, optional } from "inversify"; -import { Command, CommandExecutionContext, LocalModelSource, SModelRootImpl, TYPES } from "sprotty"; -import { Action, SModelRoot } from "sprotty-protocol"; -import { LabelType, LabelTypeRegistry } from "../labels/labelTypeRegistry"; -import { DynamicChildrenProcessor } from "../dfdElements/dynamicChildren"; -import { EditorMode, EditorModeController } from "../editorMode/editorModeController"; -import { Constraint, ConstraintRegistry } from "../constraintMenu/constraintRegistry"; - -/** - * Type that contains all data related to a diagram. - * This contains the sprotty diagram model and other data related to it. - */ -export interface SavedDiagram { - model: SModelRoot; - labelTypes?: LabelType[]; - constraints?: Constraint[]; - mode?: EditorMode; - version: number; -} -export const CURRENT_VERSION = 1; - -export interface SaveDiagramAction extends Action { - kind: typeof SaveDiagramAction.KIND; - suggestedFileName: string; -} -export namespace SaveDiagramAction { - export const KIND = "save-diagram"; - - export function create(suggestedFileName?: string): SaveDiagramAction { - return { - kind: KIND, - suggestedFileName: suggestedFileName ?? "diagram.json", - }; - } -} - -@injectable() -export class SaveDiagramCommand extends Command { - static readonly KIND = SaveDiagramAction.KIND; - @inject(TYPES.ModelSource) - private modelSource: LocalModelSource = new LocalModelSource(); - @inject(DynamicChildrenProcessor) - private dynamicChildrenProcessor: DynamicChildrenProcessor = new DynamicChildrenProcessor(); - @inject(LabelTypeRegistry) - @optional() - private labelTypeRegistry?: LabelTypeRegistry; - @inject(EditorModeController) - @optional() - private editorModeController?: EditorModeController; - @inject(ConstraintRegistry) - @optional() - private constraintRegistry?: ConstraintRegistry; - - constructor(@inject(TYPES.Action) private action: SaveDiagramAction) { - super(); - } - - execute(context: CommandExecutionContext): SModelRootImpl { - // Convert the model to JSON - // Do a copy because we're going to modify it - const modelCopy = JSON.parse(JSON.stringify(this.modelSource.model)); - // Remove element children that are implementation detail - this.dynamicChildrenProcessor.processGraphChildren(modelCopy, "remove"); - - // Export the diagram as a JSON data URL. - const diagram: SavedDiagram = { - model: modelCopy, - labelTypes: this.labelTypeRegistry?.getLabelTypes(), - constraints: this.constraintRegistry?.getConstraintList(), - mode: this.editorModeController?.getCurrentMode(), - version: CURRENT_VERSION, - }; - const diagramJson = JSON.stringify(diagram, undefined, 4); - const jsonBlob = new Blob([diagramJson], { type: "application/json" }); - const jsonUrl = URL.createObjectURL(jsonBlob); - - // Download the JSON file using a temporary anchor element. - // The cleaner way to do this would be showSaveFilePicker(), - // but safari and firefox don't support it at the time of writing this code: - // https://developer.mozilla.org/en-US/docs/web/api/window/showsavefilepicker#browser_compatibility - const tempLink = document.createElement("a"); - tempLink.href = jsonUrl; - tempLink.setAttribute("download", this.action.suggestedFileName); - tempLink.click(); - - // Free the url data - URL.revokeObjectURL(jsonUrl); - tempLink.remove(); - - return context.root; - } - - // Saving cannot be meaningfully undone/redone - - undo(context: CommandExecutionContext): SModelRootImpl { - return context.root; - } - - redo(context: CommandExecutionContext): SModelRootImpl { - return context.root; - } -} diff --git a/frontend/webEditor/src/features/serialize/saveDFDandDD.ts b/frontend/webEditor/src/features/serialize/saveDFDandDD.ts deleted file mode 100644 index 37ce5d14..00000000 --- a/frontend/webEditor/src/features/serialize/saveDFDandDD.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { inject, injectable, optional } from "inversify"; -import { Command, CommandExecutionContext, LocalModelSource, SModelRootImpl, TYPES } from "sprotty"; -import { Action } from "sprotty-protocol"; -import { LabelTypeRegistry } from "../labels/labelTypeRegistry"; -import { DynamicChildrenProcessor } from "../dfdElements/dynamicChildren"; -import { EditorModeController } from "../editorMode/editorModeController"; -import { sendMessage } from "./webSocketHandler"; -import { CURRENT_VERSION, SavedDiagram } from "./save"; -import { ConstraintRegistry } from "../constraintMenu/constraintRegistry"; -import { getModelFileName } from "../../index"; - -export interface SaveDFDandDDAction extends Action { - kind: typeof SaveDFDandDDAction.KIND; - file: File | undefined; -} -export namespace SaveDFDandDDAction { - export const KIND = "save-dfd"; - - export function create(file?: File): SaveDFDandDDAction { - return { - kind: KIND, - file, - }; - } -} - -@injectable() -export class SaveDFDandDDCommand extends Command { - static readonly KIND = SaveDFDandDDAction.KIND; - @inject(TYPES.ModelSource) - private modelSource: LocalModelSource = new LocalModelSource(); - @inject(DynamicChildrenProcessor) - private dynamicChildrenProcessor: DynamicChildrenProcessor = new DynamicChildrenProcessor(); - @inject(LabelTypeRegistry) - @optional() - private labelTypeRegistry?: LabelTypeRegistry; - @inject(EditorModeController) - @optional() - private editorModeController?: EditorModeController; - @inject(ConstraintRegistry) - @optional() - private readonly constraintRegistry?: ConstraintRegistry; - - constructor() { - super(); - } - - execute(context: CommandExecutionContext): SModelRootImpl { - // Convert the model to JSON - // Do a copy because we're going to modify it - const modelCopy = JSON.parse(JSON.stringify(this.modelSource.model)); - // Remove element children that are implementation detail - this.dynamicChildrenProcessor.processGraphChildren(modelCopy, "remove"); - - // Export the diagram as a JSON data URL. - const diagram: SavedDiagram = { - model: modelCopy, - labelTypes: this.labelTypeRegistry?.getLabelTypes(), - constraints: this.constraintRegistry?.getConstraintList(), - mode: this.editorModeController?.getCurrentMode(), - version: CURRENT_VERSION, - }; - const diagramJson = JSON.stringify(diagram, undefined, 4); - sendMessage("Json2DFD:" + diagramJson); - return context.root; - } - - // Saving cannot be meaningfully undone/redone - - undo(context: CommandExecutionContext): SModelRootImpl { - return context.root; - } - - redo(context: CommandExecutionContext): SModelRootImpl { - return context.root; - } -} - -export class SaveDFDandDD { - private dfdString: string = ""; - private ddString: string = ""; - - /** - * Constructor to initialize the XML strings and filenames. - * @param xmlString1 - The first XML string to save. - * @param xmlString2 - The second XML string to save. - * @param filename - The name for the first XML file (default: "example"). - */ - constructor(message: string) { - // Define the closing tag - const closingTag = ""; - const endIndex = message.indexOf(closingTag); - - if (endIndex !== -1) { - // Extract everything up to and including the closing tag - this.dfdString = message.slice(0, endIndex + closingTag.length).trim(); - - // Extract everything after the closing tag - this.ddString = message.slice(endIndex + closingTag.length).trim(); - } - } - - /** - * Method to save both XML files by creating Blob objects and triggering downloads. - */ - public saveDiagramAsDFD(): void { - this.saveFile(this.dfdString, ".dataflowdiagram"); - this.saveFile(this.ddString, ".datadictionary"); - } - - private saveFile(file: string, ending: string) { - const blob = new Blob([file], { type: "application/xml" }); - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.setAttribute("download", getModelFileName() + ending); - document.body.appendChild(link); // Append link to the body - link.click(); // Programmatically click to trigger download - URL.revokeObjectURL(url); // Revoke the URL after download - link.remove(); // Remove the link from the DOM - } -} diff --git a/frontend/webEditor/src/features/serialize/webSocketHandler.ts b/frontend/webEditor/src/features/serialize/webSocketHandler.ts deleted file mode 100644 index 51fa9213..00000000 --- a/frontend/webEditor/src/features/serialize/webSocketHandler.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { getModelFileName, logger, setModelSource, loadingIndicator } from "../../index"; -import { SaveDFDandDD } from "./saveDFDandDD"; - -const webSocketAdress = `wss://websocket.dataflowanalysis.org/events/`; - -let ws: WebSocket; -let wsId = 0; - -/** - * Initializes the WebSocket and sets up all event handlers. - */ -function initWebSocket() { - ws = new WebSocket(webSocketAdress); - - ws.onopen = () => { - logger.log(ws, "WebSocket connection established."); - }; - - ws.onclose = () => { - logger.log(ws, "WebSocket connection closed. Reconnecting..."); - loadingIndicator.hideIndicator(); - initWebSocket(); - }; - - ws.onerror = () => { - logger.log(ws, "WebSocket encountered an error. Reconnecting..."); - loadingIndicator.hideIndicator(); - initWebSocket(); - }; - - ws.onmessage = (event) => { - logger.log(ws, "WebSocketID:", wsId); - logger.log(ws, event.data); - - // Example of specific handling for certain messages: - if (event.data.startsWith("Error:")) { - alert(event.data); - loadingIndicator.hideIndicator(); - return; - } - if (event.data.startsWith("ID assigned:")) { - wsId = parseInt(event.data.split(":")[1].trim(), 10); - loadingIndicator.hideIndicator(); - return; - } - - let message = event.data; - const name = message.split(":")[0]; - message = message.replace(name + ":", ""); - - if (event.data.trim().endsWith("")) { - const saveDFDandDD = new SaveDFDandDD(message); - saveDFDandDD.saveDiagramAsDFD(); - loadingIndicator.hideIndicator(); - return; - } - - // Otherwise, treat incoming data as JSON for model source: - setModelSource( - new File([new Blob([message], { type: "application/json" })], name + ".json", { - type: "application/json", - }), - ); - loadingIndicator.hideIndicator(); - }; -} - -export function sendMessage(message: string) { - ws.send(wsId + ":" + getModelFileName() + ":" + message); -} - -// Initialize immediately upon module load -initWebSocket(); diff --git a/frontend/webEditor/src/features/settingsMenu/SettingsManager.ts b/frontend/webEditor/src/features/settingsMenu/SettingsManager.ts deleted file mode 100644 index a3ef9125..00000000 --- a/frontend/webEditor/src/features/settingsMenu/SettingsManager.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { inject, injectable } from "inversify"; -import { ActionDispatcher, TYPES } from "sprotty"; -import { ChangeEdgeLabelVisibilityAction, SimplifyNodeNamesAction } from "./actions"; -import { Mode } from "./annotationManager"; - -@injectable() -export class SettingsManager { - private _hideEdgeLabels = false; - private _hideEdgeLabelsCheckbox?: HTMLInputElement; - private _simplifyNodeNames = false; - private _simplifyNodeNamesCheckbox?: HTMLInputElement; - private _labelModeSelector?: HTMLSelectElement; - - constructor(@inject(TYPES.IActionDispatcher) protected readonly dispatcher: ActionDispatcher) { - } - - - public get hideEdgeLabels(): boolean { - return this._hideEdgeLabels; - } - - public set hideEdgeLabels(hideEdgeLabels: boolean) { - this._hideEdgeLabels = hideEdgeLabels; - if (this._hideEdgeLabelsCheckbox) { - this._hideEdgeLabelsCheckbox.checked = hideEdgeLabels; - } - } - - public bindHideEdgeLabelsCheckbox(checkbox: HTMLInputElement) { - this._hideEdgeLabelsCheckbox = checkbox; - this._hideEdgeLabelsCheckbox.checked = this._hideEdgeLabels; - this._hideEdgeLabelsCheckbox.addEventListener("change", () => { - this.dispatcher.dispatch(ChangeEdgeLabelVisibilityAction.create(this._hideEdgeLabelsCheckbox!.checked)); - }); - } - - public get simplifyNodeNames(): boolean { - return this._simplifyNodeNames; - } - - public set simplifyNodeNames(simplifyNodeNames: boolean) { - this._simplifyNodeNames = simplifyNodeNames; - if (this._simplifyNodeNamesCheckbox) { - this._simplifyNodeNamesCheckbox.checked = simplifyNodeNames; - } - } - - public bindSimplifyNodeNamesCheckbox(checkbox: HTMLInputElement) { - this._simplifyNodeNamesCheckbox = checkbox; - this._simplifyNodeNamesCheckbox.checked = this._simplifyNodeNames; - this._simplifyNodeNamesCheckbox.addEventListener("change", () => { - this.dispatcher.dispatch( - SimplifyNodeNamesAction.create(this._simplifyNodeNamesCheckbox!.checked ? "hide" : "show"), - ); - }); - } - - public bindLabelModeSelector(labelModeSelector: HTMLSelectElement) { - this._labelModeSelector = labelModeSelector; - labelModeSelector.value = Mode.INCOMING; - } - - public getCurrentLabelMode(): Mode { - return this._labelModeSelector!.value as Mode; - } -} diff --git a/frontend/webEditor/src/features/settingsMenu/actions.ts b/frontend/webEditor/src/features/settingsMenu/actions.ts deleted file mode 100644 index bb63138a..00000000 --- a/frontend/webEditor/src/features/settingsMenu/actions.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Action } from "sprotty-protocol"; -import { Theme } from "./themeManager"; - -export interface SimplifyNodeNamesAction extends Action { - kind: typeof SimplifyNodeNamesAction.KIND; - mode: SimplifyNodeNamesAction.Mode; -} -export namespace SimplifyNodeNamesAction { - export const KIND = "simplify-node-names"; - export type Mode = "hide" | "show"; - - export function create(mode?: SimplifyNodeNamesAction.Mode): SimplifyNodeNamesAction { - return { - kind: KIND, - mode: mode ?? "hide", - }; - } -} - -export interface ChangeEdgeLabelVisibilityAction extends Action { - kind: typeof ChangeEdgeLabelVisibilityAction.KIND; - hide: boolean; -} -export namespace ChangeEdgeLabelVisibilityAction { - export const KIND = "hide-edge-labels"; - - export function create(hide: boolean = true): ChangeEdgeLabelVisibilityAction { - return { kind: KIND, hide }; - } -} - -export interface ChangeThemeAction extends Action { - kind: typeof ChangeThemeAction.KIND; - theme: Theme; -} -export namespace ChangeThemeAction { - export const KIND = "change-theme"; - - export function create(theme: Theme = Theme.SYSTEM_DEFAULT): ChangeThemeAction { - return { kind: KIND, theme }; - } -} - -export interface ReSnapPortsAfterChangeAction extends Action { - kind: typeof ReSnapPortsAfterChangeAction.KIND; -} - -export namespace ReSnapPortsAfterChangeAction { - export const KIND = "resnap-ports-after-change"; - - export function create(): Action { - return { kind: KIND }; - } -} diff --git a/frontend/webEditor/src/features/settingsMenu/commands.ts b/frontend/webEditor/src/features/settingsMenu/commands.ts deleted file mode 100644 index 9ce35fd4..00000000 --- a/frontend/webEditor/src/features/settingsMenu/commands.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { inject, injectable } from "inversify"; -import { - Command, - CommandExecutionContext, - CommandReturn, - ISnapper, - SLabelImpl, - SModelRootImpl, - SPortImpl, - TYPES, -} from "sprotty"; -import { getBasicType } from "sprotty-protocol"; -import { DfdNodeImpl } from "../dfdElements/nodes"; -import { SettingsManager } from "./SettingsManager"; -import { - ChangeEdgeLabelVisibilityAction, - ChangeThemeAction, - ReSnapPortsAfterChangeAction, - SimplifyNodeNamesAction, -} from "./actions"; -import { ArrowEdgeImpl } from "../dfdElements/edges"; -import { Theme, ThemeManager } from "./themeManager"; -import { snapPortsOfNode } from "../dfdElements/portSnapper"; -import { EditorModeController } from "../editorMode/editorModeController"; - -@injectable() -export class NodeNameReplacementRegistry { - private registry: Map = new Map(); - private nextNumber = 1; - - public get(id: string) { - const v = this.registry.get(id); - if (v !== undefined) { - return v; - } - const newName = this.nextNumber.toString(); - this.nextNumber++; - this.registry.set(id, newName); - return newName; - } -} - -@injectable() -export class SimplifyNodeNamesCommand extends Command { - static readonly KIND = SimplifyNodeNamesAction.KIND; - private readonly portMove: ReSnapPortsAfterChangeCommand; - - constructor( - @inject(TYPES.Action) private action: SimplifyNodeNamesAction, - @inject(SettingsManager) private settings: SettingsManager, - @inject(NodeNameReplacementRegistry) private registry: NodeNameReplacementRegistry, - @inject(TYPES.ISnapper) snapper: ISnapper, - @inject(EditorModeController) private editorModeController: EditorModeController, - ) { - super(); - this.portMove = new ReSnapPortsAfterChangeCommand(snapper); - } - - execute(context: CommandExecutionContext): CommandReturn { - this.perform(context, this.action.mode); - return this.portMove.execute(context); - } - undo(context: CommandExecutionContext): CommandReturn { - this.perform(context, this.action.mode === "hide" ? "show" : "hide"); - return this.portMove.undo(context); - } - redo(context: CommandExecutionContext): CommandReturn { - this.perform(context, this.action.mode); - return this.portMove.redo(context); - } - - private perform(context: CommandExecutionContext, mode: SimplifyNodeNamesAction.Mode): CommandReturn { - this.settings.simplifyNodeNames = mode === "hide"; - const nodes = context.root.children.filter((node) => getBasicType(node) === "node") as DfdNodeImpl[]; - nodes.forEach((node) => { - const label = node.children.find((element) => element.type === "label:positional") as - | SLabelImpl - | undefined; - if (!label) { - return; - } - label.text = mode === "hide" ? this.registry.get(node.id) : (node.text ?? ""); - node.hideLabels = mode === "hide"; - node.minimumWidth = mode === "hide" ? DfdNodeImpl.DEFAULT_WIDTH / 2 : DfdNodeImpl.DEFAULT_WIDTH; - }); - if (mode === "hide") { - this.editorModeController.setMode("view"); - } - - return context.root; - } -} - -@injectable() -export class ChangeEdgeLabelVisibilityCommand extends Command { - static readonly KIND = ChangeEdgeLabelVisibilityAction.KIND; - - constructor( - @inject(TYPES.Action) private action: ChangeEdgeLabelVisibilityAction, - @inject(SettingsManager) private settings: SettingsManager, - @inject(EditorModeController) private editorModeController: EditorModeController, - ) { - super(); - } - - execute(context: CommandExecutionContext): CommandReturn { - return this.perform(context, this.action.hide); - } - undo(context: CommandExecutionContext): CommandReturn { - return this.perform(context, !this.action.hide); - } - redo(context: CommandExecutionContext): CommandReturn { - return this.perform(context, this.action.hide); - } - - private perform(context: CommandExecutionContext, hide: boolean): SModelRootImpl { - this.settings.hideEdgeLabels = hide; - const edges = context.root.children.filter((node) => getBasicType(node) === "edge") as ArrowEdgeImpl[]; - edges.forEach((edge) => { - const label = edge.children.find((element) => element.type === "label:filled-background") as - | SLabelImpl - | undefined; - if (!label) { - return; - } - label.text = hide ? "" : (edge.text ?? ""); - }); - if (hide) { - this.editorModeController.setMode("view"); - } - - return context.root; - } -} - -@injectable() -export class ChangeThemeCommand extends Command { - static readonly KIND = ChangeThemeAction.KIND; - private previousTheme?: Theme; - - constructor( - @inject(TYPES.Action) private action: ChangeThemeAction, - @inject(ThemeManager) private themeManager: ThemeManager, - ) { - super(); - } - - execute(context: CommandExecutionContext): CommandReturn { - this.previousTheme = this.themeManager.theme; - this.themeManager.theme = this.action.theme; - return context.root; - } - undo(context: CommandExecutionContext): CommandReturn { - this.themeManager.theme = this.previousTheme ?? Theme.SYSTEM_DEFAULT; - return context.root; - } - redo(context: CommandExecutionContext): CommandReturn { - this.previousTheme = this.themeManager.theme; - this.themeManager.theme = this.action.theme; - return context.root; - } -} - -@injectable() -export class ReSnapPortsAfterChangeCommand extends Command { - static readonly KIND = ReSnapPortsAfterChangeAction.KIND; - private previousPositions: Map = new Map(); - - constructor(@inject(TYPES.ISnapper) private readonly snapper: ISnapper) { - super(); - } - - execute(context: CommandExecutionContext): CommandReturn { - const model = context.root; - - model.children.forEach((node) => { - if (node instanceof DfdNodeImpl) { - this.savePortPositions(node); - } - }); - - model.children.forEach((node) => { - if (node instanceof DfdNodeImpl) { - snapPortsOfNode(node, this.snapper); - } - }); - return model; - } - undo(context: CommandExecutionContext): CommandReturn { - const model = context.root; - model.children.forEach((node) => { - if (node instanceof DfdNodeImpl) { - node.children.forEach((child) => { - if (child instanceof SPortImpl) { - const pos = this.previousPositions.get(child.id); - if (pos) { - child.position = pos; - } - } - }); - } - }); - return model; - } - redo(context: CommandExecutionContext): CommandReturn { - const model = context.root; - model.children.forEach((node) => { - if (node instanceof DfdNodeImpl) { - snapPortsOfNode(node, this.snapper); - } - }); - return model; - } - - private savePortPositions(element: DfdNodeImpl) { - element.children.forEach((child) => { - if (child instanceof SPortImpl) { - this.previousPositions.set(child.id, { x: child.position.x, y: child.position.y }); - } else if (child instanceof DfdNodeImpl) { - this.savePortPositions(child); - } - }); - } -} diff --git a/frontend/webEditor/src/features/settingsMenu/di.config.ts b/frontend/webEditor/src/features/settingsMenu/di.config.ts deleted file mode 100644 index b7623122..00000000 --- a/frontend/webEditor/src/features/settingsMenu/di.config.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ContainerModule } from "inversify"; -import { SettingsUI } from "./settingsMenu"; -import { ThemeManager } from "./themeManager"; -import { EDITOR_TYPES } from "../../utils"; -import { configureCommand, TYPES } from "sprotty"; -import { - ChangeEdgeLabelVisibilityCommand, - ChangeThemeCommand, - NodeNameReplacementRegistry, - SimplifyNodeNamesCommand, -} from "./commands"; -import { SettingsManager } from "./SettingsManager"; -import { AnnnotationsManager } from "./annotationManager"; - -export const settingsModule = new ContainerModule((bind, unbind, isBound, rebind) => { - bind(SettingsManager).toSelf().inSingletonScope(); - bind(NodeNameReplacementRegistry).toSelf().inSingletonScope(); - bind(ThemeManager).toSelf().inSingletonScope(); - bind(SettingsUI).toSelf().inSingletonScope(); - bind(AnnnotationsManager).toSelf().inSingletonScope(); - bind(TYPES.IUIExtension).toService(SettingsUI); - bind(EDITOR_TYPES.DefaultUIElement).toService(SettingsUI); - const context = { bind, unbind, isBound, rebind }; - - configureCommand(context, SimplifyNodeNamesCommand); - configureCommand(context, ChangeEdgeLabelVisibilityCommand); - configureCommand(context, ChangeThemeCommand); -}); diff --git a/frontend/webEditor/src/features/settingsMenu/settingsMenu.ts b/frontend/webEditor/src/features/settingsMenu/settingsMenu.ts deleted file mode 100644 index 8592fa3b..00000000 --- a/frontend/webEditor/src/features/settingsMenu/settingsMenu.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { AbstractUIExtension, ActionDispatcher, TYPES } from "sprotty"; -import { inject, injectable } from "inversify"; - -import "./settingsMenu.css"; -import { Theme, ThemeManager } from "./themeManager"; -import { SettingsManager } from "./SettingsManager"; -import { EditorModeController } from "../editorMode/editorModeController"; -import { ChangeEditorModeAction } from "../editorMode/command"; -import { Mode } from "./annotationManager"; - -@injectable() -export class SettingsUI extends AbstractUIExtension { - static readonly ID = "settings-ui"; - - constructor( - @inject(SettingsManager) protected readonly settings: SettingsManager, - @inject(ThemeManager) protected readonly themeManager: ThemeManager, - @inject(EditorModeController) private editorModeController: EditorModeController, - @inject(TYPES.IActionDispatcher) protected readonly dispatcher: ActionDispatcher, - ) { - super(); - } - - id(): string { - return SettingsUI.ID; - } - - containerClass(): string { - return SettingsUI.ID; - } - - protected initializeContents(containerElement: HTMLElement): void { - containerElement.classList.add("ui-float"); - containerElement.innerHTML = ` - - -
-
- - - - - - - - - - - - - -
-
- `; - - // Set `settings-enabled` class on body element when keyboard shortcut overview is open. - const checkbox = containerElement.querySelector("#accordion-state-settings") as HTMLInputElement; - const bodyElement = document.querySelector("body") as HTMLBodyElement; - checkbox.addEventListener("change", () => { - if (checkbox.checked) { - bodyElement.classList.add("settings-enabled"); - } else { - bodyElement.classList.remove("settings-enabled"); - } - }); - - const themeOptionSelect = containerElement.querySelector("#setting-theme") as HTMLSelectElement; - this.themeManager.bindThemeSelect(themeOptionSelect); - - const hideEdgeLabelsCheckbox = containerElement.querySelector("#setting-hide-edge-labels") as HTMLInputElement; - this.settings.bindHideEdgeLabelsCheckbox(hideEdgeLabelsCheckbox); - - const simplifyNodeNamesCheckbox = containerElement.querySelector( - "#setting-simplify-node-names", - ) as HTMLInputElement; - this.settings.bindSimplifyNodeNamesCheckbox(simplifyNodeNamesCheckbox); - - const readOnlyCheckbox = containerElement.querySelector("#setting-read-only") as HTMLInputElement; - this.editorModeController.onModeChange((mode) => { - readOnlyCheckbox.checked = mode !== "edit"; - }); - if (this.editorModeController.isReadOnly()) { - readOnlyCheckbox.checked = true; - } - readOnlyCheckbox.addEventListener("change", () => { - this.dispatcher.dispatch(ChangeEditorModeAction.create(readOnlyCheckbox.checked ? "view" : "edit")); - }); - - const labelModeSelector = containerElement.querySelector("#setting-mode-option") as HTMLSelectElement; - this.settings.bindLabelModeSelector(labelModeSelector); - } -} diff --git a/frontend/webEditor/src/features/settingsMenu/themeManager.ts b/frontend/webEditor/src/features/settingsMenu/themeManager.ts deleted file mode 100644 index 068b05d7..00000000 --- a/frontend/webEditor/src/features/settingsMenu/themeManager.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { inject, injectable, multiInject } from "inversify"; -import { ActionDispatcher, TYPES } from "sprotty"; -import { ChangeThemeAction } from "./actions"; - -export enum Theme { - LIGHT = "Light", - DARK = "Dark", - SYSTEM_DEFAULT = "System Default", -} - -export const SWITCHABLE = Symbol("Switchable"); - -export interface Switchable { - switchTheme(useDark: boolean): void; -} - -@injectable() -export class ThemeManager { - private static _theme: Theme = Theme.SYSTEM_DEFAULT; - private static SYSTEM_DEFAULT = - window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? Theme.DARK : Theme.LIGHT; - private themeSelect?: HTMLSelectElement; - private static readonly localStorageKey = "dfdwebeditor:theme"; - - constructor( - @multiInject(SWITCHABLE) protected switchables: Switchable[], - @inject(TYPES.IActionDispatcher) protected readonly dispatcher: ActionDispatcher, - ) { - this.theme = (localStorage.getItem(ThemeManager.localStorageKey) ?? ThemeManager.SYSTEM_DEFAULT) as Theme; - } - - get useDarkMode(): boolean { - return ThemeManager.useDarkMode; - } - - static get useDarkMode(): boolean { - if (ThemeManager._theme == Theme.SYSTEM_DEFAULT) { - return ThemeManager.SYSTEM_DEFAULT == Theme.DARK; - } - return ThemeManager._theme == Theme.DARK; - } - - get theme(): Theme { - return ThemeManager._theme; - } - - set theme(theme: Theme) { - ThemeManager._theme = theme; - if (this.themeSelect) { - this.themeSelect.value = theme; - } - - const rootElement = document.querySelector(":root") as HTMLElement; - const sprottyElement = document.querySelector("#sprotty") as HTMLElement; - - const value = this.useDarkMode ? "dark" : "light"; - rootElement.setAttribute("data-theme", value); - sprottyElement.setAttribute("data-theme", value); - localStorage.setItem(ThemeManager.localStorageKey, theme); - - this.switchables.forEach((s) => s.switchTheme(this.useDarkMode)); - } - - bindThemeSelect(themeSelect: HTMLSelectElement) { - this.themeSelect = themeSelect; - this.themeSelect.value = this.theme; - this.themeSelect.addEventListener("change", () => { - this.dispatcher.dispatch(ChangeThemeAction.create(themeSelect.value as Theme)); - }); - } -} diff --git a/frontend/webEditor/src/fileName/di.config.ts b/frontend/webEditor/src/fileName/di.config.ts new file mode 100644 index 00000000..8ba255a5 --- /dev/null +++ b/frontend/webEditor/src/fileName/di.config.ts @@ -0,0 +1,6 @@ +import { ContainerModule } from "inversify"; +import { FileName } from "./fileName"; + +export const fileNameModule = new ContainerModule((bind) => { + bind(FileName).toSelf().inSingletonScope(); +}); diff --git a/frontend/webEditor/src/fileName/fileName.ts b/frontend/webEditor/src/fileName/fileName.ts new file mode 100644 index 00000000..30156179 --- /dev/null +++ b/frontend/webEditor/src/fileName/fileName.ts @@ -0,0 +1,14 @@ +export class FileName { + private name: string = "diagram"; + + getName(): string { + return this.name; + } + + setName(newName: string): void { + const lastIndex = newName.lastIndexOf("."); + this.name = lastIndex === -1 ? newName : newName.substring(0, lastIndex); + + document.title = this.name + ".json - DFD WebEditor"; + } +} diff --git a/frontend/webEditor/src/fitToScreen/action.ts b/frontend/webEditor/src/fitToScreen/action.ts new file mode 100644 index 00000000..6d2b1009 --- /dev/null +++ b/frontend/webEditor/src/fitToScreen/action.ts @@ -0,0 +1,16 @@ +import { SModelRootImpl } from "sprotty"; +import { SModelRoot, FitToScreenAction, getBasicType } from "sprotty-protocol"; + +export const FIT_TO_SCREEN_PADDING = 75; + +export namespace DefaultFitToScreenAction { + export function create(root: SModelRootImpl | SModelRoot, animate = true): FitToScreenAction { + const elementIds = + root.children?.filter((child) => getBasicType(child) === "node").map((child) => child.id) ?? []; + + return FitToScreenAction.create(elementIds, { + padding: FIT_TO_SCREEN_PADDING, + animate, + }); + } +} diff --git a/frontend/webEditor/src/helpUi/di.config.ts b/frontend/webEditor/src/helpUi/di.config.ts new file mode 100644 index 00000000..8fd5320c --- /dev/null +++ b/frontend/webEditor/src/helpUi/di.config.ts @@ -0,0 +1,10 @@ +import { ContainerModule } from "inversify"; +import { TYPES } from "sprotty"; +import { HelpUI } from "./helpUi"; +import { EDITOR_TYPES } from "../editorTypes"; + +export const helpUiModule = new ContainerModule((bind) => { + bind(HelpUI).toSelf().inSingletonScope(); + bind(TYPES.IUIExtension).toService(HelpUI); + bind(EDITOR_TYPES.DefaultUIElement).toService(HelpUI); +}); diff --git a/frontend/webEditor/src/helpUi/helpUi.css b/frontend/webEditor/src/helpUi/helpUi.css new file mode 100644 index 00000000..a5e496ec --- /dev/null +++ b/frontend/webEditor/src/helpUi/helpUi.css @@ -0,0 +1,17 @@ +.help-ui { + left: 20px; + bottom: 20px; + padding: 10px 10px; + + .help-accordion-icon::before { + content: ""; + background-image: url("@fortawesome/fontawesome-free/svgs/regular/circle-question.svg"); + display: inline-block; + filter: invert(var(--dark-mode)); + height: 16px; + width: 16px; + background-size: 16px 16px; + vertical-align: text-top; + margin-right: 4px; + } +} diff --git a/frontend/webEditor/src/helpUi/helpUi.ts b/frontend/webEditor/src/helpUi/helpUi.ts new file mode 100644 index 00000000..76f94676 --- /dev/null +++ b/frontend/webEditor/src/helpUi/helpUi.ts @@ -0,0 +1,43 @@ +import { injectable } from "inversify"; +import "./helpUi.css"; +import { AccordionUiExtension } from "../accordionUiExtension"; + +@injectable() +export class HelpUI extends AccordionUiExtension { + static readonly ID = "help-ui"; + + constructor() { + super("right", "up"); + } + + id() { + return HelpUI.ID; + } + + containerClass() { + return HelpUI.ID; + } + + protected initializeHidableContent(contentElement: HTMLElement) { + contentElement.innerHTML = ` +

CTRL+Space: Command Palette

+

CTRL+Z: Undo

+

CTRL+Shift+Z: Redo

+

Del: Delete selected elements

+

T: Toggle Label Type Edit UI

+

CTRL+O: Load diagram from json

+

CTRL+Shift+O: Open default diagram

+

CTRL+S: Save diagram to json

+

CTRL+L: Automatically layout diagram

+

CTRL+Shift+F: Fit diagram to screen

+

CTRL+C: Copy selected elements

+

CTRL+V: Paste previously copied elements

+

Esc: Disable current creation tool

+

Toggle Creation Tool: Refer to key in the tool palette

+ `; + } + protected initializeHeaderContent(headerElement: HTMLElement) { + headerElement.classList.add("help-accordion-icon"); + headerElement.innerText = "Keyboard Shortcuts | Help"; + } +} diff --git a/frontend/webEditor/src/index.ts b/frontend/webEditor/src/index.ts index 0b285e73..ce0f74b7 100644 --- a/frontend/webEditor/src/index.ts +++ b/frontend/webEditor/src/index.ts @@ -1,40 +1,30 @@ import "reflect-metadata"; - import { Container } from "inversify"; -import { - AbstractUIExtension, - ActionDispatcher, - CommitModelAction, - ILogger, - LocalModelSource, - SetUIExtensionVisibilityAction, - TYPES, - labelEditUiModule, - loadDefaultModules, -} from "sprotty"; -import { elkLayoutModule } from "sprotty-elk"; -import { autoLayoutModule } from "./features/autoLayout/di.config"; -import { commonModule } from "./common/di.config"; -import { noScrollLabelEditUiModule } from "./common/labelEditNoScroll"; -import { dfdLabelModule } from "./features/labels/di.config"; -import { toolPaletteModule } from "./features/toolPalette/di.config"; -import { serializeModule } from "./features/serialize/di.config"; -import { LoadDefaultDiagramAction } from "./features/serialize/loadDefaultDiagram"; -import { dfdElementsModule } from "./features/dfdElements/di.config"; -import { copyPasteModule } from "./features/copyPaste/di.config"; -import { EDITOR_TYPES } from "./utils"; -import { editorModeModule } from "./features/editorMode/di.config"; -import { constraintMenuModule } from "./features/constraintMenu/di.config"; - +import { loadDefaultModules, labelEditUiModule } from "sprotty"; import "sprotty/css/sprotty.css"; -import "sprotty/css/edit-label.css"; -import "./theme.css"; -import "./page.css"; -import { settingsModule } from "./features/settingsMenu/di.config"; -import { LoadDiagramAction } from "./features/serialize/load"; -import { commandPaletteModule } from "./features/commandPalette/di.config"; -import { LoadingIndicator } from "./common/loadingIndicator"; -import { LabelTypeRegistry } from "./features/labels/labelTypeRegistry"; +import "./assets/commonStyling.css"; +import "./assets/page.css"; +import "./assets/theme.css"; +import "@vscode/codicons/dist/codicon.css"; +import "@fortawesome/fontawesome-free/css/all.min.css"; +import { helpUiModule } from "./helpUi/di.config"; +import { IStartUpAgent, StartUpAgent } from "./startUpAgent/StartUpAgent"; +import { startUpAgentModule } from "./startUpAgent/di.config"; +import { commonModule } from "./commonModule"; +import { labelModule } from "./labels/di.config"; +import { serializeModule } from "./serialize/di.config"; +import { diagramModule } from "./diagram/di.config"; +import { webSocketModule } from "./webSocket/di.config"; +import { commandPaletteModule } from "./commandPalette/di.config"; +import { layoutModule } from "./layout/di.config"; +import { elkLayoutModule } from "sprotty-elk"; +import { fileNameModule } from "./fileName/di.config"; +import { settingsModule } from "./settings/di.config"; +import { toolPaletteModule } from "./toolPalette/di.config"; +import { constraintModule } from "./constraint/di.config"; +import { assignmentModule } from "./assignment/di.config"; +import { editorModeOverwritesModule } from "./editModeOverwrites/di.config"; +import { loadingIndicatorModule } from "./loadingIndicator/di.config"; const container = new Container(); @@ -45,128 +35,27 @@ loadDefaultModules(container, { ], }); -// sprotty-elk layouting extension -container.load(elkLayoutModule); - -// Custom modules that we provide ourselves container.load( + helpUiModule, commonModule, - settingsModule, - noScrollLabelEditUiModule, - autoLayoutModule, - dfdElementsModule, + startUpAgentModule, + labelModule, + diagramModule, serializeModule, - dfdLabelModule, - editorModeModule, - toolPaletteModule, - copyPasteModule, - constraintMenuModule, + webSocketModule, commandPaletteModule, + elkLayoutModule, + layoutModule, + fileNameModule, + settingsModule, + toolPaletteModule, + constraintModule, + assignmentModule, + editorModeOverwritesModule, + loadingIndicatorModule, ); -const dispatcher = container.get(TYPES.IActionDispatcher); -const defaultUIElements = container.getAll(EDITOR_TYPES.DefaultUIElement); -const modelSource = container.get(TYPES.ModelSource); -export const logger = container.get(TYPES.ILogger); - -let modelFileName = "diagram"; - -export function setModelFileName(name: string): void { - modelFileName = name; -} - -export function getModelFileName(): string { - return modelFileName; -} -export const labelTypeRegistry = container.get(LabelTypeRegistry); - -export function setModelSource(file: File): void { - modelSource - .setModel({ - type: "graph", - id: "root", - children: [], - }) - .then(() => - dispatcher.dispatchAll([ - // Show the default uis after startup - ...defaultUIElements.map((uiElement) => { - return SetUIExtensionVisibilityAction.create({ - extensionId: uiElement.id(), - visible: true, - }); - }), - // Then load the default diagram and commit the temporary model to the model source - LoadDiagramAction.create(file), - CommitModelAction.create(), - ]), - ) - .then(() => { - // Focus the sprotty svg container to enable keyboard shortcuts - // because those only work if the svg container is focused. - // Allows to e.g. use the file open shortcut without having to click - // on the sprotty svg container first. - const sprottySvgContainer = document.getElementById("sprotty_root"); - sprottySvgContainer?.focus(); - }) - .catch((error) => { - logger.error(null, "Failed to show default UIs and load default diagram", error); - }); +const startUpAgents = container.getAll(StartUpAgent); +for (const startUpAgent of startUpAgents) { + startUpAgent.run(); } - -function getQueryFileName(): string | null { - const urlParams = new URLSearchParams(window.location.search); - return urlParams.get("file"); -} - -// Set empty model as starting point. -// In contrast to the default diagram later this is not undoable which would bring the editor -// into an invalid state where no root element is present. -modelSource - .setModel({ - type: "graph", - id: "root", - children: [], - }) - .then(async () => { - const queryFileName = getQueryFileName(); - let queryFile: File | null = null; - if (queryFileName) { - try { - const response = await fetch(queryFileName); - if (!response.ok) { - throw new Error(`Failed to fetch file: ${response.statusText}`); - } - const blob = await response.blob(); - queryFile = new File([blob], queryFileName, { type: blob.type }); - } catch (error) { - logger.error(null, `Failed to load file from query parameter: ${queryFileName}`, error); - } - } - - dispatcher.dispatchAll([ - // Show the default uis after startup - ...defaultUIElements.map((uiElement) => { - return SetUIExtensionVisibilityAction.create({ - extensionId: uiElement.id(), - visible: true, - }); - }), - // Then load the default diagram or query diagram and commit the temporary model to the model source - queryFile ? LoadDiagramAction.create(queryFile) : LoadDefaultDiagramAction.create(), - CommitModelAction.create(), - ]); - }) - .then(() => { - // Focus the sprotty svg container to enable keyboard shortcuts - // because those only work if the svg container is focused. - // Allows to e.g. use the file open shortcut without having to click - // on the sprotty svg container first. - const sprottySvgContainer = document.getElementById("sprotty_root"); - sprottySvgContainer?.focus(); - }) - .catch((error) => { - logger.error(null, "Failed to show default UIs and load default diagram", error); - }); - -export const loadingIndicator = container.get(LoadingIndicator); diff --git a/frontend/webEditor/src/labels/LabelType.ts b/frontend/webEditor/src/labels/LabelType.ts new file mode 100644 index 00000000..1e7b78e1 --- /dev/null +++ b/frontend/webEditor/src/labels/LabelType.ts @@ -0,0 +1,15 @@ +export interface LabelType { + id: string; + name: string; + values: LabelTypeValue[]; +} + +export interface LabelTypeValue { + id: string; + text: string; +} + +export interface LabelAssignment { + labelTypeId: string; + labelTypeValueId: string; +} diff --git a/frontend/webEditor/src/labels/LabelTypeEditorUi.ts b/frontend/webEditor/src/labels/LabelTypeEditorUi.ts new file mode 100644 index 00000000..9f8af3b4 --- /dev/null +++ b/frontend/webEditor/src/labels/LabelTypeEditorUi.ts @@ -0,0 +1,244 @@ +import { AccordionUiExtension } from "../accordionUiExtension"; +import { UiElementFactory } from "../utils/UiElementFactory"; +import { LabelAssignment, LabelType } from "./LabelType"; +import { inject } from "inversify"; +import { LabelTypeRegistry } from "./LabelTypeRegistry"; + +import "./labelTypeEditorUi.css"; +import { dynamicallySetInputSize } from "../utils/TextSize"; +import { LABEL_ASSIGNMENT_MIME_TYPE } from "./dragAndDrop"; +import { AddLabelAssignmentAction } from "./assignmentCommand"; +import { IActionDispatcher, TYPES } from "sprotty"; +import { SETTINGS } from "../settings/Settings"; +import { EditorModeController } from "../settings/editorMode"; +import { ReplaceAction } from "./renameCommand"; + +export class LabelTypeEditorUi extends AccordionUiExtension { + static readonly ID = "label-type-editor-ui"; + private labelSectionContainer?: HTMLElement; + + constructor( + @inject(LabelTypeRegistry) private labelTypeRegistry: LabelTypeRegistry, + @inject(TYPES.IActionDispatcher) private readonly actionDispatcher: IActionDispatcher, + @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController, + ) { + super("left", "down"); + labelTypeRegistry.onUpdate(() => this.renderLabelTypes()); + } + + id(): string { + return LabelTypeEditorUi.ID; + } + containerClass(): string { + return LabelTypeEditorUi.ID; + } + + protected initializeHidableContent(contentElement: HTMLElement) { + const addButton = UiElementFactory.buildAddButton("Label Type"); + + addButton.onclick = () => { + if (this.editorModeController.isReadOnly()) { + return; + } + this.labelTypeRegistry.registerLabelType(""); + }; + + this.labelSectionContainer = document.createElement("div"); + this.renderLabelTypes(); + + contentElement.appendChild(this.labelSectionContainer); + contentElement.appendChild(addButton); + } + protected initializeHeaderContent(headerElement: HTMLElement) { + headerElement.innerText = "Label Types"; + } + + private renderLabelTypes(): void { + if (!this.labelSectionContainer) { + return; + } + const width = this.labelSectionContainer.scrollWidth; + const height = this.labelSectionContainer.scrollHeight; + this.labelSectionContainer.style.width = `${width}px`; + this.labelSectionContainer.style.height = `${height}px`; + const fragment = document.createDocumentFragment(); + const labelTypes = this.labelTypeRegistry.getLabelTypes(); + for (let i = 0; i < labelTypes.length; i++) { + fragment.appendChild(this.buildLabelTypeSection(labelTypes[i])); + if (i < labelTypes.length - 1) { + fragment.appendChild(document.createElement("hr")); + } + } + this.labelSectionContainer!.replaceChildren(fragment); + this.labelSectionContainer!.style.width = ""; + this.labelSectionContainer!.style.height = ""; + } + + private buildLabelTypeSection(labelType: LabelType): HTMLElement { + const section = document.createElement("div"); + section.classList.add("label-section"); + + const nameInput = document.createElement("input"); + nameInput.classList.add("label-type-name"); + const deleteButton = UiElementFactory.buildDeleteButton(); + const labelTypeValueHolder = document.createElement("div"); + labelTypeValueHolder.classList.add("label-type-values"); + const addButton = UiElementFactory.buildAddButton("Value"); + addButton.classList.add("label-type-value-add"); + + nameInput.value = labelType.name; + nameInput.placeholder = "Label Type Name"; + nameInput.oninput = (e: Event) => this.onInputHandler(e as InputEvent, nameInput); + dynamicallySetInputSize(nameInput); + setTimeout(() => dynamicallySetInputSize(nameInput), 0); + nameInput.onchange = () => { + if (this.editorModeController.isReadOnly()) { + return; + } + const replacements = labelType.values.map((t) => ({ + old: `${labelType.name}.${t}`, + replacement: `${nameInput.value}.${t}`, + type: "label", + })); + this.labelTypeRegistry.updateLabelTypeName(labelType.id, nameInput.value); + this.actionDispatcher.dispatch(ReplaceAction.create(replacements)); + }; + nameInput.onfocus = () => { + if (this.editorModeController.isReadOnly()) { + nameInput.blur(); + } + }; + + for (let i = 0; i < labelType.values.length; i++) { + labelTypeValueHolder.appendChild(this.buildLabelTypeValue(labelType, i)); + } + + addButton.onclick = () => { + if (this.editorModeController.isReadOnly()) { + return; + } + this.labelTypeRegistry.registerLabelTypeValue(labelType.id, ""); + }; + + deleteButton.onclick = () => { + if (this.editorModeController.isReadOnly()) { + return; + } + this.labelTypeRegistry.unregisterLabelType(labelType.id); + }; + + section.appendChild(nameInput); + section.appendChild(deleteButton); + section.appendChild(labelTypeValueHolder); + section.appendChild(addButton); + + return section; + } + + private buildLabelTypeValue(labelType: LabelType, valueIndex: number) { + const holder = document.createElement("div"); + holder.classList.add("label-type-value"); + const nameInput = document.createElement("input"); + nameInput.classList.add("label-type-value-name"); + const deleteButton = UiElementFactory.buildDeleteButton(); + + const value = labelType.values[valueIndex]; + + nameInput.value = value.text; + nameInput.placeholder = "Value"; + nameInput.oninput = (e: Event) => this.onInputHandler(e as InputEvent, nameInput); + nameInput.style.width = "0px"; + setTimeout(() => dynamicallySetInputSize(nameInput), 0); + + nameInput.onchange = () => { + if (this.editorModeController.isReadOnly()) { + return; + } + const replacements = [ + { + old: `${labelType.name}.${value.text}`, + replacement: `${labelType.name}.${nameInput.value}`, + type: "label", + }, + ]; + this.labelTypeRegistry.updateLabelTypeValueText(labelType.id, value.id, nameInput.value); + this.actionDispatcher.dispatch(ReplaceAction.create(replacements)); + }; + + deleteButton.onclick = () => { + if (this.editorModeController.isReadOnly()) { + return; + } + this.labelTypeRegistry.unregisterLabelTypeValue(labelType.id, value.id); + }; + + // Allow dragging to create a label assignment + nameInput.draggable = true; + nameInput.ondragstart = (event) => { + if (this.editorModeController.isReadOnly()) { + return; + } + const assignment: LabelAssignment = { + labelTypeId: labelType.id, + labelTypeValueId: value.id, + }; + const assignmentJson = JSON.stringify(assignment); + event.dataTransfer?.setData(LABEL_ASSIGNMENT_MIME_TYPE, assignmentJson); + }; + + // Only edit on double click + nameInput.onclick = () => { + if (this.editorModeController.isReadOnly()) { + return; + } + if (nameInput.getAttribute("clicked") === "true") { + return; + } + + nameInput.setAttribute("clicked", "true"); + setTimeout(() => { + if (nameInput.getAttribute("clicked") === "true") { + this.actionDispatcher.dispatch( + AddLabelAssignmentAction.create({ + labelTypeId: labelType.id, + labelTypeValueId: value.id, + }), + ); + nameInput.removeAttribute("clicked"); + } + }, 500); + }; + nameInput.ondblclick = () => { + if (this.editorModeController.isReadOnly()) { + return; + } + nameInput.removeAttribute("clicked"); + nameInput.focus(); + }; + nameInput.onfocus = (event) => { + if (this.editorModeController.isReadOnly()) { + nameInput.blur(); + return; + } + // we check for the single click here, since this gets triggered before the ondblclick event + if (nameInput.getAttribute("clicked") !== "true") { + event.preventDefault(); + // the blur needs to occur with a delay, as otherwise chromium browsers prevent the drag + setTimeout(() => { + nameInput.blur(); + }, 0); + } + }; + + holder.appendChild(nameInput); + holder.appendChild(deleteButton); + return holder; + } + + private onInputHandler(event: InputEvent, input: HTMLInputElement) { + if (!event.data?.match(/^[a-zA-Z0-9]*$/)) { + event.preventDefault(); + } + dynamicallySetInputSize(input); + } +} diff --git a/frontend/webEditor/src/labels/LabelTypeRegistry.ts b/frontend/webEditor/src/labels/LabelTypeRegistry.ts new file mode 100644 index 00000000..35ee3295 --- /dev/null +++ b/frontend/webEditor/src/labels/LabelTypeRegistry.ts @@ -0,0 +1,101 @@ +import { generateRandomSprottyId } from "../utils/idGenerator"; +import { LabelType, LabelTypeValue } from "./LabelType"; + +export class LabelTypeRegistry { + private labelTypes: LabelType[] = []; + private updateCallbacks: (() => void)[] = []; + + public registerLabelType(name: string): LabelType { + const labelType: LabelType = { + id: generateRandomSprottyId(), + name, + values: [], + }; + this.labelTypes.push(labelType); + this._registerLabelTypeValue(labelType.id, "Value", true); + this.labelTypeChanged(); + return labelType; + } + + public unregisterLabelType(id: string): void { + this.labelTypes = this.labelTypes.filter((type) => type.id !== id); + this.labelTypeChanged(); + } + + public updateLabelTypeName(id: string, name: string): void { + const labelType = this.labelTypes.find((l) => l.id === id); + if (!labelType) { + throw `No Label Type with id ${id} found`; + } + labelType.name = name; + this.labelTypeChanged(); + } + + public setLabelTypes(labelTypes: LabelType[]) { + this.labelTypes = labelTypes; + this.labelTypeChanged(); + } + + public registerLabelTypeValue(labelTypeId: string, text: string): LabelTypeValue { + return this._registerLabelTypeValue(labelTypeId, text); + } + + private _registerLabelTypeValue(labelTypeId: string, text: string, surpressUpdate = false): LabelTypeValue { + const labelTypeValue: LabelTypeValue = { + id: generateRandomSprottyId(), + text, + }; + const labelType = this.labelTypes.find((type) => type.id === labelTypeId); + if (!labelType) { + throw `No Label Type with id ${labelTypeId} found`; + } + labelType.values.push(labelTypeValue); + if (!surpressUpdate) { + this.labelTypeChanged(); + } + return labelTypeValue; + } + + public unregisterLabelTypeValue(labelTypeId: string, labelTypeValueId: string): void { + const labelType = this.labelTypes.find((type) => type.id === labelTypeId); + if (!labelType) { + throw `No Label Type with id ${labelTypeId} found`; + } + labelType.values = labelType.values.filter((value) => value.id !== labelTypeValueId); + this.labelTypeChanged(); + } + + public updateLabelTypeValueText(labelTypeId: string, labelTypeValueId: string, text: string) { + const labelType = this.labelTypes.find((type) => type.id === labelTypeId); + if (!labelType) { + throw `No Label Type with id ${labelTypeId} found`; + } + const value = labelType.values.find((l) => l.id === labelTypeValueId); + if (!value) { + throw `Label Type ${labelType.name} has no value with id ${labelTypeValueId}`; + } + value.text = text; + this.labelTypeChanged(); + } + + public clearLabelTypes(): void { + this.labelTypes = []; + this.updateCallbacks.forEach((cb) => cb()); + } + + public labelTypeChanged(): void { + this.updateCallbacks.forEach((cb) => cb()); + } + + public onUpdate(callback: () => void): void { + this.updateCallbacks.push(callback); + } + + public getLabelTypes(): LabelType[] { + return this.labelTypes; + } + + public getLabelType(id: string): LabelType | undefined { + return this.labelTypes.find((type) => type.id === id); + } +} diff --git a/frontend/webEditor/src/labels/assignmentCommand.ts b/frontend/webEditor/src/labels/assignmentCommand.ts new file mode 100644 index 00000000..4a63efe5 --- /dev/null +++ b/frontend/webEditor/src/labels/assignmentCommand.ts @@ -0,0 +1,159 @@ +import { + Command, + CommandExecutionContext, + CommandReturn, + ISnapper, + isSelected, + SChildElementImpl, + SModelElementImpl, + SNodeImpl, + TYPES, +} from "sprotty"; +import { LabelAssignment } from "./LabelType"; +import { Action } from "sprotty-protocol"; +import { snapPortsOfNode } from "../diagram/ports/portSnapper"; +import { EditorModeController } from "../settings/editorMode"; +import { inject, injectable } from "inversify"; +import { SETTINGS } from "../settings/Settings"; +import { ContainsDfdLabels, containsDfdLabels } from "./feature"; + +interface LabelAssignmentAction extends Action { + action: "add" | "remove"; + element?: ContainsDfdLabels & SNodeImpl; + labelAssignment: LabelAssignment; +} + +export namespace AddLabelAssignmentAction { + export function create( + labelAssignment: LabelAssignment, + element?: ContainsDfdLabels & SNodeImpl, + ): LabelAssignmentAction { + return { + kind: LabelAssignmentCommand.KIND, + action: "add", + labelAssignment, + element, + }; + } +} + +export namespace RemoveLabelAssignmentAction { + export function create( + labelAssignment: LabelAssignment, + element?: ContainsDfdLabels & SNodeImpl, + ): LabelAssignmentAction { + return { + kind: LabelAssignmentCommand.KIND, + action: "remove", + labelAssignment, + element, + }; + } +} + +@injectable() +export class LabelAssignmentCommand implements Command { + public static readonly KIND = "labelAction"; + + private elements?: ContainsDfdLabels[]; + + constructor( + @inject(TYPES.Action) private readonly action: LabelAssignmentAction, + @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController, + @inject(TYPES.ISnapper) private readonly snapper: ISnapper, + ) {} + + execute(context: CommandExecutionContext): CommandReturn { + if (this.editorModeController.isReadOnly()) { + return context.root; + } + if (this.action.element) { + this.elements = [this.action.element]; + } else { + const allElements = getAllElements(context.root.children); + this.elements = allElements.filter((element) => isSelected(element) && containsDfdLabels(element)); + } + + if (this.action.action == "add") { + this.addLabel(); + } else { + this.removeLabel(); + } + + return context.root; + } + + undo(context: CommandExecutionContext): CommandReturn { + if (this.editorModeController.isReadOnly()) { + return context.root; + } + + if (this.action.action == "add") { + this.removeLabel(); + } else { + this.addLabel(); + } + + return context.root; + } + + redo(context: CommandExecutionContext): CommandReturn { + if (this.editorModeController.isReadOnly()) { + return context.root; + } + + if (this.action.action == "add") { + this.addLabel(); + } else { + this.removeLabel(); + } + + return context.root; + } + + private addLabel() { + this.elements?.forEach((element) => { + const hasBeenAdded = + element.labels.find((as) => { + return ( + as.labelTypeId === this.action.labelAssignment.labelTypeId && + as.labelTypeValueId === this.action.labelAssignment.labelTypeValueId + ); + }) !== undefined; + if (!hasBeenAdded) { + element.labels.push(this.action.labelAssignment); + if (element instanceof SNodeImpl) { + snapPortsOfNode(element, this.snapper); + } + } + }); + } + + private removeLabel() { + this.elements?.forEach((element) => { + const labels = element.labels; + const idx = labels.findIndex( + (l) => + l.labelTypeId == this.action.labelAssignment.labelTypeId && + l.labelTypeValueId == this.action.labelAssignment.labelTypeValueId, + ); + if (idx >= 0) { + labels.splice(idx, 1); + if (element instanceof SNodeImpl) { + snapPortsOfNode(element, this.snapper); + } + } + }); + } +} + +function getAllElements(elements: readonly SChildElementImpl[]): SModelElementImpl[] { + const elementsList: SModelElementImpl[] = []; + for (const element of elements) { + elementsList.push(element); + if ("children" in element) { + elementsList.push(...getAllElements(element.children)); + } + } + return elementsList; +} diff --git a/frontend/webEditor/src/labels/di.config.ts b/frontend/webEditor/src/labels/di.config.ts new file mode 100644 index 00000000..2ceda7fc --- /dev/null +++ b/frontend/webEditor/src/labels/di.config.ts @@ -0,0 +1,20 @@ +import { ContainerModule } from "inversify"; +import { LabelTypeRegistry } from "./LabelTypeRegistry"; +import { LabelTypeEditorUi } from "./LabelTypeEditorUi"; +import { configureCommand, TYPES } from "sprotty"; +import { EDITOR_TYPES } from "../editorTypes"; +import { LabelAssignmentCommand } from "./assignmentCommand"; +import { DfdLabelMouseDropListener } from "./dragAndDrop"; +import { ReplaceCommand } from "./renameCommand"; + +export const labelModule = new ContainerModule((bind, _, isBound) => { + bind(LabelTypeRegistry).toSelf().inSingletonScope(); + + bind(LabelTypeEditorUi).toSelf().inSingletonScope(); + bind(TYPES.IUIExtension).toService(LabelTypeEditorUi); + bind(EDITOR_TYPES.DefaultUIElement).to(LabelTypeEditorUi); + + configureCommand({ bind, isBound }, LabelAssignmentCommand); + configureCommand({ bind, isBound }, ReplaceCommand); + bind(TYPES.MouseListener).to(DfdLabelMouseDropListener); +}); diff --git a/frontend/webEditor/src/features/labels/dropListener.ts b/frontend/webEditor/src/labels/dragAndDrop.ts similarity index 80% rename from frontend/webEditor/src/features/labels/dropListener.ts rename to frontend/webEditor/src/labels/dragAndDrop.ts index f05bbff8..2b0fef80 100644 --- a/frontend/webEditor/src/features/labels/dropListener.ts +++ b/frontend/webEditor/src/labels/dragAndDrop.ts @@ -1,5 +1,4 @@ import { injectable, inject } from "inversify"; -import { LabelAssignment } from "./labelTypeRegistry"; import { Action } from "sprotty-protocol"; import { SModelElementImpl, @@ -10,8 +9,9 @@ import { TYPES, SNodeImpl, } from "sprotty"; -import { AddLabelAssignmentAction } from "./commands"; -import { getParentWithDfdLabels } from "./elementFeature"; +import { LabelAssignment } from "./LabelType"; +import { AddLabelAssignmentAction } from "./assignmentCommand"; +import { containsDfdLabels, ContainsDfdLabels } from "./feature"; export const LABEL_ASSIGNMENT_MIME_TYPE = "application/x-label-assignment"; @@ -58,3 +58,17 @@ export class DfdLabelMouseDropListener extends MouseListener { return [AddLabelAssignmentAction.create(labelAssignment, dfdLabelElement), CommitModelAction.create()]; } } + +function getParentWithDfdLabels( + element: SChildElementImpl | SModelElementImpl, +): (SModelElementImpl & ContainsDfdLabels) | undefined { + if (containsDfdLabels(element)) { + return element; + } + + if ("parent" in element) { + return getParentWithDfdLabels(element.parent); + } + + return undefined; +} diff --git a/frontend/webEditor/src/labels/feature.ts b/frontend/webEditor/src/labels/feature.ts new file mode 100644 index 00000000..2cf25d0f --- /dev/null +++ b/frontend/webEditor/src/labels/feature.ts @@ -0,0 +1,12 @@ +import { SModelElementImpl } from "sprotty"; +import { LabelAssignment } from "./LabelType"; + +export const containsDfdLabelFeature = Symbol("dfd-label-feature"); + +export interface ContainsDfdLabels extends SModelElementImpl { + labels: LabelAssignment[]; +} + +export function containsDfdLabels(element: T): element is T & ContainsDfdLabels { + return element.features?.has(containsDfdLabelFeature) ?? false; +} diff --git a/frontend/webEditor/src/labels/labelTypeEditorUi.css b/frontend/webEditor/src/labels/labelTypeEditorUi.css new file mode 100644 index 00000000..f1ac3397 --- /dev/null +++ b/frontend/webEditor/src/labels/labelTypeEditorUi.css @@ -0,0 +1,48 @@ +.label-type-editor-ui { + padding: 10px; + top: 150px; + right: 40px; + /* limit height so that max width has still 40px to bottom edge of the parent element + 100% is the full sprotty viewer height, 150px the space above the element, + 40px is the space that should be left under the editor and 2*10px is the padding */ + max-height: calc(100vh - 150px - 40px - 2 * 10px); + overflow: auto; + + * { + color: var(--color-foreground); + } + .codicon { + vertical-align: middle; + } + + hr { + height: 1px; + border: 0; + background-color: var(--color-foreground); + } + + input { + background-color: transparent; + outline: none; + border: none; + } + + .label-type-name { + font-size: 12pt; + } + + .label-type-values, + .label-type-value-add { + margin-left: 10px; + } +} + +/* Label Type value */ +.label-type-value input { + background-color: var(--color-background); + text-align: center; + border: 1px solid var(--color-foreground); + border-radius: 15px; + padding: 3px; + margin: 4px; +} diff --git a/frontend/webEditor/src/labels/registryCommands.ts b/frontend/webEditor/src/labels/registryCommands.ts new file mode 100644 index 00000000..c59eed93 --- /dev/null +++ b/frontend/webEditor/src/labels/registryCommands.ts @@ -0,0 +1,109 @@ +import { Action } from "sprotty-protocol"; +import { LabelTypeRegistry } from "./LabelTypeRegistry"; +import { Command, CommandExecutionContext, CommandReturn, SParentElementImpl, TYPES } from "sprotty"; +import { DfdNodeImpl } from "../diagram/nodes/common"; +import { LabelAssignment } from "./LabelType"; +import { inject } from "inversify"; + +// TODO: readd +abstract class LabelCommand implements Command { + constructor(protected labelTypeRegistry: LabelTypeRegistry) {} + abstract execute(context: CommandExecutionContext): CommandReturn; + abstract undo(context: CommandExecutionContext): CommandReturn; + abstract redo(context: CommandExecutionContext): CommandReturn; + + addLabelType() { + return this.labelTypeRegistry.registerLabelType("").id; + } + + deleteLabelType(id: string, root: SParentElementImpl) { + this.labelTypeRegistry.unregisterLabelType(id); + this.removeLabelAssignments(root, (a) => a.labelTypeId === id); + } + + addLabelTypeValue(typeId: string) { + return this.labelTypeRegistry.registerLabelTypeValue(typeId, "").id; + } + + deleteLabelTypeValue(typeId: string, valueId: string, root: SParentElementImpl) { + this.labelTypeRegistry.unregisterLabelTypeValue(typeId, valueId); + this.removeLabelAssignments(root, (a) => a.labelTypeId === typeId && a.labelTypeValueId === valueId); + } + + removeLabelAssignments(node: SParentElementImpl, filter: (s: LabelAssignment) => boolean) { + if (node instanceof DfdNodeImpl) { + node.labels = node.labels.filter(filter); + } + for (const child of node.children) { + this.removeLabelAssignments(child, filter); + } + } +} + +export namespace AddLabelTypeAction { + export const KIND = "add-label-type"; + export function create(): Action { + return { kind: KIND }; + } +} + +export class AddLabelTypeCommand extends LabelCommand { + static readonly KIND = AddLabelTypeAction.KIND; + private addedId?: string; + + constructor(@inject(TYPES.Action) _: Action, @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry) { + super(labelTypeRegistry); + } + + execute(context: CommandExecutionContext): CommandReturn { + this.addedId = this.addLabelType(); + return context.root; + } + undo(context: CommandExecutionContext): CommandReturn { + this.deleteLabelType(this.addedId!, context.root); + return context.root; + } + redo(context: CommandExecutionContext): CommandReturn { + this.addedId = this.addLabelType(); + return context.root; + } +} + +interface AddLabelTypeValueAction extends Action { + typeId: string; +} + +namespace AddLabelTypeValueAction { + export const KIND = "add-label-type-value"; + export function create(typeId: string): AddLabelTypeValueAction { + return { + kind: KIND, + typeId, + }; + } +} + +export class AddLabelTypeValueCommand extends LabelCommand { + static readonly KIND = AddLabelTypeValueAction.KIND; + private addedId?: string; + + constructor( + @inject(TYPES.Action) private action: AddLabelTypeValueAction, + @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, + ) { + super(labelTypeRegistry); + } + + execute(context: CommandExecutionContext): CommandReturn { + this.addedId = this.addLabelTypeValue(this.action.typeId); + return context.root; + } + undo(context: CommandExecutionContext): CommandReturn { + this.deleteLabelTypeValue(this.action.typeId, this.addedId!, context.root); + return context.root; + } + redo(context: CommandExecutionContext): CommandReturn { + this.addedId = this.addLabelTypeValue(this.action.typeId); + return context.root; + } +} diff --git a/frontend/webEditor/src/labels/renameCommand.ts b/frontend/webEditor/src/labels/renameCommand.ts new file mode 100644 index 00000000..003d43b9 --- /dev/null +++ b/frontend/webEditor/src/labels/renameCommand.ts @@ -0,0 +1,74 @@ +import { Command, CommandExecutionContext, CommandReturn, LocalModelSource, SParentElementImpl, TYPES } from "sprotty"; +import { Action } from "sprotty-protocol"; +import { ConstraintRegistry } from "../constraint/constraintRegistry"; +import { inject } from "inversify"; +import { replace, ReplacementData } from "../languages/replace"; +import { DfdOutputPortImpl } from "../diagram/ports/DfdOutputPort"; +import { LabelTypeRegistry } from "./LabelTypeRegistry"; +import { AssignmentLanguageTreeBuilder } from "../assignment/language"; +import { ConstraintDslTreeBuilder } from "../constraint/language"; + +interface ReplaceAction extends Action { + replacements: ReplacementData[]; +} + +export namespace ReplaceAction { + export const KIND = "replace-action"; + export function create(replacements: ReplacementData[]) { + return { + kind: KIND, + replacements, + }; + } +} + +export class ReplaceCommand extends Command { + static readonly KIND = ReplaceAction.KIND; + + constructor( + @inject(TYPES.Action) private readonly action: ReplaceAction, + @inject(ConstraintRegistry) private readonly constraintRegistry: ConstraintRegistry, + @inject(LabelTypeRegistry) private readonly labelTypeRegistry: LabelTypeRegistry, + @inject(TYPES.ModelSource) private readonly localModelSource: LocalModelSource, + ) { + super(); + } + + execute(context: CommandExecutionContext): CommandReturn { + this.iterateForPorts(context.root); + for (const replacement of this.action.replacements) { + this.constraintRegistry.setConstraints( + replace( + this.constraintRegistry.getConstraintsAsText().split("\n"), + ConstraintDslTreeBuilder.buildTree(this.localModelSource, this.labelTypeRegistry), + replacement, + ), + ); + } + return context.root; + } + undo(context: CommandExecutionContext): CommandReturn { + return context.root; + } + redo(context: CommandExecutionContext): CommandReturn { + return context.root; + } + + iterateForPorts(element: SParentElementImpl) { + if (element instanceof DfdOutputPortImpl) { + for (const replacement of this.action.replacements) { + element.setBehavior( + replace( + element.getBehavior().split("\n"), + AssignmentLanguageTreeBuilder.buildTree(element, this.labelTypeRegistry), + replacement, + ).join("\n"), + ); + } + } + + for (const child of element.children) { + this.iterateForPorts(child); + } + } +} diff --git a/frontend/webEditor/src/languages/autocomplete.ts b/frontend/webEditor/src/languages/autocomplete.ts new file mode 100644 index 00000000..856c77fb --- /dev/null +++ b/frontend/webEditor/src/languages/autocomplete.ts @@ -0,0 +1,108 @@ +import { LanguageTreeNode, Token, tokenize } from "./tokenize"; +import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; +import { VerifyWord } from "./verify"; + +interface RequiredCompletionParts { + kind: monaco.languages.CompletionItemKind; + insertText: string; + startOffset?: number; +} + +export type WordCompletion = RequiredCompletionParts & Partial; + +export interface CompletionWord extends VerifyWord { + completionOptions(currentWord: string): WordCompletion[]; +} +type CompletionLanguageTreeNode = LanguageTreeNode; + +export function complete(tokens: Token[], tree: CompletionLanguageTreeNode[]): monaco.languages.CompletionItem[] { + return transformResults(completeNode(tokens, tree, 0, tree), tokens); +} + +function completeNode( + tokens: Token[], + nodes: CompletionLanguageTreeNode[], + index: number, + roots: CompletionLanguageTreeNode[], + cameFromFinal = false, + skipStartCheck = false, +): WordCompletion[] { + // check for new start + if (!skipStartCheck && tokens[index].column == 1) { + const matchesAnyRoot = roots.some((n) => n.word.verify(tokens[index].text).length === 0); + if (matchesAnyRoot) { + return completeNode(tokens, roots, index, roots, cameFromFinal, true); + } else if (cameFromFinal || nodes.length == 0) { + return completeNode(tokens, [...roots, ...nodes], index, roots, cameFromFinal, true); + } + } + + let result: WordCompletion[] = []; + if (index == tokens.length - 1) { + for (const node of nodes) { + result = result.concat(node.word.completionOptions(tokens[index].text)); + } + return result; + } + for (const n of nodes) { + if (n.word.verify(tokens[index].text).length > 0) { + continue; + } + result = result.concat(completeNode(tokens, n.children, index + 1, roots, n.canBeFinal || false)); + } + return result; +} + +function transformResults(comp: WordCompletion[], tokens: Token[]): monaco.languages.CompletionItem[] { + const result: monaco.languages.CompletionItem[] = []; + const filtered = comp.filter( + (c, idx) => comp.findIndex((c2) => c2.insertText === c.insertText && c2.kind === c.kind) === idx, + ); + for (const c of filtered) { + const r = transformResult(c, tokens); + result.push(r); + } + return result; +} + +function transformResult(comp: WordCompletion, tokens: Token[]): monaco.languages.CompletionItem { + const wordStart = tokens.length == 0 ? 1 : tokens[tokens.length - 1].column; + const lineNumber = tokens.length == 0 ? 1 : tokens[tokens.length - 1].line; + return { + insertText: comp.insertText, + kind: comp.kind, + label: comp.label ?? comp.insertText, + insertTextRules: comp.insertTextRules, + range: new monaco.Range( + lineNumber, + wordStart + (comp.startOffset ?? 0), + lineNumber, + wordStart + (comp.startOffset ?? 0) + comp.insertText.length, + ), + }; +} + +export class DfdCompletionItemProvider implements monaco.languages.CompletionItemProvider { + constructor(private tree: CompletionLanguageTreeNode[]) {} + + triggerCharacters = [".", "(", " ", ","]; + + provideCompletionItems( + model: monaco.editor.ITextModel, + position: monaco.Position, + ): monaco.languages.ProviderResult { + const allLines = model.getLinesContent(); + const includedLines: string[] = []; + for (let i = 0; i < position.lineNumber - 1; i++) { + includedLines.push(allLines[i]); + } + const currentLine = allLines[position.lineNumber - 1].substring(0, position.column - 1); + includedLines.push(currentLine); + + const tokens = tokenize(includedLines); + const r = complete(tokens, this.tree); + return { + suggestions: r, + }; + } +} diff --git a/frontend/webEditor/src/languages/replace.ts b/frontend/webEditor/src/languages/replace.ts new file mode 100644 index 00000000..39a0d636 --- /dev/null +++ b/frontend/webEditor/src/languages/replace.ts @@ -0,0 +1,84 @@ +import { LanguageTreeNode, Token, tokenize } from "./tokenize"; +import { VerifyWord } from "./verify"; + +export interface ReplaceableWord { + replace?: (text: string, replacement: ReplacementData) => string; +} + +export interface ReplacementData { + old: string; + replacement: string; + type: string; +} + +export interface ReplacedToken extends Token { + newText: string; +} + +export function replace( + lines: string[], + tree: LanguageTreeNode[], + replacement: ReplacementData, +): string[] { + const tokens = tokenize(lines); + const replaced = replaceTokens(tokens, tree, tree, 0, replacement); + for (let i = 0; i < tokens.length; i++) { + replaceToken(i); + } + return lines; + + function replaceToken(index: number) { + const token = replaced[index]; + const lengthDiff = token.newText.length - token.text.length; + const lineIndex = token.line - 1; + const line = lines[lineIndex]; + const before = line.substring(0, token.column - 1); + const after = line.substring(token.column - 1 + token.text.length); + lines[lineIndex] = before + token.newText + after; + let i = index + 1; + // adjust the column of all following tokens on the same line + while (i < tokens.length && tokens[i].line == token.line) { + replaced[i].column += lengthDiff; + i++; + } + } +} + +function replaceTokens( + tokens: Token[], + tree: LanguageTreeNode[], + roots: LanguageTreeNode[], + index: number, + replacement: ReplacementData, + skipStartCheck = false, +): ReplacedToken[] { + if (index >= tokens.length) { + return []; + } + // check for new start + if (!skipStartCheck && tokens[index].column == 1) { + const matchesAnyRoot = roots.some((n) => n.word.verify(tokens[index].text).length === 0); + if (matchesAnyRoot) { + return replaceTokens(tokens, roots, roots, index, replacement, true); + } + } + let newText = tokens[index].text; + for (const n of tree) { + if (n.word.replace) { + newText = n.word.replace(newText, replacement); + } + } + return [ + { + ...tokens[index], + newText, + }, + ...replaceTokens( + tokens, + tree.flatMap((n) => n.children), + roots, + index + 1, + replacement, + ), + ]; +} diff --git a/frontend/webEditor/src/languages/tokenize.ts b/frontend/webEditor/src/languages/tokenize.ts new file mode 100644 index 00000000..8669c4bb --- /dev/null +++ b/frontend/webEditor/src/languages/tokenize.ts @@ -0,0 +1,44 @@ +export interface Token { + text: string; + line: number; + column: number; +} + +export interface LanguageTreeNode { + word: W; + children: LanguageTreeNode[]; + canBeFinal?: boolean; + viewAsLeaf?: boolean; +} + +export function tokenize(text: string[]): Token[] { + if (!text || text.length == 0) { + return []; + } + + const tokens: Token[] = []; + for (const [lineNumber, line] of text.entries()) { + const lineTokens = line.split(/(\s+)/); + let column = 0; + for (let i = 0; i < lineTokens.length; i++) { + const token = lineTokens[i]; + if (!token.match(/\s+/) && token.length > 0) { + tokens.push({ + text: token, + line: lineNumber + 1, + column: column + 1, + }); + } + column += token.length; + } + if (line.match(/\s$/) || line.length == 0) { + tokens.push({ + text: "", + line: lineNumber + 1, + column: column + 1, + }); + } + } + + return tokens; +} diff --git a/frontend/webEditor/src/languages/verify.ts b/frontend/webEditor/src/languages/verify.ts new file mode 100644 index 00000000..6dfb8f52 --- /dev/null +++ b/frontend/webEditor/src/languages/verify.ts @@ -0,0 +1,85 @@ +import { LanguageTreeNode, Token } from "./tokenize"; + +export interface ValidationError { + message: string; + line: number; + startColumn: number; + endColumn: number; +} + +export interface VerifyWord { + verify: (word: string) => string[]; +} +type VerifyLanguageTreeNode = LanguageTreeNode; + +export function verify(tokens: Token[], tree: VerifyLanguageTreeNode[]): ValidationError[] { + return verifyNode(tokens, tree, 0, false, tree, true); +} + +function verifyNode( + tokens: Token[], + nodes: VerifyLanguageTreeNode[], + index: number, + comesFromFinal: boolean, + roots: VerifyLanguageTreeNode[], + skipStartCheck = false, +): ValidationError[] { + if (index >= tokens.length) { + if (nodes.length == 0 || comesFromFinal) { + return []; + } else { + return [ + { + message: "Unexpected end of line", + line: tokens[index - 1].line, + startColumn: tokens[index - 1].column + tokens[index - 1].text.length - 1, + endColumn: tokens[index - 1].column + tokens[index - 1].text.length, + }, + ]; + } + } + if (!skipStartCheck && tokens[index].column == 1) { + const matchesAnyRoot = roots.some((r) => r.word.verify(tokens[index].text).length === 0); + if (matchesAnyRoot) { + return verifyNode(tokens, roots, index, false, roots, true); + } + } + + const foundErrors: ValidationError[] = []; + let childErrors: ValidationError[] = []; + for (const n of nodes) { + const v = n.word.verify(tokens[index].text); + if (v.length > 0) { + foundErrors.push({ + message: v[0], + startColumn: tokens[index].column, + endColumn: tokens[index].column + tokens[index].text.length, + line: tokens[index].line, + }); + continue; + } + + const childResult = verifyNode(tokens, n.children, index + 1, n.canBeFinal || false, roots); + if (childResult.length == 0) { + return []; + } else { + childErrors = childErrors.concat(childResult); + } + } + if (childErrors.length > 0) { + return deduplicateErrors(childErrors); + } + return deduplicateErrors(foundErrors); +} + +function deduplicateErrors(errors: ValidationError[]): ValidationError[] { + const seen = new Set(); + return errors.filter((error) => { + const key = `${error.line}-${error.startColumn}-${error.endColumn}-${error.message}`; + if (seen.has(key)) { + return false; + } + seen.add(key); + return true; + }); +} diff --git a/frontend/webEditor/src/languages/words.ts b/frontend/webEditor/src/languages/words.ts new file mode 100644 index 00000000..2b41ac48 --- /dev/null +++ b/frontend/webEditor/src/languages/words.ts @@ -0,0 +1,100 @@ +import { CompletionWord, WordCompletion } from "./autocomplete"; +import { ReplaceableWord, ReplacementData } from "./replace"; +import { VerifyWord } from "./verify"; +import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; + +export type Word = VerifyWord & CompletionWord & ReplaceableWord; + +export class ConstantWord implements Word { + constructor(private readonly word: string) {} + + verify(word: string) { + if (word === this.word) { + return []; + } else { + return [`Expected keyword "${this.word}"`]; + } + } + completionOptions(): WordCompletion[] { + return [ + { + insertText: this.word, + kind: monaco.languages.CompletionItemKind.Keyword, + }, + ]; + } +} + +export class AnyWord implements Word { + verify(word: string): string[] { + if (word.length > 0) { + return []; + } else { + return ["Expected a symbol"]; + } + } + completionOptions(): WordCompletion[] { + return []; + } +} + +export class NegatableWord implements Word { + constructor(protected word: Word) {} + + verify(word: string): string[] { + if (word.startsWith("!")) { + return this.word.verify(word.substring(1)); + } + return this.word.verify(word); + } + + completionOptions(part: string): WordCompletion[] { + if (part.startsWith("!")) { + const options = this.word.completionOptions(part.substring(1)); + return options.map((o) => ({ + ...o, + startOffset: (o.startOffset ?? 0) + 1, + })); + } + return this.word.completionOptions(part); + } + + replace(text: string, replacement: ReplacementData): string { + if (!this.word.replace) { + return text; + } + if (text.startsWith("!")) { + return this.replace(text.substring(1), replacement); + } + return this.word.replace(text, replacement); + } +} + +export class ListWord implements Word { + constructor(protected word: Word) {} + + verify(word: string): string[] { + const parts = word.split(","); + for (const part of parts) { + const verify = this.word.verify(part); + if (verify.length > 0) { + return verify; + } + } + return []; + } + completionOptions(word: string): WordCompletion[] { + const parts = word.split(","); + const last = parts[parts.length - 1]; + + return this.word.completionOptions(last); + } + + replace(text: string, replacement: ReplacementData): string { + if (!this.word.replace) { + return text; + } + const parts = text.split(","); + return parts.map((p) => this.word.replace!(p, replacement)).join(","); + } +} diff --git a/frontend/webEditor/src/features/autoLayout/command.ts b/frontend/webEditor/src/layout/command.ts similarity index 51% rename from frontend/webEditor/src/features/autoLayout/command.ts rename to frontend/webEditor/src/layout/command.ts index 849dc4b0..36775bfa 100644 --- a/frontend/webEditor/src/features/autoLayout/command.ts +++ b/frontend/webEditor/src/layout/command.ts @@ -1,10 +1,9 @@ -import { inject, optional } from "inversify"; +import { inject } from "inversify"; import { Command, CommandExecutionContext, SModelRootImpl, TYPES } from "sprotty"; -import { Action, IModelLayoutEngine, SGraph, SModelRoot } from "sprotty-protocol"; -import { LoadDiagramCommand } from "../serialize/load"; -import { LoadingIndicator } from "../../common/loadingIndicator"; -import { LayoutMethod } from "../settingsMenu/LayoutMethod"; -import { LayoutMethodWrapper } from "./layouter"; +import { Action, IModelLayoutEngine, SGraph } from "sprotty-protocol"; +import { DfdLayoutConfigurator } from "./layouter"; +import { LayoutMethod } from "./layoutMethod"; +import { LoadingIndicator } from "../loadingIndicator/loadingIndicator"; export interface LayoutModelAction extends Action { kind: typeof LayoutModelAction.KIND; @@ -24,30 +23,23 @@ export namespace LayoutModelAction { export class LayoutModelCommand extends Command { static readonly KIND = LayoutModelAction.KIND; - @inject(TYPES.IModelLayoutEngine) - private readonly layoutEngine?: IModelLayoutEngine; - - @inject(LoadingIndicator) - @optional() - private readonly loadingIndicator?: LoadingIndicator; - - private oldModelSchema?: SModelRoot; + private oldRoot?: SModelRootImpl; private newModel?: SModelRootImpl; constructor( @inject(TYPES.Action) private readonly action: LayoutModelAction, - @inject(LayoutMethodWrapper) private readonly method: LayoutMethodWrapper, + @inject(TYPES.IModelLayoutEngine) private readonly layoutEngine: IModelLayoutEngine, + @inject(DfdLayoutConfigurator) private readonly configurator: DfdLayoutConfigurator, + @inject(LoadingIndicator) private readonly loadingIndicator: LoadingIndicator, ) { super(); } async execute(context: CommandExecutionContext): Promise { - this.loadingIndicator?.showIndicator("Layouting..."); - this.oldModelSchema = context.modelFactory.createSchema(context.root); + this.loadingIndicator.showIndicator("Layouting..."); + this.oldRoot = context.root; - if (!this.layoutEngine) throw new Error("Missing injects"); - - this.method.layoutMethod = this.action.layoutMethod + this.configurator.method = this.action.layoutMethod; // Layouting is normally done on the graph schema. // This is not viable for us because the dfd nodes have a dynamically computed size. // This is only available on loaded classes of the elements, not the json schema. @@ -58,30 +50,15 @@ export class LayoutModelCommand extends Command { // Here we need to cast back. this.newModel = newModel as unknown as SModelRootImpl; - this.loadingIndicator?.hideIndicator(); + this.loadingIndicator.hideIndicator(); return this.newModel; } undo(context: CommandExecutionContext): SModelRootImpl { - if (!this.oldModelSchema) { - // No old schema saved because the layout was not executed due to read-only mode. - return context.root; - } - this.loadingIndicator?.showIndicator("Undoing layouting..."); - - LoadDiagramCommand.preprocessModelSchema(this.oldModelSchema); - - this.loadingIndicator?.hideIndicator(); - - return context.modelFactory.createRoot(this.oldModelSchema); + return this.oldRoot ?? context.root; } redo(context: CommandExecutionContext): SModelRootImpl { - if (!this.newModel) { - // No new model saved because the layout was not executed due to read-only mode. - return context.root; - } - - return this.newModel; + return this.newModel ?? context.root; } } diff --git a/frontend/webEditor/src/features/autoLayout/di.config.ts b/frontend/webEditor/src/layout/di.config.ts similarity index 79% rename from frontend/webEditor/src/features/autoLayout/di.config.ts rename to frontend/webEditor/src/layout/di.config.ts index 3fb889a9..9d00c07d 100644 --- a/frontend/webEditor/src/features/autoLayout/di.config.ts +++ b/frontend/webEditor/src/layout/di.config.ts @@ -2,13 +2,13 @@ import { ContainerModule } from "inversify"; import { TYPES, configureCommand } from "sprotty"; import { ElkFactory, ILayoutConfigurator, ILayoutPostprocessor } from "sprotty-elk"; import { LayoutModelCommand } from "./command"; -import { CircleLayoutPostProcessor, DfdElkLayoutEngine, DfdLayoutConfigurator, elkFactory, LayoutMethodWrapper } from "./layouter"; +import { CircleLayoutPostProcessor, DfdElkLayoutEngine, DfdLayoutConfigurator, elkFactory } from "./layouter"; import { AutoLayoutKeyListener } from "./keyListener"; -export const autoLayoutModule = new ContainerModule((bind, unbind, isBound, rebind) => { - bind(LayoutMethodWrapper).toSelf().inSingletonScope() +export const layoutModule = new ContainerModule((bind, unbind, isBound, rebind) => { bind(DfdElkLayoutEngine).toSelf().inSingletonScope(); bind(TYPES.IModelLayoutEngine).toService(DfdElkLayoutEngine); + bind(DfdLayoutConfigurator).to(DfdLayoutConfigurator); rebind(ILayoutConfigurator).to(DfdLayoutConfigurator); bind(ILayoutPostprocessor).to(CircleLayoutPostProcessor).inSingletonScope(); bind(ElkFactory).toConstantValue(elkFactory); diff --git a/frontend/webEditor/src/features/autoLayout/keyListener.ts b/frontend/webEditor/src/layout/keyListener.ts similarity index 77% rename from frontend/webEditor/src/features/autoLayout/keyListener.ts rename to frontend/webEditor/src/layout/keyListener.ts index cf08c94b..61eac364 100644 --- a/frontend/webEditor/src/features/autoLayout/keyListener.ts +++ b/frontend/webEditor/src/layout/keyListener.ts @@ -2,8 +2,8 @@ import { CommitModelAction, KeyListener, SModelElementImpl } from "sprotty"; import { matchesKeystroke } from "sprotty/lib/utils/keyboard"; import { Action } from "sprotty-protocol"; import { LayoutModelAction } from "./command"; -import { createDefaultFitToScreenAction } from "../../utils"; -import { LayoutMethod } from "../settingsMenu/LayoutMethod"; +import { DefaultFitToScreenAction } from "../fitToScreen/action"; +import { LayoutMethod } from "./layoutMethod"; export class AutoLayoutKeyListener extends KeyListener { keyDown(element: SModelElementImpl, event: KeyboardEvent): Action[] { @@ -13,7 +13,7 @@ export class AutoLayoutKeyListener extends KeyListener { return [ LayoutModelAction.create(LayoutMethod.LINES), CommitModelAction.create(), - createDefaultFitToScreenAction(element.root), + DefaultFitToScreenAction.create(element.root), ]; } diff --git a/frontend/webEditor/src/features/settingsMenu/LayoutMethod.ts b/frontend/webEditor/src/layout/layoutMethod.ts similarity index 100% rename from frontend/webEditor/src/features/settingsMenu/LayoutMethod.ts rename to frontend/webEditor/src/layout/layoutMethod.ts diff --git a/frontend/webEditor/src/features/autoLayout/layouter.ts b/frontend/webEditor/src/layout/layouter.ts similarity index 94% rename from frontend/webEditor/src/features/autoLayout/layouter.ts rename to frontend/webEditor/src/layout/layouter.ts index 25122b67..735b8ba9 100644 --- a/frontend/webEditor/src/features/autoLayout/layouter.ts +++ b/frontend/webEditor/src/layout/layouter.ts @@ -5,25 +5,22 @@ import { ElkFactory, ElkLayoutEngine, IElementFilter, - ILayoutConfigurator, ILayoutPostprocessor, } from "sprotty-elk"; import { SChildElementImpl, SShapeElementImpl, isBoundsAware } from "sprotty"; import { SShapeElement, SModelIndex, SEdge, SLabel } from "sprotty-protocol"; import { ElkShape, LayoutOptions } from "elkjs"; -import { LayoutMethod } from "../settingsMenu/LayoutMethod"; -import { calculateTextSize } from "../../utils"; - -/** - * We need this to give the sprotty elements the used method - */ -export class LayoutMethodWrapper { - public layoutMethod: LayoutMethod = LayoutMethod.LINES -} +import { LayoutMethod } from "./layoutMethod"; +import { calculateTextSize } from "../utils/TextSize"; export class DfdLayoutConfigurator extends DefaultLayoutConfigurator { - constructor(@inject(LayoutMethodWrapper) protected readonly method: LayoutMethodWrapper) { - super(); + private static _method: LayoutMethod = LayoutMethod.LINES; + + set method(method: LayoutMethod) { + DfdLayoutConfigurator._method = method; + } + get method() { + return DfdLayoutConfigurator._method; } protected override graphOptions(): LayoutOptions { @@ -63,7 +60,7 @@ export class DfdLayoutConfigurator extends DefaultLayoutConfigurator { // These are all automatically determined by our dfd node views "org.eclipse.elk.omitNodeMicroLayout": "true", }, - }[this.method.layoutMethod]; + }[this.method]; } } @@ -86,8 +83,7 @@ export class DfdElkLayoutEngine extends ElkLayoutEngine { constructor( @inject(ElkFactory) elkFactory: ElkFactory, @inject(IElementFilter) elementFilter: IElementFilter, - @inject(ILayoutConfigurator) configurator: ILayoutConfigurator, - @inject(LayoutMethodWrapper) protected readonly method: LayoutMethodWrapper, + @inject(DfdLayoutConfigurator) protected readonly configurator: DfdLayoutConfigurator, @inject(ILayoutPostprocessor) protected readonly postprocessor: ILayoutPostprocessor, ) { super(elkFactory, elementFilter, configurator, undefined, postprocessor); @@ -113,7 +109,7 @@ export class DfdElkLayoutEngine extends ElkLayoutEngine { protected override transformLabel(slabel: SLabel, index: SModelIndex): ElkLabel { const e = super.transformLabel(slabel, index); - if (this.method.layoutMethod === LayoutMethod.WRAPPING) { + if (this.configurator.method === LayoutMethod.WRAPPING) { return e; } const size = calculateTextSize(slabel.text ?? ""); @@ -141,7 +137,7 @@ export class DfdElkLayoutEngine extends ElkLayoutEngine { // depending on which edge the port is on. // depending on the mode the ports may be placed differently - if (this.method.layoutMethod === LayoutMethod.CIRCLES) { + if (this.configurator.method === LayoutMethod.CIRCLES) { if (elkShape.x <= 0) // Left edge elkShape.x -= elkShape.width / 2; @@ -185,7 +181,7 @@ export class DfdElkLayoutEngine extends ElkLayoutEngine { } protected applyEdge(sedge: SEdge, elkEdge: ElkExtendedEdge, index: SModelIndex): void { - if (this.method.layoutMethod === LayoutMethod.CIRCLES) { + if (this.configurator.method === LayoutMethod.CIRCLES) { // In the circles layout method, we want to make sure that the edge is not straight // This is because the circles layout method does not support straight edges elkEdge.sections = []; @@ -200,10 +196,10 @@ export class CircleLayoutPostProcessor implements ILayoutPostprocessor { private connectedPorts: Map = new Map(); private nodeSquares: Map = new Map(); - constructor(@inject(LayoutMethodWrapper) protected readonly method: LayoutMethodWrapper) {} + constructor(@inject(DfdLayoutConfigurator) private readonly configurator: DfdLayoutConfigurator) {} postprocess(elkGraph: ElkNode): void { - if (this.method.layoutMethod !== LayoutMethod.CIRCLES) { + if (this.configurator.method !== LayoutMethod.CIRCLES) { return; } this.connectedPorts = new Map(); diff --git a/frontend/webEditor/src/loadingIndicator/di.config.ts b/frontend/webEditor/src/loadingIndicator/di.config.ts new file mode 100644 index 00000000..b720040c --- /dev/null +++ b/frontend/webEditor/src/loadingIndicator/di.config.ts @@ -0,0 +1,10 @@ +import { ContainerModule } from "inversify"; +import { LoadingIndicator } from "./loadingIndicator"; +import { TYPES } from "sprotty"; +import { EDITOR_TYPES } from "../editorTypes"; + +export const loadingIndicatorModule = new ContainerModule((bind) => { + bind(LoadingIndicator).toSelf().inSingletonScope(); + bind(TYPES.IUIExtension).toService(LoadingIndicator); + bind(EDITOR_TYPES.DefaultUIElement).toService(LoadingIndicator); +}); diff --git a/frontend/webEditor/src/common/loadingIndicator.css b/frontend/webEditor/src/loadingIndicator/loadingIndicator.css similarity index 100% rename from frontend/webEditor/src/common/loadingIndicator.css rename to frontend/webEditor/src/loadingIndicator/loadingIndicator.css diff --git a/frontend/webEditor/src/common/loadingIndicator.ts b/frontend/webEditor/src/loadingIndicator/loadingIndicator.ts similarity index 68% rename from frontend/webEditor/src/common/loadingIndicator.ts rename to frontend/webEditor/src/loadingIndicator/loadingIndicator.ts index 60d8be44..eb8d3b8d 100644 --- a/frontend/webEditor/src/common/loadingIndicator.ts +++ b/frontend/webEditor/src/loadingIndicator/loadingIndicator.ts @@ -5,6 +5,7 @@ export class LoadingIndicator extends AbstractUIExtension { static readonly ID = "loading-indicator"; private loadingIndicatorWrapper: HTMLElement | undefined; private loadingIndicatorText: HTMLElement | undefined; + private waitTimeout?: number; id(): string { return LoadingIndicator.ID; @@ -29,16 +30,26 @@ export class LoadingIndicator extends AbstractUIExtension { } public showIndicator(text?: string) { - if (this.loadingIndicatorWrapper) { - this.loadingIndicatorWrapper.style.display = "flex"; - if (this.loadingIndicatorText) { - this.loadingIndicatorText.innerText = text || "Loading..."; + this.waitTimeout = setTimeout(() => { + if (!this.waitTimeout) { + return; } - this.loadingIndicatorWrapper.focus(); - } + if (this.loadingIndicatorWrapper) { + this.loadingIndicatorWrapper.style.display = "flex"; + if (this.loadingIndicatorText) { + this.loadingIndicatorText.innerText = text || "Loading..."; + } + this.loadingIndicatorWrapper.focus(); + this.waitTimeout = undefined; + } + }, 200); } public hideIndicator() { + if (this.waitTimeout) { + clearTimeout(this.waitTimeout); + this.waitTimeout = undefined; + } if (this.loadingIndicatorWrapper) { this.loadingIndicatorWrapper.style.display = "none"; } diff --git a/frontend/webEditor/src/serialize/ModelFactory.ts b/frontend/webEditor/src/serialize/ModelFactory.ts new file mode 100644 index 00000000..86311e35 --- /dev/null +++ b/frontend/webEditor/src/serialize/ModelFactory.ts @@ -0,0 +1,83 @@ +import { injectable } from "inversify"; +import { SChildElementImpl, SModelElementImpl, SModelFactory, SParentElementImpl } from "sprotty"; +import { DfdNode } from "../diagram/nodes/common"; +import { getBasicType, SLabel, SModelElement } from "sprotty-protocol"; +import { ArrowEdge } from "../diagram/edges/ArrowEdge"; + +@injectable() +export class DfdModelFactory extends SModelFactory { + override createElement(schema: SModelElement | SModelElementImpl, parent?: SParentElementImpl): SChildElementImpl { + if (schema instanceof SModelElementImpl) { + return super.createElement(schema, parent); + } + if (schema.type === "node:storage" || schema.type === "node:function" || schema.type === "node:input-output") { + const dfdSchema = schema as DfdNode; + schema.children = schema.children ?? []; + for (const port of dfdSchema.ports) { + if ("features" in port) { + delete port.features; + } + } + schema.children.push(...dfdSchema.ports, { + type: "label:positional", + text: dfdSchema.text ?? "", + id: schema.id + "-label", + } as SLabel); + } + + if (schema.type === "edge:arrow") { + const dfdSchema = schema as ArrowEdge; + schema.children = schema.children ?? []; + schema.children.push({ + type: "label:filled-background", + text: dfdSchema.text ?? "", + id: schema.id + "-label", + edgePlacement: { + position: 0.5, + side: "on", + rotate: false, + }, + } as SLabel); + } + + return super.createElement(schema, parent); + } + + override createSchema(element: SModelElementImpl): SModelElement { + const schema = super.createSchema(element); + + if (schema.type === "node:storage" || schema.type === "node:function" || schema.type === "node:input-output") { + const dfdSchema = schema as DfdNode; + const ports = dfdSchema.children?.filter((child) => getBasicType(child) === "port") ?? []; + dfdSchema.ports = ports; + + const labelValue = schema.children?.find((child) => child.type === "label:positional") as + | SLabel + | undefined; + + if (labelValue) { + dfdSchema.text = labelValue.text; + } + + dfdSchema.children = []; + return dfdSchema; + } + + if (schema.type === "edge:arrow") { + const dfdSchema = schema as ArrowEdge; + + const labelValue = schema.children?.find((child) => child.type === "label:filled-background") as + | SLabel + | undefined; + + if (labelValue) { + dfdSchema.text = labelValue.text; + } + + dfdSchema.children = []; + return dfdSchema; + } + + return schema; + } +} diff --git a/frontend/webEditor/src/serialize/SavedDiagram.ts b/frontend/webEditor/src/serialize/SavedDiagram.ts new file mode 100644 index 00000000..623b8752 --- /dev/null +++ b/frontend/webEditor/src/serialize/SavedDiagram.ts @@ -0,0 +1,13 @@ +import { SModelRoot } from "sprotty-protocol"; +import { Constraint } from "../constraint/Constraint"; +import { LabelType } from "../labels/LabelType"; +import { EditorMode } from "../settings/editorMode"; + +export interface SavedDiagram { + model: SModelRoot; + labelTypes?: LabelType[]; + constraints?: Constraint[]; + mode?: EditorMode; + version: number; +} +export const CURRENT_VERSION = 1; diff --git a/frontend/webEditor/src/serialize/analyze.ts b/frontend/webEditor/src/serialize/analyze.ts new file mode 100644 index 00000000..6b949abe --- /dev/null +++ b/frontend/webEditor/src/serialize/analyze.ts @@ -0,0 +1,56 @@ +import { ActionDispatcher, CommandExecutionContext, ILogger, TYPES } from "sprotty"; +import { FileData, LoadJsonCommand } from "./loadJson"; +import { CURRENT_VERSION, SavedDiagram } from "./SavedDiagram"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { SETTINGS } from "../settings/Settings"; +import { FileName } from "../fileName/fileName"; +import { DfdWebSocket } from "../webSocket/webSocket"; +import { inject } from "inversify"; +import { EditorModeController } from "../settings/editorMode"; +import { Action } from "sprotty-protocol"; +import { ConstraintRegistry } from "../constraint/constraintRegistry"; +import { LoadingIndicator } from "../loadingIndicator/loadingIndicator"; + +export namespace AnalyzeAction { + export const KIND = "analyze"; + + export function create(): Action { + return { kind: KIND }; + } +} +export class AnalyzeCommand extends LoadJsonCommand { + static readonly KIND = AnalyzeAction.KIND; + + constructor( + @inject(TYPES.Action) _: Action, + @inject(TYPES.ILogger) logger: ILogger, + @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, + @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, + @inject(SETTINGS.Mode) editorModeController: EditorModeController, + @inject(FileName) fileName: FileName, + @inject(DfdWebSocket) private readonly dfdWebSocket: DfdWebSocket, + @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, + @inject(LoadingIndicator) loadingIndicator: LoadingIndicator, + ) { + super( + logger, + labelTypeRegistry, + constraintRegistry, + editorModeController, + actionDispatcher, + fileName, + loadingIndicator, + ); + } + + protected async getFile(context: CommandExecutionContext): Promise | undefined> { + const savedDiagram = { + model: context.modelFactory.createSchema(context.root), + labelTypes: this.labelTypeRegistry.getLabelTypes(), + constraints: this.constraintRegistry.getConstraintList(), + mode: this.editorModeController.get(), + version: CURRENT_VERSION, + }; + return await this.dfdWebSocket.requestDiagram("Json:" + JSON.stringify(savedDiagram)); + } +} diff --git a/frontend/webEditor/src/features/serialize/defaultDiagram.json b/frontend/webEditor/src/serialize/defaultDiagram.json similarity index 100% rename from frontend/webEditor/src/features/serialize/defaultDiagram.json rename to frontend/webEditor/src/serialize/defaultDiagram.json diff --git a/frontend/webEditor/src/serialize/di.config.ts b/frontend/webEditor/src/serialize/di.config.ts new file mode 100644 index 00000000..2e34a49c --- /dev/null +++ b/frontend/webEditor/src/serialize/di.config.ts @@ -0,0 +1,23 @@ +import { ContainerModule } from "inversify"; +import { configureCommand, TYPES } from "sprotty"; +import { LoadDefaultDiagramCommand } from "./loadDefaultDiagram"; +import { LoadDfdAndDdFileCommand } from "./loadDfdAndDdFile"; +import { LoadJsonFileCommand } from "./loadJsonFile"; +import { LoadPalladioFileCommand } from "./loadPalladioFile"; +import { DfdModelFactory } from "./ModelFactory"; +import { SaveJsonFileCommand } from "./saveJsonFile"; +import { SaveDfdAndDdFileCommand } from "./saveDfdAndDdFile"; +import { AnalyzeCommand } from "./analyze"; + +export const serializeModule = new ContainerModule((bind, unbind, isBound, rebind) => { + const context = { bind, unbind, isBound, rebind }; + configureCommand(context, LoadDefaultDiagramCommand); + configureCommand(context, LoadJsonFileCommand); + configureCommand(context, LoadDfdAndDdFileCommand); + configureCommand(context, LoadPalladioFileCommand); + configureCommand(context, SaveJsonFileCommand); + configureCommand(context, SaveDfdAndDdFileCommand); + configureCommand(context, AnalyzeCommand); + + rebind(TYPES.IModelFactory).to(DfdModelFactory); +}); diff --git a/frontend/webEditor/src/serialize/fileChooser.ts b/frontend/webEditor/src/serialize/fileChooser.ts new file mode 100644 index 00000000..d6a820ae --- /dev/null +++ b/frontend/webEditor/src/serialize/fileChooser.ts @@ -0,0 +1,43 @@ +import { FileData } from "./loadJson"; + +function getFiles(acceptedTypes: string[], amount: number): Promise { + const input = document.createElement("input"); + input.type = "file"; + input.accept = acceptedTypes.join(","); + input.multiple = amount > 1; + const fileLoadPromise = new Promise((resolve, reject) => { + input.onchange = () => { + if (!input.files || input.files.length !== amount) { + reject("No file selected"); + return; + } + resolve(Array.from(input.files)); + }; + }); + + input.click(); + + return fileLoadPromise; +} + +function readFile(file: File): Promise> { + return new Promise>((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => + resolve({ + fileName: file.name, + content: reader.result as string, // since we use readAsText reader.result is always a string + }); + reader.onerror = () => reject(reader.error); + reader.readAsText(file); + }); +} + +export async function chooseFiles(acceptedTypes: string[], amount: number): Promise[]> { + const files = await getFiles(acceptedTypes, amount); + return Promise.all(files.map(readFile)); +} + +export function chooseFile(acceptedTypes: string[]): Promise | undefined> { + return chooseFiles(acceptedTypes, 1).then((files) => (files ? files[0] : undefined)); +} diff --git a/frontend/webEditor/src/serialize/loadDefaultDiagram.ts b/frontend/webEditor/src/serialize/loadDefaultDiagram.ts new file mode 100644 index 00000000..3e5c889b --- /dev/null +++ b/frontend/webEditor/src/serialize/loadDefaultDiagram.ts @@ -0,0 +1,52 @@ +import { FileData, LoadJsonCommand } from "./loadJson"; +import defaultDiagram from "./defaultDiagram.json"; +import { SavedDiagram } from "./SavedDiagram"; +import { Action } from "sprotty-protocol"; +import { inject } from "inversify"; +import { TYPES, ILogger, ActionDispatcher } from "sprotty"; +import { EditorModeController } from "../settings/editorMode"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { FileName } from "../fileName/fileName"; +import { SETTINGS } from "../settings/Settings"; +import { ConstraintRegistry } from "../constraint/constraintRegistry"; +import { LoadingIndicator } from "../loadingIndicator/loadingIndicator"; + +export namespace LoadDefaultDiagramAction { + export const KIND = "loadDefaultDiagram"; + + export function create(): Action { + return { kind: KIND }; + } +} + +export class LoadDefaultDiagramCommand extends LoadJsonCommand { + static readonly KIND = LoadDefaultDiagramAction.KIND; + + constructor( + @inject(TYPES.Action) _: Action, + @inject(TYPES.ILogger) logger: ILogger, + @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, + @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, + @inject(SETTINGS.Mode) editorModeController: EditorModeController, + @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, + @inject(FileName) fileName: FileName, + @inject(LoadingIndicator) loadingIndicator: LoadingIndicator, + ) { + super( + logger, + labelTypeRegistry, + constraintRegistry, + editorModeController, + actionDispatcher, + fileName, + loadingIndicator, + ); + } + + protected async getFile(): Promise | undefined> { + return { + fileName: "diagram.json", + content: defaultDiagram as SavedDiagram, + }; + } +} diff --git a/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts b/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts new file mode 100644 index 00000000..e1831bb2 --- /dev/null +++ b/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts @@ -0,0 +1,66 @@ +import { Action } from "sprotty-protocol"; +import { FileData, LoadJsonCommand } from "./loadJson"; +import { chooseFiles } from "./fileChooser"; +import { inject } from "inversify"; +import { DfdWebSocket } from "../webSocket/webSocket"; +import { TYPES, ILogger, ActionDispatcher } from "sprotty"; +import { EditorModeController } from "../settings/editorMode"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { SavedDiagram } from "./SavedDiagram"; +import { FileName } from "../fileName/fileName"; +import { SETTINGS } from "../settings/Settings"; +import { ConstraintRegistry } from "../constraint/constraintRegistry"; +import { LoadingIndicator } from "../loadingIndicator/loadingIndicator"; + +export namespace LoadDfdAndDdFileAction { + export const KIND = "loadDfdAndDdFile"; + + export function create(): Action { + return { kind: KIND }; + } +} + +export class LoadDfdAndDdFileCommand extends LoadJsonCommand { + static readonly KIND = LoadDfdAndDdFileAction.KIND; + + constructor( + @inject(TYPES.Action) _: Action, + @inject(TYPES.ILogger) logger: ILogger, + @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, + @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, + @inject(SETTINGS.Mode) editorModeController: EditorModeController, + @inject(FileName) fileName: FileName, + @inject(DfdWebSocket) private dfdWebSocket: DfdWebSocket, + @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, + @inject(LoadingIndicator) loadingIndicator: LoadingIndicator, + ) { + super( + logger, + labelTypeRegistry, + constraintRegistry, + editorModeController, + actionDispatcher, + fileName, + loadingIndicator, + ); + } + + protected async getFile(): Promise | undefined> { + const files = await chooseFiles([".dataflowdiagram", ".datadictionary"], 2); + const dataflowFileContent = files.find((file) => file.fileName.endsWith(".dataflowdiagram"))?.content; + const dictionaryFileContent = files.find((file) => file.fileName.endsWith(".datadictionary"))?.content; + if (!dataflowFileContent || !dictionaryFileContent) { + return undefined; + } + + const oldFileName = this.fileName.getName(); + this.fileName.setName(files[0].fileName); + + return this.dfdWebSocket + .requestDiagram("DFD:" + dataflowFileContent + "\n:DD:\n" + dictionaryFileContent) + .catch((e) => { + this.fileName.setName(oldFileName); + throw e; + }); + } +} diff --git a/frontend/webEditor/src/serialize/loadJson.ts b/frontend/webEditor/src/serialize/loadJson.ts new file mode 100644 index 00000000..70e37c9f --- /dev/null +++ b/frontend/webEditor/src/serialize/loadJson.ts @@ -0,0 +1,207 @@ +import { + ActionDispatcher, + Command, + CommandExecutionContext, + CommandReturn, + EMPTY_ROOT, + ILogger, + SModelRootImpl, +} from "sprotty"; +import { SavedDiagram } from "./SavedDiagram"; +import { Action, SModelElement, SModelRoot } from "sprotty-protocol"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { EditorModeController, EditorMode } from "../settings/editorMode"; +import { Constraint } from "../constraint/Constraint"; +import { LabelType } from "../labels/LabelType"; +import { DefaultFitToScreenAction } from "../fitToScreen/action"; +import { FileName } from "../fileName/fileName"; +import { ConstraintRegistry } from "../constraint/constraintRegistry"; +import { LoadingIndicator } from "../loadingIndicator/loadingIndicator"; + +export interface FileData { + fileName: string; + content: T; +} + +export abstract class LoadJsonCommand extends Command { + /* After loading a diagram, this command dispatches other actions like fit to screen and optional auto layouting. However when returning a new model in the execute method, the diagram is not directly updated. We need to wait for the InitializeCanvasBoundsCommand to be fired and finish before we can do things like fit to screen. + Because of that we block the execution newly dispatched actions including the actions we dispatched after loading the diagram until the InitializeCanvasBoundsCommand has been processed. + This works because the canvasBounds property is always removed loading a diagram, requiring the InitializeCanvasBoundsCommand to be fired. */ + readonly blockUntil = LoadJsonCommand.loadBlockUntilFn; + static readonly loadBlockUntilFn = (action: Action) => { + return action.kind === "initializeCanvasBounds"; + }; + + private oldRoot: SModelRootImpl | undefined; + private newRoot: SModelRootImpl | undefined; + private oldLabelTypes: LabelType[] | undefined; + private oldEditorMode: EditorMode | undefined; + private oldFileName: string | undefined; + private oldConstrains: Constraint[] | undefined; + private file: FileData | undefined; + + constructor( + private readonly logger: ILogger, + protected readonly labelTypeRegistry: LabelTypeRegistry, + protected constraintRegistry: ConstraintRegistry, + protected editorModeController: EditorModeController, + private actionDispatcher: ActionDispatcher, + protected fileName: FileName, + private loadingIndicator: LoadingIndicator, + ) { + super(); + } + + protected abstract getFile(context: CommandExecutionContext): Promise | undefined>; + + async execute(context: CommandExecutionContext): Promise { + this.loadingIndicator.showIndicator("Loading model..."); + this.oldRoot = context.root; + + this.file = await this.getFile(context).catch(() => undefined); + if (!this.file) { + this.loadingIndicator.hide(); + return context.root; + } + + try { + const newSchema = LoadJsonCommand.preprocessModelSchema(this.file.content.model); + this.newRoot = context.modelFactory.createRoot(newSchema); + + this.logger.info(this, "Model loaded successfully"); + + this.oldLabelTypes = this.labelTypeRegistry.getLabelTypes(); + const newLabelTypes = this.file.content.labelTypes; + this.labelTypeRegistry.clearLabelTypes(); + if (newLabelTypes) { + this.labelTypeRegistry.setLabelTypes(newLabelTypes); + this.logger.info(this, "Label types loaded successfully"); + } else { + this.labelTypeRegistry.clearLabelTypes(); + } + + this.oldEditorMode = this.editorModeController.get(); + const newEditorMode = this.file.content.mode; + if (newEditorMode) { + this.editorModeController.set(newEditorMode); + } else { + this.editorModeController.setDefault(); + } + this.logger.info(this, "Editor mode loaded successfully"); + + this.oldConstrains = this.constraintRegistry.getConstraintList(); + const newConstraints = this.file.content.constraints; + if (newConstraints) { + this.constraintRegistry.setConstraintsFromArray(newConstraints); + } else { + this.constraintRegistry.clearConstraints(); + } + + // TODO: post load actions like layout + this.actionDispatcher.dispatch(DefaultFitToScreenAction.create(this.newRoot)); + + this.oldFileName = this.fileName.getName(); + this.fileName.setName(this.file.fileName); + + this.loadingIndicator.hide(); + return this.newRoot; + } catch (error) { + this.logger.error(this, "Error loading model", error); + this.newRoot = this.oldRoot; + this.loadingIndicator.hide(); + return this.oldRoot; + } + } + + undo(context: CommandExecutionContext): CommandReturn { + this.loadingIndicator.showIndicator("Reverting model load..."); + if (this.oldLabelTypes) { + this.labelTypeRegistry.setLabelTypes(this.oldLabelTypes); + } else { + this.labelTypeRegistry.clearLabelTypes(); + } + + if (this.oldEditorMode) { + this.editorModeController.set(this.oldEditorMode); + } else { + this.editorModeController.setDefault(); + } + + if (this.oldEditorMode) { + this.editorModeController.set(this.oldEditorMode); + } + + if (this.oldConstrains) { + this.constraintRegistry.setConstraintsFromArray(this.oldConstrains); + } + + this.fileName.setName(this.oldFileName ?? "diagram"); + + this.loadingIndicator.hide(); + return this.oldRoot ?? context.modelFactory.createRoot(EMPTY_ROOT); + } + + redo(context: CommandExecutionContext): CommandReturn { + this.loadingIndicator.showIndicator("Re-applying model load..."); + const newLabelTypes = this.file?.content.labelTypes; + this.labelTypeRegistry.clearLabelTypes(); + if (newLabelTypes) { + this.labelTypeRegistry.setLabelTypes(newLabelTypes); + this.logger.info(this, "Label types loaded successfully"); + } else { + this.labelTypeRegistry.clearLabelTypes(); + } + + const newEditorMode = this.file?.content.mode; + if (newEditorMode) { + this.editorModeController.set(newEditorMode); + } else { + this.editorModeController.setDefault(); + } + this.logger.info(this, "Editor mode loaded successfully"); + + const newConstraints = this.file?.content.constraints; + if (newConstraints) { + this.constraintRegistry.setConstraintsFromArray(newConstraints); + } else { + this.constraintRegistry.clearConstraints(); + } + + this.fileName.setName(this.file?.fileName ?? "diagram"); + + this.loadingIndicator.hide(); + return this.newRoot ?? this.oldRoot ?? context.modelFactory.createRoot(EMPTY_ROOT); + } + + /** + * Before a saved model schema can be loaded, it needs to be preprocessed. + * Currently this means that the features property is removed from all model elements recursively. + * Additionally the canvasBounds property is removed from the root element, because it may change + * depending on browser window. + * In the future this method may be extended to preprocess other properties. + * + * The feature property at runtime is a js Set with the relevant features. + * E.g. for the top graph this is the viewportFeature among others. + * When converting js Sets objects into json, the result is an empty js object. + * When loading the object is converted into an empty js Set and the features are lost. + * Because of this the editor won't work properly after loading a model. + * To prevent this, the features property is removed before loading the model. + * When the features property is missing it gets rebuilt on loading with the currently used features. + * + * @param modelSchema The model schema to preprocess + */ + private static preprocessModelSchema(modelSchema: SModelRoot): SModelRoot { + // These properties are all not included in the root typing and if present are not loaded and handled correctly. So they are removed. + if ("features" in modelSchema) { + delete modelSchema["features"]; + } + if ("canvasBounds" in modelSchema) { + delete modelSchema["canvasBounds"]; + } + + if (modelSchema.children) { + modelSchema.children.forEach((child: SModelElement) => this.preprocessModelSchema(child)); + } + return modelSchema; + } +} diff --git a/frontend/webEditor/src/serialize/loadJsonFile.ts b/frontend/webEditor/src/serialize/loadJsonFile.ts new file mode 100644 index 00000000..438b24cd --- /dev/null +++ b/frontend/webEditor/src/serialize/loadJsonFile.ts @@ -0,0 +1,59 @@ +import { Action } from "sprotty-protocol"; +import { FileData, LoadJsonCommand } from "./loadJson"; +import { chooseFile } from "./fileChooser"; +import { inject } from "inversify"; +import { TYPES, ILogger, ActionDispatcher } from "sprotty"; +import { EditorModeController } from "../settings/editorMode"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { SavedDiagram } from "./SavedDiagram"; +import { FileName } from "../fileName/fileName"; +import { SETTINGS } from "../settings/Settings"; +import { ConstraintRegistry } from "../constraint/constraintRegistry"; +import { LoadingIndicator } from "../loadingIndicator/loadingIndicator"; + +export namespace LoadJsonFileAction { + export const KIND = "loadJsonFile"; + + export function create(): Action { + return { kind: KIND }; + } +} + +export class LoadJsonFileCommand extends LoadJsonCommand { + static readonly KIND = LoadJsonFileAction.KIND; + + constructor( + @inject(TYPES.Action) _: Action, + @inject(TYPES.ILogger) logger: ILogger, + @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, + @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, + @inject(SETTINGS.Mode) editorModeController: EditorModeController, + @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, + @inject(FileName) fileName: FileName, + @inject(LoadingIndicator) loadingIndicator: LoadingIndicator, + ) { + super( + logger, + labelTypeRegistry, + constraintRegistry, + editorModeController, + actionDispatcher, + fileName, + loadingIndicator, + ); + } + + protected async getFile(): Promise | undefined> { + const file = await chooseFile(["application/json"]); + if (!file) { + return undefined; + } + + this.fileName.setName(file.fileName); + + return { + fileName: file.fileName, + content: JSON.parse(file.content) as SavedDiagram, + }; + } +} diff --git a/frontend/webEditor/src/serialize/loadPalladioFile.ts b/frontend/webEditor/src/serialize/loadPalladioFile.ts new file mode 100644 index 00000000..d9576cc9 --- /dev/null +++ b/frontend/webEditor/src/serialize/loadPalladioFile.ts @@ -0,0 +1,80 @@ +import { Action } from "sprotty-protocol"; +import { FileData, LoadJsonCommand } from "./loadJson"; +import { chooseFiles } from "./fileChooser"; +import { inject } from "inversify"; +import { DfdWebSocket } from "../webSocket/webSocket"; +import { TYPES, ILogger, ActionDispatcher } from "sprotty"; +import { EditorModeController } from "../settings/editorMode"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { SavedDiagram } from "./SavedDiagram"; +import { FileName } from "../fileName/fileName"; +import { SETTINGS } from "../settings/Settings"; +import { ConstraintRegistry } from "../constraint/constraintRegistry"; +import { LoadingIndicator } from "../loadingIndicator/loadingIndicator"; + +export namespace LoadPalladioFileAction { + export const KIND = "loadPcmFile"; + + export function create(): Action { + return { kind: KIND }; + } +} + +export class LoadPalladioFileCommand extends LoadJsonCommand { + static readonly KIND = LoadPalladioFileAction.KIND; + private static readonly FILE_ENDINGS = [ + ".pddc", + ".allocation", + ".nodecharacteristics", + ".repository", + ".resourceenvironment", + ".system", + ".usagemodel", + ]; + + constructor( + @inject(TYPES.Action) _: Action, + @inject(TYPES.ILogger) logger: ILogger, + @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, + @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, + @inject(SETTINGS.Mode) editorModeController: EditorModeController, + @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, + @inject(FileName) fileName: FileName, + @inject(DfdWebSocket) private dfdWebSocket: DfdWebSocket, + @inject(LoadingIndicator) loadingIndicator: LoadingIndicator, + ) { + super( + logger, + labelTypeRegistry, + constraintRegistry, + editorModeController, + actionDispatcher, + fileName, + loadingIndicator, + ); + } + + protected async getFile(): Promise | undefined> { + const files = await chooseFiles( + LoadPalladioFileCommand.FILE_ENDINGS, + LoadPalladioFileCommand.FILE_ENDINGS.length, + ); + + if ( + LoadPalladioFileCommand.FILE_ENDINGS.some((ending) => !files.find((file) => file.fileName.endsWith(ending))) + ) { + throw new Error( + "Please select one file of each required type: .pddc, .allocation, .nodecharacteristics, .repository, .resourceenvironment, .system, .usagemodel", + ); + } + const oldFileName = this.fileName.getName(); + this.fileName.setName(files[0].fileName); + + return this.dfdWebSocket + .requestDiagram(files.map((f) => `${f.fileName}:${f.content}`).join("---FILE---")) + .catch((e) => { + this.fileName.setName(oldFileName); + throw e; + }); + } +} diff --git a/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts b/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts new file mode 100644 index 00000000..67ec2a09 --- /dev/null +++ b/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts @@ -0,0 +1,58 @@ +import { CommandExecutionContext, TYPES } from "sprotty"; +import { FileData } from "./loadJson"; +import { SaveFileCommand } from "./saveFile"; +import { inject } from "inversify"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { EditorModeController } from "../settings/editorMode"; +import { DfdWebSocket } from "../webSocket/webSocket"; +import { Action } from "sprotty-protocol"; +import { FileName } from "../fileName/fileName"; +import { SETTINGS } from "../settings/Settings"; +import { ConstraintRegistry } from "../constraint/constraintRegistry"; +import { LoadingIndicator } from "../loadingIndicator/loadingIndicator"; + +export namespace SaveDfdAndDdFileAction { + export const KIND = "saveDfdAndDdFile"; + export function create(): Action { + return { kind: KIND }; + } +} + +export class SaveDfdAndDdFileCommand extends SaveFileCommand { + static readonly KIND = SaveDfdAndDdFileAction.KIND; + private static readonly CLOSING_TAG = ""; + + constructor( + @inject(TYPES.Action) _: Action, + @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, + @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, + @inject(SETTINGS.Mode) editorModeController: EditorModeController, + @inject(DfdWebSocket) private readonly dfdWebSocket: DfdWebSocket, + @inject(FileName) private readonly fileName: FileName, + @inject(LoadingIndicator) loadingIndicator: LoadingIndicator, + ) { + super(labelTypeRegistry, constraintRegistry, editorModeController, loadingIndicator); + } + + async getFiles(context: CommandExecutionContext): Promise[]> { + const savedDiagram = this.createSavedDiagram(context); + + const response = await this.dfdWebSocket.sendMessage("Json2DFD:" + JSON.stringify(savedDiagram)); + const endIndex = + response.indexOf(SaveDfdAndDdFileCommand.CLOSING_TAG) + SaveDfdAndDdFileCommand.CLOSING_TAG.length; + const dfdContent = response.substring(0, endIndex).trim(); + const ddContent = response.substring(endIndex).trim(); + + const fileName = this.fileName.getName(); + return Promise.resolve([ + { + fileName: fileName + ".dataflowdiagram", + content: dfdContent, + }, + { + fileName: fileName + ".datadictionary", + content: ddContent, + }, + ]); + } +} diff --git a/frontend/webEditor/src/serialize/saveFile.ts b/frontend/webEditor/src/serialize/saveFile.ts new file mode 100644 index 00000000..17a414f4 --- /dev/null +++ b/frontend/webEditor/src/serialize/saveFile.ts @@ -0,0 +1,47 @@ +import { CommandExecutionContext, CommandReturn, SModelRootImpl } from "sprotty"; +import { FileData } from "./loadJson"; +import { SavedDiagramCreatorCommand } from "./savedDiagramCreator"; +import { LoadingIndicator } from "../loadingIndicator/loadingIndicator"; +import { ConstraintRegistry } from "../constraint/constraintRegistry"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { EditorModeController } from "../settings/editorMode"; + +export abstract class SaveFileCommand extends SavedDiagramCreatorCommand { + constructor( + labelTypeRegistry: LabelTypeRegistry, + constraintRegistry: ConstraintRegistry, + editorModeController: EditorModeController, + private readonly loadingIndicator: LoadingIndicator, + ) { + super(labelTypeRegistry, constraintRegistry, editorModeController); + } + + abstract getFiles(context: CommandExecutionContext): Promise[]>; + + async execute(context: CommandExecutionContext): Promise { + this.loadingIndicator.showIndicator("Saving diagram..."); + const files = await this.getFiles(context); + for (const file of files) { + this.downloadFile(file); + } + + this.loadingIndicator.hide(); + return context.root; + } + undo(context: CommandExecutionContext): CommandReturn { + return context.root; + } + redo(context: CommandExecutionContext): CommandReturn { + return context.root; + } + + private downloadFile(file: FileData) { + const element = document.createElement("a"); + const fileBlob = new Blob([file.content], { type: "application/json" }); + element.href = URL.createObjectURL(fileBlob); + element.download = file.fileName; + element.click(); + URL.revokeObjectURL(element.href); + element.remove(); + } +} diff --git a/frontend/webEditor/src/serialize/saveJsonFile.ts b/frontend/webEditor/src/serialize/saveJsonFile.ts new file mode 100644 index 00000000..c71bd7be --- /dev/null +++ b/frontend/webEditor/src/serialize/saveJsonFile.ts @@ -0,0 +1,41 @@ +import { CommandExecutionContext, TYPES } from "sprotty"; +import { FileData } from "./loadJson"; +import { SaveFileCommand } from "./saveFile"; +import { EditorModeController } from "../settings/editorMode"; +import { inject } from "inversify"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { Action } from "sprotty-protocol"; +import { FileName } from "../fileName/fileName"; +import { SETTINGS } from "../settings/Settings"; +import { ConstraintRegistry } from "../constraint/constraintRegistry"; +import { LoadingIndicator } from "../loadingIndicator/loadingIndicator"; + +export namespace SaveJsonFileAction { + export const KIND = "saveJsonFile"; + export function create(): Action { + return { kind: KIND }; + } +} + +export class SaveJsonFileCommand extends SaveFileCommand { + static readonly KIND = SaveJsonFileAction.KIND; + + constructor( + @inject(TYPES.Action) _: Action, + @inject(LabelTypeRegistry) LabelTypeRegistry: LabelTypeRegistry, + @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, + @inject(SETTINGS.Mode) editorModeController: EditorModeController, + @inject(FileName) private readonly fileName: FileName, + @inject(LoadingIndicator) loadingIndicator: LoadingIndicator, + ) { + super(LabelTypeRegistry, constraintRegistry, editorModeController, loadingIndicator); + } + + getFiles(context: CommandExecutionContext): Promise[]> { + const fileData: FileData = { + fileName: this.fileName.getName() + ".json", + content: JSON.stringify(this.createSavedDiagram(context)), + }; + return Promise.resolve([fileData]); + } +} diff --git a/frontend/webEditor/src/serialize/savedDiagramCreator.ts b/frontend/webEditor/src/serialize/savedDiagramCreator.ts new file mode 100644 index 00000000..c4823075 --- /dev/null +++ b/frontend/webEditor/src/serialize/savedDiagramCreator.ts @@ -0,0 +1,27 @@ +import { Command, CommandExecutionContext } from "sprotty"; +import { CURRENT_VERSION, SavedDiagram } from "./SavedDiagram"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { EditorModeController } from "../settings/editorMode"; +import { ConstraintRegistry } from "../constraint/constraintRegistry"; + +export abstract class SavedDiagramCreatorCommand extends Command { + constructor( + private readonly labelTypeRegistry: LabelTypeRegistry, + private readonly constraintRegistry: ConstraintRegistry, + private readonly editorModeController: EditorModeController, + ) { + super(); + } + + protected createSavedDiagram(context: CommandExecutionContext): SavedDiagram { + const schema = context.modelFactory.createSchema(context.root); + + return { + model: schema, + labelTypes: this.labelTypeRegistry.getLabelTypes(), + constraints: this.constraintRegistry.getConstraintList(), + mode: this.editorModeController.get(), + version: CURRENT_VERSION, + }; + } +} diff --git a/frontend/webEditor/src/settings/Settings.ts b/frontend/webEditor/src/settings/Settings.ts new file mode 100644 index 00000000..2bba96f6 --- /dev/null +++ b/frontend/webEditor/src/settings/Settings.ts @@ -0,0 +1,12 @@ +import { BoolSettingsValue } from "./SettingsValue"; + +export const SETTINGS = { + Theme: Symbol("Theme"), + Mode: Symbol("EditorMode"), + HideEdgeNames: Symbol("HideEdgeNames"), + SimplifyNodeNames: Symbol("SimplifyNodeNames"), + ShownLabels: Symbol("ShownLabels"), +}; + +export type SimplifyNodeNames = BoolSettingsValue; +export type HideEdgeNames = BoolSettingsValue; diff --git a/frontend/webEditor/src/settings/SettingsUi.ts b/frontend/webEditor/src/settings/SettingsUi.ts new file mode 100644 index 00000000..70f6194a --- /dev/null +++ b/frontend/webEditor/src/settings/SettingsUi.ts @@ -0,0 +1,125 @@ +import { inject, injectable } from "inversify"; +import "./settingsUi.css"; +import { SettingsValue } from "./SettingsValue"; +import { AccordionUiExtension } from "../accordionUiExtension"; +import { HideEdgeNames, SETTINGS, SimplifyNodeNames } from "./Settings"; +import { EditorModeController } from "./editorMode"; +import { Theme, ThemeManager } from "./Theme"; +import { ShownLabels, ShownLabelsValue } from "./ShownLabels"; + +@injectable() +export class SettingsUI extends AccordionUiExtension { + static readonly ID = "settings-ui"; + + constructor( + @inject(SETTINGS.Theme) private readonly themeManager: ThemeManager, + @inject(SETTINGS.ShownLabels) private readonly shownLabels: ShownLabelsValue, + @inject(SETTINGS.HideEdgeNames) private readonly hideEdgeNames: HideEdgeNames, + @inject(SETTINGS.SimplifyNodeNames) private readonly simplifyNodeNames: SimplifyNodeNames, + @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController, + ) { + super("right", "up"); + } + + id(): string { + return SettingsUI.ID; + } + + containerClass(): string { + return SettingsUI.ID; + } + + protected initializeHidableContent(contentElement: HTMLElement): void { + const grid = document.createElement("div"); + grid.id = "settings-content"; + contentElement.appendChild(grid); + this.addDropDown(grid, "Theme", this.themeManager, [Theme.SYSTEM_DEFAULT, Theme.LIGHT, Theme.DARK]); + this.addDropDown(grid, "Shown Labels", this.shownLabels, [ + ShownLabels.INCOMING, + ShownLabels.OUTGOING, + ShownLabels.ALL, + ]); + this.addBooleanSwitch(grid, "Hide Edge Names", this.hideEdgeNames); + this.addBooleanSwitch(grid, "Simplify Node Names", this.simplifyNodeNames); + this.addSwitch(grid, "Read Only", this.editorModeController, { true: "view", false: "edit" }); + } + + protected initializeHeaderContent(headerElement: HTMLElement): void { + headerElement.classList.add("settings-accordion-icon"); + headerElement.innerText = "Settings"; + } + + private addBooleanSwitch(container: HTMLElement, title: string, value: SettingsValue): void { + this.addSwitch(container, title, value, { true: true, false: false }); + } + + private addSwitch( + container: HTMLElement, + title: string, + value: SettingsValue, + map: { true: T; false: T }, + ): void { + const inversedMap = { + [map.true.toString()]: true, + [map.false.toString()]: false, + }; + const textLabel = document.createElement("label"); + textLabel.textContent = title; + textLabel.htmlFor = `setting-${title.toLowerCase().replace(/\s+/g, "-")}`; + + const switchLabel = document.createElement("label"); + switchLabel.classList.add("switch"); + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.id = `setting-${title.toLowerCase().replace(/\s+/g, "-")}`; + checkbox.checked = inversedMap[value.get().toString()]; + switchLabel.appendChild(checkbox); + const sliderSpan = document.createElement("span"); + sliderSpan.classList.add("slider", "round"); + switchLabel.appendChild(sliderSpan); + + container.appendChild(textLabel); + container.appendChild(switchLabel); + + switchLabel.addEventListener("change", () => { + value.set(map[checkbox.checked ? "true" : "false"]); + }); + value.registerListener((newValue) => { + checkbox.checked = inversedMap[newValue.toString()]; + }); + } + + private addDropDown( + container: HTMLElement, + title: string, + value: SettingsValue, + values: T[], + ) { + const textLabel = document.createElement("label"); + textLabel.textContent = title; + textLabel.htmlFor = `setting-${title.toLowerCase().replace(/\s+/g, "-")}`; + + const dropDown = document.createElement("select"); + for (const v of values) { + const option = document.createElement("option"); + option.value = v.toString(); + option.innerText = v.toString(); + dropDown.appendChild(option); + } + dropDown.value = value.get().toString(); + + dropDown.onchange = () => { + const newValue = values.find((v) => v.toString() === dropDown.value); + if (newValue) { + value.set(newValue); + } + }; + + container.appendChild(textLabel); + container.appendChild(dropDown); + } +} + +interface ToString { + toString: () => string; +} diff --git a/frontend/webEditor/src/settings/SettingsValue.ts b/frontend/webEditor/src/settings/SettingsValue.ts new file mode 100644 index 00000000..7a24289b --- /dev/null +++ b/frontend/webEditor/src/settings/SettingsValue.ts @@ -0,0 +1,30 @@ +export class SettingsValue { + private value: T; + private listeners: Array<(newValue: T) => void> = []; + + constructor(initialValue: T) { + this.value = initialValue; + } + + get(): T { + return this.value; + } + + set(newValue: T): void { + const oldValue = this.value; + this.value = newValue; + if (oldValue !== newValue) { + this.listeners.forEach((listener) => listener(newValue)); + } + } + + registerListener(listener: (newValue: T) => void): void { + this.listeners.push(listener); + } +} + +export class BoolSettingsValue extends SettingsValue { + constructor(initialValue: boolean = false) { + super(initialValue); + } +} diff --git a/frontend/webEditor/src/settings/ShownLabels.ts b/frontend/webEditor/src/settings/ShownLabels.ts new file mode 100644 index 00000000..3f0f595b --- /dev/null +++ b/frontend/webEditor/src/settings/ShownLabels.ts @@ -0,0 +1,13 @@ +import { SettingsValue } from "./SettingsValue"; + +export enum ShownLabels { + INCOMING = "Incoming", + OUTGOING = "Outgoing", + ALL = "All", +} + +export class ShownLabelsValue extends SettingsValue { + constructor() { + super(ShownLabels.ALL); + } +} diff --git a/frontend/webEditor/src/settings/Theme.ts b/frontend/webEditor/src/settings/Theme.ts new file mode 100644 index 00000000..687c6f0b --- /dev/null +++ b/frontend/webEditor/src/settings/Theme.ts @@ -0,0 +1,52 @@ +import { SettingsValue } from "./SettingsValue"; + +export enum Theme { + LIGHT = "Light", + DARK = "Dark", + SYSTEM_DEFAULT = "System Default", +} + +export type ApplyableTheme = Theme.LIGHT | Theme.DARK; + +export class ThemeManager extends SettingsValue { + private static SYSTEM_DEFAULT: ApplyableTheme = + window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? Theme.DARK : Theme.LIGHT; + public static readonly LOCAL_STORAGE_KEY = "dfdwebeditor:theme"; + + constructor() { + super((localStorage.getItem(ThemeManager.LOCAL_STORAGE_KEY) ?? ThemeManager.SYSTEM_DEFAULT) as Theme); + } + + getTheme(): ApplyableTheme { + const value = this.get(); + if (value === Theme.SYSTEM_DEFAULT) { + return ThemeManager.SYSTEM_DEFAULT; + } + return value; + } +} + +export const ThemeSwitchable = Symbol("ThemeSwitchable"); + +export interface ThemeSwitchable { + switchTheme: (newTheme: ApplyableTheme) => void; +} + +export function registerThemeSwitch(themeManager: ThemeManager, switchables: ThemeSwitchable[]) { + themeManager.registerListener(() => { + setTheme(themeManager, switchables); + }); + setTheme(themeManager, switchables); +} + +function setTheme(themeManager: ThemeManager, switchables: ThemeSwitchable[]) { + const rootElement = document.querySelector(":root") as HTMLElement; + const sprottyElement = document.querySelector("#sprotty") as HTMLElement; + + const value = themeManager.getTheme() === Theme.DARK ? "dark" : "light"; + rootElement.setAttribute("data-theme", value); + sprottyElement.setAttribute("data-theme", value); + localStorage.setItem(ThemeManager.LOCAL_STORAGE_KEY, themeManager.get()); + + switchables.forEach((s) => s.switchTheme(themeManager.getTheme())); +} diff --git a/frontend/webEditor/src/settings/di.config.ts b/frontend/webEditor/src/settings/di.config.ts new file mode 100644 index 00000000..a10e86e4 --- /dev/null +++ b/frontend/webEditor/src/settings/di.config.ts @@ -0,0 +1,28 @@ +import { ContainerModule } from "inversify"; +import { SettingsUI } from "./SettingsUi"; +import { EDITOR_TYPES } from "../editorTypes"; +import { SETTINGS } from "./Settings"; +import { BoolSettingsValue } from "./SettingsValue"; +import { configureCommand, TYPES } from "sprotty"; +import { EditorModeController } from "./editorMode"; +import { ThemeManager } from "./Theme"; +import { ShownLabelsValue } from "./ShownLabels"; +import { HideEdgeNamesCommand } from "./hideEdgeNames"; +import { NodeNameRegistry, SimplifyNodeNamesCommand } from "./simplifyNodeNames"; + +export const settingsModule = new ContainerModule((bind, _, isBound) => { + bind(SettingsUI).toSelf().inSingletonScope(); + bind(EDITOR_TYPES.DefaultUIElement).toService(SettingsUI); + bind(TYPES.IUIExtension).toService(SettingsUI); + + bind(SETTINGS.Theme).to(ThemeManager).inSingletonScope(); + bind(SETTINGS.HideEdgeNames).to(BoolSettingsValue).inSingletonScope(); + bind(SETTINGS.SimplifyNodeNames).to(BoolSettingsValue).inSingletonScope(); + bind(SETTINGS.Mode).to(EditorModeController).inSingletonScope(); + bind(SETTINGS.ShownLabels).to(ShownLabelsValue).inSingletonScope(); + + const context = { bind, isBound }; + configureCommand(context, HideEdgeNamesCommand); + configureCommand(context, SimplifyNodeNamesCommand); + bind(NodeNameRegistry).toSelf().inSingletonScope(); +}); diff --git a/frontend/webEditor/src/settings/editorMode.ts b/frontend/webEditor/src/settings/editorMode.ts new file mode 100644 index 00000000..db4a19f0 --- /dev/null +++ b/frontend/webEditor/src/settings/editorMode.ts @@ -0,0 +1,17 @@ +import { SettingsValue } from "./SettingsValue"; + +export type EditorMode = "edit" | "view"; + +export class EditorModeController extends SettingsValue { + constructor() { + super("edit"); + } + + setDefault(): void { + this.set("edit"); + } + + isReadOnly(): boolean { + return this.get() !== "edit"; + } +} diff --git a/frontend/webEditor/src/settings/hideEdgeNames.ts b/frontend/webEditor/src/settings/hideEdgeNames.ts new file mode 100644 index 00000000..f183d0c7 --- /dev/null +++ b/frontend/webEditor/src/settings/hideEdgeNames.ts @@ -0,0 +1,25 @@ +import { Command, CommandExecutionContext, CommandReturn } from "sprotty"; +import { Action } from "sprotty-protocol"; + +export namespace HideEdgeNamesAction { + export const KIND = "hide-edge-names"; + export function create(): Action { + return { + kind: KIND, + }; + } +} + +export class HideEdgeNamesCommand extends Command { + static readonly KIND = HideEdgeNamesAction.KIND; + + execute(context: CommandExecutionContext): CommandReturn { + return context.root; + } + undo(context: CommandExecutionContext): CommandReturn { + return context.root; + } + redo(context: CommandExecutionContext): CommandReturn { + return context.root; + } +} diff --git a/frontend/webEditor/src/settings/initialize.ts b/frontend/webEditor/src/settings/initialize.ts new file mode 100644 index 00000000..fcead901 --- /dev/null +++ b/frontend/webEditor/src/settings/initialize.ts @@ -0,0 +1,38 @@ +import { IActionDispatcher } from "sprotty"; +import { EditorModeController } from "./editorMode"; +import { HideEdgeNames, SimplifyNodeNames } from "./Settings"; +import { HideEdgeNamesAction } from "./hideEdgeNames"; +import { SimplifyNodeNamesAction } from "./simplifyNodeNames"; + +export function linkReadOnly( + editorModeController: EditorModeController, + simplifyNodeNames: SimplifyNodeNames, + hideEdgeNames: HideEdgeNames, +): void { + editorModeController.registerListener(() => { + if (!editorModeController.isReadOnly()) { + simplifyNodeNames.set(false); + hideEdgeNames.set(false); + } + }); + + simplifyNodeNames.registerListener((newValue) => { + if (newValue) { + editorModeController.set("view"); + } + }); + hideEdgeNames.registerListener((newValue) => { + if (newValue) { + editorModeController.set("view"); + } + }); +} + +export function addCommands( + actionDispatcher: IActionDispatcher, + simplifyNodeNames: SimplifyNodeNames, + hideEdgeNames: HideEdgeNames, +) { + hideEdgeNames.registerListener(() => actionDispatcher.dispatch(HideEdgeNamesAction.create())); + simplifyNodeNames.registerListener(() => actionDispatcher.dispatch(SimplifyNodeNamesAction.create())); +} diff --git a/frontend/webEditor/src/features/settingsMenu/settingsMenu.css b/frontend/webEditor/src/settings/settingsUi.css similarity index 97% rename from frontend/webEditor/src/features/settingsMenu/settingsMenu.css rename to frontend/webEditor/src/settings/settingsUi.css index cc839597..90517902 100644 --- a/frontend/webEditor/src/features/settingsMenu/settingsMenu.css +++ b/frontend/webEditor/src/settings/settingsUi.css @@ -4,7 +4,7 @@ div.settings-ui { padding: 10px 10px; } -#settings-ui-accordion-label .accordion-button::before { +.settings-accordion-icon::before { content: ""; background-image: url("@fortawesome/fontawesome-free/svgs/solid/gear.svg"); display: inline-block; @@ -13,6 +13,7 @@ div.settings-ui { width: 16px; background-size: 16px 16px; vertical-align: text-top; + margin-right: 4px; } #settings-content { diff --git a/frontend/webEditor/src/settings/simplifyNodeNames.ts b/frontend/webEditor/src/settings/simplifyNodeNames.ts new file mode 100644 index 00000000..d8ce802c --- /dev/null +++ b/frontend/webEditor/src/settings/simplifyNodeNames.ts @@ -0,0 +1,82 @@ +import { inject, injectable } from "inversify"; +import { Command, CommandExecutionContext, CommandReturn, SParentElementImpl, TYPES } from "sprotty"; +import { DfdNodeImpl } from "../diagram/nodes/common"; +import { Action } from "sprotty-protocol"; +import { SETTINGS, SimplifyNodeNames } from "./Settings"; + +@injectable() +export class NodeNameRegistry { + private plainNames: Map; + private anonymousNames: Map; + private nextNummber = 1; + + constructor() { + this.plainNames = new Map(); + this.anonymousNames = new Map(); + } + + public setPlainName(node: DfdNodeImpl) { + if (node.editableLabel && this.plainNames.has(node.id)) { + node.editableLabel.text = this.plainNames.get(node.id)!; + } + } + + public setAnonymousName(node: DfdNodeImpl) { + if (node instanceof DfdNodeImpl && node.editableLabel) { + this.plainNames.set(node.id, node.editableLabel.text); + } + if (!this.anonymousNames.has(node.id)) { + this.anonymousNames.set(node.id, this.nextNummber); + this.nextNummber++; + } + if (node.editableLabel) { + node.editableLabel.text = this.anonymousNames.get(node.id)!.toString(); + } + } +} + +export namespace SimplifyNodeNamesAction { + export const KIND = "simplify-node-names"; + export function create(): Action { + return { + kind: KIND, + }; + } +} + +export class SimplifyNodeNamesCommand extends Command { + static readonly KIND = SimplifyNodeNamesAction.KIND; + + constructor( + @inject(TYPES.Action) _: Action, + @inject(NodeNameRegistry) private readonly nodeNameRegistry: NodeNameRegistry, + @inject(SETTINGS.SimplifyNodeNames) private readonly simplifyNodeNames: SimplifyNodeNames, + ) { + super(); + } + + execute(context: CommandExecutionContext): CommandReturn { + this.iterate(context.root, (n) => + this.simplifyNodeNames.get() + ? this.nodeNameRegistry.setAnonymousName(n) + : this.nodeNameRegistry.setPlainName(n), + ); + return context.root; + } + undo(context: CommandExecutionContext): CommandReturn { + return context.root; + } + redo(context: CommandExecutionContext): CommandReturn { + return context.root; + } + + iterate(node: SParentElementImpl, f: (n: DfdNodeImpl) => void) { + if (node instanceof DfdNodeImpl) { + f(node); + } + + for (const child of node.children) { + this.iterate(child, f); + } + } +} diff --git a/frontend/webEditor/src/startUpAgent/LoadDefaultDiagram.ts b/frontend/webEditor/src/startUpAgent/LoadDefaultDiagram.ts new file mode 100644 index 00000000..a2e05c70 --- /dev/null +++ b/frontend/webEditor/src/startUpAgent/LoadDefaultDiagram.ts @@ -0,0 +1,12 @@ +import { ActionDispatcher, TYPES } from "sprotty"; +import { IStartUpAgent } from "./StartUpAgent"; +import { LoadDefaultDiagramAction } from "../serialize/loadDefaultDiagram"; +import { inject } from "inversify"; + +export class LoadDefaultDiagramStartUpAgent implements IStartUpAgent { + constructor(@inject(TYPES.IActionDispatcher) private actionDispatcher: ActionDispatcher) {} + + run(): void { + this.actionDispatcher.dispatch(LoadDefaultDiagramAction.create()); + } +} diff --git a/frontend/webEditor/src/startUpAgent/LoadDefaultUiExtensions.ts b/frontend/webEditor/src/startUpAgent/LoadDefaultUiExtensions.ts new file mode 100644 index 00000000..07ed36fb --- /dev/null +++ b/frontend/webEditor/src/startUpAgent/LoadDefaultUiExtensions.ts @@ -0,0 +1,19 @@ +import { inject, injectable, multiInject } from "inversify"; +import { EDITOR_TYPES } from "../editorTypes"; +import { AbstractUIExtension, ActionDispatcher, SetUIExtensionVisibilityAction, TYPES } from "sprotty"; +import { IStartUpAgent } from "./StartUpAgent"; + +@injectable() +export class LoadDefaultUiExtensionsStartUpAgent implements IStartUpAgent { + constructor( + @inject(TYPES.IActionDispatcher) private actionDispatcher: ActionDispatcher, + @multiInject(EDITOR_TYPES.DefaultUIElement) private defaultUiElements: AbstractUIExtension[], + ) {} + + public run() { + const uiVisibilityActions = this.defaultUiElements.map((e) => + SetUIExtensionVisibilityAction.create({ extensionId: e.id(), visible: true }), + ); + this.actionDispatcher.dispatchAll(uiVisibilityActions); + } +} diff --git a/frontend/webEditor/src/startUpAgent/StartUpAgent.ts b/frontend/webEditor/src/startUpAgent/StartUpAgent.ts new file mode 100644 index 00000000..3a77c8ce --- /dev/null +++ b/frontend/webEditor/src/startUpAgent/StartUpAgent.ts @@ -0,0 +1,5 @@ +export interface IStartUpAgent { + run(): void; +} + +export const StartUpAgent = Symbol("StartUpAgent"); diff --git a/frontend/webEditor/src/startUpAgent/di.config.ts b/frontend/webEditor/src/startUpAgent/di.config.ts new file mode 100644 index 00000000..47281130 --- /dev/null +++ b/frontend/webEditor/src/startUpAgent/di.config.ts @@ -0,0 +1,13 @@ +import { ContainerModule } from "inversify"; +import { StartUpAgent } from "./StartUpAgent"; +import { LoadDefaultUiExtensionsStartUpAgent } from "./LoadDefaultUiExtensions"; +import { LoadDefaultDiagramStartUpAgent } from "./LoadDefaultDiagram"; +import { WebSocketConnectStartUpAgent } from "./webSocketConnect"; +import { SettingsInitStartUpAgent } from "./settingsInit"; + +export const startUpAgentModule = new ContainerModule((bind) => { + bind(StartUpAgent).to(LoadDefaultUiExtensionsStartUpAgent); + bind(StartUpAgent).to(LoadDefaultDiagramStartUpAgent); + bind(StartUpAgent).to(WebSocketConnectStartUpAgent); + bind(StartUpAgent).to(SettingsInitStartUpAgent); +}); diff --git a/frontend/webEditor/src/startUpAgent/settingsInit.ts b/frontend/webEditor/src/startUpAgent/settingsInit.ts new file mode 100644 index 00000000..7df99711 --- /dev/null +++ b/frontend/webEditor/src/startUpAgent/settingsInit.ts @@ -0,0 +1,24 @@ +import { IStartUpAgent } from "./StartUpAgent"; +import { inject, multiInject } from "inversify"; +import { addCommands, linkReadOnly } from "../settings/initialize"; +import { EditorModeController } from "../settings/editorMode"; +import { SETTINGS, HideEdgeNames, SimplifyNodeNames } from "../settings/Settings"; +import { registerThemeSwitch, ThemeManager, ThemeSwitchable } from "../settings/Theme"; +import { ActionDispatcher, TYPES } from "sprotty"; + +export class SettingsInitStartUpAgent implements IStartUpAgent { + constructor( + @inject(SETTINGS.Theme) private readonly themeManager: ThemeManager, + @inject(SETTINGS.HideEdgeNames) private readonly hideEdgeNames: HideEdgeNames, + @inject(SETTINGS.SimplifyNodeNames) private readonly simplifyNodeNames: SimplifyNodeNames, + @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController, + @multiInject(ThemeSwitchable) private readonly switchables: ThemeSwitchable[], + @inject(TYPES.IActionDispatcher) private readonly actionDispatcher: ActionDispatcher, + ) {} + + run(): void { + linkReadOnly(this.editorModeController, this.simplifyNodeNames, this.hideEdgeNames); + registerThemeSwitch(this.themeManager, this.switchables); + addCommands(this.actionDispatcher, this.simplifyNodeNames, this.hideEdgeNames); + } +} diff --git a/frontend/webEditor/src/startUpAgent/webSocketConnect.ts b/frontend/webEditor/src/startUpAgent/webSocketConnect.ts new file mode 100644 index 00000000..1e234f9b --- /dev/null +++ b/frontend/webEditor/src/startUpAgent/webSocketConnect.ts @@ -0,0 +1,10 @@ +import { IStartUpAgent } from "./StartUpAgent"; +import { inject } from "inversify"; +import { DfdWebSocket } from "../webSocket/webSocket"; + +export class WebSocketConnectStartUpAgent implements IStartUpAgent { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + constructor(@inject(DfdWebSocket) _: DfdWebSocket) {} + + run(): void {} +} diff --git a/frontend/webEditor/src/features/toolPalette/creationTool.ts b/frontend/webEditor/src/toolPalette/creationTool.ts similarity index 96% rename from frontend/webEditor/src/features/toolPalette/creationTool.ts rename to frontend/webEditor/src/toolPalette/creationTool.ts index 4068113d..08d4ec61 100644 --- a/frontend/webEditor/src/features/toolPalette/creationTool.ts +++ b/frontend/webEditor/src/toolPalette/creationTool.ts @@ -22,9 +22,8 @@ import { TYPES, } from "sprotty"; import { inject, injectable, multiInject } from "inversify"; -import { DynamicChildrenProcessor } from "../dfdElements/dynamicChildren"; import { Action, Point, SEdge, SNode, SPort } from "sprotty-protocol"; -import { EDITOR_TYPES } from "../../utils"; +import { EDITOR_TYPES } from "../editorTypes"; type Positionable = { position?: Point }; type Schema = (SNode | SEdge | SPort) & Positionable; @@ -47,7 +46,6 @@ export abstract class CreationTool extends Mou constructor( @inject(MouseTool) protected mouseTool: MouseTool, @inject(MousePositionTracker) protected mousePositionTracker: MousePositionTracker, - @inject(DynamicChildrenProcessor) protected dynamicChildrenProcessor: DynamicChildrenProcessor, @inject(TYPES.IModelFactory) protected modelFactory: IModelFactory, @inject(TYPES.IActionDispatcher) protected actionDispatcher: ActionDispatcher, @inject(TYPES.ICommandStack) protected commandStack: CommandStack, @@ -66,9 +64,6 @@ export abstract class CreationTool extends Mou // Only set opacity if it is not already set in the schema schema.opacity ??= this.previewOpacity; - // Add any dynamically declared children to the node schema. - this.dynamicChildrenProcessor.processGraphChildren(schema, "set"); - const element = this.modelFactory.createElement(schema) as I; if (this.insertIntoGraphRootAfterCreation) { const root = await this.commandStack.executeAll([]); diff --git a/frontend/webEditor/src/features/toolPalette/di.config.ts b/frontend/webEditor/src/toolPalette/di.config.ts similarity index 97% rename from frontend/webEditor/src/features/toolPalette/di.config.ts rename to frontend/webEditor/src/toolPalette/di.config.ts index 2caa5a82..38e36297 100644 --- a/frontend/webEditor/src/features/toolPalette/di.config.ts +++ b/frontend/webEditor/src/toolPalette/di.config.ts @@ -1,5 +1,4 @@ import { ContainerModule } from "inversify"; -import { EDITOR_TYPES } from "../../utils"; import { AddElementToGraphCommand, CreationToolDisableKeyListener } from "./creationTool"; import { EdgeCreationTool } from "./edgeCreationTool"; import { NodeCreationTool } from "./nodeCreationTool"; @@ -14,6 +13,7 @@ import { configureCommand, configureModelElement, } from "sprotty"; +import { EDITOR_TYPES } from "../editorTypes"; // This module contains an UI extension that adds a tool palette to the editor. // This tool palette allows the user to create new nodes and edges. diff --git a/frontend/webEditor/src/features/toolPalette/edgeCreationTool.ts b/frontend/webEditor/src/toolPalette/edgeCreationTool.ts similarity index 98% rename from frontend/webEditor/src/features/toolPalette/edgeCreationTool.ts rename to frontend/webEditor/src/toolPalette/edgeCreationTool.ts index ead081e7..d05b4383 100644 --- a/frontend/webEditor/src/features/toolPalette/edgeCreationTool.ts +++ b/frontend/webEditor/src/toolPalette/edgeCreationTool.ts @@ -8,8 +8,8 @@ import { SParentElementImpl, } from "sprotty"; import { Action, SEdge, SNode } from "sprotty-protocol"; -import { generateRandomSprottyId } from "../../utils"; import { CreationTool } from "./creationTool"; +import { generateRandomSprottyId } from "../utils/idGenerator"; @injectable() export class EdgeCreationTool extends CreationTool { diff --git a/frontend/webEditor/src/features/toolPalette/nodeCreationTool.ts b/frontend/webEditor/src/toolPalette/nodeCreationTool.ts similarity index 88% rename from frontend/webEditor/src/features/toolPalette/nodeCreationTool.ts rename to frontend/webEditor/src/toolPalette/nodeCreationTool.ts index 8b1afe0f..afe47537 100644 --- a/frontend/webEditor/src/features/toolPalette/nodeCreationTool.ts +++ b/frontend/webEditor/src/toolPalette/nodeCreationTool.ts @@ -1,8 +1,8 @@ import { injectable } from "inversify"; -import { generateRandomSprottyId } from "../../utils"; import { SNodeImpl } from "sprotty"; import { SNode } from "sprotty-protocol"; import { CreationTool } from "./creationTool"; +import { generateRandomSprottyId } from "../utils/idGenerator"; /** * Creates a node when the user clicks somewhere on the root graph. @@ -19,6 +19,8 @@ export class NodeCreationTool extends CreationTool { id: generateRandomSprottyId(), type: this.elementType, text: defaultTextCapitalized, + ports: [], + labels: [], } as SNode; } } diff --git a/frontend/webEditor/src/features/toolPalette/portCreationTool.ts b/frontend/webEditor/src/toolPalette/portCreationTool.ts similarity index 97% rename from frontend/webEditor/src/features/toolPalette/portCreationTool.ts rename to frontend/webEditor/src/toolPalette/portCreationTool.ts index 7dab8549..88b36e1c 100644 --- a/frontend/webEditor/src/features/toolPalette/portCreationTool.ts +++ b/frontend/webEditor/src/toolPalette/portCreationTool.ts @@ -1,8 +1,8 @@ import { injectable } from "inversify"; import { CommitModelAction, SChildElementImpl, SModelElementImpl, SPortImpl, SShapeElementImpl } from "sprotty"; import { Action, SPort } from "sprotty-protocol"; -import { generateRandomSprottyId } from "../../utils"; import { CreationTool } from "./creationTool"; +import { generateRandomSprottyId } from "../utils/idGenerator"; @injectable() export class PortCreationTool extends CreationTool { diff --git a/frontend/webEditor/src/features/toolPalette/toolPalette.css b/frontend/webEditor/src/toolPalette/toolPalette.css similarity index 100% rename from frontend/webEditor/src/features/toolPalette/toolPalette.css rename to frontend/webEditor/src/toolPalette/toolPalette.css diff --git a/frontend/webEditor/src/features/toolPalette/toolPalette.tsx b/frontend/webEditor/src/toolPalette/toolPalette.tsx similarity index 97% rename from frontend/webEditor/src/features/toolPalette/toolPalette.tsx rename to frontend/webEditor/src/toolPalette/toolPalette.tsx index 78becb03..d2daa8fd 100644 --- a/frontend/webEditor/src/features/toolPalette/toolPalette.tsx +++ b/frontend/webEditor/src/toolPalette/toolPalette.tsx @@ -20,11 +20,11 @@ import { NodeCreationTool } from "./nodeCreationTool"; import { EdgeCreationTool } from "./edgeCreationTool"; import { PortCreationTool } from "./portCreationTool"; import { AnyCreationTool } from "./creationTool"; -import { EDITOR_TYPES } from "../../utils"; -import { EditorModeController } from "../editorMode/editorModeController"; -import "../../common/commonStyling.css"; import "./toolPalette.css"; +import { EDITOR_TYPES } from "../editorTypes"; +import { SETTINGS } from "../settings/Settings"; +import { EditorModeController } from "../settings/editorMode"; /** * UI extension that adds a tool palette to the diagram in the upper right. @@ -42,7 +42,7 @@ export class ToolPaletteUI extends AbstractUIExtension implements IActionHandler @inject(EdgeCreationTool) protected readonly edgeCreationTool: EdgeCreationTool, @inject(PortCreationTool) protected readonly portCreationTool: PortCreationTool, @multiInject(EDITOR_TYPES.CreationTool) protected readonly allTools: AnyCreationTool[], - @inject(EditorModeController) + @inject(SETTINGS.Mode) @optional() protected readonly editorModeController: EditorModeController, ) { diff --git a/frontend/webEditor/src/utils.ts b/frontend/webEditor/src/utils.ts deleted file mode 100644 index 5f64cfa5..00000000 --- a/frontend/webEditor/src/utils.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { SModelRootImpl } from "sprotty"; -import { FitToScreenAction, getBasicType, SModelRoot } from "sprotty-protocol"; - -/** - * Type identifiers for use with inversify. - */ -export const EDITOR_TYPES = { - // Enableable and disableable tools that can be used to create new elements. - CreationTool: Symbol("CreationTool"), - // All IUIExtension instances that are bound to this symbol will - // be loaded and enabled at editor startup. - DefaultUIElement: Symbol("DefaultUIElement"), -}; - -export const FIT_TO_SCREEN_PADDING = 75; - -/** - * Generates a fit to screen action that fits all nodes on the screen - * with the default padding. - */ -export function createDefaultFitToScreenAction(root: SModelRootImpl | SModelRoot, animate = true): FitToScreenAction { - const elementIds = root.children?.filter((child) => getBasicType(child) === "node").map((child) => child.id) ?? []; - - return FitToScreenAction.create(elementIds, { - padding: FIT_TO_SCREEN_PADDING, - animate, - }); -} - -export function generateRandomSprottyId(): string { - return Math.random().toString(36).substring(7); -} - -interface TextSize { - width: number; - height: number; -} -const contextMap = new Map }>(); - -/** - * Calculates the width and height of the given text in the given font using a 2d canvas. - * Because this operation requires interacting with the browser including styling etc. - * this is rather expensive. Therefore the result is cached with the font and text as a cache key - * The default width for empty text is 20px. - * Big diagrams with hundreds of text elements (edges, nodes, labels) would not be possible without caching this operation. - * - * @param text the text you need the size for - * @param font the font to use, defaults to "11pt sans-serif" - * @returns the width and height of the text in pixels. This does not include any padding or margin - */ -export function calculateTextSize(text: string | undefined, font: string = "11pt sans-serif"): TextSize { - if (!text || text.length === 0) { - return { width: 20, height: 20 }; - } - - // Get context for the given font or create a new one if it does not exist yet - let contextObj = contextMap.get(font); - if (!contextObj) { - const canvas = document.createElement("canvas"); - const context = canvas.getContext("2d"); - if (!context) { - throw new Error("Could not create canvas context used to measure text width"); - } - - context.font = font; // This is slow. Thats why we have one instance per font to avoid redoing this - contextObj = { context, cache: new Map() }; - contextMap.set(font, contextObj); - } - - const { context, cache } = contextObj; - - // Get text width from cache or compute it - const cachedMetrics = cache.get(text); - if (cachedMetrics) { - return cachedMetrics; - } else { - const metrics = context.measureText(text); - const textSize: TextSize = { - width: Math.round(metrics.width), - height: Math.round(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent), - }; - - cache.set(text, textSize); - return textSize; - } -} diff --git a/frontend/webEditor/src/utils/TextSize.ts b/frontend/webEditor/src/utils/TextSize.ts new file mode 100644 index 00000000..5d67856f --- /dev/null +++ b/frontend/webEditor/src/utils/TextSize.ts @@ -0,0 +1,57 @@ +export function dynamicallySetInputSize(inputElement: HTMLInputElement): void { + const displayText = inputElement.value || inputElement.placeholder; + const { width } = calculateTextSize(displayText, window.getComputedStyle(inputElement).font); + + // Values have higher padding for the rounded border + const widthPadding = inputElement.classList.contains("label-type-name") ? 2 : 8; + const finalWidth = width + widthPadding; + + inputElement.style.width = finalWidth + "px"; +} + +interface TextSize { + width: number; + height: number; +} + +const contextMap = new Map }>(); + +export function calculateTextSize(text: string | undefined, font: string = "11pt sans-serif"): TextSize { + if (!text || text.length === 0) { + return { width: 20, height: 20 }; + } + if (font == "") { + font = "11pt sans-serif"; + } + + // Get context for the given font or create a new one if it does not exist yet + let contextObj = contextMap.get(font); + if (!contextObj) { + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d"); + if (!context) { + throw new Error("Could not create canvas context used to measure text width"); + } + + context.font = font; // This is slow. Thats why we have one instance per font to avoid redoing this + contextObj = { context, cache: new Map() }; + contextMap.set(font, contextObj); + } + + const { context, cache } = contextObj; + + // Get text width from cache or compute it + const cachedMetrics = cache.get(text); + if (cachedMetrics) { + return cachedMetrics; + } else { + const metrics = context.measureText(text); + const textSize: TextSize = { + width: Math.ceil(metrics.width), + height: Math.ceil(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent), + }; + + cache.set(text, textSize); + return textSize; + } +} diff --git a/frontend/webEditor/src/utils/UiElementFactory.ts b/frontend/webEditor/src/utils/UiElementFactory.ts new file mode 100644 index 00000000..2572d4fb --- /dev/null +++ b/frontend/webEditor/src/utils/UiElementFactory.ts @@ -0,0 +1,26 @@ +import "./baseUiElements.css"; + +export class UiElementFactory { + private constructor() {} + + public static buildDeleteButton() { + const button = document.createElement("button"); + button.classList.add("delete-button"); + const symbol = document.createElement("span"); + symbol.classList.add("codicon", "codicon-trash"); + button.appendChild(symbol); + return button; + } + + public static buildAddButton(text: string) { + const button = document.createElement("button"); + button.classList.add("add-button"); + const symbol = document.createElement("span"); + symbol.classList.add("codicon", "codicon-add"); + button.appendChild(symbol); + const textHolder = document.createElement("span"); + textHolder.innerText = text; + button.appendChild(textHolder); + return button; + } +} diff --git a/frontend/webEditor/src/utils/baseUiElements.css b/frontend/webEditor/src/utils/baseUiElements.css new file mode 100644 index 00000000..c5c01b8d --- /dev/null +++ b/frontend/webEditor/src/utils/baseUiElements.css @@ -0,0 +1,7 @@ +button.delete-button, +button.add-button { + background-color: transparent; + border: none; + cursor: pointer; + padding: 0; +} diff --git a/frontend/webEditor/src/utils/idGenerator.ts b/frontend/webEditor/src/utils/idGenerator.ts new file mode 100644 index 00000000..e982b020 --- /dev/null +++ b/frontend/webEditor/src/utils/idGenerator.ts @@ -0,0 +1,3 @@ +export function generateRandomSprottyId(): string { + return Math.random().toString(36).substring(7); +} diff --git a/frontend/webEditor/src/vite-env.d.ts b/frontend/webEditor/src/vite-env.d.ts new file mode 100644 index 00000000..39fc679a --- /dev/null +++ b/frontend/webEditor/src/vite-env.d.ts @@ -0,0 +1,3 @@ +/// + +declare module "*.css"; diff --git a/frontend/webEditor/src/webSocket/di.config.ts b/frontend/webEditor/src/webSocket/di.config.ts new file mode 100644 index 00000000..f1be5b84 --- /dev/null +++ b/frontend/webEditor/src/webSocket/di.config.ts @@ -0,0 +1,6 @@ +import { ContainerModule } from "inversify"; +import { DfdWebSocket } from "./webSocket"; + +export const webSocketModule = new ContainerModule((bind) => { + bind(DfdWebSocket).toSelf().inSingletonScope(); +}); diff --git a/frontend/webEditor/src/webSocket/webSocket.ts b/frontend/webEditor/src/webSocket/webSocket.ts new file mode 100644 index 00000000..8811c528 --- /dev/null +++ b/frontend/webEditor/src/webSocket/webSocket.ts @@ -0,0 +1,96 @@ +import { inject, injectable } from "inversify"; +import { ILogger, TYPES } from "sprotty"; +import { FileName } from "../fileName/fileName"; + +@injectable() +export class DfdWebSocket { + private webSocket?: WebSocket; + private webSocketId = -1; + private lastRequest: { + resolve?: (v: string) => void; + reject?: (e: Error) => void; + } = {}; + private static readonly WS_URL = "wss://websocket.dataflowanalysis.org/events/"; + + constructor( + @inject(TYPES.ILogger) private readonly logger: ILogger, + @inject(FileName) private readonly fileName: FileName, + ) { + this.init(); + } + + private init() { + this.webSocket = new WebSocket(DfdWebSocket.WS_URL); + + this.webSocket.onopen = () => { + this.logger.log(this, "WebSocket connection established."); + }; + + this.webSocket.onclose = () => { + this.logger.log(this, "WebSocket connection closed. Reconnecting..."); + this.reject(new Error("WebSocket connection closed")); + this.init(); + }; + this.webSocket.onerror = () => { + this.logger.log(this, "WebSocket error occurred."); + this.reject(new Error("WebSocket error occurred")); + this.init(); + }; + + this.webSocket.onmessage = (event) => { + const message = event.data as string; + this.logger.log(this, "WebSocket message received: " + message); + + if (message.startsWith("Error:")) { + this.reject(new Error(message)); + } + + if (message.startsWith("ID assigned:")) { + const parts = message.split(":"); + this.webSocketId = parseInt(parts[1].trim()); + this.logger.log(this, "WebSocket ID assigned: " + this.webSocketId); + return; + } + + if (this.lastRequest.resolve) { + this.lastRequest.resolve(message); + this.lastRequest.resolve = undefined; + this.lastRequest.reject = undefined; + } else { + this.logger.log(this, "No pending request to resolve."); + } + }; + } + + private reject(error: Error) { + if (this.lastRequest.reject) { + this.lastRequest.reject(error); + this.lastRequest.resolve = undefined; + this.lastRequest.reject = undefined; + } + } + + public async requestDiagram(message: string) { + const result = await this.sendMessage(message); + const name = result.split(":")[0]; + const diagramMessage = result.replace(name + ":", ""); + return { + fileName: name, + content: JSON.parse(diagramMessage), + }; + } + + public sendMessage(message: string): Promise { + const result = new Promise((resolve, reject) => { + this.lastRequest.resolve = resolve; + this.lastRequest.reject = reject; + }); + if (!this.webSocket || this.webSocket.readyState !== WebSocket.OPEN) { + this.reject(new Error("WebSocket is not connected")); + return result; + } + + this.webSocket.send(this.webSocketId + ":" + this.fileName.getName() + ":" + message); + return result; + } +}