diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 5d47c21..0000000 --- a/.editorconfig +++ /dev/null @@ -1,12 +0,0 @@ -# EditorConfig is awesome: https://EditorConfig.org - -# top-most EditorConfig file -root = true - -[*] -indent_style = space -indent_size = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true diff --git a/.gitignore b/.gitignore index 542369d..9dd999f 100644 --- a/.gitignore +++ b/.gitignore @@ -188,3 +188,9 @@ Icon Network Trash Folder Temporary Items .apdisk + +# dependencies +node_modules/ + +# dist +web/ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..5ee7abd --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm exec lint-staged diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..8da9e7e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,12 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "singleQuote": true, + "trailingComma": "all", + "endOfLine": "lf", + "semi": false, + "plugins": [ + "prettier-plugin-organize-imports" + ] +} \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index a253ce4..0000000 --- a/.prettierrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "printWidth": 80, - "tabWidth": 2, - "useTabs": false, - "singleQuote": true, - "trailingComma": "all" -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index f2c3f14..66ffc70 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,20 +1,23 @@ { - "cSpell.words": [ - "apng", - "Civitai", - "ckpt", - "comfyui", - "FYUIKMNVB", - "gguf", - "gligen", - "jfif", - "locon", - "loras", - "noimage", - "onnx", - "rfilename", - "unet", - "upscaler" - ], - "editor.defaultFormatter": "esbenp.prettier-vscode" + "cSpell.words": [ + "apng", + "Civitai", + "ckpt", + "comfyui", + "FYUIKMNVB", + "gguf", + "gligen", + "jfif", + "locon", + "loras", + "noimage", + "onnx", + "rfilename", + "unet", + "upscaler" + ], + "editor.defaultFormatter": "esbenp.prettier-vscode", + "files.associations": { + "*.css": "tailwindcss" + } } \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..a9322a5 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,32 @@ +import globals from 'globals' +import pluginJs from '@eslint/js' +import tsEslint from 'typescript-eslint' +import pluginVue from 'eslint-plugin-vue' + +export default [ + { + files: ['src/**/*.{js,mjs,cjs,ts,vue}'], + }, + { + ignores: [ + 'src/scripts/*', + 'src/extensions/core/*', + 'src/types/vue-shim.d.ts', + ], + }, + { languageOptions: { globals: globals.browser } }, + pluginJs.configs.recommended, + ...tsEslint.configs.recommended, + ...pluginVue.configs['flat/essential'], + { + files: ['src/**/*.vue'], + languageOptions: { parserOptions: { parser: tsEslint.parser } }, + }, + { + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/prefer-as-const': 'off', + }, + }, +] diff --git a/index.html b/index.html new file mode 100644 index 0000000..5500a50 --- /dev/null +++ b/index.html @@ -0,0 +1,11 @@ + + + + + + ComfyUI-Model-Manager + + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..4b3fab8 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "comfyui-model-manager", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite build --watch", + "build": "vite build", + "prepare": "husky" + }, + "devDependencies": { + "@types/node": "^22.5.5", + "@vitejs/plugin-vue": "^5.1.4", + "autoprefixer": "^10.4.20", + "eslint": "^9.10.0", + "eslint-plugin-vue": "^9.28.0", + "husky": "^9.1.6", + "less": "^4.2.0", + "lint-staged": "^15.2.10", + "postcss": "^8.4.47", + "prettier": "^3.3.3", + "prettier-plugin-organize-imports": "^4.1.0", + "tailwindcss": "^3.4.12", + "typescript": "^5.6.2", + "typescript-eslint": "^8.6.0", + "vite": "^5.4.6" + }, + "dependencies": { + "primevue": "^4.0.7", + "vue": "^3.4.31", + "vue-i18n": "^9.13.1" + }, + "lint-staged": { + "./**/*.{js,ts,tsx,vue}": [ + "prettier --write", + "git add" + ] + } +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..f75ee96 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2953 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + primevue: + specifier: ^4.0.7 + version: 4.0.7(vue@3.5.6(typescript@5.6.2)) + vue: + specifier: ^3.4.31 + version: 3.5.6(typescript@5.6.2) + vue-i18n: + specifier: ^9.13.1 + version: 9.14.0(vue@3.5.6(typescript@5.6.2)) + devDependencies: + '@types/node': + specifier: ^22.5.5 + version: 22.5.5 + '@vitejs/plugin-vue': + specifier: ^5.1.4 + version: 5.1.4(vite@5.4.6(@types/node@22.5.5)(less@4.2.0))(vue@3.5.6(typescript@5.6.2)) + autoprefixer: + specifier: ^10.4.20 + version: 10.4.20(postcss@8.4.47) + eslint: + specifier: ^9.10.0 + version: 9.10.0(jiti@1.21.6) + eslint-plugin-vue: + specifier: ^9.28.0 + version: 9.28.0(eslint@9.10.0(jiti@1.21.6)) + husky: + specifier: ^9.1.6 + version: 9.1.6 + less: + specifier: ^4.2.0 + version: 4.2.0 + lint-staged: + specifier: ^15.2.10 + version: 15.2.10 + postcss: + specifier: ^8.4.47 + version: 8.4.47 + prettier: + specifier: ^3.3.3 + version: 3.3.3 + prettier-plugin-organize-imports: + specifier: ^4.1.0 + version: 4.1.0(prettier@3.3.3)(typescript@5.6.2) + tailwindcss: + specifier: ^3.4.12 + version: 3.4.12 + typescript: + specifier: ^5.6.2 + version: 5.6.2 + typescript-eslint: + specifier: ^8.6.0 + version: 8.6.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) + vite: + specifier: ^5.4.6 + version: 5.4.6(@types/node@22.5.5)(less@4.2.0) + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@babel/helper-string-parser@7.24.8': + resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.24.7': + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.25.6': + resolution: {integrity: sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.25.6': + resolution: {integrity: sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.4.0': + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.11.1': + resolution: {integrity: sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.18.0': + resolution: {integrity: sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.1.0': + resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.10.0': + resolution: {integrity: sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.4': + resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.1.0': + resolution: {integrity: sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.0': + resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==} + engines: {node: '>=18.18'} + + '@intlify/core-base@9.14.0': + resolution: {integrity: sha512-zJn0imh9HIsZZUtt9v8T16PeVstPv6bP2YzlrYJwoF8F30gs4brZBwW2KK6EI5WYKFi3NeqX6+UU4gniz5TkGg==} + engines: {node: '>= 16'} + + '@intlify/message-compiler@9.14.0': + resolution: {integrity: sha512-sXNsoMI0YsipSXW8SR75drmVK56tnJHoYbPXUv2Cf9lz6FzvwsosFm6JtC1oQZI/kU+n7qx0qRrEWkeYFTgETA==} + engines: {node: '>= 16'} + + '@intlify/shared@9.14.0': + resolution: {integrity: sha512-r+N8KRQL7LgN1TMTs1A2svfuAU0J94Wu9wWdJVJqYsoMMLIeJxrPjazihfHpmJqfgZq0ah3Y9Q4pgWV2O90Fyg==} + engines: {node: '>= 16'} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@primeuix/styled@0.0.5': + resolution: {integrity: sha512-pVoGn/uPkVm/DyF3TR3EmH/pL/dP4nR42FcYbVduFq9VfO3KVeOEqvcCULHXos66RZO9MCbCFUoLy6ctf9GUGQ==} + engines: {node: '>=12.11.0'} + + '@primeuix/utils@0.0.5': + resolution: {integrity: sha512-ntUiUgtRtkF8KuaxHffzhYxQxoXk6LAPHm7CVlFjdqS8Rx8xRkLkZVyo84E+pO2hcNFkOGVP/GxHhQ2s94O8zA==} + engines: {node: '>=12.11.0'} + + '@primevue/core@4.0.7': + resolution: {integrity: sha512-SvWiNBEeR6hm4wjnze+rITUjHMFLwIzpRFlq+GqmJyZmjJy4h8UUksi0EoyqAWCAwKgmwlxY6XNqGJmMVyOguQ==} + engines: {node: '>=12.11.0'} + peerDependencies: + vue: ^3.0.0 + + '@primevue/icons@4.0.7': + resolution: {integrity: sha512-tj4dfRdV5iN6O0mbkpjhMsGlT3wZTqOPL779ndY5gKuCwN5zcFmKmABWVQmr/ClRivnMkw6Yr1x6gRTV/N0ydg==} + engines: {node: '>=12.11.0'} + + '@rollup/rollup-android-arm-eabi@4.22.0': + resolution: {integrity: sha512-/IZQvg6ZR0tAkEi4tdXOraQoWeJy9gbQ/cx4I7k9dJaCk9qrXEcdouxRVz5kZXt5C2bQ9pILoAA+KB4C/d3pfw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.22.0': + resolution: {integrity: sha512-ETHi4bxrYnvOtXeM7d4V4kZWixib2jddFacJjsOjwbgYSRsyXYtZHC4ht134OsslPIcnkqT+TKV4eU8rNBKyyQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.22.0': + resolution: {integrity: sha512-ZWgARzhSKE+gVUX7QWaECoRQsPwaD8ZR0Oxb3aUpzdErTvlEadfQpORPXkKSdKbFci9v8MJfkTtoEHnnW9Ulng==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.22.0': + resolution: {integrity: sha512-h0ZAtOfHyio8Az6cwIGS+nHUfRMWBDO5jXB8PQCARVF6Na/G6XS2SFxDl8Oem+S5ZsHQgtsI7RT4JQnI1qrlaw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-linux-arm-gnueabihf@4.22.0': + resolution: {integrity: sha512-9pxQJSPwFsVi0ttOmqLY4JJ9pg9t1gKhK0JDbV1yUEETSx55fdyCjt39eBQ54OQCzAF0nVGO6LfEH1KnCPvelA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.22.0': + resolution: {integrity: sha512-YJ5Ku5BmNJZb58A4qSEo3JlIG4d3G2lWyBi13ABlXzO41SsdnUKi3HQHe83VpwBVG4jHFTW65jOQb8qyoR+qzg==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.22.0': + resolution: {integrity: sha512-U4G4u7f+QCqHlVg1Nlx+qapZy+QoG+NV6ux+upo/T7arNGwKvKP2kmGM4W5QTbdewWFgudQxi3kDNST9GT1/mg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.22.0': + resolution: {integrity: sha512-aQpNlKmx3amwkA3a5J6nlXSahE1ijl0L9KuIjVOUhfOh7uw2S4piR3mtpxpRtbnK809SBtyPsM9q15CPTsY7HQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-powerpc64le-gnu@4.22.0': + resolution: {integrity: sha512-9fx6Zj/7vve/Fp4iexUFRKb5+RjLCff6YTRQl4CoDhdMfDoobWmhAxQWV3NfShMzQk1Q/iCnageFyGfqnsmeqQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-gnu@4.22.0': + resolution: {integrity: sha512-VWQiCcN7zBgZYLjndIEh5tamtnKg5TGxyZPWcN9zBtXBwfcGSZ5cHSdQZfQH/GB4uRxk0D3VYbOEe/chJhPGLQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-s390x-gnu@4.22.0': + resolution: {integrity: sha512-EHmPnPWvyYqncObwqrosb/CpH3GOjE76vWVs0g4hWsDRUVhg61hBmlVg5TPXqF+g+PvIbqkC7i3h8wbn4Gp2Fg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.22.0': + resolution: {integrity: sha512-tsSWy3YQzmpjDKnQ1Vcpy3p9Z+kMFbSIesCdMNgLizDWFhrLZIoN21JSq01g+MZMDFF+Y1+4zxgrlqPjid5ohg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.22.0': + resolution: {integrity: sha512-anr1Y11uPOQrpuU8XOikY5lH4Qu94oS6j0xrulHk3NkLDq19MlX8Ng/pVipjxBJ9a2l3+F39REZYyWQFkZ4/fw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-win32-arm64-msvc@4.22.0': + resolution: {integrity: sha512-7LB+Bh+Ut7cfmO0m244/asvtIGQr5pG5Rvjz/l1Rnz1kDzM02pSX9jPaS0p+90H5I1x4d1FkCew+B7MOnoatNw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.22.0': + resolution: {integrity: sha512-+3qZ4rer7t/QsC5JwMpcvCVPRcJt1cJrYS/TMJZzXIJbxWFQEVhrIc26IhB+5Z9fT9umfVc+Es2mOZgl+7jdJQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.22.0': + resolution: {integrity: sha512-YdicNOSJONVx/vuPkgPTyRoAPx3GbknBZRCOUkK84FJ/YTfs/F0vl/YsMscrB6Y177d+yDRcj+JWMPMCgshwrA==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.5': + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + + '@types/node@22.5.5': + resolution: {integrity: sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==} + + '@typescript-eslint/eslint-plugin@8.6.0': + resolution: {integrity: sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@8.6.0': + resolution: {integrity: sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@8.6.0': + resolution: {integrity: sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@8.6.0': + resolution: {integrity: sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@8.6.0': + resolution: {integrity: sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.6.0': + resolution: {integrity: sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@8.6.0': + resolution: {integrity: sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + + '@typescript-eslint/visitor-keys@8.6.0': + resolution: {integrity: sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-vue@5.1.4': + resolution: {integrity: sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 + vue: ^3.2.25 + + '@vue/compiler-core@3.5.6': + resolution: {integrity: sha512-r+gNu6K4lrvaQLQGmf+1gc41p3FO2OUJyWmNqaIITaJU6YFiV5PtQSFZt8jfztYyARwqhoCayjprC7KMvT3nRA==} + + '@vue/compiler-dom@3.5.6': + resolution: {integrity: sha512-xRXqxDrIqK8v8sSScpistyYH0qYqxakpsIvqMD2e5sV/PXQ1mTwtXp4k42yHK06KXxKSmitop9e45Ui/3BrTEw==} + + '@vue/compiler-sfc@3.5.6': + resolution: {integrity: sha512-pjWJ8Kj9TDHlbF5LywjVso+BIxCY5wVOLhkEXRhuCHDxPFIeX1zaFefKs8RYoHvkSMqRWt93a0f2gNJVJixHwg==} + + '@vue/compiler-ssr@3.5.6': + resolution: {integrity: sha512-VpWbaZrEOCqnmqjE83xdwegtr5qO/2OPUC6veWgvNqTJ3bYysz6vY3VqMuOijubuUYPRpG3OOKIh9TD0Stxb9A==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/reactivity@3.5.6': + resolution: {integrity: sha512-shZ+KtBoHna5GyUxWfoFVBCVd7k56m6lGhk5e+J9AKjheHF6yob5eukssHRI+rzvHBiU1sWs/1ZhNbLExc5oYQ==} + + '@vue/runtime-core@3.5.6': + resolution: {integrity: sha512-FpFULR6+c2lI+m1fIGONLDqPQO34jxV8g6A4wBOgne8eSRHP6PQL27+kWFIx5wNhhjkO7B4rgtsHAmWv7qKvbg==} + + '@vue/runtime-dom@3.5.6': + resolution: {integrity: sha512-SDPseWre45G38ENH2zXRAHL1dw/rr5qp91lS4lt/nHvMr0MhsbCbihGAWLXNB/6VfFOJe2O+RBRkXU+CJF7/sw==} + + '@vue/server-renderer@3.5.6': + resolution: {integrity: sha512-zivnxQnOnwEXVaT9CstJ64rZFXMS5ZkKxCjDQKiMSvUhXRzFLWZVbaBiNF4HGDqGNNsTgmjcCSmU6TB/0OOxLA==} + peerDependencies: + vue: 3.5.6 + + '@vue/shared@3.5.6': + resolution: {integrity: sha512-eidH0HInnL39z6wAt6SFIwBrvGOpDWsDxlw3rCgo1B+CQ1781WzQUSU3YjxgdkcJo9Q8S6LmXTkvI+cLHGkQfA==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-escapes@7.0.0: + resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} + engines: {node: '>=18'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + autoprefixer@10.4.20: + resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.23.3: + resolution: {integrity: sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001662: + resolution: {integrity: sha512-sgMUVwLmGseH8ZIrm1d51UbrhqMCH3jvS7gF/M6byuHOnKyLOBL7W8yz5V02OHwgLGA36o/AFhWzzh4uc5aqTA==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-truncate@4.0.0: + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + copy-anything@2.0.6: + resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==} + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + electron-to-chromium@1.5.25: + resolution: {integrity: sha512-kMb204zvK3PsSlgvvwzI3wBIcAw15tRkYk+NQdsjdDtcQWTp2RABbMQ9rUBy8KNEOM+/E6ep+XC3AykiWZld4g==} + + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + errno@0.1.8: + resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} + hasBin: true + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-vue@9.28.0: + resolution: {integrity: sha512-ShrihdjIhOTxs+MfWun6oJWuk+g/LAhN+CiuOl/jjkG3l0F2AuK5NMTaWqyvBgkFtpYmyks6P4603mLmhNJW8g==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-scope@8.0.2: + resolution: {integrity: sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.0.0: + resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.10.0: + resolution: {integrity: sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.1.0: + resolution: {integrity: sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-east-asian-width@1.2.0: + resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} + engines: {node: '>=18'} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + husky@9.1.6: + resolution: {integrity: sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A==} + engines: {node: '>=18'} + hasBin: true + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + image-size@0.5.5: + resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} + engines: {node: '>=0.10.0'} + hasBin: true + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.15.1: + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + + is-fullwidth-code-point@5.0.0: + resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} + engines: {node: '>=18'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-what@3.14.1: + resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jiti@1.21.6: + resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + less@4.2.0: + resolution: {integrity: sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==} + engines: {node: '>=6'} + hasBin: true + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + + lilconfig@3.1.2: + resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lint-staged@15.2.10: + resolution: {integrity: sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==} + engines: {node: '>=18.12.0'} + hasBin: true + + listr2@8.2.4: + resolution: {integrity: sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==} + engines: {node: '>=18.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + magic-string@0.30.11: + resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} + + make-dir@2.1.0: + resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + engines: {node: '>=6'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + needle@3.3.1: + resolution: {integrity: sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==} + engines: {node: '>= 4.4.x'} + hasBin: true + + node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + package-json-from-dist@1.0.0: + resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-node-version@1.0.1: + resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==} + engines: {node: '>= 0.10'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + picocolors@1.1.0: + resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.0.1: + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@4.0.2: + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.4.47: + resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-plugin-organize-imports@4.1.0: + resolution: {integrity: sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==} + peerDependencies: + prettier: '>=2.0' + typescript: '>=2.9' + vue-tsc: ^2.1.0 + peerDependenciesMeta: + vue-tsc: + optional: true + + prettier@3.3.3: + resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} + engines: {node: '>=14'} + hasBin: true + + primevue@4.0.7: + resolution: {integrity: sha512-88qazHqldkqsCxvhjnjO65XMBfJyHQoFW3BQvrJYO6RqPheHB4f7cY61eqtBpJAjnM5x+YKTZiWx/gBuUzqT7Q==} + engines: {node: '>=12.11.0'} + + prr@1.0.1: + resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup@4.22.0: + resolution: {integrity: sha512-W21MUIFPZ4+O2Je/EU+GP3iz7PH4pVPUXSbEZdatQnxo29+3rsUjgrJmzuAZU24z7yRAnFN6ukxeAhZh/c7hzg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + + slice-ansi@7.1.0: + resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwindcss@3.4.12: + resolution: {integrity: sha512-Htf/gHj2+soPb9UayUNci/Ja3d8pTmu9ONTfh4QY8r3MATTZOzmv6UYWF7ZwikEIC8okpfqmGqrmDehua8mF8w==} + engines: {node: '>=14.0.0'} + hasBin: true + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-api-utils@1.3.0: + resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + typescript-eslint@8.6.0: + resolution: {integrity: sha512-eEhhlxCEpCd4helh3AO1hk0UP2MvbRi9CtIAJTVPQjuSXOOO2jsEacNi4UdcJzZJbeuVg1gMhtZ8UYb+NFYPrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + typescript@5.6.2: + resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + + update-browserslist-db@1.1.0: + resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@5.4.6: + resolution: {integrity: sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vue-eslint-parser@9.4.3: + resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + vue-i18n@9.14.0: + resolution: {integrity: sha512-LxmpRuCt2rI8gqU+kxeflRZMQn4D5+4M3oP3PWZdowW/ePJraHqhF7p4CuaME52mUxdw3Mmy2yAUKgfZYgCRjA==} + engines: {node: '>= 16'} + peerDependencies: + vue: ^3.0.0 + + vue@3.5.6: + resolution: {integrity: sha512-zv+20E2VIYbcJOzJPUWp03NOGFhMmpCKOfSxVTmCYyYFFko48H9tmuQFzYj7tu4qX1AeXlp9DmhIP89/sSxxhw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + yaml@2.5.1: + resolution: {integrity: sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==} + engines: {node: '>= 14'} + hasBin: true + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@babel/helper-string-parser@7.24.8': {} + + '@babel/helper-validator-identifier@7.24.7': {} + + '@babel/parser@7.25.6': + dependencies: + '@babel/types': 7.25.6 + + '@babel/types@7.25.6': + dependencies: + '@babel/helper-string-parser': 7.24.8 + '@babel/helper-validator-identifier': 7.24.7 + to-fast-properties: 2.0.0 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@eslint-community/eslint-utils@4.4.0(eslint@9.10.0(jiti@1.21.6))': + dependencies: + eslint: 9.10.0(jiti@1.21.6) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.11.1': {} + + '@eslint/config-array@0.18.0': + dependencies: + '@eslint/object-schema': 2.1.4 + debug: 4.3.7 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/eslintrc@3.1.0': + dependencies: + ajv: 6.12.6 + debug: 4.3.7 + espree: 10.1.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.10.0': {} + + '@eslint/object-schema@2.1.4': {} + + '@eslint/plugin-kit@0.1.0': + dependencies: + levn: 0.4.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.0': {} + + '@intlify/core-base@9.14.0': + dependencies: + '@intlify/message-compiler': 9.14.0 + '@intlify/shared': 9.14.0 + + '@intlify/message-compiler@9.14.0': + dependencies: + '@intlify/shared': 9.14.0 + source-map-js: 1.2.1 + + '@intlify/shared@9.14.0': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@primeuix/styled@0.0.5': + dependencies: + '@primeuix/utils': 0.0.5 + + '@primeuix/utils@0.0.5': {} + + '@primevue/core@4.0.7(vue@3.5.6(typescript@5.6.2))': + dependencies: + '@primeuix/styled': 0.0.5 + '@primeuix/utils': 0.0.5 + vue: 3.5.6(typescript@5.6.2) + + '@primevue/icons@4.0.7(vue@3.5.6(typescript@5.6.2))': + dependencies: + '@primeuix/utils': 0.0.5 + '@primevue/core': 4.0.7(vue@3.5.6(typescript@5.6.2)) + transitivePeerDependencies: + - vue + + '@rollup/rollup-android-arm-eabi@4.22.0': + optional: true + + '@rollup/rollup-android-arm64@4.22.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.22.0': + optional: true + + '@rollup/rollup-darwin-x64@4.22.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.22.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.22.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.22.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.22.0': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.22.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.22.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.22.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.22.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.22.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.22.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.22.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.22.0': + optional: true + + '@types/estree@1.0.5': {} + + '@types/node@22.5.5': + dependencies: + undici-types: 6.19.8 + + '@typescript-eslint/eslint-plugin@8.6.0(@typescript-eslint/parser@8.6.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)': + dependencies: + '@eslint-community/regexpp': 4.11.1 + '@typescript-eslint/parser': 8.6.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) + '@typescript-eslint/scope-manager': 8.6.0 + '@typescript-eslint/type-utils': 8.6.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) + '@typescript-eslint/utils': 8.6.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) + '@typescript-eslint/visitor-keys': 8.6.0 + eslint: 9.10.0(jiti@1.21.6) + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.3.0(typescript@5.6.2) + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.6.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)': + dependencies: + '@typescript-eslint/scope-manager': 8.6.0 + '@typescript-eslint/types': 8.6.0 + '@typescript-eslint/typescript-estree': 8.6.0(typescript@5.6.2) + '@typescript-eslint/visitor-keys': 8.6.0 + debug: 4.3.7 + eslint: 9.10.0(jiti@1.21.6) + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.6.0': + dependencies: + '@typescript-eslint/types': 8.6.0 + '@typescript-eslint/visitor-keys': 8.6.0 + + '@typescript-eslint/type-utils@8.6.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)': + dependencies: + '@typescript-eslint/typescript-estree': 8.6.0(typescript@5.6.2) + '@typescript-eslint/utils': 8.6.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) + debug: 4.3.7 + ts-api-utils: 1.3.0(typescript@5.6.2) + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - eslint + - supports-color + + '@typescript-eslint/types@8.6.0': {} + + '@typescript-eslint/typescript-estree@8.6.0(typescript@5.6.2)': + dependencies: + '@typescript-eslint/types': 8.6.0 + '@typescript-eslint/visitor-keys': 8.6.0 + debug: 4.3.7 + fast-glob: 3.3.2 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.3.0(typescript@5.6.2) + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.6.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)': + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.10.0(jiti@1.21.6)) + '@typescript-eslint/scope-manager': 8.6.0 + '@typescript-eslint/types': 8.6.0 + '@typescript-eslint/typescript-estree': 8.6.0(typescript@5.6.2) + eslint: 9.10.0(jiti@1.21.6) + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@8.6.0': + dependencies: + '@typescript-eslint/types': 8.6.0 + eslint-visitor-keys: 3.4.3 + + '@vitejs/plugin-vue@5.1.4(vite@5.4.6(@types/node@22.5.5)(less@4.2.0))(vue@3.5.6(typescript@5.6.2))': + dependencies: + vite: 5.4.6(@types/node@22.5.5)(less@4.2.0) + vue: 3.5.6(typescript@5.6.2) + + '@vue/compiler-core@3.5.6': + dependencies: + '@babel/parser': 7.25.6 + '@vue/shared': 3.5.6 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.6': + dependencies: + '@vue/compiler-core': 3.5.6 + '@vue/shared': 3.5.6 + + '@vue/compiler-sfc@3.5.6': + dependencies: + '@babel/parser': 7.25.6 + '@vue/compiler-core': 3.5.6 + '@vue/compiler-dom': 3.5.6 + '@vue/compiler-ssr': 3.5.6 + '@vue/shared': 3.5.6 + estree-walker: 2.0.2 + magic-string: 0.30.11 + postcss: 8.4.47 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.6': + dependencies: + '@vue/compiler-dom': 3.5.6 + '@vue/shared': 3.5.6 + + '@vue/devtools-api@6.6.4': {} + + '@vue/reactivity@3.5.6': + dependencies: + '@vue/shared': 3.5.6 + + '@vue/runtime-core@3.5.6': + dependencies: + '@vue/reactivity': 3.5.6 + '@vue/shared': 3.5.6 + + '@vue/runtime-dom@3.5.6': + dependencies: + '@vue/reactivity': 3.5.6 + '@vue/runtime-core': 3.5.6 + '@vue/shared': 3.5.6 + csstype: 3.1.3 + + '@vue/server-renderer@3.5.6(vue@3.5.6(typescript@5.6.2))': + dependencies: + '@vue/compiler-ssr': 3.5.6 + '@vue/shared': 3.5.6 + vue: 3.5.6(typescript@5.6.2) + + '@vue/shared@3.5.6': {} + + acorn-jsx@5.3.2(acorn@8.12.1): + dependencies: + acorn: 8.12.1 + + acorn@8.12.1: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-escapes@7.0.0: + dependencies: + environment: 1.1.0 + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@5.0.2: {} + + argparse@2.0.1: {} + + autoprefixer@10.4.20(postcss@8.4.47): + dependencies: + browserslist: 4.23.3 + caniuse-lite: 1.0.30001662 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.0 + postcss: 8.4.47 + postcss-value-parser: 4.2.0 + + balanced-match@1.0.2: {} + + binary-extensions@2.3.0: {} + + boolbase@1.0.0: {} + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.23.3: + dependencies: + caniuse-lite: 1.0.30001662 + electron-to-chromium: 1.5.25 + node-releases: 2.0.18 + update-browserslist-db: 1.1.0(browserslist@4.23.3) + + callsites@3.1.0: {} + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001662: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.3.0: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-truncate@4.0.0: + dependencies: + slice-ansi: 5.0.0 + string-width: 7.2.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@2.0.20: {} + + commander@12.1.0: {} + + commander@4.1.1: {} + + concat-map@0.0.1: {} + + copy-anything@2.0.6: + dependencies: + is-what: 3.14.1 + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.1.3: {} + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + didyoumean@1.2.2: {} + + dlv@1.1.3: {} + + eastasianwidth@0.2.0: {} + + electron-to-chromium@1.5.25: {} + + emoji-regex@10.4.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + entities@4.5.0: {} + + environment@1.1.0: {} + + errno@0.1.8: + dependencies: + prr: 1.0.1 + optional: true + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-vue@9.28.0(eslint@9.10.0(jiti@1.21.6)): + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.10.0(jiti@1.21.6)) + eslint: 9.10.0(jiti@1.21.6) + globals: 13.24.0 + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.1.2 + semver: 7.6.3 + vue-eslint-parser: 9.4.3(eslint@9.10.0(jiti@1.21.6)) + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - supports-color + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-scope@8.0.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.0.0: {} + + eslint@9.10.0(jiti@1.21.6): + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.10.0(jiti@1.21.6)) + '@eslint-community/regexpp': 4.11.1 + '@eslint/config-array': 0.18.0 + '@eslint/eslintrc': 3.1.0 + '@eslint/js': 9.10.0 + '@eslint/plugin-kit': 0.1.0 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.3.0 + '@nodelib/fs.walk': 1.2.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.7 + escape-string-regexp: 4.0.0 + eslint-scope: 8.0.2 + eslint-visitor-keys: 4.0.0 + espree: 10.1.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + 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.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + optionalDependencies: + jiti: 1.21.6 + transitivePeerDependencies: + - supports-color + + espree@10.1.0: + dependencies: + acorn: 8.12.1 + acorn-jsx: 5.3.2(acorn@8.12.1) + eslint-visitor-keys: 4.0.0 + + espree@9.6.1: + dependencies: + acorn: 8.12.1 + acorn-jsx: 5.3.2(acorn@8.12.1) + eslint-visitor-keys: 3.4.3 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + eventemitter3@5.0.1: {} + + execa@8.0.1: + dependencies: + cross-spawn: 7.0.3 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + + flatted@3.3.1: {} + + foreground-child@3.3.0: + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + + fraction.js@4.3.7: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-east-asian-width@1.2.0: {} + + get-stream@8.0.1: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.4.5: + dependencies: + foreground-child: 3.3.0 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.0 + path-scurry: 1.11.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globals@14.0.0: {} + + graceful-fs@4.2.11: + optional: true + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + human-signals@5.0.0: {} + + husky@9.1.6: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + optional: true + + ignore@5.3.2: {} + + image-size@0.5.5: + optional: true + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.15.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-fullwidth-code-point@4.0.0: {} + + is-fullwidth-code-point@5.0.0: + dependencies: + get-east-asian-width: 1.2.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + is-stream@3.0.0: {} + + is-what@3.14.1: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jiti@1.21.6: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + less@4.2.0: + dependencies: + copy-anything: 2.0.6 + parse-node-version: 1.0.1 + tslib: 2.7.0 + optionalDependencies: + errno: 0.1.8 + graceful-fs: 4.2.11 + image-size: 0.5.5 + make-dir: 2.1.0 + mime: 1.6.0 + needle: 3.3.1 + source-map: 0.6.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@2.1.0: {} + + lilconfig@3.1.2: {} + + lines-and-columns@1.2.4: {} + + lint-staged@15.2.10: + dependencies: + chalk: 5.3.0 + commander: 12.1.0 + debug: 4.3.7 + execa: 8.0.1 + lilconfig: 3.1.2 + listr2: 8.2.4 + micromatch: 4.0.8 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.5.1 + transitivePeerDependencies: + - supports-color + + listr2@8.2.4: + dependencies: + 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@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + lodash@4.17.21: {} + + log-update@6.1.0: + dependencies: + 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 + + lru-cache@10.4.3: {} + + magic-string@0.30.11: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + make-dir@2.1.0: + dependencies: + pify: 4.0.1 + semver: 5.7.2 + optional: true + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime@1.6.0: + optional: true + + mimic-fn@4.0.0: {} + + mimic-function@5.0.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minipass@7.1.2: {} + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.7: {} + + natural-compare@1.4.0: {} + + needle@3.3.1: + dependencies: + iconv-lite: 0.6.3 + sax: 1.4.1 + optional: true + + node-releases@2.0.18: {} + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + 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@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + package-json-from-dist@1.0.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-node-version@1.0.1: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + picocolors@1.1.0: {} + + picomatch@2.3.1: {} + + pidtree@0.6.0: {} + + pify@2.3.0: {} + + pify@4.0.1: + optional: true + + pirates@4.0.6: {} + + postcss-import@15.1.0(postcss@8.4.47): + dependencies: + postcss: 8.4.47 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.8 + + postcss-js@4.0.1(postcss@8.4.47): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.47 + + postcss-load-config@4.0.2(postcss@8.4.47): + dependencies: + lilconfig: 3.1.2 + yaml: 2.5.1 + optionalDependencies: + postcss: 8.4.47 + + postcss-nested@6.2.0(postcss@8.4.47): + dependencies: + postcss: 8.4.47 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.4.47: + dependencies: + nanoid: 3.3.7 + picocolors: 1.1.0 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier-plugin-organize-imports@4.1.0(prettier@3.3.3)(typescript@5.6.2): + dependencies: + prettier: 3.3.3 + typescript: 5.6.2 + + prettier@3.3.3: {} + + primevue@4.0.7(vue@3.5.6(typescript@5.6.2)): + dependencies: + '@primeuix/styled': 0.0.5 + '@primeuix/utils': 0.0.5 + '@primevue/core': 4.0.7(vue@3.5.6(typescript@5.6.2)) + '@primevue/icons': 4.0.7(vue@3.5.6(typescript@5.6.2)) + transitivePeerDependencies: + - vue + + prr@1.0.1: + optional: true + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + resolve-from@4.0.0: {} + + resolve@1.22.8: + dependencies: + is-core-module: 2.15.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + reusify@1.0.4: {} + + rfdc@1.4.1: {} + + rollup@4.22.0: + dependencies: + '@types/estree': 1.0.5 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.22.0 + '@rollup/rollup-android-arm64': 4.22.0 + '@rollup/rollup-darwin-arm64': 4.22.0 + '@rollup/rollup-darwin-x64': 4.22.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.22.0 + '@rollup/rollup-linux-arm-musleabihf': 4.22.0 + '@rollup/rollup-linux-arm64-gnu': 4.22.0 + '@rollup/rollup-linux-arm64-musl': 4.22.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.22.0 + '@rollup/rollup-linux-riscv64-gnu': 4.22.0 + '@rollup/rollup-linux-s390x-gnu': 4.22.0 + '@rollup/rollup-linux-x64-gnu': 4.22.0 + '@rollup/rollup-linux-x64-musl': 4.22.0 + '@rollup/rollup-win32-arm64-msvc': 4.22.0 + '@rollup/rollup-win32-ia32-msvc': 4.22.0 + '@rollup/rollup-win32-x64-msvc': 4.22.0 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safer-buffer@2.1.2: + optional: true + + sax@1.4.1: + optional: true + + semver@5.7.2: + optional: true + + semver@7.6.3: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + slice-ansi@5.0.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 4.0.0 + + slice-ansi@7.1.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 5.0.0 + + source-map-js@1.2.1: {} + + source-map@0.6.1: + optional: true + + string-argv@0.3.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.4.0 + get-east-asian-width: 1.2.0 + strip-ansi: 7.1.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-final-newline@3.0.0: {} + + strip-json-comments@3.1.1: {} + + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + commander: 4.1.1 + glob: 10.4.5 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwindcss@3.4.12: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.2 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.6 + lilconfig: 2.1.0 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.0 + postcss: 8.4.47 + postcss-import: 15.1.0(postcss@8.4.47) + postcss-js: 4.0.1(postcss@8.4.47) + postcss-load-config: 4.0.2(postcss@8.4.47) + postcss-nested: 6.2.0(postcss@8.4.47) + postcss-selector-parser: 6.1.2 + resolve: 1.22.8 + sucrase: 3.35.0 + transitivePeerDependencies: + - ts-node + + text-table@0.2.0: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + to-fast-properties@2.0.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-api-utils@1.3.0(typescript@5.6.2): + dependencies: + typescript: 5.6.2 + + ts-interface-checker@0.1.13: {} + + tslib@2.7.0: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + typescript-eslint@8.6.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2): + dependencies: + '@typescript-eslint/eslint-plugin': 8.6.0(@typescript-eslint/parser@8.6.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) + '@typescript-eslint/parser': 8.6.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) + '@typescript-eslint/utils': 8.6.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - eslint + - supports-color + + typescript@5.6.2: {} + + undici-types@6.19.8: {} + + update-browserslist-db@1.1.0(browserslist@4.23.3): + dependencies: + browserslist: 4.23.3 + escalade: 3.2.0 + picocolors: 1.1.0 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + vite@5.4.6(@types/node@22.5.5)(less@4.2.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.4.47 + rollup: 4.22.0 + optionalDependencies: + '@types/node': 22.5.5 + fsevents: 2.3.3 + less: 4.2.0 + + vue-eslint-parser@9.4.3(eslint@9.10.0(jiti@1.21.6)): + dependencies: + debug: 4.3.7 + eslint: 9.10.0(jiti@1.21.6) + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + lodash: 4.17.21 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + + vue-i18n@9.14.0(vue@3.5.6(typescript@5.6.2)): + dependencies: + '@intlify/core-base': 9.14.0 + '@intlify/shared': 9.14.0 + '@vue/devtools-api': 6.6.4 + vue: 3.5.6(typescript@5.6.2) + + vue@3.5.6(typescript@5.6.2): + dependencies: + '@vue/compiler-dom': 3.5.6 + '@vue/compiler-sfc': 3.5.6 + '@vue/runtime-dom': 3.5.6 + '@vue/server-renderer': 3.5.6(vue@3.5.6(typescript@5.6.2)) + '@vue/shared': 3.5.6 + optionalDependencies: + typescript: 5.6.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + wrap-ansi@9.0.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 7.2.0 + strip-ansi: 7.1.0 + + xml-name-validator@4.0.0: {} + + yaml@2.5.1: {} + + yocto-queue@0.1.0: {} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..5c8bc02 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..cb8982b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "incremental": true, + "sourceMap": true, + "esModuleInterop": true, + "moduleResolution": "Node", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + + /* Linting */ + "strict": false, + "strictNullChecks": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "downlevelIteration": true, + + /* AllowJs during migration phase */ + "allowJs": true, + "baseUrl": ".", + "outDir": "./web", + "rootDir": "./" + }, + "include": [ + "src/**/*", + "src/**/*.vue", + ] +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..b392268 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + + build: { + outDir: 'web', + minify: 'esbuild', + target: 'es2022', + sourcemap: true, + rollupOptions: { + // Disabling tree-shaking + // Prevent vite remove unused exports + treeshake: true, + }, + }, + + esbuild: { + minifyIdentifiers: false, + keepNames: true, + minifySyntax: true, + minifyWhitespace: true, + }, +}) diff --git a/web/downshow.js b/web/downshow.js deleted file mode 100644 index 8c36e2d..0000000 --- a/web/downshow.js +++ /dev/null @@ -1,231 +0,0 @@ -/** - * downshow.js -- A javascript library to convert HTML to markdown. - * - * Copyright (c) 2013 Alex Cornejo. - * - * Original Markdown Copyright (c) 2004-2005 John Gruber - * - * - * Redistributable under a BSD-style open source license. - * - * downshow has no external dependencies. It has been tested in chrome and - * firefox, it probably works in internet explorer, but YMMV. - * - * Basic Usage: - * - * downshow(document.getElementById('#yourid').innerHTML); - * - * TODO: - * - Remove extra whitespace between words in headers and other places. - */ - -(function () { - var doc; - - // Use browser DOM with jsdom as a fallback (for node.js) - try { - doc = document; - } catch(e) { - var jsdom = require("jsdom").jsdom; - doc = jsdom(""); - } - - /** - * Returns every element in root in their bfs traversal order. - * - * In the process it transforms any nested lists to conform to the w3c - * standard, see: http://www.w3.org/wiki/HTML_lists#Nesting_lists - */ - function bfsOrder(root) { - var inqueue = [root], outqueue = []; - root._bfs_parent = null; - while (inqueue.length > 0) { - var elem = inqueue.shift(); - outqueue.push(elem); - var children = elem.childNodes; - var liParent = null; - for (var i=0 ; i 0) { - if (prefix && suffix) - node._bfs_text = prefix + content + suffix; - else - node._bfs_text = content; - } else - node._bfs_text = ''; - } - - /** - * Get a node's content. - */ - function getContent(node) { - var text = '', atom; - for (var i = 0; i 0) - setContent(node, '[' + text + '](' + href + (title ? ' "' + title + '"' : '') + ')'); - else - setContent(node, ''); - } else if (node.tagName === 'IMG') { - var src = node.getAttribute('src') ? nltrim(node.getAttribute('src')) : '', alt = node.alt ? nltrim(node.alt) : '', caption = node.title ? nltrim(node.title) : ''; - if (src.length > 0) - setContent(node, '![' + alt + '](' + src + (caption ? ' "' + caption + '"' : '') + ')'); - else - setContent(node, ''); - } else if (node.tagName === 'BLOCKQUOTE') { - var block_content = getContent(node); - if (block_content.length > 0) - setContent(node, prefixBlock('> ', block_content), '\n\n', '\n\n'); - else - setContent(node, ''); - } else if (node.tagName === 'CODE') { - if (node._bfs_parent.tagName === 'PRE' && node._bfs_parent._bfs_parent !== null) - setContent(node, prefixBlock(' ', getContent(node))); - else - setContent(node, nltrim(getContent(node)), '`', '`'); - } else if (node.tagName === 'LI') { - var list_content = getContent(node); - if (list_content.length > 0) - if (node._bfs_parent.tagName === 'OL') - setContent(node, trim(prefixBlock(' ', list_content, true)), '1. ', '\n\n'); - else - setContent(node, trim(prefixBlock(' ', list_content, true)), '- ', '\n\n'); - else - setContent(node, ''); - } else - setContent(node, getContent(node)); - } - - function downshow(html, options) { - var root = doc.createElement('pre'); - root.innerHTML = html; - var nodes = bfsOrder(root).reverse(), i; - - if (options && options.nodeParser) { - for (i = 0; i )+[^\n]*)\n+(\n(?:> )+)/g, "$1\n$2") - // remove empty blockquotes - .replace(/\n((?:> )+[ ]*\n)+/g, '\n\n') - // remove extra newlines - .replace(/\n[ \t]*(?:\n[ \t]*)+\n/g,'\n\n') - // remove trailing whitespace - .replace(/\s\s*$/, '') - // convert lists to inline when not using paragraphs - .replace(/^([ \t]*(?:\d+\.|\+|\-)[^\n]*)\n\n+(?=[ \t]*(?:\d+\.|\+|\-|\*)[^\n]*)/gm, "$1\n") - // remove starting newlines - .replace(/^\n\n*/, ''); - } - - // Export for use in server and client. - if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') - module.exports = downshow; - else if (typeof define === 'function' && define.amd) - define([], function () {return downshow;}); - else - window.downshow = downshow; - })(); \ No newline at end of file diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs deleted file mode 100644 index 2edbef8..0000000 --- a/web/eslint.config.mjs +++ /dev/null @@ -1,8 +0,0 @@ -import globals from "globals"; -import pluginJs from "@eslint/js"; - - -export default [ - {languageOptions: { globals: globals.browser }}, - pluginJs.configs.recommended, -]; \ No newline at end of file diff --git a/web/marked.js b/web/marked.js deleted file mode 100644 index f5cea94..0000000 --- a/web/marked.js +++ /dev/null @@ -1,2498 +0,0 @@ -/** - * marked v14.1.0 - a markdown parser - * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed) - * https://github.com/markedjs/marked - */ - -/** - * DO NOT EDIT THIS FILE - * The code in this file is generated from files in ./src/ - */ - -/** - * Gets the original marked default options. - */ -function _getDefaults() { - return { - async: false, - breaks: false, - extensions: null, - gfm: true, - hooks: null, - pedantic: false, - renderer: null, - silent: false, - tokenizer: null, - walkTokens: null, - }; -} -let _defaults = _getDefaults(); -function changeDefaults(newDefaults) { - _defaults = newDefaults; -} - -/** - * Helpers - */ -const escapeTest = /[&<>"']/; -const escapeReplace = new RegExp(escapeTest.source, 'g'); -const escapeTestNoEncode = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/; -const escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, 'g'); -const escapeReplacements = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', -}; -const getEscapeReplacement = (ch) => escapeReplacements[ch]; -function escape$1(html, encode) { - if (encode) { - if (escapeTest.test(html)) { - return html.replace(escapeReplace, getEscapeReplacement); - } - } - else { - if (escapeTestNoEncode.test(html)) { - return html.replace(escapeReplaceNoEncode, getEscapeReplacement); - } - } - return html; -} -const caret = /(^|[^\[])\^/g; -function edit(regex, opt) { - let source = typeof regex === 'string' ? regex : regex.source; - opt = opt || ''; - const obj = { - replace: (name, val) => { - let valSource = typeof val === 'string' ? val : val.source; - valSource = valSource.replace(caret, '$1'); - source = source.replace(name, valSource); - return obj; - }, - getRegex: () => { - return new RegExp(source, opt); - }, - }; - return obj; -} -function cleanUrl(href) { - try { - href = encodeURI(href).replace(/%25/g, '%'); - } - catch { - return null; - } - return href; -} -const noopTest = { exec: () => null }; -function splitCells(tableRow, count) { - // ensure that every cell-delimiting pipe has a space - // before it to distinguish it from an escaped pipe - const row = tableRow.replace(/\|/g, (match, offset, str) => { - let escaped = false; - let curr = offset; - while (--curr >= 0 && str[curr] === '\\') - escaped = !escaped; - if (escaped) { - // odd number of slashes means | is escaped - // so we leave it alone - return '|'; - } - else { - // add space before unescaped | - return ' |'; - } - }), cells = row.split(/ \|/); - let i = 0; - // First/last cell in a row cannot be empty if it has no leading/trailing pipe - if (!cells[0].trim()) { - cells.shift(); - } - if (cells.length > 0 && !cells[cells.length - 1].trim()) { - cells.pop(); - } - if (count) { - if (cells.length > count) { - cells.splice(count); - } - else { - while (cells.length < count) - cells.push(''); - } - } - for (; i < cells.length; i++) { - // leading or trailing whitespace is ignored per the gfm spec - cells[i] = cells[i].trim().replace(/\\\|/g, '|'); - } - return cells; -} -/** - * Remove trailing 'c's. Equivalent to str.replace(/c*$/, ''). - * /c*$/ is vulnerable to REDOS. - * - * @param str - * @param c - * @param invert Remove suffix of non-c chars instead. Default falsey. - */ -function rtrim(str, c, invert) { - const l = str.length; - if (l === 0) { - return ''; - } - // Length of suffix matching the invert condition. - let suffLen = 0; - // Step left until we fail to match the invert condition. - while (suffLen < l) { - const currChar = str.charAt(l - suffLen - 1); - if (currChar === c && !invert) { - suffLen++; - } - else if (currChar !== c && invert) { - suffLen++; - } - else { - break; - } - } - return str.slice(0, l - suffLen); -} -function findClosingBracket(str, b) { - if (str.indexOf(b[1]) === -1) { - return -1; - } - let level = 0; - for (let i = 0; i < str.length; i++) { - if (str[i] === '\\') { - i++; - } - else if (str[i] === b[0]) { - level++; - } - else if (str[i] === b[1]) { - level--; - if (level < 0) { - return i; - } - } - } - return -1; -} - -function outputLink(cap, link, raw, lexer) { - const href = link.href; - const title = link.title ? escape$1(link.title) : null; - const text = cap[1].replace(/\\([\[\]])/g, '$1'); - if (cap[0].charAt(0) !== '!') { - lexer.state.inLink = true; - const token = { - type: 'link', - raw, - href, - title, - text, - tokens: lexer.inlineTokens(text), - }; - lexer.state.inLink = false; - return token; - } - return { - type: 'image', - raw, - href, - title, - text: escape$1(text), - }; -} -function indentCodeCompensation(raw, text) { - const matchIndentToCode = raw.match(/^(\s+)(?:```)/); - if (matchIndentToCode === null) { - return text; - } - const indentToCode = matchIndentToCode[1]; - return text - .split('\n') - .map(node => { - const matchIndentInNode = node.match(/^\s+/); - if (matchIndentInNode === null) { - return node; - } - const [indentInNode] = matchIndentInNode; - if (indentInNode.length >= indentToCode.length) { - return node.slice(indentToCode.length); - } - return node; - }) - .join('\n'); -} -/** - * Tokenizer - */ -class _Tokenizer { - options; - rules; // set by the lexer - lexer; // set by the lexer - constructor(options) { - this.options = options || _defaults; - } - space(src) { - const cap = this.rules.block.newline.exec(src); - if (cap && cap[0].length > 0) { - return { - type: 'space', - raw: cap[0], - }; - } - } - code(src) { - const cap = this.rules.block.code.exec(src); - if (cap) { - const text = cap[0].replace(/^ {1,4}/gm, ''); - return { - type: 'code', - raw: cap[0], - codeBlockStyle: 'indented', - text: !this.options.pedantic - ? rtrim(text, '\n') - : text, - }; - } - } - fences(src) { - const cap = this.rules.block.fences.exec(src); - if (cap) { - const raw = cap[0]; - const text = indentCodeCompensation(raw, cap[3] || ''); - return { - type: 'code', - raw, - lang: cap[2] ? cap[2].trim().replace(this.rules.inline.anyPunctuation, '$1') : cap[2], - text, - }; - } - } - heading(src) { - const cap = this.rules.block.heading.exec(src); - if (cap) { - let text = cap[2].trim(); - // remove trailing #s - if (/#$/.test(text)) { - const trimmed = rtrim(text, '#'); - if (this.options.pedantic) { - text = trimmed.trim(); - } - else if (!trimmed || / $/.test(trimmed)) { - // CommonMark requires space before trailing #s - text = trimmed.trim(); - } - } - return { - type: 'heading', - raw: cap[0], - depth: cap[1].length, - text, - tokens: this.lexer.inline(text), - }; - } - } - hr(src) { - const cap = this.rules.block.hr.exec(src); - if (cap) { - return { - type: 'hr', - raw: rtrim(cap[0], '\n'), - }; - } - } - blockquote(src) { - const cap = this.rules.block.blockquote.exec(src); - if (cap) { - let lines = rtrim(cap[0], '\n').split('\n'); - let raw = ''; - let text = ''; - const tokens = []; - while (lines.length > 0) { - let inBlockquote = false; - const currentLines = []; - let i; - for (i = 0; i < lines.length; i++) { - // get lines up to a continuation - if (/^ {0,3}>/.test(lines[i])) { - currentLines.push(lines[i]); - inBlockquote = true; - } - else if (!inBlockquote) { - currentLines.push(lines[i]); - } - else { - break; - } - } - lines = lines.slice(i); - const currentRaw = currentLines.join('\n'); - const currentText = currentRaw - // precede setext continuation with 4 spaces so it isn't a setext - .replace(/\n {0,3}((?:=+|-+) *)(?=\n|$)/g, '\n $1') - .replace(/^ {0,3}>[ \t]?/gm, ''); - raw = raw ? `${raw}\n${currentRaw}` : currentRaw; - text = text ? `${text}\n${currentText}` : currentText; - // parse blockquote lines as top level tokens - // merge paragraphs if this is a continuation - const top = this.lexer.state.top; - this.lexer.state.top = true; - this.lexer.blockTokens(currentText, tokens, true); - this.lexer.state.top = top; - // if there is no continuation then we are done - if (lines.length === 0) { - break; - } - const lastToken = tokens[tokens.length - 1]; - if (lastToken?.type === 'code') { - // blockquote continuation cannot be preceded by a code block - break; - } - else if (lastToken?.type === 'blockquote') { - // include continuation in nested blockquote - const oldToken = lastToken; - const newText = oldToken.raw + '\n' + lines.join('\n'); - const newToken = this.blockquote(newText); - tokens[tokens.length - 1] = newToken; - raw = raw.substring(0, raw.length - oldToken.raw.length) + newToken.raw; - text = text.substring(0, text.length - oldToken.text.length) + newToken.text; - break; - } - else if (lastToken?.type === 'list') { - // include continuation in nested list - const oldToken = lastToken; - const newText = oldToken.raw + '\n' + lines.join('\n'); - const newToken = this.list(newText); - tokens[tokens.length - 1] = newToken; - raw = raw.substring(0, raw.length - lastToken.raw.length) + newToken.raw; - text = text.substring(0, text.length - oldToken.raw.length) + newToken.raw; - lines = newText.substring(tokens[tokens.length - 1].raw.length).split('\n'); - continue; - } - } - return { - type: 'blockquote', - raw, - tokens, - text, - }; - } - } - list(src) { - let cap = this.rules.block.list.exec(src); - if (cap) { - let bull = cap[1].trim(); - const isordered = bull.length > 1; - const list = { - type: 'list', - raw: '', - ordered: isordered, - start: isordered ? +bull.slice(0, -1) : '', - loose: false, - items: [], - }; - bull = isordered ? `\\d{1,9}\\${bull.slice(-1)}` : `\\${bull}`; - if (this.options.pedantic) { - bull = isordered ? bull : '[*+-]'; - } - // Get next list item - const itemRegex = new RegExp(`^( {0,3}${bull})((?:[\t ][^\\n]*)?(?:\\n|$))`); - let endsWithBlankLine = false; - // Check if current bullet point can start a new List Item - while (src) { - let endEarly = false; - let raw = ''; - let itemContents = ''; - if (!(cap = itemRegex.exec(src))) { - break; - } - if (this.rules.block.hr.test(src)) { // End list if bullet was actually HR (possibly move into itemRegex?) - break; - } - raw = cap[0]; - src = src.substring(raw.length); - let line = cap[2].split('\n', 1)[0].replace(/^\t+/, (t) => ' '.repeat(3 * t.length)); - let nextLine = src.split('\n', 1)[0]; - let blankLine = !line.trim(); - let indent = 0; - if (this.options.pedantic) { - indent = 2; - itemContents = line.trimStart(); - } - else if (blankLine) { - indent = cap[1].length + 1; - } - else { - indent = cap[2].search(/[^ ]/); // Find first non-space char - indent = indent > 4 ? 1 : indent; // Treat indented code blocks (> 4 spaces) as having only 1 indent - itemContents = line.slice(indent); - indent += cap[1].length; - } - if (blankLine && /^ *$/.test(nextLine)) { // Items begin with at most one blank line - raw += nextLine + '\n'; - src = src.substring(nextLine.length + 1); - endEarly = true; - } - if (!endEarly) { - const nextBulletRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`); - const hrRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`); - const fencesBeginRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:\`\`\`|~~~)`); - const headingBeginRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}#`); - // Check if following lines should be included in List Item - while (src) { - const rawLine = src.split('\n', 1)[0]; - nextLine = rawLine; - // Re-align to follow commonmark nesting rules - if (this.options.pedantic) { - nextLine = nextLine.replace(/^ {1,4}(?=( {4})*[^ ])/g, ' '); - } - // End list item if found code fences - if (fencesBeginRegex.test(nextLine)) { - break; - } - // End list item if found start of new heading - if (headingBeginRegex.test(nextLine)) { - break; - } - // End list item if found start of new bullet - if (nextBulletRegex.test(nextLine)) { - break; - } - // Horizontal rule found - if (hrRegex.test(src)) { - break; - } - if (nextLine.search(/[^ ]/) >= indent || !nextLine.trim()) { // Dedent if possible - itemContents += '\n' + nextLine.slice(indent); - } - else { - // not enough indentation - if (blankLine) { - break; - } - // paragraph continuation unless last line was a different block level element - if (line.search(/[^ ]/) >= 4) { // indented code block - break; - } - if (fencesBeginRegex.test(line)) { - break; - } - if (headingBeginRegex.test(line)) { - break; - } - if (hrRegex.test(line)) { - break; - } - itemContents += '\n' + nextLine; - } - if (!blankLine && !nextLine.trim()) { // Check if current line is blank - blankLine = true; - } - raw += rawLine + '\n'; - src = src.substring(rawLine.length + 1); - line = nextLine.slice(indent); - } - } - if (!list.loose) { - // If the previous item ended with a blank line, the list is loose - if (endsWithBlankLine) { - list.loose = true; - } - else if (/\n *\n *$/.test(raw)) { - endsWithBlankLine = true; - } - } - let istask = null; - let ischecked; - // Check for task list items - if (this.options.gfm) { - istask = /^\[[ xX]\] /.exec(itemContents); - if (istask) { - ischecked = istask[0] !== '[ ] '; - itemContents = itemContents.replace(/^\[[ xX]\] +/, ''); - } - } - list.items.push({ - type: 'list_item', - raw, - task: !!istask, - checked: ischecked, - loose: false, - text: itemContents, - tokens: [], - }); - list.raw += raw; - } - // Do not consume newlines at end of final item. Alternatively, make itemRegex *start* with any newlines to simplify/speed up endsWithBlankLine logic - list.items[list.items.length - 1].raw = list.items[list.items.length - 1].raw.trimEnd(); - list.items[list.items.length - 1].text = list.items[list.items.length - 1].text.trimEnd(); - list.raw = list.raw.trimEnd(); - // Item child tokens handled here at end because we needed to have the final item to trim it first - for (let i = 0; i < list.items.length; i++) { - this.lexer.state.top = false; - list.items[i].tokens = this.lexer.blockTokens(list.items[i].text, []); - if (!list.loose) { - // Check if list should be loose - const spacers = list.items[i].tokens.filter(t => t.type === 'space'); - const hasMultipleLineBreaks = spacers.length > 0 && spacers.some(t => /\n.*\n/.test(t.raw)); - list.loose = hasMultipleLineBreaks; - } - } - // Set all items to loose if list is loose - if (list.loose) { - for (let i = 0; i < list.items.length; i++) { - list.items[i].loose = true; - } - } - return list; - } - } - html(src) { - const cap = this.rules.block.html.exec(src); - if (cap) { - const token = { - type: 'html', - block: true, - raw: cap[0], - pre: cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style', - text: cap[0], - }; - return token; - } - } - def(src) { - const cap = this.rules.block.def.exec(src); - if (cap) { - const tag = cap[1].toLowerCase().replace(/\s+/g, ' '); - const href = cap[2] ? cap[2].replace(/^<(.*)>$/, '$1').replace(this.rules.inline.anyPunctuation, '$1') : ''; - const title = cap[3] ? cap[3].substring(1, cap[3].length - 1).replace(this.rules.inline.anyPunctuation, '$1') : cap[3]; - return { - type: 'def', - tag, - raw: cap[0], - href, - title, - }; - } - } - table(src) { - const cap = this.rules.block.table.exec(src); - if (!cap) { - return; - } - if (!/[:|]/.test(cap[2])) { - // delimiter row must have a pipe (|) or colon (:) otherwise it is a setext heading - return; - } - const headers = splitCells(cap[1]); - const aligns = cap[2].replace(/^\||\| *$/g, '').split('|'); - const rows = cap[3] && cap[3].trim() ? cap[3].replace(/\n[ \t]*$/, '').split('\n') : []; - const item = { - type: 'table', - raw: cap[0], - header: [], - align: [], - rows: [], - }; - if (headers.length !== aligns.length) { - // header and align columns must be equal, rows can be different. - return; - } - for (const align of aligns) { - if (/^ *-+: *$/.test(align)) { - item.align.push('right'); - } - else if (/^ *:-+: *$/.test(align)) { - item.align.push('center'); - } - else if (/^ *:-+ *$/.test(align)) { - item.align.push('left'); - } - else { - item.align.push(null); - } - } - for (let i = 0; i < headers.length; i++) { - item.header.push({ - text: headers[i], - tokens: this.lexer.inline(headers[i]), - header: true, - align: item.align[i], - }); - } - for (const row of rows) { - item.rows.push(splitCells(row, item.header.length).map((cell, i) => { - return { - text: cell, - tokens: this.lexer.inline(cell), - header: false, - align: item.align[i], - }; - })); - } - return item; - } - lheading(src) { - const cap = this.rules.block.lheading.exec(src); - if (cap) { - return { - type: 'heading', - raw: cap[0], - depth: cap[2].charAt(0) === '=' ? 1 : 2, - text: cap[1], - tokens: this.lexer.inline(cap[1]), - }; - } - } - paragraph(src) { - const cap = this.rules.block.paragraph.exec(src); - if (cap) { - const text = cap[1].charAt(cap[1].length - 1) === '\n' - ? cap[1].slice(0, -1) - : cap[1]; - return { - type: 'paragraph', - raw: cap[0], - text, - tokens: this.lexer.inline(text), - }; - } - } - text(src) { - const cap = this.rules.block.text.exec(src); - if (cap) { - return { - type: 'text', - raw: cap[0], - text: cap[0], - tokens: this.lexer.inline(cap[0]), - }; - } - } - escape(src) { - const cap = this.rules.inline.escape.exec(src); - if (cap) { - return { - type: 'escape', - raw: cap[0], - text: escape$1(cap[1]), - }; - } - } - tag(src) { - const cap = this.rules.inline.tag.exec(src); - if (cap) { - if (!this.lexer.state.inLink && /^/i.test(cap[0])) { - this.lexer.state.inLink = false; - } - if (!this.lexer.state.inRawBlock && /^<(pre|code|kbd|script)(\s|>)/i.test(cap[0])) { - this.lexer.state.inRawBlock = true; - } - else if (this.lexer.state.inRawBlock && /^<\/(pre|code|kbd|script)(\s|>)/i.test(cap[0])) { - this.lexer.state.inRawBlock = false; - } - return { - type: 'html', - raw: cap[0], - inLink: this.lexer.state.inLink, - inRawBlock: this.lexer.state.inRawBlock, - block: false, - text: cap[0], - }; - } - } - link(src) { - const cap = this.rules.inline.link.exec(src); - if (cap) { - const trimmedUrl = cap[2].trim(); - if (!this.options.pedantic && /^$/.test(trimmedUrl))) { - return; - } - // ending angle bracket cannot be escaped - const rtrimSlash = rtrim(trimmedUrl.slice(0, -1), '\\'); - if ((trimmedUrl.length - rtrimSlash.length) % 2 === 0) { - return; - } - } - else { - // find closing parenthesis - const lastParenIndex = findClosingBracket(cap[2], '()'); - if (lastParenIndex > -1) { - const start = cap[0].indexOf('!') === 0 ? 5 : 4; - const linkLen = start + cap[1].length + lastParenIndex; - cap[2] = cap[2].substring(0, lastParenIndex); - cap[0] = cap[0].substring(0, linkLen).trim(); - cap[3] = ''; - } - } - let href = cap[2]; - let title = ''; - if (this.options.pedantic) { - // split pedantic href and title - const link = /^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(href); - if (link) { - href = link[1]; - title = link[3]; - } - } - else { - title = cap[3] ? cap[3].slice(1, -1) : ''; - } - href = href.trim(); - if (/^$/.test(trimmedUrl))) { - // pedantic allows starting angle bracket without ending angle bracket - href = href.slice(1); - } - else { - href = href.slice(1, -1); - } - } - return outputLink(cap, { - href: href ? href.replace(this.rules.inline.anyPunctuation, '$1') : href, - title: title ? title.replace(this.rules.inline.anyPunctuation, '$1') : title, - }, cap[0], this.lexer); - } - } - reflink(src, links) { - let cap; - if ((cap = this.rules.inline.reflink.exec(src)) - || (cap = this.rules.inline.nolink.exec(src))) { - const linkString = (cap[2] || cap[1]).replace(/\s+/g, ' '); - const link = links[linkString.toLowerCase()]; - if (!link) { - const text = cap[0].charAt(0); - return { - type: 'text', - raw: text, - text, - }; - } - return outputLink(cap, link, cap[0], this.lexer); - } - } - emStrong(src, maskedSrc, prevChar = '') { - let match = this.rules.inline.emStrongLDelim.exec(src); - if (!match) - return; - // _ can't be between two alphanumerics. \p{L}\p{N} includes non-english alphabet/numbers as well - if (match[3] && prevChar.match(/[\p{L}\p{N}]/u)) - return; - const nextChar = match[1] || match[2] || ''; - if (!nextChar || !prevChar || this.rules.inline.punctuation.exec(prevChar)) { - // unicode Regex counts emoji as 1 char; spread into array for proper count (used multiple times below) - const lLength = [...match[0]].length - 1; - let rDelim, rLength, delimTotal = lLength, midDelimTotal = 0; - const endReg = match[0][0] === '*' ? this.rules.inline.emStrongRDelimAst : this.rules.inline.emStrongRDelimUnd; - endReg.lastIndex = 0; - // Clip maskedSrc to same section of string as src (move to lexer?) - maskedSrc = maskedSrc.slice(-1 * src.length + lLength); - while ((match = endReg.exec(maskedSrc)) != null) { - rDelim = match[1] || match[2] || match[3] || match[4] || match[5] || match[6]; - if (!rDelim) - continue; // skip single * in __abc*abc__ - rLength = [...rDelim].length; - if (match[3] || match[4]) { // found another Left Delim - delimTotal += rLength; - continue; - } - else if (match[5] || match[6]) { // either Left or Right Delim - if (lLength % 3 && !((lLength + rLength) % 3)) { - midDelimTotal += rLength; - continue; // CommonMark Emphasis Rules 9-10 - } - } - delimTotal -= rLength; - if (delimTotal > 0) - continue; // Haven't found enough closing delimiters - // Remove extra characters. *a*** -> *a* - rLength = Math.min(rLength, rLength + delimTotal + midDelimTotal); - // char length can be >1 for unicode characters; - const lastCharLength = [...match[0]][0].length; - const raw = src.slice(0, lLength + match.index + lastCharLength + rLength); - // Create `em` if smallest delimiter has odd char count. *a*** - if (Math.min(lLength, rLength) % 2) { - const text = raw.slice(1, -1); - return { - type: 'em', - raw, - text, - tokens: this.lexer.inlineTokens(text), - }; - } - // Create 'strong' if smallest delimiter has even char count. **a*** - const text = raw.slice(2, -2); - return { - type: 'strong', - raw, - text, - tokens: this.lexer.inlineTokens(text), - }; - } - } - } - codespan(src) { - const cap = this.rules.inline.code.exec(src); - if (cap) { - let text = cap[2].replace(/\n/g, ' '); - const hasNonSpaceChars = /[^ ]/.test(text); - const hasSpaceCharsOnBothEnds = /^ /.test(text) && / $/.test(text); - if (hasNonSpaceChars && hasSpaceCharsOnBothEnds) { - text = text.substring(1, text.length - 1); - } - text = escape$1(text, true); - return { - type: 'codespan', - raw: cap[0], - text, - }; - } - } - br(src) { - const cap = this.rules.inline.br.exec(src); - if (cap) { - return { - type: 'br', - raw: cap[0], - }; - } - } - del(src) { - const cap = this.rules.inline.del.exec(src); - if (cap) { - return { - type: 'del', - raw: cap[0], - text: cap[2], - tokens: this.lexer.inlineTokens(cap[2]), - }; - } - } - autolink(src) { - const cap = this.rules.inline.autolink.exec(src); - if (cap) { - let text, href; - if (cap[2] === '@') { - text = escape$1(cap[1]); - href = 'mailto:' + text; - } - else { - text = escape$1(cap[1]); - href = text; - } - return { - type: 'link', - raw: cap[0], - text, - href, - tokens: [ - { - type: 'text', - raw: text, - text, - }, - ], - }; - } - } - url(src) { - let cap; - if (cap = this.rules.inline.url.exec(src)) { - let text, href; - if (cap[2] === '@') { - text = escape$1(cap[0]); - href = 'mailto:' + text; - } - else { - // do extended autolink path validation - let prevCapZero; - do { - prevCapZero = cap[0]; - cap[0] = this.rules.inline._backpedal.exec(cap[0])?.[0] ?? ''; - } while (prevCapZero !== cap[0]); - text = escape$1(cap[0]); - if (cap[1] === 'www.') { - href = 'http://' + cap[0]; - } - else { - href = cap[0]; - } - } - return { - type: 'link', - raw: cap[0], - text, - href, - tokens: [ - { - type: 'text', - raw: text, - text, - }, - ], - }; - } - } - inlineText(src) { - const cap = this.rules.inline.text.exec(src); - if (cap) { - let text; - if (this.lexer.state.inRawBlock) { - text = cap[0]; - } - else { - text = escape$1(cap[0]); - } - return { - type: 'text', - raw: cap[0], - text, - }; - } - } -} - -/** - * Block-Level Grammar - */ -const newline = /^(?: *(?:\n|$))+/; -const blockCode = /^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/; -const fences = /^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/; -const hr = /^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/; -const heading = /^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/; -const bullet = /(?:[*+-]|\d{1,9}[.)])/; -const lheading = edit(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/) - .replace(/bull/g, bullet) // lists can interrupt - .replace(/blockCode/g, / {4}/) // indented code blocks can interrupt - .replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/) // fenced code blocks can interrupt - .replace(/blockquote/g, / {0,3}>/) // blockquote can interrupt - .replace(/heading/g, / {0,3}#{1,6}/) // ATX heading can interrupt - .replace(/html/g, / {0,3}<[^\n>]+>\n/) // block html can interrupt - .getRegex(); -const _paragraph = /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/; -const blockText = /^[^\n]+/; -const _blockLabel = /(?!\s*\])(?:\\.|[^\[\]\\])+/; -const def = edit(/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/) - .replace('label', _blockLabel) - .replace('title', /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/) - .getRegex(); -const list = edit(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/) - .replace(/bull/g, bullet) - .getRegex(); -const _tag = 'address|article|aside|base|basefont|blockquote|body|caption' - + '|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption' - + '|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe' - + '|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option' - + '|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title' - + '|tr|track|ul'; -const _comment = /|$))/; -const html = edit('^ {0,3}(?:' // optional indentation - + '<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)' // (1) - + '|comment[^\\n]*(\\n+|$)' // (2) - + '|<\\?[\\s\\S]*?(?:\\?>\\n*|$)' // (3) - + '|\\n*|$)' // (4) - + '|\\n*|$)' // (5) - + '|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)' // (6) - + '|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)' // (7) open tag - + '|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)' // (7) closing tag - + ')', 'i') - .replace('comment', _comment) - .replace('tag', _tag) - .replace('attribute', / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/) - .getRegex(); -const paragraph = edit(_paragraph) - .replace('hr', hr) - .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') - .replace('|lheading', '') // setext headings don't interrupt commonmark paragraphs - .replace('|table', '') - .replace('blockquote', ' {0,3}>') - .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') - .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt - .replace('html', ')|<(?:script|pre|style|textarea|!--)') - .replace('tag', _tag) // pars can be interrupted by type (6) html blocks - .getRegex(); -const blockquote = edit(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/) - .replace('paragraph', paragraph) - .getRegex(); -/** - * Normal Block Grammar - */ -const blockNormal = { - blockquote, - code: blockCode, - def, - fences, - heading, - hr, - html, - lheading, - list, - newline, - paragraph, - table: noopTest, - text: blockText, -}; -/** - * GFM Block Grammar - */ -const gfmTable = edit('^ *([^\\n ].*)\\n' // Header - + ' {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)' // Align - + '(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)') // Cells - .replace('hr', hr) - .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') - .replace('blockquote', ' {0,3}>') - .replace('code', ' {4}[^\\n]') - .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') - .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt - .replace('html', ')|<(?:script|pre|style|textarea|!--)') - .replace('tag', _tag) // tables can be interrupted by type (6) html blocks - .getRegex(); -const blockGfm = { - ...blockNormal, - table: gfmTable, - paragraph: edit(_paragraph) - .replace('hr', hr) - .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') - .replace('|lheading', '') // setext headings don't interrupt commonmark paragraphs - .replace('table', gfmTable) // interrupt paragraphs with table - .replace('blockquote', ' {0,3}>') - .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') - .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt - .replace('html', ')|<(?:script|pre|style|textarea|!--)') - .replace('tag', _tag) // pars can be interrupted by type (6) html blocks - .getRegex(), -}; -/** - * Pedantic grammar (original John Gruber's loose markdown specification) - */ -const blockPedantic = { - ...blockNormal, - html: edit('^ *(?:comment *(?:\\n|\\s*$)' - + '|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)' // closed tag - + '|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))') - .replace('comment', _comment) - .replace(/tag/g, '(?!(?:' - + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub' - + '|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)' - + '\\b)\\w+(?!:|[^\\w\\s@]*@)\\b') - .getRegex(), - def: /^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/, - heading: /^(#{1,6})(.*)(?:\n+|$)/, - fences: noopTest, // fences not supported - lheading: /^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/, - paragraph: edit(_paragraph) - .replace('hr', hr) - .replace('heading', ' *#{1,6} *[^\n]') - .replace('lheading', lheading) - .replace('|table', '') - .replace('blockquote', ' {0,3}>') - .replace('|fences', '') - .replace('|list', '') - .replace('|html', '') - .replace('|tag', '') - .getRegex(), -}; -/** - * Inline-Level Grammar - */ -const escape = /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/; -const inlineCode = /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/; -const br = /^( {2,}|\\)\n(?!\s*$)/; -const inlineText = /^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\ -const blockSkip = /\[[^[\]]*?\]\([^\(\)]*?\)|`[^`]*?`|<[^<>]*?>/g; -const emStrongLDelim = edit(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/, 'u') - .replace(/punct/g, _punctuation) - .getRegex(); -const emStrongRDelimAst = edit('^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)' // Skip orphan inside strong - + '|[^*]+(?=[^*])' // Consume to delim - + '|(?!\\*)[punct](\\*+)(?=[\\s]|$)' // (1) #*** can only be a Right Delimiter - + '|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)' // (2) a***#, a*** can only be a Right Delimiter - + '|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])' // (3) #***a, ***a can only be Left Delimiter - + '|[\\s](\\*+)(?!\\*)(?=[punct])' // (4) ***# can only be Left Delimiter - + '|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])' // (5) #***# can be either Left or Right Delimiter - + '|[^punct\\s](\\*+)(?=[^punct\\s])', 'gu') // (6) a***a can be either Left or Right Delimiter - .replace(/punct/g, _punctuation) - .getRegex(); -// (6) Not allowed for _ -const emStrongRDelimUnd = edit('^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)' // Skip orphan inside strong - + '|[^_]+(?=[^_])' // Consume to delim - + '|(?!_)[punct](_+)(?=[\\s]|$)' // (1) #___ can only be a Right Delimiter - + '|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)' // (2) a___#, a___ can only be a Right Delimiter - + '|(?!_)[punct\\s](_+)(?=[^punct\\s])' // (3) #___a, ___a can only be Left Delimiter - + '|[\\s](_+)(?!_)(?=[punct])' // (4) ___# can only be Left Delimiter - + '|(?!_)[punct](_+)(?!_)(?=[punct])', 'gu') // (5) #___# can be either Left or Right Delimiter - .replace(/punct/g, _punctuation) - .getRegex(); -const anyPunctuation = edit(/\\([punct])/, 'gu') - .replace(/punct/g, _punctuation) - .getRegex(); -const autolink = edit(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/) - .replace('scheme', /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/) - .replace('email', /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/) - .getRegex(); -const _inlineComment = edit(_comment).replace('(?:-->|$)', '-->').getRegex(); -const tag = edit('^comment' - + '|^' // self-closing tag - + '|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>' // open tag - + '|^<\\?[\\s\\S]*?\\?>' // processing instruction, e.g. - + '|^' // declaration, e.g. - + '|^') // CDATA section - .replace('comment', _inlineComment) - .replace('attribute', /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/) - .getRegex(); -const _inlineLabel = /(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/; -const link = edit(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/) - .replace('label', _inlineLabel) - .replace('href', /<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/) - .replace('title', /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/) - .getRegex(); -const reflink = edit(/^!?\[(label)\]\[(ref)\]/) - .replace('label', _inlineLabel) - .replace('ref', _blockLabel) - .getRegex(); -const nolink = edit(/^!?\[(ref)\](?:\[\])?/) - .replace('ref', _blockLabel) - .getRegex(); -const reflinkSearch = edit('reflink|nolink(?!\\()', 'g') - .replace('reflink', reflink) - .replace('nolink', nolink) - .getRegex(); -/** - * Normal Inline Grammar - */ -const inlineNormal = { - _backpedal: noopTest, // only used for GFM url - anyPunctuation, - autolink, - blockSkip, - br, - code: inlineCode, - del: noopTest, - emStrongLDelim, - emStrongRDelimAst, - emStrongRDelimUnd, - escape, - link, - nolink, - punctuation, - reflink, - reflinkSearch, - tag, - text: inlineText, - url: noopTest, -}; -/** - * Pedantic Inline Grammar - */ -const inlinePedantic = { - ...inlineNormal, - link: edit(/^!?\[(label)\]\((.*?)\)/) - .replace('label', _inlineLabel) - .getRegex(), - reflink: edit(/^!?\[(label)\]\s*\[([^\]]*)\]/) - .replace('label', _inlineLabel) - .getRegex(), -}; -/** - * GFM Inline Grammar - */ -const inlineGfm = { - ...inlineNormal, - escape: edit(escape).replace('])', '~|])').getRegex(), - url: edit(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/, 'i') - .replace('email', /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/) - .getRegex(), - _backpedal: /(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/, - del: /^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/, - text: /^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\ { - return leading + ' '.repeat(tabs.length); - }); - } - let token; - let lastToken; - let cutSrc; - while (src) { - if (this.options.extensions - && this.options.extensions.block - && this.options.extensions.block.some((extTokenizer) => { - if (token = extTokenizer.call({ lexer: this }, src, tokens)) { - src = src.substring(token.raw.length); - tokens.push(token); - return true; - } - return false; - })) { - continue; - } - // newline - if (token = this.tokenizer.space(src)) { - src = src.substring(token.raw.length); - if (token.raw.length === 1 && tokens.length > 0) { - // if there's a single \n as a spacer, it's terminating the last line, - // so move it there so that we don't get unnecessary paragraph tags - tokens[tokens.length - 1].raw += '\n'; - } - else { - tokens.push(token); - } - continue; - } - // code - if (token = this.tokenizer.code(src)) { - src = src.substring(token.raw.length); - lastToken = tokens[tokens.length - 1]; - // An indented code block cannot interrupt a paragraph. - if (lastToken && (lastToken.type === 'paragraph' || lastToken.type === 'text')) { - lastToken.raw += '\n' + token.raw; - lastToken.text += '\n' + token.text; - this.inlineQueue[this.inlineQueue.length - 1].src = lastToken.text; - } - else { - tokens.push(token); - } - continue; - } - // fences - if (token = this.tokenizer.fences(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - // heading - if (token = this.tokenizer.heading(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - // hr - if (token = this.tokenizer.hr(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - // blockquote - if (token = this.tokenizer.blockquote(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - // list - if (token = this.tokenizer.list(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - // html - if (token = this.tokenizer.html(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - // def - if (token = this.tokenizer.def(src)) { - src = src.substring(token.raw.length); - lastToken = tokens[tokens.length - 1]; - if (lastToken && (lastToken.type === 'paragraph' || lastToken.type === 'text')) { - lastToken.raw += '\n' + token.raw; - lastToken.text += '\n' + token.raw; - this.inlineQueue[this.inlineQueue.length - 1].src = lastToken.text; - } - else if (!this.tokens.links[token.tag]) { - this.tokens.links[token.tag] = { - href: token.href, - title: token.title, - }; - } - continue; - } - // table (gfm) - if (token = this.tokenizer.table(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - // lheading - if (token = this.tokenizer.lheading(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - // top-level paragraph - // prevent paragraph consuming extensions by clipping 'src' to extension start - cutSrc = src; - if (this.options.extensions && this.options.extensions.startBlock) { - let startIndex = Infinity; - const tempSrc = src.slice(1); - let tempStart; - this.options.extensions.startBlock.forEach((getStartIndex) => { - tempStart = getStartIndex.call({ lexer: this }, tempSrc); - if (typeof tempStart === 'number' && tempStart >= 0) { - startIndex = Math.min(startIndex, tempStart); - } - }); - if (startIndex < Infinity && startIndex >= 0) { - cutSrc = src.substring(0, startIndex + 1); - } - } - if (this.state.top && (token = this.tokenizer.paragraph(cutSrc))) { - lastToken = tokens[tokens.length - 1]; - if (lastParagraphClipped && lastToken?.type === 'paragraph') { - lastToken.raw += '\n' + token.raw; - lastToken.text += '\n' + token.text; - this.inlineQueue.pop(); - this.inlineQueue[this.inlineQueue.length - 1].src = lastToken.text; - } - else { - tokens.push(token); - } - lastParagraphClipped = (cutSrc.length !== src.length); - src = src.substring(token.raw.length); - continue; - } - // text - if (token = this.tokenizer.text(src)) { - src = src.substring(token.raw.length); - lastToken = tokens[tokens.length - 1]; - if (lastToken && lastToken.type === 'text') { - lastToken.raw += '\n' + token.raw; - lastToken.text += '\n' + token.text; - this.inlineQueue.pop(); - this.inlineQueue[this.inlineQueue.length - 1].src = lastToken.text; - } - else { - tokens.push(token); - } - continue; - } - if (src) { - const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0); - if (this.options.silent) { - console.error(errMsg); - break; - } - else { - throw new Error(errMsg); - } - } - } - this.state.top = true; - return tokens; - } - inline(src, tokens = []) { - this.inlineQueue.push({ src, tokens }); - return tokens; - } - /** - * Lexing/Compiling - */ - inlineTokens(src, tokens = []) { - let token, lastToken, cutSrc; - // String with links masked to avoid interference with em and strong - let maskedSrc = src; - let match; - let keepPrevChar, prevChar; - // Mask out reflinks - if (this.tokens.links) { - const links = Object.keys(this.tokens.links); - if (links.length > 0) { - while ((match = this.tokenizer.rules.inline.reflinkSearch.exec(maskedSrc)) != null) { - if (links.includes(match[0].slice(match[0].lastIndexOf('[') + 1, -1))) { - maskedSrc = maskedSrc.slice(0, match.index) + '[' + 'a'.repeat(match[0].length - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex); - } - } - } - } - // Mask out other blocks - while ((match = this.tokenizer.rules.inline.blockSkip.exec(maskedSrc)) != null) { - maskedSrc = maskedSrc.slice(0, match.index) + '[' + 'a'.repeat(match[0].length - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.blockSkip.lastIndex); - } - // Mask out escaped characters - while ((match = this.tokenizer.rules.inline.anyPunctuation.exec(maskedSrc)) != null) { - maskedSrc = maskedSrc.slice(0, match.index) + '++' + maskedSrc.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex); - } - while (src) { - if (!keepPrevChar) { - prevChar = ''; - } - keepPrevChar = false; - // extensions - if (this.options.extensions - && this.options.extensions.inline - && this.options.extensions.inline.some((extTokenizer) => { - if (token = extTokenizer.call({ lexer: this }, src, tokens)) { - src = src.substring(token.raw.length); - tokens.push(token); - return true; - } - return false; - })) { - continue; - } - // escape - if (token = this.tokenizer.escape(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - // tag - if (token = this.tokenizer.tag(src)) { - src = src.substring(token.raw.length); - lastToken = tokens[tokens.length - 1]; - if (lastToken && token.type === 'text' && lastToken.type === 'text') { - lastToken.raw += token.raw; - lastToken.text += token.text; - } - else { - tokens.push(token); - } - continue; - } - // link - if (token = this.tokenizer.link(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - // reflink, nolink - if (token = this.tokenizer.reflink(src, this.tokens.links)) { - src = src.substring(token.raw.length); - lastToken = tokens[tokens.length - 1]; - if (lastToken && token.type === 'text' && lastToken.type === 'text') { - lastToken.raw += token.raw; - lastToken.text += token.text; - } - else { - tokens.push(token); - } - continue; - } - // em & strong - if (token = this.tokenizer.emStrong(src, maskedSrc, prevChar)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - // code - if (token = this.tokenizer.codespan(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - // br - if (token = this.tokenizer.br(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - // del (gfm) - if (token = this.tokenizer.del(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - // autolink - if (token = this.tokenizer.autolink(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - // url (gfm) - if (!this.state.inLink && (token = this.tokenizer.url(src))) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - // text - // prevent inlineText consuming extensions by clipping 'src' to extension start - cutSrc = src; - if (this.options.extensions && this.options.extensions.startInline) { - let startIndex = Infinity; - const tempSrc = src.slice(1); - let tempStart; - this.options.extensions.startInline.forEach((getStartIndex) => { - tempStart = getStartIndex.call({ lexer: this }, tempSrc); - if (typeof tempStart === 'number' && tempStart >= 0) { - startIndex = Math.min(startIndex, tempStart); - } - }); - if (startIndex < Infinity && startIndex >= 0) { - cutSrc = src.substring(0, startIndex + 1); - } - } - if (token = this.tokenizer.inlineText(cutSrc)) { - src = src.substring(token.raw.length); - if (token.raw.slice(-1) !== '_') { // Track prevChar before string of ____ started - prevChar = token.raw.slice(-1); - } - keepPrevChar = true; - lastToken = tokens[tokens.length - 1]; - if (lastToken && lastToken.type === 'text') { - lastToken.raw += token.raw; - lastToken.text += token.text; - } - else { - tokens.push(token); - } - continue; - } - if (src) { - const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0); - if (this.options.silent) { - console.error(errMsg); - break; - } - else { - throw new Error(errMsg); - } - } - } - return tokens; - } -} - -/** - * Renderer - */ -class _Renderer { - options; - parser; // set by the parser - constructor(options) { - this.options = options || _defaults; - } - space(token) { - return ''; - } - code({ text, lang, escaped }) { - const langString = (lang || '').match(/^\S*/)?.[0]; - const code = text.replace(/\n$/, '') + '\n'; - if (!langString) { - return '
'
-                + (escaped ? code : escape$1(code, true))
-                + '
\n'; - } - return '
'
-            + (escaped ? code : escape$1(code, true))
-            + '
\n'; - } - blockquote({ tokens }) { - const body = this.parser.parse(tokens); - return `
\n${body}
\n`; - } - html({ text }) { - return text; - } - heading({ tokens, depth }) { - return `${this.parser.parseInline(tokens)}\n`; - } - hr(token) { - return '
\n'; - } - list(token) { - const ordered = token.ordered; - const start = token.start; - let body = ''; - for (let j = 0; j < token.items.length; j++) { - const item = token.items[j]; - body += this.listitem(item); - } - const type = ordered ? 'ol' : 'ul'; - const startAttr = (ordered && start !== 1) ? (' start="' + start + '"') : ''; - return '<' + type + startAttr + '>\n' + body + '\n'; - } - listitem(item) { - let itemBody = ''; - if (item.task) { - const checkbox = this.checkbox({ checked: !!item.checked }); - if (item.loose) { - if (item.tokens.length > 0 && item.tokens[0].type === 'paragraph') { - item.tokens[0].text = checkbox + ' ' + item.tokens[0].text; - if (item.tokens[0].tokens && item.tokens[0].tokens.length > 0 && item.tokens[0].tokens[0].type === 'text') { - item.tokens[0].tokens[0].text = checkbox + ' ' + item.tokens[0].tokens[0].text; - } - } - else { - item.tokens.unshift({ - type: 'text', - raw: checkbox + ' ', - text: checkbox + ' ', - }); - } - } - else { - itemBody += checkbox + ' '; - } - } - itemBody += this.parser.parse(item.tokens, !!item.loose); - return `
  • ${itemBody}
  • \n`; - } - checkbox({ checked }) { - return ''; - } - paragraph({ tokens }) { - return `

    ${this.parser.parseInline(tokens)}

    \n`; - } - table(token) { - let header = ''; - // header - let cell = ''; - for (let j = 0; j < token.header.length; j++) { - cell += this.tablecell(token.header[j]); - } - header += this.tablerow({ text: cell }); - let body = ''; - for (let j = 0; j < token.rows.length; j++) { - const row = token.rows[j]; - cell = ''; - for (let k = 0; k < row.length; k++) { - cell += this.tablecell(row[k]); - } - body += this.tablerow({ text: cell }); - } - if (body) - body = `${body}`; - return '\n' - + '\n' - + header - + '\n' - + body - + '
    \n'; - } - tablerow({ text }) { - return `\n${text}\n`; - } - tablecell(token) { - const content = this.parser.parseInline(token.tokens); - const type = token.header ? 'th' : 'td'; - const tag = token.align - ? `<${type} align="${token.align}">` - : `<${type}>`; - return tag + content + `\n`; - } - /** - * span level renderer - */ - strong({ tokens }) { - return `${this.parser.parseInline(tokens)}`; - } - em({ tokens }) { - return `${this.parser.parseInline(tokens)}`; - } - codespan({ text }) { - return `${text}`; - } - br(token) { - return '
    '; - } - del({ tokens }) { - return `${this.parser.parseInline(tokens)}`; - } - link({ href, title, tokens }) { - const text = this.parser.parseInline(tokens); - const cleanHref = cleanUrl(href); - if (cleanHref === null) { - return text; - } - href = cleanHref; - let out = '
    '; - return out; - } - image({ href, title, text }) { - const cleanHref = cleanUrl(href); - if (cleanHref === null) { - return text; - } - href = cleanHref; - let out = `${text} { - const tokens = genericToken[childTokens].flat(Infinity); - values = values.concat(this.walkTokens(tokens, callback)); - }); - } - else if (genericToken.tokens) { - values = values.concat(this.walkTokens(genericToken.tokens, callback)); - } - } - } - } - return values; - } - use(...args) { - const extensions = this.defaults.extensions || { renderers: {}, childTokens: {} }; - args.forEach((pack) => { - // copy options to new object - const opts = { ...pack }; - // set async to true if it was set to true before - opts.async = this.defaults.async || opts.async || false; - // ==-- Parse "addon" extensions --== // - if (pack.extensions) { - pack.extensions.forEach((ext) => { - if (!ext.name) { - throw new Error('extension name required'); - } - if ('renderer' in ext) { // Renderer extensions - const prevRenderer = extensions.renderers[ext.name]; - if (prevRenderer) { - // Replace extension with func to run new extension but fall back if false - extensions.renderers[ext.name] = function (...args) { - let ret = ext.renderer.apply(this, args); - if (ret === false) { - ret = prevRenderer.apply(this, args); - } - return ret; - }; - } - else { - extensions.renderers[ext.name] = ext.renderer; - } - } - if ('tokenizer' in ext) { // Tokenizer Extensions - if (!ext.level || (ext.level !== 'block' && ext.level !== 'inline')) { - throw new Error("extension level must be 'block' or 'inline'"); - } - const extLevel = extensions[ext.level]; - if (extLevel) { - extLevel.unshift(ext.tokenizer); - } - else { - extensions[ext.level] = [ext.tokenizer]; - } - if (ext.start) { // Function to check for start of token - if (ext.level === 'block') { - if (extensions.startBlock) { - extensions.startBlock.push(ext.start); - } - else { - extensions.startBlock = [ext.start]; - } - } - else if (ext.level === 'inline') { - if (extensions.startInline) { - extensions.startInline.push(ext.start); - } - else { - extensions.startInline = [ext.start]; - } - } - } - } - if ('childTokens' in ext && ext.childTokens) { // Child tokens to be visited by walkTokens - extensions.childTokens[ext.name] = ext.childTokens; - } - }); - opts.extensions = extensions; - } - // ==-- Parse "overwrite" extensions --== // - if (pack.renderer) { - const renderer = this.defaults.renderer || new _Renderer(this.defaults); - for (const prop in pack.renderer) { - if (!(prop in renderer)) { - throw new Error(`renderer '${prop}' does not exist`); - } - if (['options', 'parser'].includes(prop)) { - // ignore options property - continue; - } - const rendererProp = prop; - const rendererFunc = pack.renderer[rendererProp]; - const prevRenderer = renderer[rendererProp]; - // Replace renderer with func to run extension, but fall back if false - renderer[rendererProp] = (...args) => { - let ret = rendererFunc.apply(renderer, args); - if (ret === false) { - ret = prevRenderer.apply(renderer, args); - } - return ret || ''; - }; - } - opts.renderer = renderer; - } - if (pack.tokenizer) { - const tokenizer = this.defaults.tokenizer || new _Tokenizer(this.defaults); - for (const prop in pack.tokenizer) { - if (!(prop in tokenizer)) { - throw new Error(`tokenizer '${prop}' does not exist`); - } - if (['options', 'rules', 'lexer'].includes(prop)) { - // ignore options, rules, and lexer properties - continue; - } - const tokenizerProp = prop; - const tokenizerFunc = pack.tokenizer[tokenizerProp]; - const prevTokenizer = tokenizer[tokenizerProp]; - // Replace tokenizer with func to run extension, but fall back if false - // @ts-expect-error cannot type tokenizer function dynamically - tokenizer[tokenizerProp] = (...args) => { - let ret = tokenizerFunc.apply(tokenizer, args); - if (ret === false) { - ret = prevTokenizer.apply(tokenizer, args); - } - return ret; - }; - } - opts.tokenizer = tokenizer; - } - // ==-- Parse Hooks extensions --== // - if (pack.hooks) { - const hooks = this.defaults.hooks || new _Hooks(); - for (const prop in pack.hooks) { - if (!(prop in hooks)) { - throw new Error(`hook '${prop}' does not exist`); - } - if (['options', 'block'].includes(prop)) { - // ignore options and block properties - continue; - } - const hooksProp = prop; - const hooksFunc = pack.hooks[hooksProp]; - const prevHook = hooks[hooksProp]; - if (_Hooks.passThroughHooks.has(prop)) { - // @ts-expect-error cannot type hook function dynamically - hooks[hooksProp] = (arg) => { - if (this.defaults.async) { - return Promise.resolve(hooksFunc.call(hooks, arg)).then(ret => { - return prevHook.call(hooks, ret); - }); - } - const ret = hooksFunc.call(hooks, arg); - return prevHook.call(hooks, ret); - }; - } - else { - // @ts-expect-error cannot type hook function dynamically - hooks[hooksProp] = (...args) => { - let ret = hooksFunc.apply(hooks, args); - if (ret === false) { - ret = prevHook.apply(hooks, args); - } - return ret; - }; - } - } - opts.hooks = hooks; - } - // ==-- Parse WalkTokens extensions --== // - if (pack.walkTokens) { - const walkTokens = this.defaults.walkTokens; - const packWalktokens = pack.walkTokens; - opts.walkTokens = function (token) { - let values = []; - values.push(packWalktokens.call(this, token)); - if (walkTokens) { - values = values.concat(walkTokens.call(this, token)); - } - return values; - }; - } - this.defaults = { ...this.defaults, ...opts }; - }); - return this; - } - setOptions(opt) { - this.defaults = { ...this.defaults, ...opt }; - return this; - } - lexer(src, options) { - return _Lexer.lex(src, options ?? this.defaults); - } - parser(tokens, options) { - return _Parser.parse(tokens, options ?? this.defaults); - } - parseMarkdown(blockType) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const parse = (src, options) => { - const origOpt = { ...options }; - const opt = { ...this.defaults, ...origOpt }; - const throwError = this.onError(!!opt.silent, !!opt.async); - // throw error if an extension set async to true but parse was called with async: false - if (this.defaults.async === true && origOpt.async === false) { - return throwError(new Error('marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise.')); - } - // throw error in case of non string input - if (typeof src === 'undefined' || src === null) { - return throwError(new Error('marked(): input parameter is undefined or null')); - } - if (typeof src !== 'string') { - return throwError(new Error('marked(): input parameter is of type ' - + Object.prototype.toString.call(src) + ', string expected')); - } - if (opt.hooks) { - opt.hooks.options = opt; - opt.hooks.block = blockType; - } - const lexer = opt.hooks ? opt.hooks.provideLexer() : (blockType ? _Lexer.lex : _Lexer.lexInline); - const parser = opt.hooks ? opt.hooks.provideParser() : (blockType ? _Parser.parse : _Parser.parseInline); - if (opt.async) { - return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src) - .then(src => lexer(src, opt)) - .then(tokens => opt.hooks ? opt.hooks.processAllTokens(tokens) : tokens) - .then(tokens => opt.walkTokens ? Promise.all(this.walkTokens(tokens, opt.walkTokens)).then(() => tokens) : tokens) - .then(tokens => parser(tokens, opt)) - .then(html => opt.hooks ? opt.hooks.postprocess(html) : html) - .catch(throwError); - } - try { - if (opt.hooks) { - src = opt.hooks.preprocess(src); - } - let tokens = lexer(src, opt); - if (opt.hooks) { - tokens = opt.hooks.processAllTokens(tokens); - } - if (opt.walkTokens) { - this.walkTokens(tokens, opt.walkTokens); - } - let html = parser(tokens, opt); - if (opt.hooks) { - html = opt.hooks.postprocess(html); - } - return html; - } - catch (e) { - return throwError(e); - } - }; - return parse; - } - onError(silent, async) { - return (e) => { - e.message += '\nPlease report this to https://github.com/markedjs/marked.'; - if (silent) { - const msg = '

    An error occurred:

    '
    -                    + escape$1(e.message + '', true)
    -                    + '
    '; - if (async) { - return Promise.resolve(msg); - } - return msg; - } - if (async) { - return Promise.reject(e); - } - throw e; - }; - } -} - -const markedInstance = new Marked(); -function marked(src, opt) { - return markedInstance.parse(src, opt); -} -/** - * Sets the default options. - * - * @param options Hash of options - */ -marked.options = - marked.setOptions = function (options) { - markedInstance.setOptions(options); - marked.defaults = markedInstance.defaults; - changeDefaults(marked.defaults); - return marked; - }; -/** - * Gets the original marked default options. - */ -marked.getDefaults = _getDefaults; -marked.defaults = _defaults; -/** - * Use Extension - */ -marked.use = function (...args) { - markedInstance.use(...args); - marked.defaults = markedInstance.defaults; - changeDefaults(marked.defaults); - return marked; -}; -/** - * Run callback for every token - */ -marked.walkTokens = function (tokens, callback) { - return markedInstance.walkTokens(tokens, callback); -}; -/** - * Compiles markdown to HTML without enclosing `p` tag. - * - * @param src String of markdown source to be compiled - * @param options Hash of options - * @return String of compiled HTML - */ -marked.parseInline = markedInstance.parseInline; -/** - * Expose - */ -marked.Parser = _Parser; -marked.parser = _Parser.parse; -marked.Renderer = _Renderer; -marked.TextRenderer = _TextRenderer; -marked.Lexer = _Lexer; -marked.lexer = _Lexer.lex; -marked.Tokenizer = _Tokenizer; -marked.Hooks = _Hooks; -marked.parse = marked; -const options = marked.options; -const setOptions = marked.setOptions; -const use = marked.use; -const walkTokens = marked.walkTokens; -const parseInline = marked.parseInline; -const parse = marked; -const parser = _Parser.parse; -const lexer = _Lexer.lex; - -export { _Hooks as Hooks, _Lexer as Lexer, Marked, _Parser as Parser, _Renderer as Renderer, _TextRenderer as TextRenderer, _Tokenizer as Tokenizer, _defaults as defaults, _getDefaults as getDefaults, lexer, marked, options, parse, parseInline, parser, setOptions, use, walkTokens }; -//# sourceMappingURL=marked.esm.js.map \ No newline at end of file diff --git a/web/model-manager.css b/web/model-manager.css deleted file mode 100644 index 305ced2..0000000 --- a/web/model-manager.css +++ /dev/null @@ -1,698 +0,0 @@ -/* model manager */ -.model-manager { - background-color: var(--comfy-menu-bg); - box-sizing: border-box; - color: var(--bg-color); - font-family: monospace; - font-size: 15px; - height: 100%; - padding: 8px; - position: fixed; - overflow: hidden; - width: 100%; - z-index: 2000; - - /*override comfy-modal settings*/ - border-radius: 0; - box-shadow: none; - justify-content: unset; - max-height: 100vh; - max-width: 100vw; - transform: none; - /*disable double-tap zoom on model manager*/ - touch-action: manipulation; -} - -.model-manager .comfy-modal-content { - width: 100%; - gap: 16px; -} - -.model-manager .no-highlight { - user-select: none; - -moz-user-select: none; - -webkit-text-select: none; - -webkit-user-select: none; -} - -.model-manager label:has(> *){ - pointer-events: none; -} - -.model-manager label > * { - pointer-events: auto; -} - -/* sidebar */ - -.model-manager { - --model-manager-sidebar-width-left: 50vw; - --model-manager-sidebar-width-right: 50vw; - --model-manager-sidebar-height-top: 50vh; - --model-manager-sidebar-height-bottom: 50vh; - - --model-manager-left: 0; - --model-manager-right: 0; - --model-manager-top: 0; - --model-manager-bottom: 0; - - left: var(--model-manager-left); - top: var(--model-manager-right); - right: var(--model-manager-top); - bottom: var(--model-manager-bottom); -} - -.model-manager.cursor-drag-left, -.model-manager.cursor-drag-right { - cursor: ew-resize; -} - -.model-manager.cursor-drag-top, -.model-manager.cursor-drag-bottom { - cursor: ns-resize; -} - -.model-manager.cursor-drag-top.cursor-drag-left, -.model-manager.cursor-drag-bottom.cursor-drag-right { - cursor: nwse-resize; -} - -.model-manager.cursor-drag-top.cursor-drag-right, -.model-manager.cursor-drag-bottom.cursor-drag-left { - cursor: nesw-resize; -} - -/* sidebar buttons */ -.model-manager .sidebar-buttons { - overflow: hidden; - color: var(--input-text); - display: flex; - flex-direction: row-reverse; - flex-wrap: wrap; -} - -.model-manager .sidebar-buttons .radio-button-group-active { - border-color: var(--fg-color); - color: var(--fg-color); - overflow: hidden; -} - -.model-manager[data-sidebar-state="left"] { - width: var(--model-manager-sidebar-width-left); - max-width: 95vw; - min-width: 22vw; - right: auto; - border-right: solid var(--border-color) 2px; -} - -.model-manager[data-sidebar-state="top"] { - height: var(--model-manager-sidebar-height-top); - max-height: 95vh; - min-height: 22vh; - bottom: auto; - border-bottom: solid var(--border-color) 2px; -} - -.model-manager[data-sidebar-state="bottom"] { - height: var(--model-manager-sidebar-height-bottom); - max-height: 95vh; - min-height: 22vh; - top: auto; - border-top: solid var(--border-color) 2px; -} - -.model-manager[data-sidebar-state="right"] { - width: var(--model-manager-sidebar-width-right); - max-width: 95vw; - min-width: 22vw; - left: auto; - border-left: solid var(--border-color) 2px; -} - -/* common */ -.model-manager h1 { - min-width: 0; - overflow-wrap: break-word; -} - -.model-manager textarea { - border: solid 2px var(--border-color); - border-radius: 8px; - font-size: 1.2em; - resize: vertical; - width: 100%; - height: 100%; -} - -.model-manager input[type="file"] { - width: 100%; -} - -.model-manager button { - margin: 0; - border: 2px solid var(--border-color); -} - -.model-manager button:not(.icon-button), -.model-manager select, -.model-manager input { - padding: 4px 8px; - margin: 0; -} - -.model-manager button:disabled, -.model-manager select:disabled, -.model-manager input:disabled { - background-color: var(--comfy-menu-bg); - filter: brightness(1.2); - cursor: not-allowed; -} - -.model-manager button.block { - width: 100%; -} - -.model-manager ::-webkit-scrollbar { - width: 16px; -} - -.model-manager ::-webkit-scrollbar-track { - background-color: var(--comfy-input-bg); - border-right: 1px solid var(--border-color); - border-bottom: 1px solid var(--border-color); -} - -.model-manager ::-webkit-scrollbar-thumb { - background-color: var(--fg-color); - border-radius: 3px; -} - -.model-manager .search-text-area::-webkit-input-placeholder { - font-style: italic; -} -.model-manager .search-text-area:-moz-placeholder { - font-style: italic; -} -.model-manager .search-text-area::-moz-placeholder { - font-style: italic; -} -.model-manager .search-text-area:-ms-input-placeholder { - font-style: italic; -} - -.model-manager .icon-button { - height: 40px; - width: 40px; - line-height: 1.15; -} - -.model-manager .row { - display: flex; - min-width: 0; - gap: 8px; -} - -.model-manager .tab-header { - display: flex; - padding: 8px 0px; - flex-direction: column; - background-color: var(--bg-color); -} - -.model-manager .tab-header-flex-block { - width: 100%; - min-width: 0; -} - -.model-manager .comfy-button-success { - color: green; - border-color: green; -} - -.model-manager .comfy-button-failure { - color: darkred; - border-color: darkred; -} - -.model-manager .no-select { - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; -} - -/* main content */ -.model-manager .model-manager-panel { - color: var(--fg-color); -} - -.model-manager .model-tab-group { - display: flex; - gap: 4px; - height: 40px; -} - -.model-manager .model-tab-group .tab-button { - background-color: var(--comfy-menu-bg); - border: 2px solid var(--border-color); - border-bottom: none; - border-radius: 8px 8px 0px 0px; - cursor: pointer; - padding: 8px 12px; - margin-bottom: 0px; - z-index: 1; -} - -.model-manager .model-tab-group .tab-button.active { - background-color: var(--bg-color); - cursor: default; - position: relative; - z-index: 1; - pointer-events: none; -} - -.model-manager .model-manager-body { - background-color: var(--bg-color); - border: 2px solid var(--border-color); -} - -.model-manager .model-manager-panel { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.model-manager .model-manager-body { - flex: 1; - overflow: hidden; - padding: 8px 0px 8px 16px; -} - -.model-manager .model-manager-body .tab-contents { - position: relative; - display: flex; - flex-direction: column; - height: 100%; - width: auto; - overflow-x: auto; - overflow-y: hidden; -} - -.model-manager .model-manager-body .tab-content { - display: flex; - flex-direction: column; - height: 100%; - overflow-y: auto; - padding-right: 16px; -} - -/* model info view */ -.model-manager .model-info-container { - background-color: var(--bg-color); - border-radius: 16px; - color: var(--fg-color); - width: auto; -} - -.model-manager .model-metadata { - table-layout: fixed; - text-align: left; - width: 100%; -} - -.model-manager .model-metadata-key { - overflow-wrap: break-word; - width: 20%; -} - -.model-manager .model-metadata-value { - overflow-wrap: anywhere; - width: 80%; -} - -.model-manager table { - border-collapse: collapse; -} - -.model-manager th { - border: 1px solid; - padding: 4px 8px; -} - -/* download tab */ - -.model-manager .download-model-infos { - display: flex; - flex-direction: column; - padding: 0; - row-gap: 10px; -} - -.model-manager .download-details summary { - background-color: var(--comfy-menu-bg); - border-radius: 16px; - padding: 16px; - word-wrap: break-word; -} - -.model-manager .download-details[open] summary { - background-color: var(--border-color); -} - -.model-manager .download-details > div { - column-gap: 8px; - display: flex; - flex-direction: row; - flex-wrap: wrap; - padding: 8px; - row-gap: 16px; -} - -.model-manager [data-name="Download"] .download-settings-wrapper { - flex: 1; -} - -.model-manager [data-name="Download"] .download-settings { - display: flex; - flex-direction: column; - row-gap: 16px; -} - -.model-manager .download-button { - max-width: fit-content; -} - -/* models tab */ -.model-manager [data-name="Models"] .row { - position: sticky; - z-index: 1; - top: 0; -} - -/* preview image */ -.model-manager .item { - position: relative; - width: 240px; - height: 360px; - text-align: center; - overflow: hidden; - border-radius: 8px; -} - -.model-manager .item img { - width: 100%; - height: 100%; - object-fit: cover; - border-radius: 8px; -} - -.model-manager .model-info-container .item { - width: auto; - height: auto; -} -.model-manager .model-info-container .item img { - height: auto; - width: auto; - max-width: 100%; - max-height: 50vh; -} - -.model-manager .model-preview-button-left, -.model-manager .model-preview-button-right { - position: absolute; - top: 0; - bottom: 0; - margin: auto; - border-radius: 20px; -} - -.model-manager .model-preview-button-right { - right: 4px; -} - -.model-manager .model-preview-button-left { - left: 4px; -} - -.model-manager .item .model-preview-overlay { - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; - background-color: rgba(0, 0, 0, 0); -} - -/* grid */ -.model-manager .comfy-grid { - display: flex; - flex-wrap: wrap; - gap: 16px; -} - -.model-manager .comfy-grid .model-label { - background-color: rgb(from var(--content-hover-bg) r g b / 0.5); - width: 100%; - height: 2.2rem; - position: absolute; - bottom: 0; - text-align: center; - line-height: 2.2rem; -} - -.model-manager .comfy-grid .model-label > p { - width: calc(100% - 2rem); - overflow-x: scroll; - white-space: nowrap; - display: inline-block; - vertical-align: middle; - margin: 0; -} - -.model-manager .comfy-grid .model-label { - scrollbar-width: none; - -ms-overflow-style: none; -} - -.model-manager .comfy-grid .model-label ::-webkit-scrollbar { - width: 0; - height: 0; -} - -.model-manager .comfy-grid .model-preview-top-right, -.model-manager .comfy-grid .model-preview-top-left { - position: absolute; - display: flex; - flex-direction: column; - gap: 8px; - top: 8px; -} - -.model-manager .comfy-grid .model-preview-top-right { - right: 8px; -} - -.model-manager .comfy-grid .model-preview-top-left { - left: 8px; -} - -.model-manager .comfy-grid .model-button { - opacity: 0.65; -} - -.model-manager .comfy-grid .model-button:hover { - opacity: 1; -} - -.model-manager .comfy-grid .model-label { - user-select: text; -} - -/* radio */ -.model-manager .comfy-radio-group { - display: flex; - gap: 8px; - flex-wrap: wrap; - min-width: 0; -} - -.model-manager .comfy-radio { - display: flex; - gap: 4px; - padding: 4px 16px; - color: var(--input-text); - border: 2px solid var(--border-color); - border-radius: 16px; - background-color: var(--comfy-input-bg); - font-size: 18px; -} - -.model-manager .comfy-radio:has(> input[type="radio"]:checked) { - border-color: var(--border-color); - background-color: var(--comfy-menu-bg); -} - -.model-manager .comfy-radio input[type="radio"]:checked + label { - color: var(--fg-color); -} - -.model-manager .radio-input { - opacity: 0; - position: absolute; -} - -/* model preview select */ -.model-manager .model-preview-select-radio-container { - min-width: 0; - flex: 1; -} - -.model-manager .model-preview-select-radio-inputs > div { - padding: 16px 0 8px 0; -} - -.model-manager .model-preview-select-radio-container img { - position: relative; - width: 230px; - height: 345px; - text-align: center; - overflow: hidden; - border-radius: 8px; - object-fit: cover; -} - -/* topbar */ -.model-manager .topbar-buttons { - display: flex; - float: right; -} - -.model-manager .topbar-buttons button { - height: 33px; - padding: 1px 6px; - width: 33px; -} - -.model-manager .model-manager-head .topbar-left { - display: flex; - float: left; -} - -.model-manager .model-manager-head .topbar-right { - column-gap: 4px; - display: flex; - flex-direction: row-reverse; - float: right; -} - -.model-manager .model-manager-head .topbar-right select { - position: relative; - top: 0; - bottom: 0; - font-size: 24px; - -o-appearance: none; - -ms-appearance: none; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} - -/* search dropdown */ -.model-manager .input-dropdown-container { - position: relative; -} - -.model-manager .search-models { - display: flex; - flex: 1; - flex-direction: row; - min-width: 0; -} - -.model-manager .model-select-dropdown { - min-width: 0; - overflow: auto; -} - -.model-manager .search-text-area, -.model-manager .plain-text-area, -.model-manager .model-select-dropdown { - flex: 1; - min-height: 36px; - padding-block: 0; - min-width: 36px; -} - -.model-manager .model-select-dropdown { - min-height: 40px; -} - -.model-manager .search-directory-dropdown { - background-color: var(--bg-color); - border: 2px var(--border-color) solid; - border-radius: 10px; - color: var(--fg-color); - max-height: 40vh; - overflow: auto; - position: absolute; - z-index: 1; -} - -@media (pointer:none), (pointer:coarse) { - .model-manager .search-directory-dropdown { - max-height: 17.5vh; - } -} - -.model-manager .search-directory-dropdown:empty { - display: none; -} - -.model-manager .search-directory-dropdown > p { - margin: 0; - padding: 0.85em 20px; - min-width: 0; -} -.model-manager .search-directory-dropdown > p { - -ms-overflow-style: none; /* Internet Explorer 10+ */ - scrollbar-width: none; /* Firefox */ -} -.model-manager .search-directory-dropdown > p::-webkit-scrollbar { - display: none; /* Safari and Chrome */ -} - -.model-manager .search-directory-dropdown > p.search-directory-dropdown-key-selected, -.model-manager .search-directory-dropdown > p.search-directory-dropdown-mouse-selected { - background-color: var(--border-color); -} - -.model-manager .search-directory-dropdown > p.search-directory-dropdown-key-selected { - border-left: 1mm solid var(--input-text); -} - -/* model manager settings */ -.model-manager .model-manager-settings > div, -.model-manager .model-manager-settings > label, -.model-manager .tag-generator-settings > label, -.model-manager .tag-generator-settings > div { - display: flex; - flex-direction: row; - align-items: center; - gap: 8px; - margin: 16px 0; -} - -.model-manager .model-manager-settings button { - height: 40px; - min-width: 120px; - justify-content: center; -} - -.model-manager .model-manager-settings input[type="number"], -.model-manager .tag-generator-settings input[type="number"]{ - width: 50px; -} - -.model-manager .search-settings-text { - width: 100%; -} diff --git a/web/model-manager.js b/web/model-manager.js deleted file mode 100644 index fd55c67..0000000 --- a/web/model-manager.js +++ /dev/null @@ -1,5545 +0,0 @@ -import { app } from '../../scripts/app.js'; -import { api } from '../../scripts/api.js'; -import { ComfyDialog, $el } from '../../scripts/ui.js'; -import { ComfyButton } from '../../scripts/ui/components/button.js'; -import { marked } from './marked.js'; -import('./downshow.js'); - -function clamp(x, min, max) { - return Math.min(Math.max(x, min), max); -} - -/** - * @param {string} url - * @param {any} [options=undefined] - * @returns {Promise} - */ -function comfyRequest(url, options = undefined) { - return new Promise((resolve, reject) => { - api - .fetchApi(url, options) - .then((response) => response.json()) - .then(resolve) - .catch(reject); - }); -} - -/** - * @param {(...args) => Promise} callback - * @param {number | undefined} delay - * @returns {(...args) => void} - */ -function debounce(callback, delay) { - let timeoutId = null; - return (...args) => { - window.clearTimeout(timeoutId); - timeoutId = window.setTimeout(() => { - callback(...args); - }, delay); - }; -} - -class KeyComboListener { - /** @type {string[]} */ - #keyCodes = []; - - /** @type {() => Promise} */ - action; - - /** @type {Element} */ - element; - - /** @type {string[]} */ - #combo = []; - - /** - * @param {string[]} keyCodes - * @param {() => Promise} action - * @param {Element} element - */ - constructor(keyCodes, action, element) { - this.#keyCodes = keyCodes; - this.action = action; - this.element = element; - - document.addEventListener('keydown', (e) => { - const code = e.code; - const keyCodes = this.#keyCodes; - const combo = this.#combo; - if (keyCodes.includes(code) && !combo.includes(code)) { - combo.push(code); - } - if (combo.length === 0 || keyCodes.length !== combo.length) { - return; - } - for (let i = 0; i < combo.length; i++) { - if (keyCodes[i] !== combo[i]) { - return; - } - } - if (document.activeElement !== this.element) { - return; - } - e.preventDefault(); - e.stopPropagation(); - this.action(); - this.#combo.length = 0; - }); - document.addEventListener('keyup', (e) => { - // Mac keyup doesn't fire when meta key is held: https://stackoverflow.com/a/73419500 - const code = e.code; - if (code === 'MetaLeft' || code === 'MetaRight') { - this.#combo.length = 0; - } else { - this.#combo = this.#combo.filter((x) => x !== code); - } - }); - } -} - -/** - * Handles Firefox's drag event, which returns different coordinates and then fails when calling `elementFromPoint`. - * @param {DragEvent} event - * @returns {[Number, Number, HTMLElement]} [clientX, clientY, targetElement] - */ -function elementFromDragEvent(event) { - let clientX = null; - let clientY = null; - let target; - const userAgentString = navigator.userAgent; - if (userAgentString.indexOf('Firefox') > -1) { - clientX = event.clientX; - clientY = event.clientY; - const screenOffsetX = window.screenLeft; - if (clientX >= screenOffsetX) { - clientX = clientX - screenOffsetX; - } - const screenOffsetY = window.screenTop; - if (clientY >= screenOffsetY) { - clientY = clientY - screenOffsetY; - } - target = document.elementFromPoint(clientX, clientY); - } else { - clientX = event.clientX; - clientY = event.clientY; - target = document.elementFromPoint(event.clientX, event.clientY); - } - return [clientX, clientY, target]; -} - -/** - * @param {string} url - */ -async function loadWorkflow(url) { - const uri = new URL(url).searchParams.get('uri'); - const fileNameIndex = - Math.max(uri.lastIndexOf('/'), uri.lastIndexOf('\\')) + 1; - const fileName = uri.substring(fileNameIndex); - const response = await fetch(url); - const data = await response.blob(); - const file = new File([data], fileName, { type: data.type }); - app.handleFile(file); -} - -const modelNodeType = { - checkpoints: 'CheckpointLoaderSimple', - clip: 'CLIPLoader', - clip_vision: 'CLIPVisionLoader', - controlnet: 'ControlNetLoader', - diffusers: 'DiffusersLoader', - embeddings: 'Embedding', - gligen: 'GLIGENLoader', - hypernetworks: 'HypernetworkLoader', - photomaker: 'PhotoMakerLoader', - loras: 'LoraLoader', - style_models: 'StyleModelLoader', - unet: 'UNETLoader', - upscale_models: 'UpscaleModelLoader', - vae: 'VAELoader', - vae_approx: undefined, -}; - -const MODEL_EXTENSIONS = [ - '.bin', - '.ckpt', - 'gguf', - '.onnx', - '.pt', - '.pth', - '.safetensors', -]; // TODO: ask server for? -const IMAGE_EXTENSIONS = [ - '.png', - '.webp', - '.jpeg', - '.jpg', - '.jfif', - '.gif', - '.apng', - - '.preview.png', - '.preview.webp', - '.preview.jpeg', - '.preview.jpg', - '.preview.jfif', - '.preview.gif', - '.preview.apng', -]; // TODO: /model-manager/image/extensions - -/** - * @param {string} s - * @param {string} prefix - * @returns {string} - */ -function removePrefix(s, prefix) { - if (s.length >= prefix.length && s.startsWith(prefix)) { - return s.substring(prefix.length); - } - return s; -} - -/** - * @param {string} s - * @param {string} suffix - * @returns {string} - */ -function removeSuffix(s, suffix) { - if (s.length >= suffix.length && s.endsWith(suffix)) { - return s.substring(0, s.length - suffix.length); - } - return s; -} - -class SearchPath { - /** - * @param {string} path - * @returns {[string, string]} - */ - static split(path) { - const i = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')) + 1; - return [path.slice(0, i), path.slice(i)]; - } - - /** - * @param {string} path - * @param {string[]} extensions - * @returns {[string, string]} - */ - static splitExtension(path) { - const i = path.lastIndexOf('.'); - if (i === -1) { - return [path, '']; - } - return [path.slice(0, i), path.slice(i)]; - } - - /** - * @param {string} path - * @returns {string} - */ - static systemPath(path, searchSeparator, systemSeparator) { - const i1 = path.indexOf(searchSeparator, 1); - const i2 = path.indexOf(searchSeparator, i1 + 1); - return path.slice(i2 + 1).replaceAll(searchSeparator, systemSeparator); - } -} - -/** - * @param {string | undefined} [searchPath=undefined] - * @param {string | undefined} [dateImageModified=undefined] - * @param {string | undefined} [width=undefined] - * @param {string | undefined} [height=undefined] - * @param {string | undefined} [imageFormat=undefined] - * @returns {string} - */ -function imageUri( - imageSearchPath = undefined, - dateImageModified = undefined, - width = undefined, - height = undefined, - imageFormat = undefined, -) { - const path = imageSearchPath ?? 'no-preview'; - const date = dateImageModified; - let uri = `/model-manager/preview/get?uri=${path}`; - if (width !== undefined && width !== null) { - uri += `&width=${width}`; - } - if (height !== undefined && height !== null) { - uri += `&height=${height}`; - } - if (date !== undefined && date !== null) { - uri += `&v=${date}`; - } - if (imageFormat !== undefined && imageFormat !== null) { - uri += `&image-format=${imageFormat}`; - } - return uri; -} -const PREVIEW_NONE_URI = imageUri(); -const PREVIEW_THUMBNAIL_WIDTH = 320; -const PREVIEW_THUMBNAIL_HEIGHT = 480; - -/** - * - * @param {HTMLButtonElement} element - * @returns {[HTMLButtonElement | undefined, HTMLElement | undefined, HTMLSpanElement | undefined]} [button, icon, span] - */ -function comfyButtonDisambiguate(element) { - // TODO: This likely can be removed by using a css rule that disables clicking on the inner elements of the button. - let button = undefined; - let icon = undefined; - let span = undefined; - const nodeName = element.nodeName.toLowerCase(); - if (nodeName === 'button') { - button = element; - icon = button.getElementsByTagName('i')[0]; - span = button.getElementsByTagName('span')[0]; - } else if (nodeName === 'i') { - icon = element; - button = element.parentElement; - span = button.getElementsByTagName('span')[0]; - } else if (nodeName === 'span') { - button = element.parentElement; - icon = button.getElementsByTagName('i')[0]; - span = element; - } - return [button, icon, span]; -} - -/** - * @param {HTMLButtonElement} element - * @param {boolean} success - * @param {string?} successClassName - * @param {string?} failureClassName - * @param {boolean?} [disableCallback=false] - */ -function comfyButtonAlert( - element, - success, - successClassName = undefined, - failureClassName = undefined, - disableCallback = false, -) { - if (element === undefined || element === null) { - return; - } - - const [button, icon, span] = comfyButtonDisambiguate(element); - if (button === undefined) { - console.warn('Unable to find button element!'); - console.warn(element); - return; - } - - // TODO: debounce would be nice, but needs some sort of "global" to avoid creating/destroying many objects - - const colorClassName = success - ? 'comfy-button-success' - : 'comfy-button-failure'; - - if (icon) { - const iconClassName = (success ? successClassName : failureClassName) ?? ''; - if (iconClassName !== '') { - icon.classList.add(iconClassName); - } - icon.classList.add(colorClassName); - if (!disableCallback) { - window.setTimeout( - (element, iconClassName, colorClassName) => { - if (iconClassName !== '') { - element.classList.remove(iconClassName); - } - element.classList.remove(colorClassName); - }, - 1000, - icon, - iconClassName, - colorClassName, - ); - } - } - - button.classList.add(colorClassName); - if (!disableCallback) { - window.setTimeout( - (element, colorClassName) => { - element.classList.remove(colorClassName); - }, - 1000, - button, - colorClassName, - ); - } -} - -/** - * - * @param {string} modelPath - * @param {string} newValue - * @returns {Promise} - */ -async function saveNotes(modelPath, newValue) { - const timestamp = await comfyRequest('/model-manager/timestamp').catch( - (err) => { - console.warn(err); - return false; - }, - ); - return await comfyRequest('/model-manager/notes/save', { - method: 'POST', - body: JSON.stringify({ - path: modelPath, - notes: newValue, - }), - timestamp: timestamp, - }) - .then((result) => { - const saved = result['success']; - const message = result['alert']; - if (message !== undefined) { - window.alert(message); - } - return saved; - }) - .catch((err) => { - console.warn(err); - return false; - }); -} - -/** - * @returns {HTMLLabelElement} - */ -function $checkbox(x = { $: (el) => {}, textContent: '', checked: false }) { - const text = x.textContent; - const input = $el('input', { - type: 'checkbox', - name: text ?? 'checkbox', - checked: x.checked ?? false, - }); - const label = $el('label', [ - input, - text === '' || text === undefined || text === null ? '' : ' ' + text, - ]); - if (x.$ !== undefined) { - x.$(input); - } - return label; -} - -/** - * @returns {HTMLLabelElement} - */ -function $select(x = { $: (el) => {}, textContent: '', options: [''] }) { - const text = x.textContent; - const select = $el( - 'select', - { - name: text ?? 'select', - }, - x.options.map((option) => { - return $el( - 'option', - { - value: option, - }, - option, - ); - }), - ); - const label = $el('label', [ - text === '' || text === undefined || text === null ? '' : ' ' + text, - select, - ]); - if (x.$ !== undefined) { - x.$(select); - } - return label; -} - -/** - * @param {Any} attr - * @returns {HTMLDivElement} - */ -function $radioGroup(attr) { - const { name = Date.now(), onchange, options = [], $ } = attr; - - /** @type {HTMLDivElement[]} */ - const radioGroup = options.map((item, index) => { - const inputRef = { value: null }; - - return $el('div.comfy-radio', { onclick: () => inputRef.value.click() }, [ - $el('input.radio-input', { - type: 'radio', - name: name, - value: item.value, - checked: index === 0, - $: (el) => (inputRef.value = el), - }), - $el('label.no-highlight', item.label ?? item.value), - ]); - }); - - const element = $el('input', { - name: name + '-group', - value: options[0]?.value, - }); - $?.(element); - - radioGroup.forEach((radio) => { - radio.addEventListener('change', (event) => { - const selectedValue = event.target.value; - element.value = selectedValue; - onchange?.(selectedValue); - }); - }); - - return $el('div.comfy-radio-group', radioGroup); -} - -/** - * @param {{name: string, icon: string, tabContent: HTMLDivElement}[]} tabData - * @returns {[HTMLDivElement[], HTMLDivElement[]]} - */ -function GenerateTabGroup(tabData) { - const ACTIVE_TAB_CLASS = 'active'; - - /** @type {HTMLDivElement[]} */ - const tabButtons = []; - - /** @type {HTMLDivElement[]} */ - const tabContents = []; - - tabData.forEach((data) => { - const name = data.name; - const icon = data.icon; - /** @type {HTMLDivElement} */ - const tab = new ComfyButton({ - icon: icon, - tooltip: 'Open ' + name.toLowerCase() + ' tab', - classList: 'comfyui-button tab-button', - content: name, - action: () => { - tabButtons.forEach((tabButton) => { - if (name === tabButton.getAttribute('data-name')) { - tabButton.classList.add(ACTIVE_TAB_CLASS); - } else { - tabButton.classList.remove(ACTIVE_TAB_CLASS); - } - }); - tabContents.forEach((tabContent) => { - if (name === tabContent.getAttribute('data-name')) { - tabContent.scrollTop = tabContent.dataset['scrollTop'] ?? 0; - tabContent.style.display = ''; - } else { - tabContent.dataset['scrollTop'] = tabContent.scrollTop; - tabContent.style.display = 'none'; - } - }); - }, - }).element; - tab.dataset.name = name; - const content = $el( - 'div.tab-content', - { - dataset: { - name: data.name, - }, - }, - [data.tabContent], - ); - tabButtons.push(tab); - tabContents.push(content); - }); - - return [tabButtons, tabContents]; -} - -/** - * @param {HTMLDivElement} element - * @param {Record[]} tabButtons - */ -function GenerateDynamicTabTextCallback(element, tabButtons, minWidth) { - return () => { - if (element.style.display === 'none') { - return; - } - const managerRect = element.getBoundingClientRect(); - const isIcon = managerRect.width < minWidth; // TODO: `minWidth` is a magic value - const iconDisplay = isIcon ? '' : 'none'; - const spanDisplay = isIcon ? 'none' : ''; - tabButtons.forEach((tabButton) => { - tabButton.getElementsByTagName('i')[0].style.display = iconDisplay; - tabButton.getElementsByTagName('span')[0].style.display = spanDisplay; - }); - }; -} - -/** - * @param {[String, int][]} map - * @returns {String} - */ -function TagCountMapToParagraph(map) { - let text = '

    '; - for (let i = 0; i < map.length; i++) { - const v = map[i]; - const tag = v[0]; - const count = v[1]; - text += tag + ' (' + count + ')'; - if (i !== map.length - 1) { - text += ', '; - } - } - text += '

    '; - return text; -} - -/** - * @param {String} p - * @returns {[String, int][]} - */ -function ParseTagParagraph(p) { - return p.split(',').map((x) => { - const text = x.endsWith(', ') ? x.substring(0, x.length - 2) : x; - const i = text.lastIndexOf('('); - const tag = text.substring(0, i).trim(); - const frequency = parseInt(text.substring(i + 1, text.length - 1)); - return [tag, frequency]; - }); -} - -class ImageSelect { - /** @constant {string} */ #PREVIEW_DEFAULT = 'Default'; - /** @constant {string} */ #PREVIEW_UPLOAD = 'Upload'; - /** @constant {string} */ #PREVIEW_URL = 'URL'; - /** @constant {string} */ #PREVIEW_NONE = 'No Preview'; - - elements = { - /** @type {HTMLDivElement} */ radioGroup: null, - /** @type {HTMLDivElement} */ radioButtons: null, - /** @type {HTMLDivElement} */ previews: null, - - /** @type {HTMLImageElement} */ defaultPreviewNoImage: null, - /** @type {HTMLDivElement} */ defaultPreviews: null, - /** @type {HTMLDivElement} */ defaultUrl: null, - - /** @type {HTMLImageElement} */ customUrlPreview: null, - /** @type {HTMLInputElement} */ customUrl: null, - /** @type {HTMLDivElement} */ custom: null, - - /** @type {HTMLImageElement} */ uploadPreview: null, - /** @type {HTMLInputElement} */ uploadFile: null, - /** @type {HTMLDivElement} */ upload: null, - }; - - /** @type {string} */ - #name = null; - - /** @returns {Promise | Promise} */ - async getImage() { - const name = this.#name; - const value = document.querySelector(`input[name="${name}"]:checked`).value; - const elements = this.elements; - switch (value) { - case this.#PREVIEW_DEFAULT: { - const children = elements.defaultPreviews.children; - const noImage = PREVIEW_NONE_URI; - let url = ''; - for (let i = 0; i < children.length; i++) { - const child = children[i]; - if ( - child.style.display !== 'none' && - child.nodeName === 'IMG' && - !child.src.endsWith(noImage) - ) { - url = child.src; - } - } - if (url.startsWith(Civitai.imageUrlPrefix())) { - url = await Civitai.getFullSizeImageUrl(url).catch((err) => { - console.warn(err); - return url; - }); - } - return url; - } - case this.#PREVIEW_URL: { - const value = elements.customUrl.value; - if (value.startsWith(Civitai.imagePostUrlPrefix())) { - try { - const imageInfo = await Civitai.getImageInfo(value); - const items = imageInfo['items']; - if (items.length === 0) { - console.warn('Civitai /api/v1/images returned 0 items.'); - return value; - } - return items[0]['url']; - } catch (error) { - console.error('Failed to get image info from Civitai!', error); - return value; - } - } - return value; - } - case this.#PREVIEW_UPLOAD: - return elements.uploadFile.files[0] ?? ''; - case this.#PREVIEW_NONE: - return PREVIEW_NONE_URI; - } - return ''; - } - - /** @returns {void} */ - resetModelInfoPreview() { - let noimage = this.elements.defaultUrl.dataset.noimage; - [ - this.elements.defaultPreviewNoImage, - this.elements.defaultPreviews, - this.elements.customUrlPreview, - this.elements.uploadPreview, - ].forEach((el) => { - el.style.display = 'none'; - if (this.elements.defaultPreviewNoImage !== el) { - if (el.nodeName === 'IMG') { - el.src = noimage; - } else { - el.children[0].src = noimage; - } - } else { - el.src = PREVIEW_NONE_URI; - } - }); - this.checkDefault(); - this.elements.uploadFile.value = ''; - this.elements.customUrl.value = ''; - this.elements.upload.style.display = 'none'; - this.elements.custom.style.display = 'none'; - } - - /** @returns {boolean} */ - defaultIsChecked() { - const children = this.elements.radioButtons.children; - for (let i = 0; i < children.length; i++) { - const child = children[i]; - const radioButton = child.children[0]; - if (radioButton.value === this.#PREVIEW_DEFAULT) { - return radioButton.checked; - } - } - return false; - } - - /** @returns {void} */ - checkDefault() { - const children = this.elements.radioButtons.children; - for (let i = 0; i < children.length; i++) { - const child = children[i]; - const radioButton = child.children[0]; - if (radioButton.value === this.#PREVIEW_DEFAULT) { - this.elements.defaultPreviews.style.display = 'block'; - radioButton.checked = true; - break; - } - } - } - - /** - * @param {1 | -1} step - */ - stepDefaultPreviews(step) { - const children = this.elements.defaultPreviews.children; - if (children.length === 0) { - return; - } - let currentIndex = -step; - for (let i = 0; i < children.length; i++) { - const previewImage = children[i]; - const display = previewImage.style.display; - if (display !== 'none') { - currentIndex = i; - } - previewImage.style.display = 'none'; - } - currentIndex = currentIndex + step; - if (currentIndex >= children.length) { - currentIndex = 0; - } else if (currentIndex < 0) { - currentIndex = children.length - 1; - } - children[currentIndex].style.display = 'block'; - } - - /** - * @param {string} radioGroupName - Should be unique for every radio group. - * @param {string[]|undefined} defaultPreviews - */ - constructor(radioGroupName, defaultPreviews = []) { - if ( - (defaultPreviews === undefined) | - (defaultPreviews === null) | - (defaultPreviews.length === 0) - ) { - defaultPreviews = [PREVIEW_NONE_URI]; - } - this.#name = radioGroupName; - - const el_defaultUri = $el('div', { - $: (el) => (this.elements.defaultUrl = el), - style: { display: 'none' }, - 'data-noimage': PREVIEW_NONE_URI, - }); - - const el_defaultPreviewNoImage = $el('img', { - $: (el) => (this.elements.defaultPreviewNoImage = el), - loading: - 'lazy' /* `loading` BEFORE `src`; Known bug in Firefox 124.0.2 and Safari for iOS 17.4.1 (https://stackoverflow.com/a/76252772) */, - src: PREVIEW_NONE_URI, - style: { display: 'none' }, - }); - - const el_defaultPreviews = $el( - 'div', - { - $: (el) => (this.elements.defaultPreviews = el), - style: { - width: '100%', - height: '100%', - }, - }, - (() => { - const imgs = defaultPreviews.map((url) => { - return $el('img', { - loading: - 'lazy' /* `loading` BEFORE `src`; Known bug in Firefox 124.0.2 and Safari for iOS 17.4.1 (https://stackoverflow.com/a/76252772) */, - src: url, - style: { display: 'none' }, - onerror: (e) => { - e.target.src = el_defaultUri.dataset.noimage ?? PREVIEW_NONE_URI; - }, - }); - }); - if (imgs.length > 0) { - imgs[0].style.display = 'block'; - } - return imgs; - })(), - ); - - const el_uploadPreview = $el('img', { - $: (el) => (this.elements.uploadPreview = el), - src: PREVIEW_NONE_URI, - style: { display: 'none' }, - onerror: (e) => { - e.target.src = el_defaultUri.dataset.noimage ?? PREVIEW_NONE_URI; - }, - }); - const el_uploadFile = $el('input', { - $: (el) => (this.elements.uploadFile = el), - type: 'file', - name: 'upload preview image', - accept: IMAGE_EXTENSIONS.join(', '), - onchange: (e) => { - const file = e.target.files[0]; - if (file) { - el_uploadPreview.src = URL.createObjectURL(file); - } else { - el_uploadPreview.src = el_defaultUri.dataset.noimage; - } - }, - }); - const el_upload = $el( - 'div.row.tab-header-flex-block', - { - $: (el) => (this.elements.upload = el), - style: { display: 'none' }, - }, - [el_uploadFile], - ); - - /** - * @param {string} url - * @returns {Promise} - */ - const getCustomPreviewUrl = async (url) => { - if (url.startsWith(Civitai.imagePostUrlPrefix())) { - return await Civitai.getImageInfo(url) - .then((imageInfo) => { - const items = imageInfo['items']; - if (items.length > 0) { - return items[0]['url']; - } else { - console.warn('Civitai /api/v1/images returned 0 items.'); - return url; - } - }) - .catch((error) => { - console.error('Failed to get image info from Civitai!', error); - return url; - }); - } else { - return url; - } - }; - - const el_customUrlPreview = $el('img', { - $: (el) => (this.elements.customUrlPreview = el), - src: PREVIEW_NONE_URI, - style: { display: 'none' }, - onerror: (e) => { - e.target.src = el_defaultUri.dataset.noimage ?? PREVIEW_NONE_URI; - }, - }); - const el_customUrl = $el('input.search-text-area', { - $: (el) => (this.elements.customUrl = el), - type: 'text', - name: 'custom preview image url', - autocomplete: 'off', - placeholder: 'https://custom-image-preview.png', - onkeydown: async (e) => { - if (e.key === 'Enter') { - const value = e.target.value; - el_customUrlPreview.src = await getCustomPreviewUrl(value); - e.stopPropagation(); - e.target.blur(); - } - }, - }); - const el_custom = $el( - 'div.row.tab-header-flex-block', - { - $: (el) => (this.elements.custom = el), - style: { display: 'none' }, - }, - [ - el_customUrl, - new ComfyButton({ - icon: 'magnify', - tooltip: 'Search models', - classList: 'comfyui-button icon-button', - action: async (e) => { - const [button, icon, span] = comfyButtonDisambiguate(e.target); - button.disabled = true; - const value = el_customUrl.value; - el_customUrlPreview.src = await getCustomPreviewUrl(value); - e.stopPropagation(); - el_customUrl.blur(); - button.disabled = false; - }, - }).element, - ], - ); - - const el_previewButtons = $el( - 'div.model-preview-overlay', - { - style: { - display: el_defaultPreviews.children.length > 1 ? 'block' : 'none', - }, - }, - [ - new ComfyButton({ - icon: 'arrow-left', - tooltip: 'Previous image', - classList: 'comfyui-button icon-button model-preview-button-left', - action: () => this.stepDefaultPreviews(-1), - }).element, - new ComfyButton({ - icon: 'arrow-right', - tooltip: 'Next image', - classList: 'comfyui-button icon-button model-preview-button-right', - action: () => this.stepDefaultPreviews(1), - }).element, - ], - ); - const el_previews = $el( - 'div.item', - { - $: (el) => (this.elements.previews = el), - }, - [ - $el( - 'div', - { - style: { - width: '100%', - height: '100%', - }, - }, - [ - el_defaultPreviewNoImage, - el_defaultPreviews, - el_customUrlPreview, - el_uploadPreview, - ], - ), - el_previewButtons, - ], - ); - - const el_radioButtons = $radioGroup({ - name: radioGroupName, - onchange: (value) => { - el_custom.style.display = 'none'; - el_upload.style.display = 'none'; - - el_defaultPreviews.style.display = 'none'; - el_previewButtons.style.display = 'none'; - - el_defaultPreviewNoImage.style.display = 'none'; - el_uploadPreview.style.display = 'none'; - el_customUrlPreview.style.display = 'none'; - - switch (value) { - case this.#PREVIEW_DEFAULT: - el_defaultPreviews.style.display = 'block'; - el_previewButtons.style.display = - el_defaultPreviews.children.length > 1 ? 'block' : 'none'; - break; - case this.#PREVIEW_UPLOAD: - el_upload.style.display = 'flex'; - el_uploadPreview.style.display = 'block'; - break; - case this.#PREVIEW_URL: - el_custom.style.display = 'flex'; - el_customUrlPreview.style.display = 'block'; - break; - case this.#PREVIEW_NONE: - default: - el_defaultPreviewNoImage.style.display = 'block'; - break; - } - }, - options: [ - this.#PREVIEW_DEFAULT, - this.#PREVIEW_URL, - this.#PREVIEW_UPLOAD, - this.#PREVIEW_NONE, - ].map((value) => { - return { value: value }; - }), - }); - this.elements.radioButtons = el_radioButtons; - - const children = el_radioButtons.children; - for (let i = 0; i < children.length; i++) { - const child = children[i]; - const radioButton = child.children[0]; - if (radioButton.value === this.#PREVIEW_DEFAULT) { - radioButton.checked = true; - break; - } - } - - const el_radioGroup = $el( - 'div.model-preview-select-radio-container', - { - $: (el) => (this.elements.radioGroup = el), - }, - [ - $el('div.row.tab-header-flex-block', [el_radioButtons]), - $el('div.model-preview-select-radio-inputs', [el_custom, el_upload]), - ], - ); - } -} - -/** - * @typedef {Object} DirectoryItem - * @property {String} name - * @property {number | undefined} childCount - * @property {number | undefined} childIndex - */ - -class ModelDirectories { - /** @type {DirectoryItem[]} */ - data = []; - - /** - * @returns {number} - */ - rootIndex() { - return 0; - } - - /** - * @param {any} index - * @returns {boolean} - */ - isValidIndex(index) { - return typeof index === 'number' && 0 <= index && index < this.data.length; - } - - /** - * @param {number} index - * @returns {DirectoryItem} - */ - getItem(index) { - if (!this.isValidIndex(index)) { - throw new Error(`Index '${index}' is not valid!`); - } - return this.data[index]; - } - - /** - * @param {DirectoryItem | number} item - * @returns {boolean} - */ - isDirectory(item) { - if (typeof item === 'number') { - item = this.getItem(item); - } - const childCount = item.childCount; - return childCount !== undefined && childCount != null; - } - - /** - * @param {DirectoryItem | number} item - * @returns {boolean} - */ - isEmpty(item) { - if (typeof item === 'number') { - item = this.getItem(item); - } - if (!this.isDirectory(item)) { - throw new Error('Item is not a directory!'); - } - return item.childCount === 0; - } - - /** - * Returns a slice of children from the directory list. - * @param {DirectoryItem | number} item - * @returns {DirectoryItem[]} - */ - getChildren(item) { - if (typeof item === 'number') { - item = this.getItem(item); - if (!this.isDirectory(item)) { - throw new Error('Item is not a directory!'); - } - } else if (!this.isDirectory(item)) { - throw new Error('Item is not a directory!'); - } - const count = item.childCount; - const index = item.childIndex; - return this.data.slice(index, index + count); - } - - /** - * Returns index of child in parent directory. Returns -1 if DNE. - * @param {DirectoryItem | number} parent - * @param {string} name - * @returns {number} - */ - findChildIndex(parent, name) { - const item = this.getItem(parent); - if (!this.isDirectory(item)) { - throw new Error('Item is not a directory!'); - } - const start = item.childIndex; - const children = this.getChildren(item); - const index = children.findIndex((item) => { - return item.name === name; - }); - if (index === -1) { - return -1; - } - return index + start; - } - - /** - * Returns a list of matching search results and valid path. - * @param {string} filter - * @param {string} searchSeparator - * @param {boolean} directoriesOnly - * @returns {[string[], string]} - */ - search(filter, searchSeparator, directoriesOnly) { - let cwd = this.rootIndex(); - let indexLastWord = 1; - while (true) { - const indexNextWord = filter.indexOf(searchSeparator, indexLastWord); - if (indexNextWord === -1) { - // end of filter - break; - } - - const item = this.getItem(cwd); - if (!this.isDirectory(item) || this.isEmpty(item)) { - break; - } - - const word = filter.substring(indexLastWord, indexNextWord); - cwd = this.findChildIndex(cwd, word); - if (!this.isValidIndex(cwd)) { - return [[], '']; - } - indexLastWord = indexNextWord + 1; - } - //const cwdPath = filter.substring(0, indexLastWord); - - const lastWord = filter.substring(indexLastWord); - const children = this.getChildren(cwd); - if (directoriesOnly) { - let indexPathEnd = indexLastWord; - const results = children - .filter((child) => { - return this.isDirectory(child) && child.name.startsWith(lastWord); - }) - .map((directory) => { - const children = this.getChildren(directory); - const hasChildren = children.some((item) => { - return this.isDirectory(item); - }); - const suffix = hasChildren ? searchSeparator : ''; - //const suffix = searchSeparator; - if (directory.name == lastWord) { - indexPathEnd += searchSeparator.length + directory.name.length + 1; - } - return directory.name + suffix; - }); - const path = filter.substring(0, indexPathEnd); - return [results, path]; - } else { - let indexPathEnd = indexLastWord; - const results = children - .filter((child) => { - return child.name.startsWith(lastWord); - }) - .map((item) => { - const isDir = this.isDirectory(item); - const isNonEmptyDirectory = isDir && item.childCount > 0; - const suffix = isNonEmptyDirectory ? searchSeparator : ''; - //const suffix = isDir ? searchSeparator : ""; - if (!isDir && item.name == lastWord) { - indexPathEnd += searchSeparator.length + item.name.length + 1; - } - return item.name + suffix; - }); - const path = filter.substring(0, indexPathEnd); - return [results, path]; - } - } -} - -const DROPDOWN_DIRECTORY_SELECTION_KEY_CLASS = - 'search-directory-dropdown-key-selected'; -const DROPDOWN_DIRECTORY_SELECTION_MOUSE_CLASS = - 'search-directory-dropdown-mouse-selected'; - -class ModelData { - /** @type {string} */ - searchSeparator = '/'; // TODO: other client or server code may be assuming this to always be "/" - - /** @type {string} */ - systemSeparator = null; - - /** @type {Object} */ - models = {}; - - /** @type {ModelDirectories} */ - directories = null; - - constructor() { - this.directories = new ModelDirectories(); - } -} - -class DirectoryDropdown { - /** @type {HTMLDivElement} */ - element = null; - - /** @type {Boolean} */ - showDirectoriesOnly = false; - - /** @type {HTMLInputElement} */ - #input = null; - - /** @type {() => string} */ - #getModelType = null; - - /** @type {ModelData} */ - #modelData = null; // READ ONLY - - /** @type {() => void} */ - #updateCallback = null; - - /** @type {() => Promise} */ - #submitCallback = null; - - /** @type {string} */ - #deepestPreviousPath = '/'; - - /** @type {Any} */ - #touchSelectionStart = null; - - /** @type {() => Boolean} */ - #isDynamicSearch = () => { - return false; - }; - - /** - * @param {ModelData} modelData - * @param {HTMLInputElement} input - * @param {Boolean} [showDirectoriesOnly=false] - * @param {() => string} [getModelType= () => { return ""; }] - * @param {() => void} [updateCallback= () => {}] - * @param {() => Promise} [submitCallback= () => {}] - * @param {() => Boolean} [isDynamicSearch= () => { return false; }] - */ - constructor( - modelData, - input, - showDirectoriesOnly = false, - getModelType = () => { - return ''; - }, - updateCallback = () => {}, - submitCallback = () => {}, - isDynamicSearch = () => { - return false; - }, - ) { - /** @type {HTMLDivElement} */ - const dropdown = $el('div.search-directory-dropdown', { - style: { - display: 'none', - }, - }); - this.element = dropdown; - this.#modelData = modelData; - this.#input = input; - this.#getModelType = getModelType; - this.#updateCallback = updateCallback; - this.#submitCallback = submitCallback; - this.showDirectoriesOnly = showDirectoriesOnly; - this.#isDynamicSearch = isDynamicSearch; - - input.addEventListener('input', async (e) => { - const path = this.#updateOptions(); - if (path !== undefined) { - this.#restoreSelectedOption(path); - this.#updateDeepestPath(path); - } - updateCallback(); - if (isDynamicSearch()) { - await submitCallback(); - } - }); - input.addEventListener('focus', () => { - const path = this.#updateOptions(); - if (path !== undefined) { - this.#deepestPreviousPath = path; - this.#restoreSelectedOption(path); - } - updateCallback(); - }); - input.addEventListener('blur', () => { - dropdown.style.display = 'none'; - }); - input.addEventListener('keydown', async (e) => { - const options = dropdown.children; - let iSelection; - for (iSelection = 0; iSelection < options.length; iSelection++) { - const selection = options[iSelection]; - if ( - selection.classList.contains(DROPDOWN_DIRECTORY_SELECTION_KEY_CLASS) - ) { - break; - } - } - if (e.key === 'Escape') { - e.stopPropagation(); - if (iSelection < options.length) { - const selection = options[iSelection]; - selection.classList.remove(DROPDOWN_DIRECTORY_SELECTION_KEY_CLASS); - } else { - e.target.blur(); - } - } else if (e.key === 'ArrowRight' && dropdown.style.display !== 'none') { - const selection = options[iSelection]; - if (selection !== undefined && selection !== null) { - e.stopPropagation(); - e.preventDefault(); // prevent cursor move - const input = e.target; - const searchSeparator = modelData.searchSeparator; - DirectoryDropdown.selectionToInput( - input, - selection, - searchSeparator, - DROPDOWN_DIRECTORY_SELECTION_KEY_CLASS, - ); - const path = this.#updateOptions(); - if (path !== undefined) { - this.#restoreSelectedOption(path); - this.#updateDeepestPath(path); - } - updateCallback(); - if (isDynamicSearch()) { - await submitCallback(); - } - } - } else if (e.key === 'ArrowLeft' && dropdown.style.display !== 'none') { - const input = e.target; - const oldFilterText = input.value; - const searchSeparator = modelData.searchSeparator; - const iSep = oldFilterText.lastIndexOf( - searchSeparator, - oldFilterText.length - 2, - ); - const newFilterText = oldFilterText.substring(0, iSep + 1); - if (oldFilterText !== newFilterText) { - const delta = oldFilterText.substring(iSep + 1); - let isMatch = delta[delta.length - 1] === searchSeparator; - if (!isMatch) { - const options = dropdown.children; - for (let i = 0; i < options.length; i++) { - const option = options[i]; - if (option.innerText.startsWith(delta)) { - isMatch = true; - break; - } - } - } - if (isMatch) { - e.stopPropagation(); - e.preventDefault(); // prevent cursor move - input.value = newFilterText; - const path = this.#updateOptions(); - if (path !== undefined) { - this.#restoreSelectedOption(path); - this.#updateDeepestPath(path); - } - updateCallback(); - if (isDynamicSearch()) { - await submitCallback(); - } - } - } - } else if (e.key === 'Enter') { - e.stopPropagation(); - const input = e.target; - if (dropdown.style.display !== 'none') { - /* - // This is WAY too confusing. - const selection = options[iSelection]; - if (selection !== undefined && selection !== null) { - DirectoryDropdown.selectionToInput( - input, - selection, - modelData.searchSeparator, - DROPDOWN_DIRECTORY_SELECTION_KEY_CLASS - ); - const path = this.#updateOptions(); - if (path !== undefined) { - this.#updateDeepestPath(path); - } - updateCallback(); - } - */ - } - await submitCallback(); - input.blur(); - } else if ( - (e.key === 'ArrowDown' || e.key === 'ArrowUp') && - dropdown.style.display !== 'none' - ) { - e.stopPropagation(); - e.preventDefault(); // prevent cursor move - let iNext = options.length; - if (iSelection < options.length) { - const selection = options[iSelection]; - selection.classList.remove(DROPDOWN_DIRECTORY_SELECTION_KEY_CLASS); - const delta = e.key === 'ArrowDown' ? 1 : -1; - iNext = iSelection + delta; - if (iNext < 0) { - iNext = options.length - 1; - } else if (iNext >= options.length) { - iNext = 0; - } - const selectionNext = options[iNext]; - selectionNext.classList.add(DROPDOWN_DIRECTORY_SELECTION_KEY_CLASS); - } else if (iSelection === options.length) { - // none - iNext = e.key === 'ArrowDown' ? 0 : options.length - 1; - const selection = options[iNext]; - selection.classList.add(DROPDOWN_DIRECTORY_SELECTION_KEY_CLASS); - } - if (0 <= iNext && iNext < options.length) { - DirectoryDropdown.#clampDropdownScrollTop(dropdown, options[iNext]); - } else { - dropdown.scrollTop = 0; - const options = dropdown.children; - for (iSelection = 0; iSelection < options.length; iSelection++) { - const selection = options[iSelection]; - if ( - selection.classList.contains( - DROPDOWN_DIRECTORY_SELECTION_KEY_CLASS, - ) - ) { - selection.classList.remove( - DROPDOWN_DIRECTORY_SELECTION_KEY_CLASS, - ); - } - } - } - } - }); - } - - /** - * @param {HTMLInputElement} input - * @param {HTMLParagraphElement | undefined | null} selection - * @param {String} searchSeparator - * @param {String} className - * @returns {boolean} changed - */ - static selectionToInput(input, selection, searchSeparator, className) { - selection.classList.remove(className); - const selectedText = selection.innerText; - const oldFilterText = input.value; - const iSep = oldFilterText.lastIndexOf(searchSeparator); - const previousPath = oldFilterText.substring(0, iSep + 1); - const newFilterText = previousPath + selectedText; - input.value = newFilterText; - return newFilterText !== oldFilterText; - } - - /** - * @param {string} path - */ - #updateDeepestPath = (path) => { - const deepestPath = this.#deepestPreviousPath; - if (path.length > deepestPath.length || !deepestPath.startsWith(path)) { - this.#deepestPreviousPath = path; - } - }; - - /** - * @param {HTMLDivElement} dropdown - * @param {HTMLParagraphElement} selection - */ - static #clampDropdownScrollTop = (dropdown, selection) => { - let dropdownTop = dropdown.scrollTop; - const dropdownHeight = dropdown.offsetHeight; - const selectionHeight = selection.offsetHeight; - const selectionTop = selection.offsetTop; - dropdownTop = Math.max( - dropdownTop, - selectionTop - dropdownHeight + selectionHeight, - ); - dropdownTop = Math.min(dropdownTop, selectionTop); - dropdown.scrollTop = dropdownTop; - }; - - /** - * @param {string} path - */ - #restoreSelectedOption(path) { - const searchSeparator = this.#modelData.searchSeparator; - const deepest = this.#deepestPreviousPath; - if (deepest.length >= path.length && deepest.startsWith(path)) { - let name = deepest.substring(path.length); - name = removePrefix(name, searchSeparator); - const i1 = name.indexOf(searchSeparator); - if (i1 !== -1) { - name = name.substring(0, i1); - } - - const dropdown = this.element; - const options = dropdown.children; - let iSelection; - for (iSelection = 0; iSelection < options.length; iSelection++) { - const selection = options[iSelection]; - let text = removeSuffix(selection.innerText, searchSeparator); - if (text === name) { - selection.classList.add(DROPDOWN_DIRECTORY_SELECTION_KEY_CLASS); - dropdown.scrollTop = dropdown.scrollHeight; // snap to top - DirectoryDropdown.#clampDropdownScrollTop(dropdown, selection); - break; - } - } - if (iSelection === options.length) { - dropdown.scrollTop = 0; - } - } - } - - /** - * Returns path if update was successful. - * @returns {string | undefined} - */ - #updateOptions() { - const dropdown = this.element; - const input = this.#input; - - const searchSeparator = this.#modelData.searchSeparator; - const filter = input.value; - if (filter[0] !== searchSeparator) { - dropdown.style.display = 'none'; - return undefined; - } - - const modelType = this.#getModelType(); - const searchPrefix = modelType !== '' ? searchSeparator + modelType : ''; - const directories = this.#modelData.directories; - const [options, path] = directories.search( - searchPrefix + filter, - searchSeparator, - this.showDirectoriesOnly, - ); - if (options.length === 0) { - dropdown.style.display = 'none'; - return undefined; - } - - const mouse_selection_select = (e) => { - const selection = e.target; - if (e.movementX === 0 && e.movementY === 0) { - return; - } - if ( - !selection.classList.contains(DROPDOWN_DIRECTORY_SELECTION_MOUSE_CLASS) - ) { - // assumes only one will ever selected at a time - e.stopPropagation(); - const children = dropdown.children; - for (let iChild = 0; iChild < children.length; iChild++) { - const child = children[iChild]; - child.classList.remove(DROPDOWN_DIRECTORY_SELECTION_MOUSE_CLASS); - } - selection.classList.add(DROPDOWN_DIRECTORY_SELECTION_MOUSE_CLASS); - } - }; - const mouse_selection_deselect = (e) => { - e.stopPropagation(); - e.target.classList.remove(DROPDOWN_DIRECTORY_SELECTION_MOUSE_CLASS); - }; - const selection_submit = async (e) => { - e.stopPropagation(); - e.preventDefault(); - const selection = e.target; - const changed = DirectoryDropdown.selectionToInput( - input, - selection, - searchSeparator, - DROPDOWN_DIRECTORY_SELECTION_MOUSE_CLASS, - ); - if (!changed) { - dropdown.style.display = 'none'; - input.blur(); - } else { - const path = this.#updateOptions(); // TODO: is this needed? - if (path !== undefined) { - this.#updateDeepestPath(path); - } - } - this.#updateCallback(); - if (this.#isDynamicSearch()) { - await this.#submitCallback(); - } - }; - const touch_selection_select = async (e) => { - const [startX, startY] = this.#touchSelectionStart; - const [endX, endY] = [ - e.changedTouches[0].clientX, - e.changedTouches[0].clientY, - ]; - if (startX === endX && startY === endY) { - const touch = e.changedTouches[0]; - const box = dropdown.getBoundingClientRect(); - if ( - touch.clientX >= box.left && - touch.clientX <= box.right && - touch.clientY >= box.top && - touch.clientY <= box.bottom - ) { - selection_submit(e); - } - } - }; - const touch_start = (e) => { - this.#touchSelectionStart = [ - e.changedTouches[0].clientX, - e.changedTouches[0].clientY, - ]; - }; - dropdown.innerHTML = ''; - dropdown.append.apply( - dropdown, - options.map((text) => { - /** @type {HTMLParagraphElement} */ - const p = $el( - 'p', - { - onmouseenter: (e) => mouse_selection_select(e), - onmousemove: (e) => mouse_selection_select(e), - onmouseleave: (e) => mouse_selection_deselect(e), - onmousedown: (e) => selection_submit(e), - ontouchstart: (e) => touch_start(e), - ontouchmove: (e) => touch_move(e), - ontouchend: (e) => touch_selection_select(e), - }, - [text], - ); - return p; - }), - ); - // TODO: handle when dropdown is near the bottom of the window - const inputRect = input.getBoundingClientRect(); - dropdown.style.width = inputRect.width + 'px'; - dropdown.style.top = input.offsetTop + inputRect.height + 'px'; - dropdown.style.left = input.offsetLeft + 'px'; - dropdown.style.display = 'block'; - - return path; - } -} - -const MODEL_SORT_DATE_CREATED = 'dateCreated'; -const MODEL_SORT_DATE_MODIFIED = 'dateModified'; -const MODEL_SORT_SIZE_BYTES = 'sizeBytes'; -const MODEL_SORT_DATE_NAME = 'name'; - -class ModelGrid { - /** - * @param {string} nodeType - * @returns {int} - */ - static modelWidgetIndex(nodeType) { - return nodeType === undefined ? -1 : 0; - } - - /** - * @param {string} text - * @param {string} file - * @param {boolean} removeExtension - * @returns {string} - */ - static insertEmbeddingIntoText(text, file, removeExtension) { - let name = file; - if (removeExtension) { - name = SearchPath.splitExtension(name)[0]; - } - const sep = text.length === 0 || text.slice(-1).match(/\s/) ? '' : ' '; - return text + sep + '(embedding:' + name + ':1.0)'; - } - - /** - * @param {Array} list - * @param {string} searchString - * @returns {Array} - */ - static #filter(list, searchString) { - /** @type {string[]} */ - const keywords = searchString - //.replace("*", " ") // TODO: this is wrong for wildcards - .split(/(-?".*?"|[^\s"]+)+/g) - .map((item) => - item - .trim() - .replace(/(?:")+/g, '') - .toLowerCase(), - ) - .filter(Boolean); - - const regexSHA256 = /^[a-f0-9]{64}$/gi; - const fields = ['name', 'path']; - return list.filter((element) => { - const text = fields - .reduce((memo, field) => memo + ' ' + element[field], '') - .toLowerCase(); - return keywords.reduce((memo, target) => { - const excludeTarget = target[0] === '-'; - if (excludeTarget && target.length === 1) { - return memo; - } - const filteredTarget = excludeTarget ? target.slice(1) : target; - if ( - element['SHA256'] !== undefined && - regexSHA256.test(filteredTarget) - ) { - return ( - memo && excludeTarget !== (filteredTarget === element['SHA256']) - ); - } else { - return memo && excludeTarget !== text.includes(filteredTarget); - } - }, true); - }); - } - - /** - * In-place sort. Returns an array alias. - * @param {Array} list - * @param {string} sortBy - * @param {bool} [reverse=false] - * @returns {Array} - */ - static #sort(list, sortBy, reverse = false) { - let compareFn = null; - switch (sortBy) { - case MODEL_SORT_DATE_NAME: - compareFn = (a, b) => { - return a[MODEL_SORT_DATE_NAME].localeCompare(b[MODEL_SORT_DATE_NAME]); - }; - break; - case MODEL_SORT_DATE_MODIFIED: - compareFn = (a, b) => { - return b[MODEL_SORT_DATE_MODIFIED] - a[MODEL_SORT_DATE_MODIFIED]; - }; - break; - case MODEL_SORT_DATE_CREATED: - compareFn = (a, b) => { - return b[MODEL_SORT_DATE_CREATED] - a[MODEL_SORT_DATE_CREATED]; - }; - break; - case MODEL_SORT_SIZE_BYTES: - compareFn = (a, b) => { - return b[MODEL_SORT_SIZE_BYTES] - a[MODEL_SORT_SIZE_BYTES]; - }; - break; - default: - console.warn("Invalid filter sort value: '" + sortBy + "'"); - return list; - } - const sorted = list.sort(compareFn); - return reverse ? sorted.reverse() : sorted; - } - - /** - * @param {Event} event - * @param {string} modelType - * @param {string} path - * @param {boolean} removeEmbeddingExtension - * @param {int} addOffset - */ - static #addModel( - event, - modelType, - path, - removeEmbeddingExtension, - addOffset, - ) { - let success = false; - if (modelType !== 'embeddings') { - const nodeType = modelNodeType[modelType]; - const widgetIndex = ModelGrid.modelWidgetIndex(nodeType); - let node = LiteGraph.createNode(nodeType, null, []); - if (widgetIndex !== -1 && node) { - node.widgets[widgetIndex].value = path; - const selectedNodes = app.canvas.selected_nodes; - let isSelectedNode = false; - for (var i in selectedNodes) { - const selectedNode = selectedNodes[i]; - node.pos[0] = selectedNode.pos[0] + addOffset; - node.pos[1] = selectedNode.pos[1] + addOffset; - isSelectedNode = true; - break; - } - if (!isSelectedNode) { - const graphMouse = app.canvas.graph_mouse; - node.pos[0] = graphMouse[0]; - node.pos[1] = graphMouse[1]; - } - app.graph.add(node, { doProcessChange: true }); - app.canvas.selectNode(node); - success = true; - } - event.stopPropagation(); - } else if (modelType === 'embeddings') { - const [embeddingDirectory, embeddingFile] = SearchPath.split(path); - const selectedNodes = app.canvas.selected_nodes; - for (var i in selectedNodes) { - const selectedNode = selectedNodes[i]; - const nodeType = modelNodeType[modelType]; - const widgetIndex = ModelGrid.modelWidgetIndex(nodeType); - const target = selectedNode?.widgets[widgetIndex]?.element; - if (target && target.type === 'textarea') { - target.value = ModelGrid.insertEmbeddingIntoText( - target.value, - embeddingFile, - removeEmbeddingExtension, - ); - success = true; - } - } - if (!success) { - console.warn('Try selecting a node before adding the embedding.'); - } - event.stopPropagation(); - } - comfyButtonAlert( - event.target, - success, - 'mdi-check-bold', - 'mdi-close-thick', - ); - } - - static #getWidgetComboIndices(node, value) { - const widgetIndices = []; - node?.widgets?.forEach((widget, index) => { - if (widget.type === 'combo' && widget.options.values?.includes(value)) { - widgetIndices.push(index); - } - }); - return widgetIndices; - } - - /** - * @param {DragEvent} event - * @param {string} modelType - * @param {string} path - * @param {boolean} removeEmbeddingExtension - * @param {boolean} strictlyOnWidget - */ - static #dragAddModel( - event, - modelType, - path, - removeEmbeddingExtension, - strictlyOnWidget, - ) { - const [clientX, clientY, target] = elementFromDragEvent(event); - if (modelType !== 'embeddings' && target.id === 'graph-canvas') { - //const pos = app.canvas.convertEventToCanvasOffset(event); - const pos = app.canvas.convertEventToCanvasOffset({ - clientX: clientX, - clientY: clientY, - }); - - const node = app.graph.getNodeOnPos( - pos[0], - pos[1], - app.canvas.visible_nodes, - ); - - let widgetIndex = -1; - if (widgetIndex === -1) { - const widgetIndices = this.#getWidgetComboIndices(node, path); - if (widgetIndices.length === 0) { - widgetIndex = -1; - } else if (widgetIndices.length === 1) { - widgetIndex = widgetIndices[0]; - if (strictlyOnWidget) { - const draggedWidget = app.canvas.processNodeWidgets( - node, - pos, - event, - ); - const widget = node.widgets[widgetIndex]; - if (draggedWidget != widget) { - // != check NOT same object - widgetIndex = -1; - } - } - } else { - // ambiguous widget (strictlyOnWidget always true) - const draggedWidget = app.canvas.processNodeWidgets(node, pos, event); - widgetIndex = widgetIndices.findIndex((index) => { - return draggedWidget == node.widgets[index]; // == check same object - }); - } - } - - if (widgetIndex !== -1) { - node.widgets[widgetIndex].value = path; - app.canvas.selectNode(node); - } else { - const expectedNodeType = modelNodeType[modelType]; - const newNode = LiteGraph.createNode(expectedNodeType, null, []); - let newWidgetIndex = ModelGrid.modelWidgetIndex(expectedNodeType); - if (newWidgetIndex === -1) { - newWidgetIndex = this.#getWidgetComboIndices(newNode, path)[0] ?? -1; - } - if ( - newNode !== undefined && - newNode !== null && - newWidgetIndex !== -1 - ) { - newNode.pos[0] = pos[0]; - newNode.pos[1] = pos[1]; - newNode.widgets[newWidgetIndex].value = path; - app.graph.add(newNode, { doProcessChange: true }); - app.canvas.selectNode(newNode); - } - } - event.stopPropagation(); - } else if (modelType === 'embeddings' && target.type === 'textarea') { - const pos = app.canvas.convertEventToCanvasOffset(event); - const nodeAtPos = app.graph.getNodeOnPos( - pos[0], - pos[1], - app.canvas.visible_nodes, - ); - if (nodeAtPos) { - app.canvas.selectNode(nodeAtPos); - const [embeddingDirectory, embeddingFile] = SearchPath.split(path); - target.value = ModelGrid.insertEmbeddingIntoText( - target.value, - embeddingFile, - removeEmbeddingExtension, - ); - event.stopPropagation(); - } - } - } - - /** - * @param {Event} event - * @param {string} modelType - * @param {string} path - * @param {boolean} removeEmbeddingExtension - */ - static #copyModelToClipboard( - event, - modelType, - path, - removeEmbeddingExtension, - ) { - const nodeType = modelNodeType[modelType]; - let success = false; - if (nodeType === 'Embedding') { - if (navigator.clipboard) { - const [embeddingDirectory, embeddingFile] = SearchPath.split(path); - const embeddingText = ModelGrid.insertEmbeddingIntoText( - '', - embeddingFile, - removeEmbeddingExtension, - ); - navigator.clipboard.writeText(embeddingText); - success = true; - } else { - console.warn( - 'Cannot copy the embedding to the system clipboard; Try dragging it instead.', - ); - } - } else if (nodeType) { - const node = LiteGraph.createNode(nodeType, null, []); - const widgetIndex = ModelGrid.modelWidgetIndex(nodeType); - if (widgetIndex !== -1) { - node.widgets[widgetIndex].value = path; - app.canvas.copyToClipboard([node]); - success = true; - } - } else { - console.warn(`Unable to copy unknown model type '${modelType}.`); - } - comfyButtonAlert( - event.target, - success, - 'mdi-check-bold', - 'mdi-close-thick', - ); - } - - /** - * @param {Array} models - * @param {string} modelType - * @param {Object.} settingsElements - * @param {String} searchSeparator - * @param {String} systemSeparator - * @param {(searchPath: string) => Promise} showModelInfo - * @returns {HTMLElement[]} - */ - static #generateInnerHtml( - models, - modelType, - settingsElements, - searchSeparator, - systemSeparator, - showModelInfo, - ) { - // TODO: separate text and model logic; getting too messy - // TODO: fallback on button failure to copy text? - const canShowButtons = modelNodeType[modelType] !== undefined; - const showAddButton = - canShowButtons && settingsElements['model-show-add-button'].checked; - const showCopyButton = - canShowButtons && settingsElements['model-show-copy-button'].checked; - const showLoadWorkflowButton = - canShowButtons && - settingsElements['model-show-load-workflow-button'].checked; - const strictDragToAdd = - settingsElements['model-add-drag-strict-on-field'].checked; - const addOffset = parseInt(settingsElements['model-add-offset'].value); - const showModelExtension = - settingsElements['model-show-label-extensions'].checked; - const modelInfoButtonOnLeft = - !settingsElements['model-info-button-on-left'].checked; - const removeEmbeddingExtension = - !settingsElements['model-add-embedding-extension'].checked; - const previewThumbnailFormat = - settingsElements['model-preview-thumbnail-type'].value; - if (models.length > 0) { - return models.map((item) => { - const previewInfo = item.preview; - const previewThumbnail = $el('img.model-preview', { - loading: - 'lazy' /* `loading` BEFORE `src`; Known bug in Firefox 124.0.2 and Safari for iOS 17.4.1 (https://stackoverflow.com/a/76252772) */, - src: imageUri( - previewInfo?.path, - previewInfo?.dateModified, - PREVIEW_THUMBNAIL_WIDTH, - PREVIEW_THUMBNAIL_HEIGHT, - previewThumbnailFormat, - ), - draggable: false, - }); - const searchPath = item.path; - const path = SearchPath.systemPath( - searchPath, - searchSeparator, - systemSeparator, - ); - let actionButtons = []; - if ( - showAddButton && - !(modelType === 'embeddings' && !navigator.clipboard) - ) { - actionButtons.push( - new ComfyButton({ - icon: 'content-copy', - tooltip: 'Copy model to clipboard', - classList: 'comfyui-button icon-button model-button', - action: (e) => - ModelGrid.#copyModelToClipboard( - e, - modelType, - path, - removeEmbeddingExtension, - ), - }).element, - ); - } - if (showCopyButton) { - actionButtons.push( - new ComfyButton({ - icon: 'plus-box-outline', - tooltip: 'Add model to node grid', - classList: 'comfyui-button icon-button model-button', - action: (e) => - ModelGrid.#addModel( - e, - modelType, - path, - removeEmbeddingExtension, - addOffset, - ), - }).element, - ); - } - if (showLoadWorkflowButton) { - actionButtons.push( - new ComfyButton({ - icon: 'arrow-bottom-left-bold-box-outline', - tooltip: 'Load preview workflow', - classList: 'comfyui-button icon-button model-button', - action: async (e) => { - const urlString = previewThumbnail.src; - const url = new URL(urlString); - const urlSearchParams = url.searchParams; - const uri = urlSearchParams.get('uri'); - const v = urlSearchParams.get('v'); - const urlFull = - urlString.substring(0, urlString.indexOf('?')) + - '?uri=' + - uri + - '&v=' + - v; - await loadWorkflow(urlFull); - }, - }).element, - ); - } - const infoButtons = [ - new ComfyButton({ - icon: 'information-outline', - tooltip: 'View model information', - classList: 'comfyui-button icon-button model-button', - action: async () => { - await showModelInfo(searchPath); - }, - }).element, - ]; - const dragAdd = (e) => - ModelGrid.#dragAddModel( - e, - modelType, - path, - removeEmbeddingExtension, - strictDragToAdd, - ); - return $el('div.item', {}, [ - previewThumbnail, - $el('div.model-preview-overlay', { - ondragend: (e) => dragAdd(e), - draggable: true, - }), - $el( - 'div.model-preview-top-right', - { - draggable: false, - }, - modelInfoButtonOnLeft ? infoButtons : actionButtons, - ), - $el( - 'div.model-preview-top-left', - { - draggable: false, - }, - modelInfoButtonOnLeft ? actionButtons : infoButtons, - ), - $el( - 'div.model-label', - { - draggable: false, - }, - [ - $el('p', [ - showModelExtension - ? item.name - : SearchPath.splitExtension(item.name)[0], - ]), - ], - ), - ]); - }); - } else { - return [$el('h2', ['No Models'])]; - } - } - - /** - * @param {HTMLDivElement} modelGrid - * @param {ModelData} modelData - * @param {HTMLSelectElement} modelSelect - * @param {Object.<{value: string}>} previousModelType - * @param {Object} settings - * @param {string} sortBy - * @param {boolean} reverseSort - * @param {Array} previousModelFilters - * @param {HTMLInputElement} modelFilter - * @param {(searchPath: string) => Promise} showModelInfo - */ - static update( - modelGrid, - modelData, - modelSelect, - previousModelType, - settings, - sortBy, - reverseSort, - previousModelFilters, - modelFilter, - showModelInfo, - ) { - const models = modelData.models; - let modelType = modelSelect.value; - if (models[modelType] === undefined) { - modelType = settings['model-default-browser-model-type'].value; - } - if (models[modelType] === undefined) { - modelType = 'checkpoints'; // panic fallback - } - - if (modelType !== previousModelType.value) { - if (settings['model-persistent-search'].checked) { - previousModelFilters.splice(0, previousModelFilters.length); // TODO: make sure this actually worked! - } else { - // cache previous filter text - previousModelFilters[previousModelType.value] = modelFilter.value; - // read cached filter text - modelFilter.value = previousModelFilters[modelType] ?? ''; - } - previousModelType.value = modelType; - } - - let modelTypeOptions = []; - for (const [key, value] of Object.entries(models)) { - const el = $el('option', [key]); - modelTypeOptions.push(el); - } - modelSelect.innerHTML = ''; - modelTypeOptions.forEach((option) => modelSelect.add(option)); - modelSelect.value = modelType; - - const searchAppend = settings['model-search-always-append'].value; - const searchText = modelFilter.value + ' ' + searchAppend; - const modelList = ModelGrid.#filter(models[modelType], searchText); - ModelGrid.#sort(modelList, sortBy, reverseSort); - - modelGrid.innerHTML = ''; - const modelGridModels = ModelGrid.#generateInnerHtml( - modelList, - modelType, - settings, - modelData.searchSeparator, - modelData.systemSeparator, - showModelInfo, - ); - modelGrid.append.apply(modelGrid, modelGridModels); - } -} - -class ModelInfo { - /** @type {HTMLDivElement} */ - element = null; - - elements = { - /** @type {HTMLDivElement[]} */ tabButtons: null, - /** @type {HTMLDivElement[]} */ tabContents: null, - /** @type {HTMLDivElement} */ info: null, - /** @type {HTMLTextAreaElement} */ notes: null, - /** @type {HTMLButtonElement} */ setPreviewButton: null, - /** @type {HTMLInputElement} */ moveDestinationInput: null, - }; - - /** @type {ImageSelect} */ - previewSelect = null; - - /** @type {string} */ - #savedNotesValue = null; - - /** @type {[HTMLElement][]} */ - #settingsElements = null; - - /** - * @param {ModelData} modelData - * @param {() => Promise} updateModels - * @param {any} settingsElements - */ - constructor(modelData, updateModels, settingsElements) { - this.#settingsElements = settingsElements; - const moveDestinationInput = $el('input.search-text-area', { - name: 'move directory', - autocomplete: 'off', - placeholder: modelData.searchSeparator, - value: modelData.searchSeparator, - }); - this.elements.moveDestinationInput = moveDestinationInput; - - const searchDropdown = new DirectoryDropdown( - modelData, - moveDestinationInput, - true, - ); - - const previewSelect = new ImageSelect('model-info-preview-model-FYUIKMNVB'); - this.previewSelect = previewSelect; - previewSelect.elements.previews.style.display = 'flex'; - - const setPreviewButton = new ComfyButton({ - tooltip: 'Overwrite current preview with selected image', - content: 'Set as Preview', - action: async (e) => { - const [button, icon, span] = comfyButtonDisambiguate(e.target); - button.disabled = true; - const confirmation = window.confirm( - 'Change preview image(s) PERMANENTLY?', - ); - let updatedPreview = false; - if (confirmation) { - const container = this.elements.info; - const path = container.dataset.path; - const imageUrl = await previewSelect.getImage(); - if (imageUrl === PREVIEW_NONE_URI) { - const encodedPath = encodeURIComponent(path); - updatedPreview = await comfyRequest( - `/model-manager/preview/delete?path=${encodedPath}`, - { - method: 'POST', - body: JSON.stringify({}), - }, - ) - .then((result) => { - const message = result['alert']; - if (message !== undefined) { - window.alert(message); - } - return result['success']; - }) - .catch((err) => { - return false; - }); - } else { - const formData = new FormData(); - formData.append('path', path); - const image = imageUrl[0] == '/' ? '' : imageUrl; - formData.append('image', image); - updatedPreview = await comfyRequest(`/model-manager/preview/set`, { - method: 'POST', - body: formData, - }) - .then((result) => { - const message = result['alert']; - if (message !== undefined) { - window.alert(message); - } - return result['success']; - }) - .catch((err) => { - return false; - }); - } - if (updatedPreview) { - updateModels(); - const previewSelect = this.previewSelect; - previewSelect.elements.defaultUrl.dataset.noimage = - PREVIEW_NONE_URI; - previewSelect.resetModelInfoPreview(); - this.element.style.display = 'none'; - } - } - comfyButtonAlert(e.target, updatedPreview); - button.disabled = false; - }, - }).element; - this.elements.setPreviewButton = setPreviewButton; - previewSelect.elements.radioButtons.addEventListener('change', (e) => { - setPreviewButton.style.display = previewSelect.defaultIsChecked() - ? 'none' - : 'block'; - }); - - this.element = $el( - 'div', - { - style: { display: 'none' }, - }, - [ - $el( - 'div.row.tab-header', - { - display: 'block', - }, - [ - $el('div.row.tab-header-flex-block', [ - new ComfyButton({ - icon: 'trash-can-outline', - tooltip: 'Delete model FOREVER', - classList: 'comfyui-button icon-button', - action: async (e) => { - const [button, icon, span] = comfyButtonDisambiguate( - e.target, - ); - button.disabled = true; - const affirmation = 'delete'; - const confirmation = window.prompt( - 'Type "' + - affirmation + - '" to delete the model PERMANENTLY.\n\nThis includes all image or text files.', - ); - let deleted = false; - if (confirmation === affirmation) { - const container = this.elements.info; - const path = encodeURIComponent(container.dataset.path); - deleted = await comfyRequest( - `/model-manager/model/delete?path=${path}`, - { - method: 'POST', - }, - ) - .then((result) => { - const deleted = result['success']; - const message = result['alert']; - if (message !== undefined) { - window.alert(message); - } - if (deleted) { - container.innerHTML = ''; - this.element.style.display = 'none'; - updateModels(); - } - return deleted; - }) - .catch((err) => { - return false; - }); - } - if (!deleted) { - comfyButtonAlert(e.target, false); - } - button.disabled = false; - }, - }).element, - $el('div.search-models.input-dropdown-container', [ - // TODO: magic class - moveDestinationInput, - searchDropdown.element, - ]), - new ComfyButton({ - icon: 'file-move-outline', - tooltip: 'Move file', - action: async (e) => { - const [button, icon, span] = comfyButtonDisambiguate( - e.target, - ); - button.disabled = true; - const confirmation = window.confirm('Move this file?'); - let moved = false; - if (confirmation) { - const container = this.elements.info; - const oldFile = container.dataset.path; - const [oldFilePath, oldFileName] = - SearchPath.split(oldFile); - const newFile = - moveDestinationInput.value + - modelData.searchSeparator + - oldFileName; - moved = await comfyRequest(`/model-manager/model/move`, { - method: 'POST', - body: JSON.stringify({ - oldFile: oldFile, - newFile: newFile, - }), - }) - .then((result) => { - const moved = result['success']; - const message = result['alert']; - if (message !== undefined) { - window.alert(message); - } - if (moved) { - moveDestinationInput.value = ''; - container.innerHTML = ''; - this.element.style.display = 'none'; - updateModels(); - } - return moved; - }) - .catch((err) => { - return false; - }); - } - comfyButtonAlert(e.target, moved); - button.disabled = false; - }, - }).element, - ]), - ], - ), - $el('div.model-info-container', { - $: (el) => (this.elements.info = el), - 'data-path': '', - }), - ], - ); - - [this.elements.tabButtons, this.elements.tabContents] = GenerateTabGroup([ - { - name: 'Overview', - icon: 'information-box-outline', - tabContent: this.element, - }, - { - name: 'Metadata', - icon: 'file-document-outline', - tabContent: $el('div', ['Metadata']), - }, - { - name: 'Tags', - icon: 'tag-outline', - tabContent: $el('div', ['Tags']), - }, - { - name: 'Notes', - icon: 'pencil-outline', - tabContent: $el('div', ['Notes']), - }, - ]); - } - - /** @returns {void} */ - show() { - this.element.style = ''; - this.element.scrollTop = 0; - } - - /** - * @param {boolean} promptUser - * @returns {Promise} - */ - async trySave(promptUser) { - if (this.element.style.display === 'none') { - return true; - } - - const noteValue = this.elements.notes.value; - const savedNotesValue = this.#savedNotesValue; - if (noteValue.trim() === savedNotesValue.trim()) { - return true; - } - const saveChanges = !promptUser || window.confirm('Save notes?'); - if (saveChanges) { - const path = this.elements.info.dataset.path; - const saved = await saveNotes(path, noteValue); - if (!saved) { - window.alert('Failed to save notes!'); - return false; - } - this.#savedNotesValue = noteValue; - this.elements.markdown.innerHTML = marked.parse(noteValue); - } else { - const discardChanges = window.confirm('Discard changes?'); - if (!discardChanges) { - return false; - } else { - this.elements.notes.value = savedNotesValue; - } - } - return true; - } - - /** - * @param {boolean?} promptSave - * @returns {Promise} - */ - async tryHide(promptSave = true) { - const notes = this.elements.notes; - if (promptSave && notes !== undefined && notes !== null) { - const saved = await this.trySave(promptSave); - if (!saved) { - return false; - } - this.#savedNotesValue = ''; - this.elements.notes.value = ''; - } - this.element.style.display = 'none'; - return true; - } - - /** - * @param {string} searchPath - * @param {() => Promise} updateModels - * @param {string} searchSeparator - */ - async update(searchPath, updateModels, searchSeparator) { - const path = encodeURIComponent(searchPath); - const [info, metadata, tags, noteText] = await comfyRequest( - `/model-manager/model/info?path=${path}`, - ) - .then((result) => { - const success = result['success']; - const message = result['alert']; - if (message !== undefined) { - window.alert(message); - } - if (!success) { - return undefined; - } - return [ - result['info'], - result['metadata'], - result['tags'], - result['notes'], - ]; - }) - .catch((err) => { - console.log(err); - return undefined; - }); - if (info === undefined || info === null) { - return; - } - const infoHtml = this.elements.info; - infoHtml.innerHTML = ''; - infoHtml.dataset.path = searchPath; - const innerHtml = []; - const filename = info['File Name']; - if (filename !== undefined && filename !== null && filename !== '') { - innerHtml.push( - $el( - 'div.row', - { - style: { margin: '8px 0 16px 0' }, - }, - [ - $el( - 'h1', - { - style: { margin: '0' }, - }, - [filename], - ), - $el('div', [ - new ComfyButton({ - icon: 'pencil', - tooltip: 'Change file name', - classList: 'comfyui-button icon-button', - action: async (e) => { - const [button, icon, span] = comfyButtonDisambiguate( - e.target, - ); - button.disabled = true; - const container = this.elements.info; - const oldFile = container.dataset.path; - const [oldFilePath, oldFileName] = SearchPath.split(oldFile); - const oldName = SearchPath.splitExtension(oldFileName)[0]; - const newName = window.prompt('New model name:', oldName); - let renamed = false; - if ( - newName !== null && - newName !== '' && - newName != oldName - ) { - const newFile = - oldFilePath + - searchSeparator + - newName + - SearchPath.splitExtension(oldFile)[1]; - renamed = await comfyRequest(`/model-manager/model/move`, { - method: 'POST', - body: JSON.stringify({ - oldFile: oldFile, - newFile: newFile, - }), - }) - .then((result) => { - const renamed = result['success']; - const message = result['alert']; - if (message !== undefined) { - window.alert(message); - } - if (renamed) { - container.innerHTML = ''; - this.element.style.display = 'none'; - updateModels(); - } - return renamed; - }) - .catch((err) => { - console.log(err); - return false; - }); - } - comfyButtonAlert(e.target, renamed); - button.disabled = false; - }, - }).element, - ]), - ], - ), - ); - } - - const fileDirectory = info['File Directory']; - if ( - fileDirectory !== undefined && - fileDirectory !== null && - fileDirectory !== '' - ) { - this.elements.moveDestinationInput.placeholder = fileDirectory; - this.elements.moveDestinationInput.value = fileDirectory; // TODO: noise vs convenience - } else { - this.elements.moveDestinationInput.placeholder = searchSeparator; - this.elements.moveDestinationInput.value = searchSeparator; - } - - const previewSelect = this.previewSelect; - const defaultUrl = previewSelect.elements.defaultUrl; - if (info['Preview']) { - const imagePath = info['Preview']['path']; - const imageDateModified = info['Preview']['dateModified']; - defaultUrl.dataset.noimage = imageUri(imagePath, imageDateModified); - } else { - defaultUrl.dataset.noimage = PREVIEW_NONE_URI; - } - previewSelect.resetModelInfoPreview(); - const setPreviewButton = this.elements.setPreviewButton; - setPreviewButton.style.display = previewSelect.defaultIsChecked() - ? 'none' - : 'block'; - - innerHtml.push( - $el('div', [ - previewSelect.elements.previews, - $el('div.row.tab-header', [ - $el('div', [ - new ComfyButton({ - content: 'Load Workflow', - tooltip: 'Attempt to load preview image workflow', - action: async () => { - const urlString = - previewSelect.elements.defaultPreviews.children[0].src; - await loadWorkflow(urlString); - }, - }).element, - ]), - $el('div.row.tab-header-flex-block', [ - previewSelect.elements.radioGroup, - ]), - $el('div.row.tab-header-flex-block', [setPreviewButton]), - ]), - $el('h2', ['File Info:']), - $el( - 'div', - (() => { - const elements = []; - for (const [key, value] of Object.entries(info)) { - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - // currently only used for "Bucket Resolutions" - if (value.length > 0) { - elements.push($el('h2', [key + ':'])); - const text = TagCountMapToParagraph(value); - const div = $el('div'); - div.innerHTML = text; - elements.push(div); - } - } else { - if (key === 'Description') { - if (value !== '') { - elements.push($el('h2', [key + ':'])); - elements.push($el('p', [value])); - } - } else if (key === 'Preview') { - // - } else { - if (value !== '') { - elements.push($el('p', [key + ': ' + value])); - } - } - } - } - return elements; - })(), - ), - ]), - ); - infoHtml.append.apply(infoHtml, innerHtml); - // TODO: set default value of dropdown and value to model type? - - /** @type {HTMLDivElement} */ - const metadataElement = this.elements.tabContents[1]; // TODO: remove magic value - const isMetadata = - typeof metadata === 'object' && - metadata !== null && - Object.keys(metadata).length > 0; - metadataElement.innerHTML = ''; - metadataElement.append.apply(metadataElement, [ - $el('h1', ['Metadata']), - $el( - 'div', - (() => { - const tableRows = []; - if (isMetadata) { - for (const [key, value] of Object.entries(metadata)) { - if (value === undefined || value === null) { - continue; - } - if (value !== '') { - tableRows.push( - $el('tr', [ - $el('th.model-metadata-key', [key]), - $el('th.model-metadata-value', [value]), - ]), - ); - } - } - } - return $el('table.model-metadata', tableRows); - })(), - ), - ]); - const metadataButton = this.elements.tabButtons[1]; // TODO: remove magic value - metadataButton.style.display = isMetadata ? '' : 'none'; - - /** @type {HTMLDivElement} */ - const tagsElement = this.elements.tabContents[2]; // TODO: remove magic value - const isTags = Array.isArray(tags) && tags.length > 0; - const tagsParagraph = $el( - 'div', - (() => { - const elements = []; - if (isTags) { - let text = TagCountMapToParagraph(tags); - const div = $el('div'); - div.innerHTML = text; - elements.push(div); - } - return elements; - })(), - ); - const tagGeneratorRandomizedOutput = $el('textarea.comfy-multiline-input', { - name: 'random tag generator output', - rows: 4, - }); - const TAG_GENERATOR_SAMPLER_NAME = 'model manager tag generator sampler'; - const tagGenerationCount = $el('input', { - type: 'number', - name: 'tag generator count', - step: 1, - min: 1, - value: this.#settingsElements['tag-generator-count'].value, - }); - const tagGenerationThreshold = $el('input', { - type: 'number', - name: 'tag generator threshold', - step: 1, - min: 1, - value: this.#settingsElements['tag-generator-threshold'].value, - }); - const selectedSamplerOption = - this.#settingsElements['tag-generator-sampler-method'].value; - const samplerOptions = ['Frequency', 'Uniform']; - const samplerRadioGroup = $radioGroup({ - name: TAG_GENERATOR_SAMPLER_NAME, - onchange: (value) => {}, - options: samplerOptions.map((option) => { - return { value: option }; - }), - }); - const samplerOptionInputs = samplerRadioGroup.getElementsByTagName('input'); - for (let i = 0; i < samplerOptionInputs.length; i++) { - const samplerOptionInput = samplerOptionInputs[i]; - if (samplerOptionInput.value === selectedSamplerOption) { - samplerOptionInput.click(); - break; - } - } - const tagGenerator = $el('div', [ - $el('h1', ['Tags']), - $el('h2', { style: { margin: '0px 0px 16px 0px' } }, [ - 'Random Tag Generator', - ]), - $el('div', [ - $el( - 'details.tag-generator-settings', - { - style: { margin: '10px 0', display: 'none' }, - open: false, - }, - [ - $el('summary', ['Settings']), - $el('div', ['Sampling Method', samplerRadioGroup]), - $el('label', ['Count', tagGenerationCount]), - $el('label', ['Threshold', tagGenerationThreshold]), - ], - ), - tagGeneratorRandomizedOutput, - new ComfyButton({ - content: 'Randomize', - tooltip: 'Randomly generate subset of tags', - action: () => { - const samplerName = document.querySelector( - `input[name="${TAG_GENERATOR_SAMPLER_NAME}"]:checked`, - ).value; - const sampler = - samplerName === 'Frequency' - ? ModelInfo.ProbabilisticTagSampling - : ModelInfo.UniformTagSampling; - const sampleCount = tagGenerationCount.value; - const frequencyThreshold = tagGenerationThreshold.value; - const tags = ParseTagParagraph(tagsParagraph.innerText); - const sampledTags = sampler(tags, sampleCount, frequencyThreshold); - tagGeneratorRandomizedOutput.value = sampledTags.join(', '); - }, - }).element, - ]), - ]); - tagsElement.innerHTML = ''; - tagsElement.append.apply(tagsElement, [ - tagGenerator, - $el('div', [ - $el( - 'h2', - { - style: { - margin: '24px 0px 8px 0px', - }, - }, - ['Tags'], - ), - tagsParagraph, - ]), - ]); - const tagButton = this.elements.tabButtons[2]; // TODO: remove magic value - tagButton.style.display = isTags ? '' : 'none'; - - const saveIcon = 'content-save'; - const savingIcon = 'cloud-upload-outline'; - - const saveNotesButton = new ComfyButton({ - icon: saveIcon, - tooltip: 'Save note', - classList: 'comfyui-button icon-button', - action: async (e) => { - const [button, icon, span] = comfyButtonDisambiguate(e.target); - button.disabled = true; - const saved = await this.trySave(false); - comfyButtonAlert(e.target, saved); - button.disabled = false; - }, - }).element; - - const saveDebounce = debounce(async () => { - const saveIconClass = 'mdi-' + saveIcon; - const savingIconClass = 'mdi-' + savingIcon; - const iconElement = saveNotesButton.getElementsByTagName('i')[0]; - iconElement.classList.remove(saveIconClass); - iconElement.classList.add(savingIconClass); - const saved = await this.trySave(false); - iconElement.classList.remove(savingIconClass); - iconElement.classList.add(saveIconClass); - }, 1000); - - /** @type {HTMLDivElement} */ - const notesElement = this.elements.tabContents[3]; // TODO: remove magic value - notesElement.innerHTML = ''; - const markdown = $el('div', {}, ''); - markdown.innerHTML = marked.parse(noteText); - - notesElement.append.apply( - notesElement, - (() => { - const notes = $el('textarea.comfy-multiline-input', { - name: 'model notes', - value: noteText, - oninput: (e) => { - if (this.#settingsElements['model-info-autosave-notes'].checked) { - saveDebounce(); - } - }, - }); - - if (navigator.userAgent.includes('Mac')) { - new KeyComboListener(['MetaLeft', 'KeyS'], saveDebounce, notes); - new KeyComboListener(['MetaRight', 'KeyS'], saveDebounce, notes); - } else { - new KeyComboListener(['ControlLeft', 'KeyS'], saveDebounce, notes); - new KeyComboListener(['ControlRight', 'KeyS'], saveDebounce, notes); - } - - this.elements.notes = notes; - this.elements.markdown = markdown; - this.#savedNotesValue = noteText; - - const notes_editor = $el( - 'div', - { - style: { - display: noteText == '' ? 'flex' : 'none', - height: '100%', - 'min-height': '60px', - }, - }, - notes, - ); - const notes_viewer = $el( - 'div', - { - style: { - display: noteText == '' ? 'none' : 'flex', - height: '100%', - 'min-height': '60px', - overflow: 'scroll', - 'overflow-wrap': 'anywhere', - }, - }, - markdown, - ); - - const editNotesButton = new ComfyButton({ - icon: 'pencil', - tooltip: 'Change file name', - classList: 'comfyui-button icon-button', - action: async () => { - notes_editor.style.display = - notes_editor.style.display == 'flex' ? 'none' : 'flex'; - notes_viewer.style.display = - notes_viewer.style.display == 'none' ? 'flex' : 'none'; - }, - }).element; - - return [ - $el( - 'div.row', - { - style: { 'align-items': 'center' }, - }, - [$el('h1', ['Notes']), saveNotesButton, editNotesButton], - ), - notes_editor, - notes_viewer, - ]; - })(), - ); - } - - static UniformTagSampling( - tagsAndCounts, - sampleCount, - frequencyThreshold = 0, - ) { - const data = tagsAndCounts.filter((x) => x[1] >= frequencyThreshold); - let count = data.length; - const samples = []; - for (let i = 0; i < sampleCount; i++) { - if (count === 0) { - break; - } - const index = Math.floor(Math.random() * count); - const pair = data.splice(index, 1)[0]; - samples.push(pair); - count -= 1; - } - const sortedSamples = samples.sort((x1, x2) => { - return parseInt(x2[1]) - parseInt(x1[1]); - }); - return sortedSamples.map((x) => x[0]); - } - - static ProbabilisticTagSampling( - tagsAndCounts, - sampleCount, - frequencyThreshold = 0, - ) { - const data = tagsAndCounts.filter((x) => x[1] >= frequencyThreshold); - let tagFrequenciesSum = data.reduce( - (accumulator, x) => accumulator + x[1], - 0, - ); - let count = data.length; - const samples = []; - for (let i = 0; i < sampleCount; i++) { - if (count === 0) { - break; - } - const index = (() => { - let frequencyIndex = Math.floor(Math.random() * tagFrequenciesSum); - return data.findIndex((x) => { - const frequency = x[1]; - if (frequency > frequencyIndex) { - return true; - } - frequencyIndex = frequencyIndex - frequency; - return false; - }); - })(); - const pair = data.splice(index, 1)[0]; - samples.push(pair); - tagFrequenciesSum -= pair[1]; - count -= 1; - } - const sortedSamples = samples.sort((x1, x2) => { - return parseInt(x2[1]) - parseInt(x1[1]); - }); - return sortedSamples.map((x) => x[0]); - } -} - -class Civitai { - /** - * Get model info from Civitai. - * - * @param {string} id - Model ID. - * @param {string} apiPath - Civitai request subdirectory. "models" for 'model' urls. "model-version" for 'api' urls. - * - * @returns {Promise} Dictionary containing received model info. Returns an empty if fails. - */ - static async requestInfo(id, apiPath) { - const url = 'https://civitai.com/api/v1/' + apiPath + '/' + id; - try { - const response = await fetch(url); - const data = await response.json(); - return data; - } catch (error) { - console.error('Failed to get model info from Civitai!', error); - return {}; - } - } - - /** - * Extract file information from the given model version information. - * - * @param {Object} modelVersionInfo - Model version information. - * @param {(string|null)} [type=null] - Optional select by model type. - * @param {(string|null)} [fp=null] - Optional select by floating point quantization. - * @param {(string|null)} [size=null] - Optional select by sizing. - * @param {(string|null)} [format=null] - Optional select by file format. - * - * @returns {Object} - Extracted list of information on each file of the given model version. - */ - static getModelFilesInfo( - modelVersionInfo, - type = null, - fp = null, - size = null, - format = null, - ) { - const files = []; - const modelVersionFiles = modelVersionInfo['files']; - for (let i = 0; i < modelVersionFiles.length; i++) { - const modelVersionFile = modelVersionFiles[i]; - - const fileType = modelVersionFile['type']; - if (type instanceof String && type != fileType) { - continue; - } - - const fileMeta = modelVersionFile['metadata']; - - const fileFp = fileMeta['fp']; - if (fp instanceof String && fp != fileFp) { - continue; - } - - const fileSize = fileMeta['size']; - if (size instanceof String && size != fileSize) { - continue; - } - - const fileFormat = fileMeta['format']; - if (format instanceof String && format != fileFormat) { - continue; - } - - files.push({ - downloadUrl: modelVersionFile['downloadUrl'], - format: fileFormat, - fp: fileFp, - hashes: modelVersionFile['hashes'], - name: modelVersionFile['name'], - size: fileSize, - sizeKB: modelVersionFile['sizeKB'], - type: fileType, - }); - } - return { - files: files, - id: modelVersionInfo['id'], - images: modelVersionInfo['images'].map((image) => { - // TODO: do I need to double-check image matches resource? - return image['url']; - }), - name: modelVersionInfo['name'], - description: modelVersionInfo['description'], - tags: modelVersionInfo['trainedWords'], - }; - } - - /** - * @param {string} stringUrl - Model url. - * - * @returns {Promise} - Download information for the given url. - */ - static async getFilteredInfo(stringUrl) { - const url = new URL(stringUrl); - if (url.hostname != 'civitai.com') { - return {}; - } - if (url.pathname == '/') { - return {}; - } - const urlPath = url.pathname; - if (urlPath.startsWith('/api')) { - const idEnd = urlPath.length - (urlPath.at(-1) == '/' ? 1 : 0); - const idStart = urlPath.lastIndexOf('/', idEnd - 1) + 1; - const modelVersionId = urlPath.substring(idStart, idEnd); - if (parseInt(modelVersionId, 10) == NaN) { - return {}; - } - const modelVersionInfo = await Civitai.requestInfo( - modelVersionId, - 'model-versions', - ); - if (Object.keys(modelVersionInfo).length == 0) { - return {}; - } - const searchParams = url.searchParams; - const filesInfo = Civitai.getModelFilesInfo( - modelVersionInfo, - searchParams.get('type'), - searchParams.get('fp'), - searchParams.get('size'), - searchParams.get('format'), - ); - return { - name: modelVersionInfo['model']['name'], - type: modelVersionInfo['model']['type'], - description: modelVersionInfo['description'], - tags: modelVersionInfo['trainedWords'], - versions: [filesInfo], - }; - } else if (urlPath.startsWith('/models')) { - const idStart = urlPath.indexOf('models/') + 'models/'.length; - const idEnd = (() => { - const idEnd = urlPath.indexOf('/', idStart); - return idEnd === -1 ? urlPath.length : idEnd; - })(); - const modelId = urlPath.substring(idStart, idEnd); - if (parseInt(modelId, 10) == NaN) { - return {}; - } - const modelInfo = await Civitai.requestInfo(modelId, 'models'); - if (Object.keys(modelInfo).length == 0) { - return {}; - } - const modelVersionId = parseInt(url.searchParams.get('modelVersionId')); - const modelVersions = []; - const modelVersionInfos = modelInfo['modelVersions']; - for (let i = 0; i < modelVersionInfos.length; i++) { - const versionInfo = modelVersionInfos[i]; - if (!Number.isNaN(modelVersionId)) { - if (modelVersionId != versionInfo['id']) { - continue; - } - } - const filesInfo = Civitai.getModelFilesInfo(versionInfo); - modelVersions.push(filesInfo); - } - return { - name: modelInfo['name'], - type: modelInfo['type'], - description: modelInfo['description'], - versions: modelVersions, - }; - } else { - return {}; - } - } - - /** - * @returns {string} - */ - static imagePostUrlPrefix() { - return 'https://civitai.com/images/'; - } - - /** - * @returns {string} - */ - static imageUrlPrefix() { - return 'https://image.civitai.com/'; - } - - /** - * @param {string} stringUrl - https://civitai.com/images/{imageId}. - * - * @returns {Promise} - Image information. - */ - static async getImageInfo(stringUrl) { - const imagePostUrlPrefix = Civitai.imagePostUrlPrefix(); - if (!stringUrl.startsWith(imagePostUrlPrefix)) { - return {}; - } - const id = stringUrl.substring(imagePostUrlPrefix.length).match(/^\d+/)[0]; - const url = `https://civitai.com/api/v1/images?imageId=${id}`; - try { - const response = await fetch(url); - const data = await response.json(); - return data; - } catch (error) { - console.error('Failed to get image info from Civitai!', error); - return {}; - } - } - - /** - * @param {string} stringUrl - https://image.civitai.com/... - * - * @returns {Promise} - */ - static async getFullSizeImageUrl(stringUrl) { - const imageUrlPrefix = Civitai.imageUrlPrefix(); - if (!stringUrl.startsWith(imageUrlPrefix)) { - return ''; - } - const i0 = stringUrl.lastIndexOf('/'); - const i1 = stringUrl.lastIndexOf('.'); - if (i0 === -1 || i1 === -1) { - return ''; - } - const id = parseInt(stringUrl.substring(i0 + 1, i1)).toString(); - const url = `https://civitai.com/api/v1/images?imageId=${id}`; - try { - const response = await fetch(url); - const imageInfo = await response.json(); - const items = imageInfo['items']; - if (items.length === 0) { - console.warn('Civitai /api/v1/images returned 0 items.'); - return stringUrl; - } - return items[0]['url']; - } catch (error) { - console.error('Failed to get image info from Civitai!', error); - return stringUrl; - } - } -} - -class HuggingFace { - /** - * Get model info from Huggingface. - * - * @param {string} id - Model ID. - * @param {string} apiPath - API path. - * - * @returns {Promise} Dictionary containing received model info. Returns an empty if fails. - */ - static async requestInfo(id, apiPath = 'models') { - const url = 'https://huggingface.co/api/' + apiPath + '/' + id; - try { - const response = await fetch(url); - const data = await response.json(); - return data; - } catch (error) { - console.error('Failed to get model info from HuggingFace!', error); - return {}; - } - } - - /** - * - * - * @param {string} stringUrl - Model url. - * - * @returns {Promise} - */ - static async getFilteredInfo(stringUrl) { - const url = new URL(stringUrl); - if (url.hostname != 'huggingface.co') { - return {}; - } - if (url.pathname == '/') { - return {}; - } - const urlPath = url.pathname; - const i0 = 1; - const i1 = urlPath.indexOf('/', i0); - if (i1 == -1 || urlPath.length - 1 == i1) { - // user-name only - return {}; - } - let i2 = urlPath.indexOf('/', i1 + 1); - if (i2 == -1) { - // model id only - i2 = urlPath.length; - } - const modelId = urlPath.substring(i0, i2); - const urlPathEnd = urlPath.substring(i2); - - const isValidBranch = - urlPathEnd.startsWith('/resolve') || - urlPathEnd.startsWith('/blob') || - urlPathEnd.startsWith('/tree'); - - let branch = '/main'; - let filePath = ''; - if (isValidBranch) { - const i0 = branch.length; - const i1 = urlPathEnd.indexOf('/', i0 + 1); - if (i1 == -1) { - if (i0 != urlPathEnd.length) { - // ends with branch - branch = urlPathEnd.substring(i0); - } - } else { - branch = urlPathEnd.substring(i0, i1); - if (urlPathEnd.length - 1 > i1) { - filePath = urlPathEnd.substring(i1); - } - } - } - - const modelInfo = await HuggingFace.requestInfo(modelId); - //const modelInfo = await requestInfo(modelId + "/tree" + branch); // this only gives you the files at the given branch path... - // oid: SHA-1?, lfs.oid: SHA-256 - - const clippedFilePath = filePath.substring(filePath[0] === '/' ? 1 : 0); - const modelFiles = modelInfo['siblings'] - .filter((sib) => { - const filename = sib['rfilename']; - for (let i = 0; i < MODEL_EXTENSIONS.length; i++) { - if (filename.endsWith(MODEL_EXTENSIONS[i])) { - return filename.startsWith(clippedFilePath); - } - } - return false; - }) - .map((sib) => { - const filename = sib['rfilename']; - return filename; - }); - if (modelFiles.length === 0) { - return {}; - } - - const baseDownloadUrl = - url.origin + urlPath.substring(0, i2) + '/resolve' + branch; - - const images = modelInfo['siblings'] - .filter((sib) => { - const filename = sib['rfilename']; - for (let i = 0; i < IMAGE_EXTENSIONS.length; i++) { - if (filename.endsWith(IMAGE_EXTENSIONS[i])) { - return filename.startsWith(clippedFilePath); - } - } - return false; - }) - .map((sib) => { - return baseDownloadUrl + '/' + sib['rfilename']; - }); - - return { - baseDownloadUrl: baseDownloadUrl, - modelFiles: modelFiles, - images: images, - name: modelId, - }; - } -} - -/** - * @param {string} urlText - * @returns {Promise<[string, any[]]>} [name, modelInfos] - */ -async function getModelInfos(urlText) { - // TODO: class for proper return type - return await (async () => { - if (urlText.startsWith('https://civitai.com')) { - const civitaiInfo = await Civitai.getFilteredInfo(urlText); - if (Object.keys(civitaiInfo).length === 0) { - return ['', []]; - } - const name = civitaiInfo['name']; - const infos = []; - const type = civitaiInfo['type']; - - civitaiInfo['versions'].forEach((version) => { - const images = version['images']; - const tags = version['tags']?.map((tag) => - tag.trim().replace(/,$/, ''), - ); - const description = [ - tags !== undefined ? '# Trigger Words' : undefined, - tags?.join( - tags.some((tag) => { - return tag.includes(','); - }) - ? '\n' - : ', ', - ), - version['description'] !== undefined - ? '# About this version ' - : undefined, - version['description'], - civitaiInfo['description'] !== undefined ? '# ' + name : undefined, - civitaiInfo['description'], - ] - .filter((x) => x !== undefined) - .join('\n\n'); - version['files'].forEach((file) => { - infos.push({ - images: images, - fileName: file['name'], - modelType: type, - downloadUrl: file['downloadUrl'], - downloadFilePath: '', - description: downshow(description), - details: { - fileSizeKB: file['sizeKB'], - fileType: file['type'], - fp: file['fp'], - quant: file['size'], - fileFormat: file['format'], - }, - }); - }); - }); - return [name, infos]; - } - if (urlText.startsWith('https://huggingface.co')) { - const hfInfo = await HuggingFace.getFilteredInfo(urlText); - if (Object.keys(hfInfo).length === 0) { - return ['', []]; - } - const files = hfInfo['modelFiles']; - if (files.length === 0) { - return ['', []]; - } - const name = hfInfo['name']; - const baseDownloadUrl = hfInfo['baseDownloadUrl']; - const infos = hfInfo['modelFiles'].map((file) => { - const indexSep = file.lastIndexOf('/'); - const filename = file.substring(indexSep + 1); - return { - images: hfInfo['images'], - fileName: filename, - modelType: '', - downloadUrl: baseDownloadUrl + '/' + file + '?download=true', - downloadFilePath: file.substring(0, indexSep + 1), - description: '', - details: { - fileSizeKB: undefined, // TODO: too hard? - }, - }; - }); - return [name, infos]; - } - if (urlText.endsWith('.json')) { - const indexInfo = await (async () => { - try { - const response = await fetch(url); - const data = await response.json(); - return data; - } catch { - return []; - } - })(); - const name = urlText.substring(math.max(urlText.lastIndexOf('/'), 0)); - const infos = indexInfo.map((file) => { - return { - images: [], - fileName: file['name'], - modelType: - DownloadView.modelTypeToComfyUiDirectory(file['type'], '') ?? '', - downloadUrl: file['download'], - downloadFilePath: '', - description: file['description'], - details: {}, - }; - }); - return [name, infos]; - } - return ['', []]; - })(); -} - -class DownloadView { - /** @type {HTMLDivElement} */ - element = null; - - elements = { - /** @type {HTMLInputElement} */ url: null, - /** @type {HTMLDivElement} */ infos: null, - /** @type {HTMLInputElement} */ overwrite: null, - /** @type {HTMLInputElement} */ downloadNotes: null, - /** @type {HTMLButtonElement} */ searchButton: null, - /** @type {HTMLButtonElement} */ clearSearchButton: null, - }; - - /** @type {DOMParser} */ - #domParser = null; - - /** @type {Object.} */ - #settings = null; - - /** @type {() => Promise} */ - #updateModels = () => {}; - - /** - * @param {ModelData} modelData - * @param {Object.} settings - * @param {() => Promise} updateModels - */ - constructor(modelData, settings, updateModels) { - this.#domParser = new DOMParser(); - this.#updateModels = updateModels; - const update = async () => { - await this.#update(modelData, settings); - }; - const reset = () => { - this.elements.infos.innerHTML = ''; - this.elements.infos.appendChild( - $el('h1', ['Input a URL to select a model to download.']), - ); - }; - - const searchButton = new ComfyButton({ - icon: 'magnify', - tooltip: 'Search url', - classList: 'comfyui-button icon-button', - action: async (e) => { - const [button, icon, span] = comfyButtonDisambiguate(e.target); - button.disabled = true; - if (this.elements.url.value === '') { - reset(); - } else { - await update(); - } - button.disabled = false; - }, - }).element; - settings['model-real-time-search'].addEventListener('change', () => { - const hideSearchButton = - settings['text-input-always-hide-search-button'].checked; - searchButton.style.display = hideSearchButton ? 'none' : ''; - }); - settings['text-input-always-hide-search-button'].addEventListener( - 'change', - () => { - const hideSearchButton = - settings['text-input-always-hide-search-button'].checked; - searchButton.style.display = hideSearchButton ? 'none' : ''; - }, - ); - this.elements.searchButton = searchButton; - - const clearSearchButton = new ComfyButton({ - icon: 'close', - tooltip: 'Clear search', - classList: 'comfyui-button icon-button', - action: async (e) => { - e.stopPropagation(); - this.elements.url.value = ''; - reset(); - }, - }).element; - settings['text-input-always-hide-clear-button'].addEventListener( - 'change', - () => { - const hideClearButton = - settings['text-input-always-hide-clear-button'].checked; - clearSearchButton.style.display = hideClearButton ? 'none' : ''; - }, - ); - this.elements.clearSearchButton = clearSearchButton; - - $el( - 'div.tab-header', - { - $: (el) => (this.element = el), - }, - [ - $el('div.row.tab-header-flex-block', [ - $el('input.search-text-area', { - $: (el) => (this.elements.url = el), - type: 'text', - name: 'model download url', - autocomplete: 'off', - placeholder: 'Search URL', - onkeydown: async (e) => { - if (e.key === 'Enter') { - e.stopPropagation(); - if (this.elements.url.value === '') { - reset(); - } else { - await update(); - } - e.target.blur(); - } - }, - }), - clearSearchButton, - searchButton, - ]), - $el( - 'div.download-model-infos', - { - $: (el) => (this.elements.infos = el), - }, - [$el('h1', ['Input a URL to select a model to download.'])], - ), - ], - ); - } - - /** - * Tries to return the related ComfyUI model directory if unambiguous. - * - * @param {string | undefined} modelType - Model type. - * @param {string | undefined} [fileType] - File type. Relevant for "Diffusers". - * - * @returns {(string | null)} Logical base directory name for model type. May be null if the directory is ambiguous or not a model type. - */ - static modelTypeToComfyUiDirectory(modelType, fileType) { - if (fileType !== undefined && fileType !== null) { - const f = fileType.toLowerCase(); - if (f == 'diffusers') { - return 'diffusers'; - } // TODO: is this correct? - } - - if (modelType !== undefined && modelType !== null) { - const m = modelType.toLowerCase(); - // TODO: somehow allow for SERVER to set dir? - // TODO: allow user to choose EXISTING folder override/null? (style_models, HuggingFace) (use an object/map instead so settings can be dynamically set) - if (m == 'aestheticGradient') { - return null; - } else if (m == 'checkpoint' || m == 'checkpoints') { - return 'checkpoints'; - } - //else if (m == "") { return "clip"; } - //else if (m == "") { return "clip_vision"; } - else if (m == 'controlnet') { - return 'controlnet'; - } - //else if (m == "Controlnet") { return "style_models"; } // are these controlnets? (TI-Adapter) - //else if (m == "") { return "gligen"; } - else if (m == 'hypernetwork' || m == 'hypernetworks') { - return 'hypernetworks'; - } else if (m == 'lora' || m == 'loras') { - return 'loras'; - } else if (m == 'locon') { - return 'loras'; - } else if (m == 'motionmodule') { - return null; - } else if (m == 'other') { - return null; - } else if (m == 'pose') { - return null; - } else if ( - m == 'textualinversion' || - m == 'embedding' || - m == 'embeddings' - ) { - return 'embeddings'; - } - //else if (m == "") { return "unet"; } - else if ( - m == 'upscaler' || - m == 'upscale_model' || - m == 'upscale_models' - ) { - return 'upscale_models'; - } else if (m == 'vae') { - return 'vae'; - } else if (m == 'wildcard' || m == 'wildcards') { - return null; - } else if (m == 'workflow' || m == 'workflows') { - return null; - } - } - return null; - } - - /** - * Returns empty string on failure - * @param {float | undefined} fileSizeKB - * @returns {string} - */ - static #fileSizeToFormattedString(fileSizeKB) { - if (fileSizeKB === undefined) { - return ''; - } - const sizes = ['KB', 'MB', 'GB', 'TB', 'PB']; - let fileSizeString = fileSizeKB.toString(); - const index = fileSizeString.indexOf('.'); - const indexMove = index % 3 === 0 ? 3 : index % 3; - const sizeIndex = Math.floor((index - indexMove) / 3); - if (sizeIndex >= sizes.length || sizeIndex < 0) { - fileSizeString = fileSizeString.substring( - 0, - fileSizeString.indexOf('.') + 3, - ); - return `(${fileSizeString} ${sizes[0]})`; - } - const split = fileSizeString.split('.'); - fileSizeString = - split[0].substring(0, indexMove) + - '.' + - split[0].substring(indexMove) + - split[1]; - fileSizeString = fileSizeString.substring( - 0, - fileSizeString.indexOf('.') + 3, - ); - return `(${fileSizeString} ${sizes[sizeIndex]})`; - } - - /** - * @param {Object} info - * @param {ModelData} modelData - * @param {int} id - * @param {any} settings - * @returns {HTMLDivElement} - */ - #modelInfoHtml(info, modelData, id, settings) { - const downloadPreviewSelect = new ImageSelect( - 'model-download-info-preview-model' + '-' + id, - info['images'], - ); - - const comfyUIModelType = - DownloadView.modelTypeToComfyUiDirectory(info['details']['fileType']) ?? - DownloadView.modelTypeToComfyUiDirectory(info['modelType']) ?? - ''; - const searchSeparator = modelData.searchSeparator; - const defaultBasePath = - searchSeparator + - (comfyUIModelType === '' ? '' : comfyUIModelType + searchSeparator + '0'); - - const el_saveDirectoryPath = $el('input.search-text-area', { - type: 'text', - name: 'save directory', - autocomplete: 'off', - placeholder: defaultBasePath, - value: defaultBasePath, - }); - const searchDropdown = new DirectoryDropdown( - modelData, - el_saveDirectoryPath, - true, - ); - - const default_name = (() => { - const filename = info['fileName']; - // TODO: only remove valid model file extensions - const i = filename.lastIndexOf('.'); - return i === -1 ? filename : filename.substring(0, i); - })(); - const el_filename = $el('input.plain-text-area', { - type: 'text', - name: 'model save file name', - autocomplete: 'off', - placeholder: default_name, - value: default_name, - onkeydown: (e) => { - if (e.key === 'Enter') { - e.stopPropagation(); - e.target.blur(); - } - }, - }); - - const infoNotes = $el('textarea.comfy-multiline-input.model-info-notes', { - name: 'model info notes', - value: info['description'] ?? '', - rows: 6, - disabled: true, - style: { - display: - info['description'] === undefined || info['description'] === '' - ? 'none' - : '', - }, - }); - - const filepath = info['downloadFilePath']; - const modelInfo = $el('details.download-details', [ - $el('summary', [filepath + info['fileName']]), - $el('div', [ - downloadPreviewSelect.elements.previews, - $el('div.download-settings-wrapper', [ - $el('div.download-settings', [ - new ComfyButton({ - icon: 'arrow-collapse-down', - tooltip: 'Download model', - content: - 'Download ' + - DownloadView.#fileSizeToFormattedString( - info['details']['fileSizeKB'], - ), - classList: 'comfyui-button download-button', - action: async (e) => { - const pathDirectory = el_saveDirectoryPath.value; - const modelName = (() => { - const filename = info['fileName']; - const name = el_filename.value; - if (name === '') { - return filename; - } - const ext = - MODEL_EXTENSIONS.find((ext) => { - return filename.endsWith(ext); - }) ?? ''; - return name + ext; - })(); - const formData = new FormData(); - formData.append('download', info['downloadUrl']); - formData.append('path', pathDirectory); - formData.append('name', modelName); - const image = await downloadPreviewSelect.getImage(); - formData.append( - 'image', - image === PREVIEW_NONE_URI ? '' : image, - ); - formData.append('overwrite', this.elements.overwrite.checked); - const [button, icon, span] = comfyButtonDisambiguate(e.target); - button.disabled = true; - const [success, resultText] = await comfyRequest( - '/model-manager/model/download', - { - method: 'POST', - body: formData, - }, - ) - .then((data) => { - const success = data['success']; - const message = data['alert']; - if (message !== undefined) { - window.alert(message); - } - return [success, success ? '✔' : '📥︎']; - }) - .catch((err) => { - return [false, '📥︎']; - }); - if (success) { - const description = infoNotes.value; - if ( - this.elements.downloadNotes.checked && - description !== '' - ) { - const modelPath = - pathDirectory + searchSeparator + modelName; - const saved = await saveNotes(modelPath, description); - if (!saved) { - console.warn('Model description was not saved!'); - } - } - this.#updateModels(); - } - comfyButtonAlert( - e.target, - success, - 'mdi-check-bold', - 'mdi-close-thick', - success, - ); - button.disabled = success; - }, - }).element, - $el('div.row.tab-header-flex-block.input-dropdown-container', [ - // TODO: magic class - el_saveDirectoryPath, - searchDropdown.element, - ]), - $el('div.row.tab-header-flex-block', [el_filename]), - downloadPreviewSelect.elements.radioGroup, - infoNotes, - ]), - ]), - ]), - ]); - - return modelInfo; - } - - /** - * @param {ModelData} modelData - * @param {any} settings - */ - async #update(modelData, settings) { - const [name, modelInfos] = await getModelInfos(this.elements.url.value); - const modelInfosHtml = modelInfos - .filter((modelInfo) => { - const filename = modelInfo['fileName']; - return ( - MODEL_EXTENSIONS.find((ext) => { - return filename.endsWith(ext); - }) ?? false - ); - }) - .map((modelInfo, id) => { - return this.#modelInfoHtml(modelInfo, modelData, id, settings); - }); - if (modelInfosHtml.length === 0) { - modelInfosHtml.push($el('h1', ['No models found.'])); - } else { - if (modelInfosHtml.length === 1) { - modelInfosHtml[0].open = true; - } - - const header = $el('div', [ - $el('h1', [name]), - $el('div.model-manager-settings', [ - $checkbox({ - $: (el) => { - this.elements.overwrite = el; - }, - textContent: 'Overwrite Existing Files.', - checked: false, - }), - $checkbox({ - $: (el) => { - this.elements.downloadNotes = el; - }, - textContent: 'Save Notes.', - checked: false, - }), - ]), - ]); - modelInfosHtml.unshift(header); - } - - const infosHtml = this.elements.infos; - infosHtml.innerHTML = ''; - infosHtml.append.apply(infosHtml, modelInfosHtml); - - const downloadNotes = this.elements.downloadNotes; - if (downloadNotes !== undefined && downloadNotes !== null) { - downloadNotes.addEventListener('change', (e) => { - const modelInfoNotes = infosHtml.querySelectorAll( - `textarea.model-info-notes`, - ); - const disabled = !e.currentTarget.checked; - for (let i = 0; i < modelInfoNotes.length; i++) { - modelInfoNotes[i].disabled = disabled; - } - }); - downloadNotes.checked = - settings['download-save-description-as-text-file'].checked; - downloadNotes.dispatchEvent(new Event('change')); - } - - const hideSearchButtons = - settings['text-input-always-hide-search-button'].checked; - this.elements.searchButton.style.display = hideSearchButtons ? 'none' : ''; - - const hideClearSearchButtons = - settings['text-input-always-hide-clear-button'].checked; - this.elements.clearSearchButton.style.display = hideClearSearchButtons - ? 'none' - : ''; - } -} - -class BrowseView { - /** @type {HTMLDivElement} */ - element = null; - - elements = { - /** @type {HTMLDivElement} */ modelGrid: null, - /** @type {HTMLSelectElement} */ modelTypeSelect: null, - /** @type {HTMLSelectElement} */ modelSortSelect: null, - /** @type {HTMLInputElement} */ modelContentFilter: null, - /** @type {HTMLButtonElement} */ searchButton: null, - /** @type {HTMLButtonElement} */ clearSearchButton: null, - }; - - /** @type {Array} */ - previousModelFilters = []; - - /** @type {Object.<{value: string}>} */ - previousModelType = { value: null }; - - /** @type {DirectoryDropdown} */ - directoryDropdown = null; - - /** @type {ModelData} */ - #modelData = null; - - /** @type {@param {() => Promise}} */ - #updateModels = null; - - /** */ - #settingsElements = null; - - /** @type {() => void} */ - updateModelGrid = () => {}; - - /** - * @param {() => Promise} updateModels - * @param {ModelData} modelData - * @param {(searchPath: string) => Promise} showModelInfo - * @param {() => void} updateModelGridCallback - * @param {any} settingsElements - */ - constructor( - updateModels, - modelData, - showModelInfo, - updateModelGridCallback, - settingsElements, - ) { - /** @type {HTMLDivElement} */ - const modelGrid = $el('div.comfy-grid'); - this.elements.modelGrid = modelGrid; - - this.#updateModels = updateModels; - this.#modelData = modelData; - this.#settingsElements = settingsElements; - - const searchInput = $el('input.search-text-area', { - $: (el) => (this.elements.modelContentFilter = el), - type: 'text', - name: 'model search', - autocomplete: 'off', - placeholder: '/Search', - }); - - const updatePreviousModelFilter = () => { - const modelType = this.elements.modelTypeSelect.value; - const value = this.elements.modelContentFilter.value; - this.previousModelFilters[modelType] = value; - }; - - const updateModelGrid = () => { - const sortValue = this.elements.modelSortSelect.value; - const reverseSort = sortValue[0] === '-'; - const sortBy = reverseSort ? sortValue.substring(1) : sortValue; - ModelGrid.update( - this.elements.modelGrid, - this.#modelData, - this.elements.modelTypeSelect, - this.previousModelType, - this.#settingsElements, - sortBy, - reverseSort, - this.previousModelFilters, - this.elements.modelContentFilter, - showModelInfo, - ); - updateModelGridCallback(); - - const hideSearchButtons = - this.#settingsElements['model-real-time-search'].checked | - this.#settingsElements['text-input-always-hide-search-button'].checked; - this.elements.searchButton.style.display = hideSearchButtons - ? 'none' - : ''; - - const hideClearSearchButtons = - this.#settingsElements['text-input-always-hide-clear-button'].checked; - this.elements.clearSearchButton.style.display = hideClearSearchButtons - ? 'none' - : ''; - }; - this.updateModelGrid = updateModelGrid; - - const searchDropdown = new DirectoryDropdown( - modelData, - searchInput, - false, - () => { - return this.elements.modelTypeSelect.value; - }, - updatePreviousModelFilter, - updateModelGrid, - () => { - return this.#settingsElements['model-real-time-search'].checked; - }, - ); - this.directoryDropdown = searchDropdown; - - const searchButton = new ComfyButton({ - icon: 'magnify', - tooltip: 'Search models', - classList: 'comfyui-button icon-button', - action: (e) => { - e.stopPropagation(); - const [button, icon, span] = comfyButtonDisambiguate(e.target); - button.disabled = true; - updateModelGrid(); - button.disabled = false; - }, - }).element; - settingsElements['model-real-time-search'].addEventListener( - 'change', - () => { - const hideSearchButton = - this.#settingsElements['text-input-always-hide-search-button'] - .checked || - this.#settingsElements['model-real-time-search'].checked; - searchButton.style.display = hideSearchButton ? 'none' : ''; - }, - ); - settingsElements['text-input-always-hide-search-button'].addEventListener( - 'change', - () => { - const hideSearchButton = - this.#settingsElements['text-input-always-hide-search-button'] - .checked || - this.#settingsElements['model-real-time-search'].checked; - searchButton.style.display = hideSearchButton ? 'none' : ''; - }, - ); - this.elements.searchButton = searchButton; - - const clearSearchButton = new ComfyButton({ - icon: 'close', - tooltip: 'Clear search', - classList: 'comfyui-button icon-button', - action: (e) => { - e.stopPropagation(); - const [button, icon, span] = comfyButtonDisambiguate(e.target); - button.disabled = true; - this.elements.modelContentFilter.value = ''; - updateModelGrid(); - button.disabled = false; - }, - }).element; - settingsElements['text-input-always-hide-clear-button'].addEventListener( - 'change', - () => { - const hideClearSearchButton = - this.#settingsElements['text-input-always-hide-clear-button'].checked; - clearSearchButton.style.display = hideClearSearchButton ? 'none' : ''; - }, - ); - this.elements.clearSearchButton = clearSearchButton; - - this.element = $el('div', [ - $el('div.row.tab-header', [ - $el('div.row.tab-header-flex-block', [ - new ComfyButton({ - icon: 'reload', - tooltip: 'Reload model grid', - classList: 'comfyui-button icon-button', - action: async (e) => { - const [button, icon, span] = comfyButtonDisambiguate(e.target); - button.disabled = true; - updateModels(); - button.disabled = false; - }, - }).element, - $el('select.model-select-dropdown', { - $: (el) => (this.elements.modelTypeSelect = el), - name: 'model-type', - onchange: (e) => { - const select = e.target; - select.disabled = true; - updateModelGrid(); - select.disabled = false; - }, - }), - $el( - 'select.model-select-dropdown', - { - $: (el) => (this.elements.modelSortSelect = el), - name: 'model select dropdown', - onchange: (e) => { - const select = e.target; - select.disabled = true; - updateModelGrid(); - select.disabled = false; - }, - }, - [ - $el('option', { value: MODEL_SORT_DATE_CREATED }, [ - 'Created (newest first)', - ]), - $el('option', { value: '-' + MODEL_SORT_DATE_CREATED }, [ - 'Created (oldest first)', - ]), - $el('option', { value: MODEL_SORT_DATE_MODIFIED }, [ - 'Modified (newest first)', - ]), - $el('option', { value: '-' + MODEL_SORT_DATE_MODIFIED }, [ - 'Modified (oldest first)', - ]), - $el('option', { value: MODEL_SORT_DATE_NAME }, ['Name (A-Z)']), - $el('option', { value: '-' + MODEL_SORT_DATE_NAME }, [ - 'Name (Z-A)', - ]), - $el('option', { value: MODEL_SORT_SIZE_BYTES }, [ - 'Size (largest first)', - ]), - $el('option', { value: '-' + MODEL_SORT_SIZE_BYTES }, [ - 'Size (smallest first)', - ]), - ], - ), - ]), - $el('div.row.tab-header-flex-block', [ - $el('div.search-models.input-dropdown-container', [ - // TODO: magic class - searchInput, - searchDropdown.element, - ]), - clearSearchButton, - searchButton, - ]), - ]), - modelGrid, - ]); - } -} - -class SettingsView { - /** @type {HTMLDivElement} */ - element = null; - - elements = { - /** @type {HTMLButtonElement} */ reloadButton: null, - /** @type {HTMLButtonElement} */ saveButton: null, - /** @type {HTMLDivElement} */ setPreviewButton: null, - settings: { - /** @type {HTMLTextAreaElement} */ 'model-search-always-append': null, - /** @type {HTMLInputElement} */ 'model-default-browser-model-type': null, - /** @type {HTMLInputElement} */ 'model-real-time-search': null, - /** @type {HTMLInputElement} */ 'model-persistent-search': null, - - /** @type {HTMLInputElement} */ 'model-preview-thumbnail-type': null, - /** @type {HTMLInputElement} */ 'model-preview-fallback-search-safetensors-thumbnail': - null, - /** @type {HTMLInputElement} */ 'model-show-label-extensions': null, - /** @type {HTMLInputElement} */ 'model-show-add-button': null, - /** @type {HTMLInputElement} */ 'model-show-copy-button': null, - /** @type {HTMLInputElement} */ 'model-show-load-workflow-button': null, - /** @type {HTMLInputElement} */ 'model-info-button-on-left': null, - - /** @type {HTMLInputElement} */ 'model-add-embedding-extension': null, - /** @type {HTMLInputElement} */ 'model-add-drag-strict-on-field': null, - /** @type {HTMLInputElement} */ 'model-add-offset': null, - - /** @type {HTMLInputElement} */ 'model-info-autosave-notes': null, - - /** @type {HTMLInputElement} */ 'download-save-description-as-text-file': - null, - - /** @type {HTMLInputElement} */ 'sidebar-default-width': null, - /** @type {HTMLInputElement} */ 'sidebar-default-height': null, - /** @type {HTMLInputElement} */ 'sidebar-control-always-compact': null, - /** @type {HTMLInputElement} */ 'text-input-always-hide-search-button': - null, - /** @type {HTMLInputElement} */ 'text-input-always-hide-clear-button': - null, - - /** @type {HTMLInputElement} */ 'tag-generator-sampler-method': null, - /** @type {HTMLInputElement} */ 'tag-generator-count': null, - /** @type {HTMLInputElement} */ 'tag-generator-threshold': null, - }, - }; - - /** @return {() => Promise} */ - #updateModels = () => {}; - - /** - * @param {Object} settingsData - * @param {boolean} updateModels - */ - async #setSettings(settingsData, updateModels) { - const settings = this.elements.settings; - for (const [key, value] of Object.entries(settingsData)) { - const setting = settings[key]; - if (setting === undefined || setting === null) { - continue; - } - const type = setting.type; - switch (type) { - case 'checkbox': - setting.checked = Boolean(value); - break; - case 'range': - setting.value = parseFloat(value); - break; - case 'textarea': - setting.value = value; - break; - case 'number': - setting.value = parseInt(value); - break; - case 'select-one': - setting.value = value; - break; - default: - console.warn(`Unknown settings input type '${type}'!`); - } - } - - if (updateModels) { - await this.#updateModels(); // Is this slow? - } - } - - /** - * @param {boolean} updateModels - * @returns {Promise} - */ - async reload(updateModels) { - const data = await comfyRequest('/model-manager/settings/load'); - const settingsData = data['settings']; - await this.#setSettings(settingsData, updateModels); - comfyButtonAlert(this.elements.reloadButton, true); - } - - /** @returns {Promise} */ - async save() { - let settingsData = {}; - for (const [setting, el] of Object.entries(this.elements.settings)) { - if (!el) { - continue; - } // hack - const type = el.type; - let value = null; - switch (type) { - case 'checkbox': - value = el.checked; - break; - case 'range': - value = el.value; - break; - case 'textarea': - value = el.value; - break; - case 'number': - value = el.value; - break; - case 'select-one': - value = el.value; - break; - default: - console.warn('Unknown settings input type!'); - } - settingsData[setting] = value; - } - - const data = await comfyRequest('/model-manager/settings/save', { - method: 'POST', - body: JSON.stringify({ settings: settingsData }), - }).catch((err) => { - return { success: false }; - }); - const success = data['success']; - if (success) { - const settingsData = data['settings']; - await this.#setSettings(settingsData, true); - } - comfyButtonAlert(this.elements.saveButton, success); - } - - /** - * @param {() => Promise} updateModels - * @param {() => void} updateSidebarButtons - */ - constructor(updateModels, updateSidebarButtons) { - this.#updateModels = updateModels; - const settings = this.elements.settings; - - const sidebarControl = $checkbox({ - $: (el) => (settings['sidebar-control-always-compact'] = el), - textContent: 'Sidebar controls always compact', - }); - sidebarControl - .getElementsByTagName('input')[0] - .addEventListener('change', () => { - updateSidebarButtons(); - }); - - const reloadButton = new ComfyButton({ - content: 'Reload', - tooltip: 'Reload settings and model manager files', - action: async (e) => { - const [button, icon, span] = comfyButtonDisambiguate(e.target); - button.disabled = true; - await this.reload(true); - button.disabled = false; - }, - }).element; - this.elements.reloadButton = reloadButton; - - const saveButton = new ComfyButton({ - content: 'Save', - tooltip: 'Save settings and reload model manager', - action: async (e) => { - const [button, icon, span] = comfyButtonDisambiguate(e.target); - button.disabled = true; - await this.save(); - button.disabled = false; - }, - }).element; - this.elements.saveButton = saveButton; - - const correctPreviewsButton = new ComfyButton({ - content: 'Fix Extensions', - tooltip: 'Correct image file extensions in all model directories', - action: async (e) => { - const [button, icon, span] = comfyButtonDisambiguate(e.target); - button.disabled = true; - const data = await comfyRequest( - '/model-manager/preview/correct-extensions', - ).catch((err) => { - return { success: false }; - }); - const success = data['success']; - if (success) { - const detectPlural = data['detected'] === 1 ? '' : 's'; - const correctPlural = data['corrected'] === 1 ? '' : 's'; - const message = `Detected ${data['detected']} extension${detectPlural}.\nCorrected ${data['corrected']} extension${correctPlural}.`; - window.alert(message); - } - comfyButtonAlert(e.target, success); - if (data['corrected'] > 0) { - await this.reload(true); - } - button.disabled = false; - }, - }).element; - - $el( - 'div.model-manager-settings', - { - $: (el) => (this.element = el), - }, - [ - $el('h1', ['Settings']), - $el('div', [reloadButton, saveButton]), - $el( - 'a', - { - style: { color: 'var(--fg-color)' }, - href: 'https://github.com/hayden-fr/ComfyUI-Model-Manager/issues/', - }, - ['File bugs and issues here.'], - ), - $el('h2', ['Model Search']), - $el('div', [ - $el('div.search-settings-text', [ - $el('p', ['Always include in model search:']), - $el('textarea.comfy-multiline-input', { - $: (el) => (settings['model-search-always-append'] = el), - name: 'always include in model search', - placeholder: 'example: /0/sd1.5/styles "pastel style" -3d', - rows: '6', - }), - ]), - ]), - $select({ - $: (el) => (settings['model-default-browser-model-type'] = el), - textContent: 'Default model search type (on start up)', - options: [ - 'checkpoints', - 'clip', - 'clip_vision', - 'controlnet', - 'diffusers', - 'embeddings', - 'gligen', - 'hypernetworks', - 'loras', - 'photomaker', - 'style_models', - 'unet', - 'vae', - 'vae_approx', - ], - }), - $checkbox({ - $: (el) => (settings['model-real-time-search'] = el), - textContent: 'Real-time search', - }), - $checkbox({ - $: (el) => (settings['model-persistent-search'] = el), - textContent: 'Persistent search text (across model types)', - }), - $el('h2', ['Model Search Thumbnails']), - $select({ - $: (el) => (settings['model-preview-thumbnail-type'] = el), - textContent: 'Preview thumbnail type', - options: ['AUTO', 'JPEG'], // should use AUTO to avoid artifacts from changing between formats; use JPEG for backward compatibility - }), - $checkbox({ - $: (el) => - (settings['model-preview-fallback-search-safetensors-thumbnail'] = - el), - textContent: 'Fallback to embedded safetensors image (slow)', - }), - $checkbox({ - $: (el) => (settings['model-show-label-extensions'] = el), - textContent: 'Show file extension', - }), - $checkbox({ - $: (el) => (settings['model-show-copy-button'] = el), - textContent: 'Show "Copy" button', - }), - $checkbox({ - $: (el) => (settings['model-show-add-button'] = el), - textContent: 'Show "Add" button', - }), - $checkbox({ - $: (el) => (settings['model-show-load-workflow-button'] = el), - textContent: 'Show "Load Workflow" button', - }), - $checkbox({ - $: (el) => (settings['model-info-button-on-left'] = el), - textContent: '"Model Info" button on left', - }), - $el('h2', ['Node Graph']), - $checkbox({ - $: (el) => (settings['model-add-embedding-extension'] = el), - textContent: 'Add embedding with extension', - }), - $checkbox({ - $: (el) => (settings['model-add-drag-strict-on-field'] = el), // true -> must drag on field; false -> can drag on node when unambiguous - textContent: "Must always drag thumbnail onto node's input field", - }), - $el('label', [ - 'Add offset', // if a node already was added to the same spot, add the next one with an offset - $el('input', { - $: (el) => (settings['model-add-offset'] = el), - type: 'number', - name: 'model add offset', - step: 5, - }), - ]), - $el('h2', ['Model Info']), - $checkbox({ - $: (el) => (settings['model-info-autosave-notes'] = el), // note history deleted on model info close - textContent: 'Autosave notes', - }), - $el('h2', ['Download']), - $checkbox({ - $: (el) => (settings['download-save-description-as-text-file'] = el), - textContent: 'Save notes by default.', - }), - $el('h2', ['Window']), - sidebarControl, - $el('label', [ - 'Sidebar width (on start up)', - $el('input', { - $: (el) => (settings['sidebar-default-width'] = el), - type: 'range', - name: 'default sidebar width', - value: 0.5, - min: 0.0, - max: 1.0, - step: 0.05, - }), - ]), - $el('label', [ - 'Sidebar height (on start up)', - $el('input', { - $: (el) => (settings['sidebar-default-height'] = el), - type: 'range', - name: 'default sidebar height', - value: 0.5, - min: 0.0, - max: 1.0, - step: 0.05, - }), - ]), - $checkbox({ - $: (el) => (settings['text-input-always-hide-search-button'] = el), - textContent: 'Always hide "Search" buttons.', - }), - $checkbox({ - $: (el) => (settings['text-input-always-hide-clear-button'] = el), - textContent: 'Always hide "Clear Search" buttons.', - }), - $el('h2', ['Model Preview Images']), - $el('div', [correctPreviewsButton]), - $el('h2', ['Random Tag Generator']), - $select({ - $: (el) => (settings['tag-generator-sampler-method'] = el), - textContent: 'Default sampling method', - options: ['Frequency', 'Uniform'], - }), - $el('label', [ - 'Default count', - $el('input', { - $: (el) => (settings['tag-generator-count'] = el), - type: 'number', - name: 'tag generator count', - step: 1, - min: 1, - }), - ]), - $el('label', [ - 'Default minimum threshold', - $el('input', { - $: (el) => (settings['tag-generator-threshold'] = el), - type: 'number', - name: 'tag generator threshold', - step: 1, - min: 1, - }), - ]), - ], - ); - } -} - -/** - * @param {String[]} labels - * @param {[(event: Event) => Promise]} callbacks - * @returns {HTMLDivElement} - */ -function GenerateRadioButtonGroup(labels, callbacks = []) { - const RADIO_BUTTON_GROUP_ACTIVE = 'radio-button-group-active'; - const radioButtonGroup = $el('div.radio-button-group', []); - const buttons = []; - for (let i = 0; i < labels.length; i++) { - const text = labels[i]; - const callback = callbacks[i] ?? (() => {}); - buttons.push( - $el('button.radio-button', { - textContent: text, - onclick: (event) => { - const targetIsActive = event.target.classList.contains( - RADIO_BUTTON_GROUP_ACTIVE, - ); - if (targetIsActive) { - return; - } - const children = radioButtonGroup.children; - for (let i = 0; i < children.length; i++) { - children[i].classList.remove(RADIO_BUTTON_GROUP_ACTIVE); - } - event.target.classList.add(RADIO_BUTTON_GROUP_ACTIVE); - callback(event); - }, - }), - ); - } - radioButtonGroup.append.apply(radioButtonGroup, buttons); - buttons[0]?.classList.add(RADIO_BUTTON_GROUP_ACTIVE); - return radioButtonGroup; -} - -/** - * @param {String[]} labels - * @param {[(event: Event) => Promise]} activationCallbacks - * @param {(event: Event) => Promise} deactivationCallback - * @returns {HTMLDivElement} - */ -function GenerateToggleRadioButtonGroup( - labels, - activationCallbacks = [], - deactivationCallback = () => {}, -) { - const RADIO_BUTTON_GROUP_ACTIVE = 'radio-button-group-active'; - const radioButtonGroup = $el('div.radio-button-group', []); - const buttons = []; - for (let i = 0; i < labels.length; i++) { - const text = labels[i]; - const activationCallback = activationCallbacks[i] ?? (() => {}); - buttons.push( - $el('button.radio-button', { - textContent: text, - onclick: (event) => { - const targetIsActive = event.target.classList.contains( - RADIO_BUTTON_GROUP_ACTIVE, - ); - const children = radioButtonGroup.children; - for (let i = 0; i < children.length; i++) { - children[i].classList.remove(RADIO_BUTTON_GROUP_ACTIVE); - } - if (targetIsActive) { - deactivationCallback(event); - } else { - event.target.classList.add(RADIO_BUTTON_GROUP_ACTIVE); - activationCallback(event); - } - }, - }), - ); - } - radioButtonGroup.append.apply(radioButtonGroup, buttons); - return radioButtonGroup; -} - -/** - * Coupled-state select and radio buttons (hidden first radio button) - * @param {String[]} labels - * @param {[(button: HTMLButtonElement) => Promise]} activationCallbacks - * @returns {[HTMLDivElement, HTMLSelectElement]} - */ -function GenerateSidebarToggleRadioAndSelect(labels, activationCallbacks = []) { - const RADIO_BUTTON_GROUP_ACTIVE = 'radio-button-group-active'; - const radioButtonGroup = $el('div.radio-button-group', []); - const buttons = []; - - const select = $el( - 'select', - { - name: 'sidebar-select', - onchange: (event) => { - const select = event.target; - const children = select.children; - let value = undefined; - for (let i = 0; i < children.length; i++) { - const child = children[i]; - if (child.selected) { - value = child.value; - } - } - for (let i = 0; i < buttons.length; i++) { - const button = buttons[i]; - if (button.textContent === value) { - for (let i = 0; i < buttons.length; i++) { - buttons[i].classList.remove(RADIO_BUTTON_GROUP_ACTIVE); - } - button.classList.add(RADIO_BUTTON_GROUP_ACTIVE); - activationCallbacks[i](button); - break; - } - } - }, - }, - labels.map((option) => { - return $el( - 'option', - { - value: option, - }, - option, - ); - }), - ); - - for (let i = 0; i < labels.length; i++) { - const text = labels[i]; - const activationCallback = activationCallbacks[i] ?? (() => {}); - buttons.push( - $el('button.radio-button', { - textContent: text, - onclick: (event) => { - const button = event.target; - let textContent = button.textContent; - const targetIsActive = button.classList.contains( - RADIO_BUTTON_GROUP_ACTIVE, - ); - if ( - button === buttons[0] && - buttons[0].classList.contains(RADIO_BUTTON_GROUP_ACTIVE) - ) { - // do not deactivate 0 - return; - } - // update button - const children = radioButtonGroup.children; - for (let i = 0; i < children.length; i++) { - children[i].classList.remove(RADIO_BUTTON_GROUP_ACTIVE); - } - if (targetIsActive) { - // return to 0 - textContent = labels[0]; - buttons[0].classList.add(RADIO_BUTTON_GROUP_ACTIVE); - activationCallbacks[0](buttons[0]); - } else { - // move to >0 - button.classList.add(RADIO_BUTTON_GROUP_ACTIVE); - activationCallback(button); - } - // update selection - for (let i = 0; i < select.children.length; i++) { - const option = select.children[i]; - option.selected = option.value === textContent; - } - }, - }), - ); - } - radioButtonGroup.append.apply(radioButtonGroup, buttons); - buttons[0].click(); - buttons[0].style.display = 'none'; - - return [radioButtonGroup, select]; -} - -class ModelManager extends ComfyDialog { - /** @type {HTMLDivElement} */ - element = null; - - /** @type {ModelData} */ - #modelData = null; - - /** @type {ModelInfo} */ - #modelInfo = null; - - /** @type {DownloadView} */ - #downloadView = null; - - /** @type {BrowseView} */ - #browseView = null; - - /** @type {SettingsView} */ - #settingsView = null; - - /** @type {HTMLDivElement} */ - #topbarRight = null; - - /** @type {HTMLDivElement} */ - #tabManagerButtons = null; - - /** @type {HTMLDivElement} */ - #tabManagerContents = null; - - /** @type {HTMLDivElement} */ - #tabInfoButtons = null; - - /** @type {HTMLDivElement} */ - #tabInfoContents = null; - - /** @type {HTMLButtonElement} */ - #sidebarButtonGroup = null; - - /** @type {HTMLButtonElement} */ - #sidebarSelect = null; - - /** @type {HTMLButtonElement} */ - #closeModelInfoButton = null; - - /** @type {String} */ - #dragSidebarState = ''; - - constructor() { - super(); - - this.#modelData = new ModelData(); - - this.#settingsView = new SettingsView(this.#refreshModels, () => - this.#updateSidebarButtons(), - ); - - this.#modelInfo = new ModelInfo( - this.#modelData, - this.#refreshModels, - this.#settingsView.elements.settings, - ); - - this.#browseView = new BrowseView( - this.#refreshModels, - this.#modelData, - this.#showModelInfo, - this.#resetManagerContentsScroll, - this.#settingsView.elements.settings, // TODO: decouple settingsData from elements? - ); - - this.#downloadView = new DownloadView( - this.#modelData, - this.#settingsView.elements.settings, - this.#refreshModels, - ); - - const [tabManagerButtons, tabManagerContents] = GenerateTabGroup([ - { - name: 'Download', - icon: 'arrow-collapse-down', - tabContent: this.#downloadView.element, - }, - { - name: 'Models', - icon: 'folder-search-outline', - tabContent: this.#browseView.element, - }, - { - name: 'Settings', - icon: 'cog-outline', - tabContent: this.#settingsView.element, - }, - ]); - tabManagerButtons[0]?.click(); - - const tabInfoButtons = this.#modelInfo.elements.tabButtons; - const tabInfoContents = this.#modelInfo.elements.tabContents; - - const [sidebarButtonGroup, sidebarSelect] = - GenerateSidebarToggleRadioAndSelect( - ['◼', '◨', '⬒', '⬓', '◧'], - [ - () => { - const element = this.element; - if (element) { - // callback on initialization as default state - element.dataset['sidebarState'] = 'none'; - } - }, - () => { - this.element.dataset['sidebarState'] = 'right'; - }, - () => { - this.element.dataset['sidebarState'] = 'top'; - }, - () => { - this.element.dataset['sidebarState'] = 'bottom'; - }, - () => { - this.element.dataset['sidebarState'] = 'left'; - }, - ], - ); - this.#sidebarButtonGroup = sidebarButtonGroup; - this.#sidebarSelect = sidebarSelect; - sidebarButtonGroup.classList.add('sidebar-buttons'); - const sidebarButtonGroupChildren = sidebarButtonGroup.children; - for (let i = 0; i < sidebarButtonGroupChildren.length; i++) { - sidebarButtonGroupChildren[i].classList.add('icon-button'); - } - - const closeModelInfoButton = new ComfyButton({ - icon: 'arrow-u-left-bottom', - tooltip: 'Return to model search', - classList: 'comfyui-button icon-button', - action: async () => await this.#tryHideModelInfo(true), - }).element; - this.#closeModelInfoButton = closeModelInfoButton; - closeModelInfoButton.style.display = 'none'; - - const modelManager = $el( - 'div.comfy-modal.model-manager', - { - $: (el) => (this.element = el), - parent: document.body, - dataset: { - sidebarState: 'none', - sidebarLeftWidthDecimal: '', - sidebarRightWidthDecimal: '', - sidebarTopHeightDecimal: '', - sidebarBottomHeightDecimal: '', - }, - }, - [ - $el('div.comfy-modal-content', [ - // TODO: settings.top_bar_left_to_right or settings.top_bar_right_to_left - $el('div.model-manager-panel', [ - $el('div.model-manager-head', [ - $el( - 'div.topbar-right', - { - $: (el) => (this.#topbarRight = el), - }, - [ - new ComfyButton({ - icon: 'window-close', - tooltip: 'Close model manager', - classList: 'comfyui-button icon-button', - action: async () => { - const saved = await this.#modelInfo.trySave(true); - if (saved) { - this.close(); - } - }, - }).element, - closeModelInfoButton, - sidebarSelect, - sidebarButtonGroup, - ], - ), - $el('div.topbar-left', [ - $el('div', [ - $el( - 'div.model-tab-group.no-highlight', - { - $: (el) => (this.#tabManagerButtons = el), - }, - tabManagerButtons, - ), - $el( - 'div.model-tab-group.no-highlight', - { - $: (el) => (this.#tabInfoButtons = el), - style: { display: 'none' }, - }, - tabInfoButtons, - ), - ]), - ]), - ]), - $el('div.model-manager-body', [ - $el( - 'div.tab-contents', - { - $: (el) => (this.#tabManagerContents = el), - }, - tabManagerContents, - ), - $el( - 'div.tab-contents', - { - $: (el) => (this.#tabInfoContents = el), - style: { display: 'none' }, - }, - tabInfoContents, - ), - ]), - ]), - ]), - ], - ); - - new ResizeObserver( - GenerateDynamicTabTextCallback(modelManager, tabManagerButtons, 704), - ).observe(modelManager); - new ResizeObserver( - GenerateDynamicTabTextCallback(modelManager, tabInfoButtons, 704), - ).observe(modelManager); - new ResizeObserver(() => this.#updateSidebarButtons()).observe( - modelManager, - ); - window.addEventListener('resize', () => { - const width = window.innerWidth; - const height = window.innerHeight; - - const leftDecimal = modelManager.dataset['sidebarLeftWidthDecimal']; - const rightDecimal = modelManager.dataset['sidebarRightWidthDecimal']; - const topDecimal = modelManager.dataset['sidebarTopHeightDecimal']; - const bottomDecimal = modelManager.dataset['sidebarBottomHeightDecimal']; - - // restore decimal after resize - modelManager.style.setProperty( - '--model-manager-sidebar-width-left', - leftDecimal * width + 'px', - ); - modelManager.style.setProperty( - '--model-manager-sidebar-width-right', - rightDecimal * width + 'px', - ); - modelManager.style.setProperty( - '--model-manager-sidebar-height-top', - +(topDecimal * height) + 'px', - ); - modelManager.style.setProperty( - '--model-manager-sidebar-height-bottom', - bottomDecimal * height + 'px', - ); - }); - - const EDGE_DELTA = 8; - - const endDragSidebar = (e) => { - this.#dragSidebarState = ''; - - modelManager.classList.remove('cursor-drag-left'); - modelManager.classList.remove('cursor-drag-top'); - modelManager.classList.remove('cursor-drag-right'); - modelManager.classList.remove('cursor-drag-bottom'); - - // cache for window resize - modelManager.dataset['sidebarLeftWidthDecimal'] = - parseInt( - modelManager.style.getPropertyValue( - '--model-manager-sidebar-width-left', - ), - ) / window.innerWidth; - modelManager.dataset['sidebarRightWidthDecimal'] = - parseInt( - modelManager.style.getPropertyValue( - '--model-manager-sidebar-width-right', - ), - ) / window.innerWidth; - modelManager.dataset['sidebarTopHeightDecimal'] = - parseInt( - modelManager.style.getPropertyValue( - '--model-manager-sidebar-height-top', - ), - ) / window.innerHeight; - modelManager.dataset['sidebarBottomHeightDecimal'] = - parseInt( - modelManager.style.getPropertyValue( - '--model-manager-sidebar-height-bottom', - ), - ) / window.innerHeight; - }; - document.addEventListener('mouseup', (e) => endDragSidebar(e)); - document.addEventListener('touchend', (e) => endDragSidebar(e)); - - const detectDragSidebar = (e, x, y) => { - const left = modelManager.offsetLeft; - const top = modelManager.offsetTop; - const width = modelManager.offsetWidth; - const height = modelManager.offsetHeight; - const right = left + width; - const bottom = top + height; - - if (!(x >= left && x <= right && y >= top && y <= bottom)) { - // click was not in model manager - return; - } - - const isOnEdgeLeft = x - left <= EDGE_DELTA; - const isOnEdgeRight = right - x <= EDGE_DELTA; - const isOnEdgeTop = y - top <= EDGE_DELTA; - const isOnEdgeBottom = bottom - y <= EDGE_DELTA; - - const sidebarState = this.element.dataset['sidebarState']; - if (sidebarState === 'left' && isOnEdgeRight) { - this.#dragSidebarState = sidebarState; - } else if (sidebarState === 'right' && isOnEdgeLeft) { - this.#dragSidebarState = sidebarState; - } else if (sidebarState === 'top' && isOnEdgeBottom) { - this.#dragSidebarState = sidebarState; - } else if (sidebarState === 'bottom' && isOnEdgeTop) { - this.#dragSidebarState = sidebarState; - } - - if (this.#dragSidebarState !== '') { - e.preventDefault(); - e.stopPropagation(); - } - }; - modelManager.addEventListener('mousedown', (e) => - detectDragSidebar(e, e.clientX, e.clientY), - ); - modelManager.addEventListener('touchstart', (e) => - detectDragSidebar(e, e.touches[0].clientX, e.touches[0].clientY), - ); - - const updateSidebarCursor = (e, x, y) => { - if (this.#dragSidebarState !== '') { - // do not update cursor style while dragging - return; - } - - const left = modelManager.offsetLeft; - const top = modelManager.offsetTop; - const width = modelManager.offsetWidth; - const height = modelManager.offsetHeight; - const right = left + width; - const bottom = top + height; - - const isOnEdgeLeft = x - left <= EDGE_DELTA; - const isOnEdgeRight = right - x <= EDGE_DELTA; - const isOnEdgeTop = y - top <= EDGE_DELTA; - const isOnEdgeBottom = bottom - y <= EDGE_DELTA; - - const updateClass = (add, className) => { - if (add) { - modelManager.classList.add(className); - } else { - modelManager.classList.remove(className); - } - }; - - const sidebarState = this.element.dataset['sidebarState']; - updateClass(sidebarState === 'right' && isOnEdgeLeft, 'cursor-drag-left'); - updateClass(sidebarState === 'bottom' && isOnEdgeTop, 'cursor-drag-top'); - updateClass( - sidebarState === 'left' && isOnEdgeRight, - 'cursor-drag-right', - ); - updateClass( - sidebarState === 'top' && isOnEdgeBottom, - 'cursor-drag-bottom', - ); - }; - modelManager.addEventListener('mousemove', (e) => - updateSidebarCursor(e, e.clientX, e.clientY), - ); - modelManager.addEventListener('touchmove', (e) => - updateSidebarCursor(e, e.touches[0].clientX, e.touches[0].clientY), - ); - - const updateDragSidebar = (e, x, y) => { - const sidebarState = this.#dragSidebarState; - if (sidebarState === '') { - return; - } - - e.preventDefault(); - - const width = window.innerWidth; - const height = window.innerHeight; - - if (sidebarState === 'left') { - const pixels = clamp(x, 0, width).toString() + 'px'; - modelManager.style.setProperty( - '--model-manager-sidebar-width-left', - pixels, - ); - } else if (sidebarState === 'right') { - const pixels = clamp(width - x, 0, width).toString() + 'px'; - modelManager.style.setProperty( - '--model-manager-sidebar-width-right', - pixels, - ); - } else if (sidebarState === 'top') { - const pixels = clamp(y, 0, height).toString() + 'px'; - modelManager.style.setProperty( - '--model-manager-sidebar-height-top', - pixels, - ); - } else if (sidebarState === 'bottom') { - const pixels = clamp(height - y, 0, height).toString() + 'px'; - modelManager.style.setProperty( - '--model-manager-sidebar-height-bottom', - pixels, - ); - } - }; - document.addEventListener('mousemove', (e) => - updateDragSidebar(e, e.clientX, e.clientY), - ); - document.addEventListener('touchmove', (e) => - updateDragSidebar(e, e.touches[0].clientX, e.touches[0].clientY), - ); - - this.#init(); - } - - async #init() { - await this.#settingsView.reload(false); - await this.#refreshModels(); - - const settings = this.#settingsView.elements.settings; - - { - // initialize buttons' visibility state - const hideSearchButtons = - settings['text-input-always-hide-search-button'].checked; - const hideClearSearchButtons = - settings['text-input-always-hide-clear-button'].checked; - this.#downloadView.elements.searchButton.style.display = hideSearchButtons - ? 'none' - : ''; - this.#downloadView.elements.clearSearchButton.style.display = - hideClearSearchButtons ? 'none' : ''; - } - - { - // set initial sidebar widths & heights - const width = window.innerWidth; - const height = window.innerHeight; - - const xDecimal = settings['sidebar-default-width'].value; - const yDecimal = settings['sidebar-default-height'].value; - - this.element.dataset['sidebarLeftWidthDecimal'] = xDecimal; - this.element.dataset['sidebarRightWidthDecimal'] = xDecimal; - this.element.dataset['sidebarTopHeightDecimal'] = yDecimal; - this.element.dataset['sidebarBottomHeightDecimal'] = yDecimal; - - const x = Math.floor(width * xDecimal); - const y = Math.floor(height * yDecimal); - - const leftPixels = x.toString() + 'px'; - this.element.style.setProperty( - '--model-manager-sidebar-width-left', - leftPixels, - ); - - const rightPixels = x.toString() + 'px'; - this.element.style.setProperty( - '--model-manager-sidebar-width-right', - rightPixels, - ); - - const topPixels = y.toString() + 'px'; - this.element.style.setProperty( - '--model-manager-sidebar-height-top', - topPixels, - ); - - const bottomPixels = y.toString() + 'px'; - this.element.style.setProperty( - '--model-manager-sidebar-height-bottom', - bottomPixels, - ); - } - } - - #resetManagerContentsScroll = () => { - this.#tabManagerContents.scrollTop = 0; - }; - - #refreshModels = async () => { - const modelData = this.#modelData; - modelData.systemSeparator = await comfyRequest( - '/model-manager/system-separator', - ); - const newModels = await comfyRequest('/model-manager/models/list'); - Object.assign(modelData.models, newModels); // NOTE: do NOT create a new object - const newModelDirectories = await comfyRequest( - '/model-manager/models/directory-list', - ); - modelData.directories.data.splice(0, Infinity, ...newModelDirectories); // NOTE: do NOT create a new array - - this.#browseView.updateModelGrid(); - await this.#tryHideModelInfo(false); - - document.getElementById('comfy-refresh-button')?.click(); - }; - - /** - * @param {searchPath: string} - * @return {Promise} - */ - #showModelInfo = async (searchPath) => { - await this.#modelInfo - .update(searchPath, this.#refreshModels, this.#modelData.searchSeparator) - .then(() => { - this.#tabManagerButtons.style.display = 'none'; - this.#tabManagerContents.style.display = 'none'; - - this.#closeModelInfoButton.style.display = ''; - this.#tabInfoButtons.style.display = ''; - this.#tabInfoContents.style.display = ''; - - this.#tabInfoButtons.children[0]?.click(); - this.#modelInfo.show(); - this.#tabInfoContents.scrollTop = 0; - }); - }; - - /** - * @param {boolean} promptSave - * @returns {Promise} - */ - #tryHideModelInfo = async (promptSave) => { - if (this.#tabInfoContents.style.display !== 'none') { - if (!(await this.#modelInfo.tryHide(promptSave))) { - return false; - } - - this.#closeModelInfoButton.style.display = 'none'; - this.#tabInfoButtons.style.display = 'none'; - this.#tabInfoContents.style.display = 'none'; - - this.#tabManagerButtons.style.display = ''; - this.#tabManagerContents.style.display = ''; - } - return true; - }; - - #updateSidebarButtons = () => { - const managerRect = this.element.getBoundingClientRect(); - const isNarrow = managerRect.width < 768; // TODO: `minWidth` is a magic value - const alwaysShowCompactSidebarControls = - this.#settingsView.elements.settings['sidebar-control-always-compact'] - .checked; - if (isNarrow || alwaysShowCompactSidebarControls) { - this.#sidebarButtonGroup.style.display = 'none'; - this.#sidebarSelect.style.display = ''; - } else { - this.#sidebarButtonGroup.style.display = ''; - this.#sidebarSelect.style.display = 'none'; - } - }; -} - -/** @type {ModelManager | undefined} */ -let instance; - -/** - * @returns {ModelManager} - */ -function getInstance() { - if (!instance) { - instance = new ModelManager(); - } - return instance; -} - -const toggleModelManager = () => { - const modelManager = getInstance(); - const style = modelManager.element.style; - if (style.display === '' || style.display === 'none') { - modelManager.show(); - } else { - modelManager.close(); - } -}; - -app.registerExtension({ - name: 'Comfy.ModelManager', - init() {}, - async setup() { - const cssFileUrl = new URL(import.meta.url).pathname.replace('.js', '.css'); - - $el('link', { - parent: document.head, - rel: 'stylesheet', - href: cssFileUrl, - }); - - app.ui?.menuContainer?.appendChild( - $el('button', { - id: 'comfyui-model-manager-button', - parent: document.querySelector('.comfy-menu'), - textContent: 'Models', - onclick: () => toggleModelManager(), - }), - ); - - // [Beta] mobile menu - app.menu?.settingsGroup?.append( - new ComfyButton({ - icon: 'folder-search', - tooltip: 'Opens model manager', - action: () => toggleModelManager(), - content: 'Model Manager', - popup: getInstance(), - }), - ); - }, -});