diff --git a/README.md b/README.md index a48c842..58ca733 100644 --- a/README.md +++ b/README.md @@ -88,13 +88,13 @@ code --install-extension Fuzionix.devtool-plus Base64 Encoder / Decoder - 1.0.2 + 1.0.3 βœ… URL Encoder - 1.0.1 + 1.1.0 βœ… @@ -274,16 +274,22 @@ code --install-extension Fuzionix.devtool-plus Number Base Convertor 1.0.0 - πŸ†• + βœ… Crontab Generator + 1.0.0 - - 🚧 + πŸ†• Chmod Calculator + 1.0.0 + + πŸ†• + + + User Agent Generator 🚧 @@ -292,7 +298,7 @@ code --install-extension Fuzionix.devtool-plus Docker Compose Generator - 🚧 + πŸ“ diff --git a/media/tools/dark/alarm-clock.svg b/media/tools/dark/alarm-clock.svg new file mode 100644 index 0000000..322e290 --- /dev/null +++ b/media/tools/dark/alarm-clock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/media/tools/dark/id-card-lanyard.svg b/media/tools/dark/id-card-lanyard.svg new file mode 100644 index 0000000..899bef4 --- /dev/null +++ b/media/tools/dark/id-card-lanyard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/media/tools/light/alarm-clock.svg b/media/tools/light/alarm-clock.svg new file mode 100644 index 0000000..122c4f6 --- /dev/null +++ b/media/tools/light/alarm-clock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/media/tools/light/id-card-lanyard.svg b/media/tools/light/id-card-lanyard.svg new file mode 100644 index 0000000..763f0e0 --- /dev/null +++ b/media/tools/light/id-card-lanyard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index aee0317..8cb5758 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,15 @@ { "name": "devtool-plus", - "version": "0.4.0", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "devtool-plus", - "version": "0.4.0", + "version": "0.6.0", "license": "MIT", "dependencies": { "@types/qrcode": "^1.5.5", - "bezier-easing": "^2.1.0", "colord": "^2.9.3", "fast-xml-parser": "^5.2.5", "fs-extra": "^11.3.1", @@ -20,7 +19,7 @@ "mime-db": "^1.53.0", "monaco-editor-core": "^0.52.2", "qrcode": "^1.5.4", - "toml": "^3.0.0", + "smol-toml": "^1.6.1", "uuid": "^11.0.5", "xml2js": "^0.6.2" }, @@ -514,20 +513,22 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -560,20 +561,22 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -601,27 +604,6 @@ "deprecated": "Use @eslint/object-schema instead", "dev": true }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "dev": true, - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -736,6 +718,18 @@ "@lit-labs/ssr-dom-shim": "^1.2.0" } }, + "node_modules/@nodable/entities": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz", + "integrity": "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1053,10 +1047,12 @@ } }, "node_modules/@vscode/test-cli/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -1156,10 +1152,11 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1221,6 +1218,18 @@ "node": ">= 8" } }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -1356,10 +1365,18 @@ } ] }, - "node_modules/bezier-easing": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz", - "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==" + "node_modules/baseline-browser-mapping": { + "version": "2.11.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", + "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } }, "node_modules/binary-extensions": { "version": "2.3.0", @@ -1399,10 +1416,11 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -1426,9 +1444,9 @@ "dev": true }, "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", "dev": true, "funding": [ { @@ -1444,11 +1462,13 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" }, "bin": { "browserslist": "cli.js" @@ -1596,9 +1616,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001692", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz", - "integrity": "sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==", + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", "dev": true, "funding": [ { @@ -1613,7 +1633,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "4.1.2", @@ -1995,10 +2016,11 @@ "dev": true }, "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -2059,10 +2081,11 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.5.83", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.83.tgz", - "integrity": "sha512-LcUDPqSt+V0QmI47XLzZrz5OqILSMGsPFkDYus22rIbgorSvBYEFqq854ltTmUdHkY92FSdAAvsh4jWEULMdfQ==", - "dev": true + "version": "1.5.422", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.422.tgz", + "integrity": "sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==", + "dev": true, + "license": "ISC" }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -2408,10 +2431,11 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2430,10 +2454,11 @@ } }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2534,18 +2559,40 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-xml-builder": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.1.tgz", + "integrity": "sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.6.2", + "xml-naming": "^0.3.0" + } + }, "node_modules/fast-xml-parser": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", - "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.11.1.tgz", + "integrity": "sha512-TBw6K/fxoQGGjCmZDw9w/ZwP3uDcnTM4YH/g+PFRWr8sbe5idXtxNN6vITh4+1ruCZaho6uBFurElsA7F0zzgw==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } ], + "license": "MIT", "dependencies": { - "strnum": "^2.1.0" + "@nodable/entities": "^3.0.0", + "fast-xml-builder": "^1.2.0", + "is-unsafe": "^2.0.0", + "path-expression-matcher": "^1.6.2", + "strnum": "^2.4.2", + "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" @@ -2624,10 +2671,11 @@ } }, "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", - "dev": true + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" }, "node_modules/for-each": { "version": "0.3.3", @@ -2801,14 +2849,16 @@ } }, "node_modules/glob": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", - "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", + "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" @@ -2835,16 +2885,40 @@ "node": ">= 6" } }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "balanced-match": "^4.0.2" }, "engines": { "node": "20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -3567,6 +3641,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-unsafe": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.2.tgz", + "integrity": "sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -3701,9 +3787,20 @@ "dev": true }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -3996,12 +4093,13 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -4092,10 +4190,11 @@ } }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -4205,9 +4304,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -4215,6 +4314,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -4235,10 +4335,14 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/normalize-package-data": { "version": "2.5.0", @@ -4317,10 +4421,11 @@ } }, "node_modules/npm-run-all/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4390,10 +4495,11 @@ } }, "node_modules/npm-run-all/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -4777,6 +4883,21 @@ "node": ">=8" } }, + "node_modules/path-expression-matcher": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", + "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -4833,10 +4954,11 @@ "dev": true }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -4901,9 +5023,9 @@ } }, "node_modules/postcss": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", - "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "dev": true, "funding": [ { @@ -4919,8 +5041,9 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5025,10 +5148,11 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "dev": true, + "license": "MIT", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -5609,10 +5733,11 @@ } }, "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5640,10 +5765,11 @@ } }, "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5849,10 +5975,11 @@ } }, "node_modules/shell-quote": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", - "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5953,6 +6080,18 @@ "node": ">=8" } }, + "node_modules/smol-toml": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", + "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6215,15 +6354,19 @@ } }, "node_modules/strnum": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", - "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.2.tgz", + "integrity": "sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } - ] + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.1" + } }, "node_modules/sucrase": { "version": "3.35.0", @@ -6248,10 +6391,12 @@ } }, "node_modules/sucrase/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -6401,10 +6546,11 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6432,10 +6578,11 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -6482,12 +6629,6 @@ "node": ">=8.0" } }, - "node_modules/toml": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", - "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", - "license": "MIT" - }, "node_modules/ts-api-utils": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", @@ -6649,9 +6790,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", - "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", "dev": true, "funding": [ { @@ -6667,6 +6808,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" @@ -6694,13 +6836,14 @@ "dev": true }, "node_modules/uuid": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", - "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], + "license": "MIT", "bin": { "uuid": "dist/esm/bin/uuid" } @@ -6954,6 +7097,21 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/xml-naming": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", + "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/xml2js": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", @@ -6984,15 +7142,19 @@ } }, "node_modules/yaml": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", - "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true, + "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yargs": { diff --git a/package.json b/package.json index de0b55e..b4fbe8c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "DevTool+", "publisher": "Fuzionix", "description": "An extension that provides common I/O tools directly in code editor", - "version": "0.5.0", + "version": "0.6.0", "repository": { "type": "git", "url": "https://github.com/fuzionix/devtool-plus.git" @@ -186,7 +186,6 @@ }, "dependencies": { "@types/qrcode": "^1.5.5", - "bezier-easing": "^2.1.0", "colord": "^2.9.3", "fast-xml-parser": "^5.2.5", "fs-extra": "^11.3.1", @@ -196,7 +195,7 @@ "mime-db": "^1.53.0", "monaco-editor-core": "^0.52.2", "qrcode": "^1.5.4", - "toml": "^3.0.0", + "smol-toml": "^1.6.1", "uuid": "^11.0.5", "xml2js": "^0.6.2" }, diff --git a/src/constants/tools.ts b/src/constants/tools.ts index 2ab64fe..36775db 100644 --- a/src/constants/tools.ts +++ b/src/constants/tools.ts @@ -4,7 +4,7 @@ export const TOOLS: Tool[] = [ { id: 'base64-encoder', label: 'Base64 Encoder / Decoder', - version: '1.0.2', + version: '1.0.3', category: ToolCategory.Encode, template: '', icon: 'file-digit', @@ -13,7 +13,7 @@ export const TOOLS: Tool[] = [ { id: 'url-encoder', label: 'URL Encoder', - version: '1.0.1', + version: '1.1.0', category: ToolCategory.Encode, template: '', icon: 'link-2', @@ -53,13 +53,12 @@ export const TOOLS: Tool[] = [ category: ToolCategory.Encode, template: '', icon: 'code-xml', - isNew: true, tags: ['html', 'escape', 'entities', 'encode', 'decode', 'text', 'string', 'web'] }, { id: 'color-convertor', label: 'Color Convertor', - version: '1.1.0', + version: '1.2.0', category: ToolCategory.Design, template: '', icon: 'paintbrush-vertical', @@ -218,7 +217,6 @@ export const TOOLS: Tool[] = [ category: ToolCategory.Text, template: '', icon: 'notepad-text', - isNew: true, tags: ['lorem', 'ipsum', 'placeholder', 'text', 'dummy', 'content', 'filler', 'paragraphs', 'words'], }, { @@ -231,13 +229,24 @@ export const TOOLS: Tool[] = [ tags: ['slug', 'url', 'identifier', 'string', 'text', 'format', 'convert', 'title', 'web', 'link', 'seo'], }, { - id: 'qr-code-generator', - label: 'QR Code Generator', + id: 'crontab-generator', + label: 'Crontab Generator', version: '1.0.0', category: ToolCategory.Util, - template: '', - icon: 'qr-code', - tags: ['qr', 'code', 'generator', 'barcode', 'scan', 'url', 'data', 'link'] + template: '', + icon: 'alarm-clock', + isNew: true, + tags: ['crontab', 'generator', 'schedule', 'cron expression', 'time', 'job', 'task', 'automation'] + }, + { + id: 'chmod-calculator', + label: 'Chmod Calculator', + version: '1.0.0', + category: ToolCategory.Util, + template: '', + icon: 'id-card-lanyard', + isNew: true, + tags: ['chmod', 'calculator', 'permissions', 'unix', 'linux', 'file', 'system', 'access', 'mode'] }, { id: 'http-status-code', @@ -248,6 +257,15 @@ export const TOOLS: Tool[] = [ icon: 'server', tags: ['http', 'status', 'code', 'response', 'request', 'web', 'protocol', 'error', 'success', 'client', 'server'] }, + { + id: 'qr-code-generator', + label: 'QR Code Generator', + version: '1.0.0', + category: ToolCategory.Util, + template: '', + icon: 'qr-code', + tags: ['qr', 'code', 'generator', 'barcode', 'scan', 'url', 'data', 'link'] + }, { id: 'data-unit-convertor', label: 'Data Unit Convertor', @@ -269,11 +287,10 @@ export const TOOLS: Tool[] = [ { id: 'number-base-convertor', label: 'Number Base Convertor', - version: '1.0.0', + version: '1.0.1', category: ToolCategory.Util, template: '', icon: 'binary', - isNew: true, tags: ['number', 'base', 'convertor', 'binary', 'decimal', 'hexadecimal', 'octal', 'math'], }, { diff --git a/src/providers/SidePanelProvider.ts b/src/providers/SidePanelProvider.ts index 4dff758..225fd63 100644 --- a/src/providers/SidePanelProvider.ts +++ b/src/providers/SidePanelProvider.ts @@ -141,7 +141,7 @@ export class SidePanelProvider implements vscode.WebviewViewProvider {

The extension will keep evolving. Your feedback is invaluable in shaping its future. Feel free to share your thoughts and suggestions!

-
Version: 0.5.0 beta
+
Version: 0.6.0 beta
diff --git a/src/tools/components/aes-encryption/AesEncryption.ts b/src/tools/components/aes-encryption/AesEncryption.ts index deb6edb..f845b33 100644 --- a/src/tools/components/aes-encryption/AesEncryption.ts +++ b/src/tools/components/aes-encryption/AesEncryption.ts @@ -33,6 +33,11 @@ export class AesEncryption extends BaseTool { @query('#input') private input!: HTMLTextAreaElement; @query('#output') private output!: HTMLTextAreaElement; @query('#file-input') private fileInput!: HTMLInputElement; + @query('#password') private passwordInput!: HTMLInputElement; + + firstUpdated() { + setTimeout(() => this.passwordInput?.focus(), 0); + } private styles = css` ${BaseTool.styles} diff --git a/src/tools/components/ascii-encoder/AsciiEncoder.ts b/src/tools/components/ascii-encoder/AsciiEncoder.ts index 0e59771..c0974dc 100644 --- a/src/tools/components/ascii-encoder/AsciiEncoder.ts +++ b/src/tools/components/ascii-encoder/AsciiEncoder.ts @@ -1,5 +1,5 @@ import { html, css } from 'lit'; -import { customElement, state } from 'lit/decorators.js'; +import { customElement, query, state } from 'lit/decorators.js'; import { BaseTool } from '../../base/BaseTool'; import { adjustTextareaHeight, @@ -14,6 +14,12 @@ export class AsciiEncoder extends BaseTool { @state() private alert: { type: 'error' | 'warning'; message: string } | null = null; @state() private isCopied = false; + @query('#input') inputElement!: HTMLTextAreaElement; + + firstUpdated() { + setTimeout(() => this.inputElement?.focus(), 0); + } + private styles = css` ${BaseTool.styles} /* Minimal local styling if needed. */ diff --git a/src/tools/components/base64-encoder/Base64Encoder.ts b/src/tools/components/base64-encoder/Base64Encoder.ts index db5970e..c2ff872 100644 --- a/src/tools/components/base64-encoder/Base64Encoder.ts +++ b/src/tools/components/base64-encoder/Base64Encoder.ts @@ -6,6 +6,7 @@ import { renderCopyButton } from '../../../utils/util'; import mimeDb from 'mime-db'; +import { detectMimeType } from './mimeUtils'; @customElement('base64-encoder') export class Base64Encoder extends BaseTool { @@ -23,10 +24,14 @@ export class Base64Encoder extends BaseTool { @state() private isShowUriHeader = false; @state() private decodedFileSize = 0; - @query('#input') input!: HTMLTextAreaElement; - @query('#output') output!: HTMLTextAreaElement; + @query('#input') inputElement!: HTMLTextAreaElement; + @query('#output') outputElement!: HTMLTextAreaElement; @query('#file-input') fileInput!: HTMLInputElement; + firstUpdated() { + setTimeout(() => this.inputElement?.focus(), 0); + } + private styles = css` ${BaseTool.styles} /* Minimal local styling if needed. */ @@ -177,8 +182,8 @@ export class Base64Encoder extends BaseTool { protected updated(changedProperties: Map): void { super.updated(changedProperties); - if (this.output && changedProperties.has('outputText')) { - adjustTextareaHeight(this.output); + if (this.outputElement && changedProperties.has('outputText')) { + adjustTextareaHeight(this.outputElement); } } @@ -253,14 +258,41 @@ export class Base64Encoder extends BaseTool { // Handle data:URI format (e.g. data:image/png;base64,...) if (base64Data.startsWith('data:')) { - const [header, content] = base64Data.split(','); + const commaIndex = base64Data.indexOf(','); + if (commaIndex === -1) { + this.hideOutput(); + this.alert = { + type: 'error', + message: 'Invalid data URI format: missing comma separator' + }; + return; + } + + const header = base64Data.substring(0, commaIndex); + const content = base64Data.substring(commaIndex + 1); + + // Check if the data URI explicitly contains base64 encoding + if (!header.includes(';base64')) { + this.hideOutput(); + this.alert = { + type: 'error', + message: 'Data URI must contain ";base64" for Base64 decoding. Plain data URIs are not supported.' + }; + return; + } + base64Data = content; this.decodedMimeType = header.split(';')[0].split(':')[1]; - this.decodedData = this.base64ToUint8Array(content); + if (this.decodedMimeType.startsWith('text/plain')) { + const bytes = this.base64ToUint8Array(content); + this.decodedData = new TextDecoder('utf-8').decode(bytes); + } else { + this.decodedData = this.base64ToUint8Array(content); + } } else { try { this.decodedData = this.base64ToUint8Array(base64Data); - this.decodedMimeType = this.detectMimeType(this.decodedData); + this.decodedMimeType = detectMimeType(this.decodedData); if (this.decodedMimeType === 'text/plain') { // Attempt to decode as Base64 @@ -334,118 +366,6 @@ export class Base64Encoder extends BaseTool { return bytes; } - /** - * Detects the MIME type of a file based on its binary signature/magic numbers - * @param data - Uint8Array containing the file's binary data to analyze - * @returns string - The detected MIME type - */ - private detectMimeType(data: Uint8Array): string { - if (data.length === 0) { - return 'text/plain'; - } - - // Define file signatures with variable lengths for better accuracy - // Format: [mimeType, [signature bytes], minBytesRequired] - const signatures: Array<[string, number[], number]> = [ - // Images - ['image/jpeg', [0xFF, 0xD8, 0xFF], 3], - ['image/png', [0x89, 0x50, 0x4E, 0x47], 4], - ['image/gif', [0x47, 0x49, 0x46, 0x38], 4], - ['image/webp', [0x52, 0x49, 0x46, 0x46], 4], - ['image/bmp', [0x42, 0x4D], 2], - ['image/tiff', [0x49, 0x49, 0x2A, 0x00], 4], // Little-endian - ['image/tiff', [0x4D, 0x4D, 0x00, 0x2A], 4], // Big-endian - ['image/svg+xml', [0x3C, 0x3F, 0x78, 0x6D], 4], - ['image/x-icon', [0x00, 0x00, 0x01, 0x00], 4], - - // Documents & Archives - ['application/pdf', [0x25, 0x50, 0x44, 0x46], 4], // %PDF - ['application/zip', [0x50, 0x4B, 0x03, 0x04], 4], // PK.. (zip, jar, docx, xlsx, etc) - ['application/zip', [0x50, 0x4B, 0x05, 0x06], 4], // PK.. (empty zip) - ['application/zip', [0x50, 0x4B, 0x07, 0x08], 4], // PK.. (spanned zip) - ['application/x-rar-compressed', [0x52, 0x61, 0x72, 0x21, 0x1A, 0x07], 6], - ['application/x-7z-compressed', [0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C], 6], - ['application/gzip', [0x1F, 0x8B, 0x08], 3], - ['application/x-tar', [0x75, 0x73, 0x74, 0x61, 0x72], 5], - ['application/vnd.openxmlformats-officedocument.wordprocessingml.document', [0x50, 0x4B, 0x03, 0x04], 4], - ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', [0x50, 0x4B, 0x03, 0x04], 4], - ['application/vnd.openxmlformats-officedocument.presentationml.presentation', [0x50, 0x4B, 0x03, 0x04], 4], - ['application/xml', [0x3C, 0x3F, 0x78, 0x6D], 4], - - // Text formats - ['application/json', [0x7B], 1], - ['application/json', [0x5B], 1], - ['text/html', [0x3C, 0x21, 0x44, 0x4F], 4], - ['text/html', [0x3C, 0x48, 0x54, 0x4D], 4], - ['text/html', [0x3C, 0x68, 0x74, 0x6D], 4], - ['text/css', [0x2F, 0x2A], 2], - ['application/javascript', [0x2F, 0x2F], 2], - ['text/plain', [0xEF, 0xBB, 0xBF], 3], - - // Audio - ['audio/mpeg', [0xFF, 0xFB], 2], // MP3 (MPEG-1) - ['audio/mpeg', [0xFF, 0xFA], 2], // MP3 (MPEG-2) - ['audio/mpeg', [0xFF, 0xF3], 2], // MP3 (MPEG-2.5) - ['audio/mpeg', [0x49, 0x44, 0x33], 3], // ID3 tag - ['audio/wav', [0x52, 0x49, 0x46, 0x46], 4], - ['audio/flac', [0x66, 0x4C, 0x61, 0x43], 4], - ['audio/aac', [0xFF, 0xF1], 2], - ['audio/ogg', [0x4F, 0x67, 0x67, 0x53], 4], - - // Video - ['video/mp4', [0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70], 8], - ['video/quicktime', [0x00, 0x00, 0x00, 0x14, 0x66, 0x74, 0x79, 0x70], 8], - ['video/x-msvideo', [0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x41, 0x56, 0x49], 11], - ['video/x-matroska', [0x1A, 0x45, 0xDF, 0xA3], 4], - ['video/mpeg', [0x00, 0x00, 0x01, 0xB3], 4], - ['video/x-flv', [0x46, 0x4C, 0x56, 0x01], 4], - ['video/quicktime', [0x66, 0x74, 0x79, 0x70, 0x71, 0x74, 0x20], 7], - - // Executables & System - ['application/x-msdownload', [0x4D, 0x5A], 2], - ['application/x-mach-binary', [0xFE, 0xED, 0xFA, 0xCE], 4], - ['application/x-mach-binary', [0xFE, 0xED, 0xFA, 0xCF], 4], - ['application/x-executable', [0x7F, 0x45, 0x4C, 0x46], 4], - ['application/x-sharedlib', [0x7F, 0x45, 0x4C, 0x46], 4], - - // Fonts - ['font/ttf', [0x00, 0x01, 0x00, 0x00], 4], - ['font/otf', [0x4F, 0x54, 0x54, 0x4F], 4], - ['font/woff', [0x77, 0x4F, 0x46, 0x46], 4], - ['font/woff2', [0x77, 0x4F, 0x46, 0x32], 4], - - // Database & Data - ['application/x-sqlite3', [0x53, 0x51, 0x4C, 0x69], 4], - ['application/vnd.google-earth.kml+xml', [0x3C, 0x3F, 0x78, 0x6D], 4], - ]; - - for (const [mimeType, signature, minBytes] of signatures) { - if (data.length >= minBytes) { - // Handle variable-length signatures and masks - let match = true; - for (let i = 0; i < signature.length; i++) { - if (data[i] !== signature[i]) { - match = false; - break; - } - } - if (match) { - return mimeType; - } - } - } - - // Secondary detection: Check for tar format (signature at offset 257) - if (data.length > 262) { - const tarSignature = [0x75, 0x73, 0x74, 0x61, 0x72]; - if (tarSignature.every((byte, i) => data[257 + i] === byte)) { - return 'application/x-tar'; - } - } - - return 'text/plain'; - } - private triggerFileInput(): void { this.fileInput.click(); } @@ -491,20 +411,20 @@ export class Base64Encoder extends BaseTool { return new Promise((resolve, reject) => { const reader = new FileReader(); - reader.onload = async (e) => { + reader.onload = (e) => { try { const result = e.target?.result; - if (typeof result === 'string') { - this.outputText = btoa(result); - } else if (result instanceof ArrayBuffer) { - const bytes = new Uint8Array(result); - let binary = ''; - // Convert byte array to binary string - bytes.forEach(byte => binary += String.fromCharCode(byte)); - this.outputText = btoa(binary); + if (!(result instanceof ArrayBuffer)) { + reject(new Error('Failed to read file as binary data.')); + return; } + + this.outputText = this.arrayBufferToBase64(result); this.uriHeader = `data:${this.inputMimeType};base64,`; - this.outputText = this.isShowUriHeader ? `${this.uriHeader}${this.outputText}` : this.outputText; + this.outputText = this.isShowUriHeader + ? `${this.uriHeader}${this.outputText}` + : this.outputText; + this.requestUpdate(); resolve(); } catch (error) { @@ -513,51 +433,10 @@ export class Base64Encoder extends BaseTool { }; reader.onerror = () => reject(reader.error); - if (file.type.startsWith('text/')) { - reader.readAsText(file); - } else { - reader.readAsArrayBuffer(file); - } + reader.readAsArrayBuffer(file); }); } - private clearAll(): void { - this.inputText = ''; - this.outputText = ''; - this.fileName = ''; - this.file = null; - this.fileInput.value = ''; - this.outputMode = 'text'; - this.input.style.height = `28px`; - this.uriHeader = ''; - this.alert = null; - this.decodedFileSize = 0; - this.renderOutput(); - this.requestUpdate(); - } - - private async copyToClipboard() { - if (!this.outputText) { - return; - } - - try { - await navigator.clipboard.writeText(this.outputText); - this.isCopied = true; - setTimeout(() => { - this.isCopied = false; - }, 2000); - } catch (err) { - this.isCopied = false; - } - } - - private hideOutput() { - this.outputText = ''; - this.outputMode = 'error'; - this.requestUpdate(); - } - private getBase64String(base64Data: string): { base64: string; mimeType: string } { if (base64Data.startsWith('data:')) { const [header, content] = base64Data.split(','); @@ -567,6 +446,19 @@ export class Base64Encoder extends BaseTool { return { base64: base64Data, mimeType: this.decodedMimeType }; } + private arrayBufferToBase64(buffer: ArrayBuffer): string { + const bytes = new Uint8Array(buffer); + const chunkSize = 0x8000; // avoid call stack/memory issues on larger files + let binary = ''; + + for (let i = 0; i < bytes.length; i += chunkSize) { + const chunk = bytes.subarray(i, i + chunkSize); + binary += String.fromCharCode(...chunk); + } + + return btoa(binary); + } + private getDownloadButtonText(): string { const extension = this.getFileExtension(); if (extension) { @@ -605,4 +497,41 @@ export class Base64Encoder extends BaseTool { } return ''; } + + private clearAll(): void { + this.inputText = ''; + this.outputText = ''; + this.fileName = ''; + this.file = null; + this.fileInput.value = ''; + this.outputMode = 'text'; + this.inputElement.style.height = `28px`; + this.uriHeader = ''; + this.alert = null; + this.decodedFileSize = 0; + this.renderOutput(); + this.requestUpdate(); + } + + private async copyToClipboard() { + if (!this.outputText) { + return; + } + + try { + await navigator.clipboard.writeText(this.outputText); + this.isCopied = true; + setTimeout(() => { + this.isCopied = false; + }, 2000); + } catch (err) { + this.isCopied = false; + } + } + + private hideOutput() { + this.outputText = ''; + this.outputMode = 'error'; + this.requestUpdate(); + } } \ No newline at end of file diff --git a/src/tools/components/base64-encoder/mimeUtils.ts b/src/tools/components/base64-encoder/mimeUtils.ts new file mode 100644 index 0000000..045250f --- /dev/null +++ b/src/tools/components/base64-encoder/mimeUtils.ts @@ -0,0 +1,140 @@ +/** + * Detects the MIME type of a file based on its binary signature/magic numbers + * @param data - Uint8Array containing the file's binary data to analyze + * @returns string - The detected MIME type + */ +export function detectMimeType(data: Uint8Array): string { + if (!data || data.length === 0) { + return 'application/octet-stream'; + } + + // RIFF family (must check subtype at offset 8) + // RIFF....WEBP / WAVE / AVI + if (hasAsciiAt(data, 0, 'RIFF') && data.length >= 12) { + const riffType = readAscii(data, 8, 4); + if (riffType === 'WEBP') return 'image/webp'; + if (riffType === 'WAVE') return 'audio/wav'; + if (riffType === 'AVI ') return 'video/x-msvideo'; + } + + // ISO BMFF / MP4 family + // Typical structure: [size:4][ftyp:4][major_brand:4]... + if (data.length >= 12 && hasAsciiAt(data, 4, 'ftyp')) { + const majorBrand = readAscii(data, 8, 4); + + // Known brands (not exhaustive but robust for mp4/audio mp4/quicktime) + const videoMp4Brands = new Set([ + 'isom', 'iso2', 'iso3', 'iso4', + 'mp41', 'mp42', + 'avc1', 'dash', + '3gp4', '3gp5', '3gp6', + 'F4V ', 'M4V ' + ]); + + const audioMp4Brands = new Set([ + 'M4A ', 'M4B ', 'M4P ', 'f4a ', 'f4b ' + ]); + + if (majorBrand === 'qt ') return 'video/quicktime'; + if (videoMp4Brands.has(majorBrand)) return 'video/mp4'; + if (audioMp4Brands.has(majorBrand)) return 'audio/mp4'; + + // Unknown BMFF brand: still safer than text/plain + return 'application/octet-stream'; + } + + // High-confidence signatures + if (matchBytes(data, [0xFF, 0xD8, 0xFF])) return 'image/jpeg'; + if (matchBytes(data, [0x89, 0x50, 0x4E, 0x47])) return 'image/png'; + if (matchBytes(data, [0x47, 0x49, 0x46, 0x38])) return 'image/gif'; + if (matchBytes(data, [0x42, 0x4D])) return 'image/bmp'; + if (matchBytes(data, [0x00, 0x00, 0x01, 0x00])) return 'image/x-icon'; + + if (matchBytes(data, [0x25, 0x50, 0x44, 0x46])) return 'application/pdf'; + if (matchBytes(data, [0x50, 0x4B, 0x03, 0x04]) || matchBytes(data, [0x50, 0x4B, 0x05, 0x06]) || matchBytes(data, [0x50, 0x4B, 0x07, 0x08])) return 'application/zip'; + if (matchBytes(data, [0x1F, 0x8B, 0x08])) return 'application/gzip'; + if (matchBytes(data, [0x52, 0x61, 0x72, 0x21, 0x1A, 0x07])) return 'application/x-rar-compressed'; + if (matchBytes(data, [0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C])) return 'application/x-7z-compressed'; + + if (matchBytes(data, [0x49, 0x44, 0x33])) return 'audio/mpeg'; + if (matchBytes(data, [0xFF, 0xFB]) || matchBytes(data, [0xFF, 0xFA]) || matchBytes(data, [0xFF, 0xF3])) return 'audio/mpeg'; + if (matchBytes(data, [0x66, 0x4C, 0x61, 0x43])) return 'audio/flac'; + if (matchBytes(data, [0x4F, 0x67, 0x67, 0x53])) return 'audio/ogg'; + + if (matchBytes(data, [0x1A, 0x45, 0xDF, 0xA3])) return 'video/x-matroska'; + if (matchBytes(data, [0x46, 0x4C, 0x56, 0x01])) return 'video/x-flv'; + + if (matchBytes(data, [0x7F, 0x45, 0x4C, 0x46])) return 'application/x-executable'; + if (matchBytes(data, [0x4D, 0x5A])) return 'application/x-msdownload'; + + if (isLikelyTextContent(data)) { + return 'text/plain'; + } + + return 'application/octet-stream'; +} + +function matchBytes(data: Uint8Array, signature: number[], offset = 0): boolean { + if (data.length < offset + signature.length) return false; + for (let i = 0; i < signature.length; i++) { + if (data[offset + i] !== signature[i]) return false; + } + return true; +} + +function hasAsciiAt(data: Uint8Array, offset: number, text: string): boolean { + if (data.length < offset + text.length) return false; + for (let i = 0; i < text.length; i++) { + if (data[offset + i] !== text.charCodeAt(i)) return false; + } + return true; +} + +function readAscii(data: Uint8Array, offset: number, length: number): string { + if (data.length < offset + length) return ''; + let out = ''; + for (let i = 0; i < length; i++) { + out += String.fromCharCode(data[offset + i]); + } + return out; +} + +function isLikelyTextContent(data: Uint8Array): boolean { + if (data.length === 0) return false; + + const hasUtf8Bom = data.length >= 3 && data[0] === 0xEF && data[1] === 0xBB && data[2] === 0xBF; + const hasUtf16LEBom = data.length >= 2 && data[0] === 0xFF && data[1] === 0xFE; + const hasUtf16BEBom = data.length >= 2 && data[0] === 0xFE && data[1] === 0xFF; + + // Fast binary guard: null bytes are strong binary indicator (unless UTF-16 BOM exists) + if (!hasUtf16LEBom && !hasUtf16BEBom) { + const sample = data.subarray(0, Math.min(data.length, 4096)); + for (let i = 0; i < sample.length; i++) { + if (sample[i] === 0x00) return false; + } + } + + try { + let decoded = ''; + + if (hasUtf16LEBom) { + decoded = new TextDecoder('utf-16le', { fatal: false }).decode(data); + } else if (hasUtf16BEBom) { + const beData = data.subarray(2); // skip BOM + const swapped = new Uint8Array(beData.length); + for (let i = 0; i + 1 < beData.length; i += 2) { + swapped[i] = beData[i + 1]; + swapped[i + 1] = beData[i]; + } + decoded = new TextDecoder('utf-16le', { fatal: false }).decode(swapped); + } else { + decoded = new TextDecoder('utf-8', { fatal: true }).decode(hasUtf8Bom ? data.subarray(3) : data); + } + + if (!decoded) return false; + + return true; + } catch { + return false; + } +} \ No newline at end of file diff --git a/src/tools/components/chmod-calculator/ChmodCalculator.ts b/src/tools/components/chmod-calculator/ChmodCalculator.ts new file mode 100644 index 0000000..02a8c4d --- /dev/null +++ b/src/tools/components/chmod-calculator/ChmodCalculator.ts @@ -0,0 +1,366 @@ +import { css, html } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { BaseTool } from '../../base/BaseTool'; + +type Subject = 'owner' | 'group' | 'public'; +type Permission = 'read' | 'write' | 'execute'; + +interface PermissionSet { + read: boolean; + write: boolean; + execute: boolean; +} + +interface PermissionMatrix { + owner: PermissionSet; + group: PermissionSet; + public: PermissionSet; +} + +type AlertState = { type: 'error' | 'warning'; message: string } | null; + +@customElement('chmod-calculator') +export class ChmodCalculator extends BaseTool { + private static readonly SUBJECTS: Subject[] = ['owner', 'group', 'public']; + private static readonly PERMISSIONS: Permission[] = ['read', 'write', 'execute']; + + @state() + private permissions: PermissionMatrix = { + owner: { read: true, write: true, execute: false }, + group: { read: true, write: false, execute: false }, + public: { read: true, write: false, execute: false } + }; + + @state() private octalInput = '644'; + @state() private symbolicInput = 'rw-r--r--'; + @state() private alert: AlertState = null; + + private readonly styles = css` + ${BaseTool.styles} + + .chmod-grid { + display: grid; + grid-template-columns: 88px repeat(3, 1fr); + gap: 6px; + align-items: center; + } + + .grid-header { + font-size: 0.75rem; + text-align: center; + } + + .grid-row-label { + font-size: 0.75rem; + } + + .perm-btn { + min-height: 30px; + border: 1px solid var(--vscode-panel-border); + border-radius: 2px; + background: var(--vscode-panel-background); + color: var(--vscode-foreground); + cursor: pointer; + transition: background-color 100ms ease-in-out, border-color 100ms ease-in-out; + } + + .perm-btn:hover { + background: var(--vscode-toolbar-hoverBackground); + } + + .perm-btn[aria-pressed='true'] { + background: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border-color: var(--vscode-button-background); + } + + .output-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + margin-top: 16px; + } + + .output-label { + font-size: 0.75rem; + opacity: 0.75; + margin-bottom: 4px; + } + + .output-value { + font-family: var(--vscode-editor-font-family); + letter-spacing: 0.02em; + } + `; + + protected renderTool() { + return html` + +
+

+ Chmod is a command in Unix-like operating systems used to change file and directory permissions. +

+
+ +
+
+ ${ChmodCalculator.PERMISSIONS.map((permission) => html` +
+
+ ${this.getPermissionWeight(permission)} +
+
${this.permissionLabel(permission)}
+
+ `)} + + ${ChmodCalculator.SUBJECTS.map((subject) => html` +
${this.subjectLabel(subject)}
+ ${ChmodCalculator.PERMISSIONS.map((permission) => { + const active = this.permissions[subject][permission]; + return html` + + `; + })} + `)} +
+ + ${this.alert ? html` + + ` : ''} + +
+
+
Octal
+ +
+ +
+
Symbolic
+ +
+
+
+ `; + } + + private togglePermission(subject: Subject, permission: Permission): void { + this.permissions = { + ...this.permissions, + [subject]: { + ...this.permissions[subject], + [permission]: !this.permissions[subject][permission] + } + }; + + this.syncOutputsFromPermissions(); + this.alert = null; + } + + private handleOctalInput(event: Event): void { + const value = (event.target as HTMLInputElement).value.trim(); + this.octalInput = value; + + if (value.length === 0) { + this.alert = { + type: 'warning', + message: 'Octal permission is empty. Use 3 digits from 0 to 7 (e.g. 755).' + }; + return; + } + + const parsed = this.parseOctalPermission(value); + if (parsed) { + this.permissions = parsed; + this.symbolicInput = this.permissionsToSymbolic(parsed); + this.alert = null; + return; + } + + if (/^[0-7]{1,2}$/.test(value)) { + this.alert = { + type: 'warning', + message: 'Octal permission is incomplete. Please provide exactly 3 digits.' + }; + return; + } + + this.alert = { + type: 'error', + message: 'Invalid octal permission. Use exactly 3 digits, each between 0 and 7 (e.g. 421, 644, 755).' + }; + } + + private handleSymbolicInput(event: Event): void { + const rawValue = (event.target as HTMLInputElement).value.trim().toLowerCase(); + this.symbolicInput = rawValue; + + if (rawValue.length === 0) { + this.alert = { + type: 'warning', + message: 'Symbolic permission is empty. Use 9 characters such as rwxr-xr-x.' + }; + return; + } + + const normalized = this.normalizeSymbolicValue(rawValue); + const parsed = this.parseSymbolicPermission(normalized); + + if (parsed) { + this.permissions = parsed; + this.octalInput = this.permissionsToOctal(parsed); + this.symbolicInput = normalized; + this.alert = null; + return; + } + + if (normalized.length < 9) { + this.alert = { + type: 'warning', + message: 'Symbolic permission is incomplete. Please provide 9 characters, e.g. r---w---x.' + }; + return; + } + + this.alert = { + type: 'error', + message: 'Invalid symbolic permission. Use 9 chars with r/w/x or -, e.g. rw-r--r-- or r---w---x.' + }; + } + + private syncOutputsFromPermissions(): void { + this.octalInput = this.permissionsToOctal(this.permissions); + this.symbolicInput = this.permissionsToSymbolic(this.permissions); + } + + private permissionsToOctal(matrix: PermissionMatrix): string { + const owner = this.toOctalDigit(matrix.owner); + const group = this.toOctalDigit(matrix.group); + const publicValue = this.toOctalDigit(matrix.public); + return `${owner}${group}${publicValue}`; + } + + private permissionsToSymbolic(matrix: PermissionMatrix): string { + return [ + this.toSymbolicTriplet(matrix.owner), + this.toSymbolicTriplet(matrix.group), + this.toSymbolicTriplet(matrix.public) + ].join(''); + } + + private parseOctalPermission(value: string): PermissionMatrix | null { + if (!/^[0-7]{3}$/.test(value)) { + return null; + } + + const [ownerDigit, groupDigit, publicDigit] = value.split('').map(Number); + + return { + owner: this.permissionSetFromDigit(ownerDigit), + group: this.permissionSetFromDigit(groupDigit), + public: this.permissionSetFromDigit(publicDigit) + }; + } + + private parseSymbolicPermission(value: string): PermissionMatrix | null { + if (!/^[r-][w-][x-][r-][w-][x-][r-][w-][x-]$/.test(value)) { + return null; + } + + return { + owner: this.permissionSetFromTriplet(value.slice(0, 3)), + group: this.permissionSetFromTriplet(value.slice(3, 6)), + public: this.permissionSetFromTriplet(value.slice(6, 9)) + }; + } + + private normalizeSymbolicValue(value: string): string { + // Accept optional file type prefix (e.g. -rwxr-xr-x, drwxr-xr-x) + if (/^[\-dcbpls][rwx-]{9}$/.test(value)) { + return value.slice(1); + } + return value; + } + + private permissionSetFromDigit(digit: number): PermissionSet { + return { + read: (digit & 4) !== 0, + write: (digit & 2) !== 0, + execute: (digit & 1) !== 0 + }; + } + + private permissionSetFromTriplet(triplet: string): PermissionSet { + return { + read: triplet[0] === 'r', + write: triplet[1] === 'w', + execute: triplet[2] === 'x' + }; + } + + private getPermissionWeight(permission: Permission): number { + if (permission === 'read') return 4; + if (permission === 'write') return 2; + return 1; + } + + private toOctalDigit(permissionSet: PermissionSet): number { + let value = 0; + if (permissionSet.read) value += 4; + if (permissionSet.write) value += 2; + if (permissionSet.execute) value += 1; + return value; + } + + private toSymbolicTriplet(permissionSet: PermissionSet): string { + const read = permissionSet.read ? 'r' : '-'; + const write = permissionSet.write ? 'w' : '-'; + const execute = permissionSet.execute ? 'x' : '-'; + return `${read}${write}${execute}`; + } + + private subjectLabel(subject: Subject): string { + if (subject === 'owner') return 'Owner'; + if (subject === 'group') return 'Group'; + return 'Public'; + } + + private permissionLabel(permission: Permission): string { + if (permission === 'read') return 'Read'; + if (permission === 'write') return 'Write'; + return 'Execute'; + } + + private permissionShortLabel(permission: Permission): string { + if (permission === 'read') return 'R'; + if (permission === 'write') return 'W'; + return 'X'; + } +} \ No newline at end of file diff --git a/src/tools/components/crontab-generator/CrontabGenerator.ts b/src/tools/components/crontab-generator/CrontabGenerator.ts new file mode 100644 index 0000000..2a60636 --- /dev/null +++ b/src/tools/components/crontab-generator/CrontabGenerator.ts @@ -0,0 +1,846 @@ +import { css, html } from 'lit'; +import { customElement, query, state } from 'lit/decorators.js'; +import { BaseTool } from '../../base/BaseTool'; +import { adjustTextareaHeight } from '../../../utils/util'; + +type AlertKind = 'error' | 'warning'; + +interface AlertState { + type: AlertKind; + message: string; +} + +interface ParsedField { + raw: string; + values: number[]; + set: Set; + any: boolean; +} + +interface ParsedCron { + rawFields: [string, string, string, string, string]; + minute: ParsedField; + hour: ParsedField; + dayOfMonth: ParsedField; + month: ParsedField; + dayOfWeek: ParsedField; +} + +interface CommonExample { + expression: string; + description: string; +} + +interface CronFieldInfo { + field: string; + range: string; + meaning: string; +} + +@customElement('crontab-generator') +export class CrontabGenerator extends BaseTool { + private static readonly MONTH_NAME_MAP: Record = { + JAN: 1, + FEB: 2, + MAR: 3, + APR: 4, + MAY: 5, + JUN: 6, + JUL: 7, + AUG: 8, + SEP: 9, + OCT: 10, + NOV: 11, + DEC: 12 + }; + + private static readonly WEEKDAY_NAME_MAP: Record = { + SUN: 0, + MON: 1, + TUE: 2, + WED: 3, + THU: 4, + FRI: 5, + SAT: 6 + }; + + private static readonly MONTH_LABELS: string[] = [ + 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' + ]; + + private static readonly WEEKDAY_LABELS: string[] = [ + 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' + ]; + + private static readonly COMMON_EXAMPLES: CommonExample[] = [ + { expression: '* * * * *', description: 'Every minute.' }, + { expression: '*/5 * * * *', description: 'Every 5 minutes.' }, + { expression: '0 * * * *', description: 'At minute 0 of every hour.' }, + { expression: '30 9 * * 1-5', description: 'At 09:30 on weekdays (Mon-Fri).' }, + { expression: '0 0 * * *', description: 'Every day at midnight.' }, + { expression: '0 0 1 * *', description: 'At midnight on the 1st of each month.' }, + ]; + + private static readonly FIELD_INFO: CronFieldInfo[] = [ + { field: 'Minute', range: '0-59', meaning: 'Which minute of the hour to run.' }, + { field: 'Hour', range: '0-23', meaning: 'Which hour of the day to run.' }, + { field: 'Day of month', range: '1-31', meaning: 'Which day number in the month to run.' }, + { field: 'Month', range: '1-12 or JAN-DEC', meaning: 'Which month to run.' }, + { field: 'Day of week', range: '0-7 or SUN-SAT', meaning: 'Which weekday to run (0 or 7 = Sunday).' } + ]; + + private readonly styles = css` + ${BaseTool.styles} + + .summary-card { + background-color: var(--vscode-panel-background); + box-shadow: inset 0 0 0 1px var(--vscode-panel-border); + border-radius: 2px; + padding: 8px 10px; + } + + .next-at-value { + font-family: var(--vscode-editor-font-family); + } + + .example-item { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 6px 8px; + border-radius: 2px; + border: 1px solid var(--vscode-panel-border); + background-color: var(--vscode-panel-background); + color: var(--vscode-foreground); + cursor: pointer; + text-align: left; + transition: background-color 100ms ease-in-out, border-color 100ms ease-in-out; + } + + .example-item:hover { + background-color: var(--vscode-toolbar-hoverBackground); + } + + .example-item.is-active { + border-color: var(--vscode-focusBorder); + background-color: var(--vscode-list-activeSelectionBackground); + color: var(--vscode-list-activeSelectionForeground); + } + + .example-cron { + font-family: var(--vscode-editor-font-family); + font-size: 0.75rem; + white-space: nowrap; + } + + .example-desc { + font-size: 0.75rem; + opacity: 0.85; + text-align: right; + } + + .field-row { + display: grid; + grid-template-columns: 88px 96px 1fr; + gap: 8px; + align-items: center; + min-height: 28px; + margin-bottom: 6px; + padding: 6px 8px; + border-radius: 2px; + background-color: var(--vscode-panel-background); + box-shadow: inset 0 0 0 1px var(--vscode-panel-border); + } + + .field-label { + font-size: 0.75rem; + color: var(--vscode-descriptionForeground); + } + + .field-range { + font-family: var(--vscode-editor-font-family); + font-size: 0.75rem; + opacity: 0.9; + } + + .field-meaning { + font-size: 0.75rem; + opacity: 0.9; + } + `; + + @state() private input = '*/5 * * * *'; + @state() private humanReadable = ''; + @state() private nextAt = 'β€”'; + @state() private alert: AlertState | null = null; + + @query('#cron-input') private inputArea!: HTMLTextAreaElement; + + constructor() { + super(); + this.evaluateExpression(); + } + + protected firstUpdated(): void { + if (this.inputArea) { + adjustTextareaHeight(this.inputArea); + setTimeout(() => this.inputArea?.focus(), 0); + } + } + + protected renderTool() { + return html` + +
+

Crontab is a time-based job scheduler in Unix-like operating systems. It allows users to schedule tasks to run automatically at specified times and intervals.

+
+ +
+ + +
+ + + +
+
+ + ${this.alert ? html` + + ` : ''} + +
+
Human readable
+
${this.humanReadable}
+
+ +
+
+ Next at + ${this.nextAt} +
+
+ +
+
Common Examples
+
+ ${CrontabGenerator.COMMON_EXAMPLES.map((example) => html` + + `)} +
+
+ +
+
Cron Format
+ ${CrontabGenerator.FIELD_INFO.map((info) => html` +
+ ${info.field} + ${info.range} + ${info.meaning} +
+ `)} +

+ Special characters: + * any, + , list, + - range, + / step. +

+
+
+ `; + } + + private handleInput(event: Event): void { + const target = event.target as HTMLTextAreaElement; + this.input = target.value; + adjustTextareaHeight(this.inputArea); + this.evaluateExpression(); + } + + private clearAll(): void { + this.input = ''; + this.humanReadable = 'Enter a cron expression to generate description.'; + this.nextAt = 'β€”'; + this.alert = null; + + if (this.inputArea) { + this.inputArea.style.height = '28px'; + } + } + + private applyExample(expression: string): void { + this.input = expression; + this.evaluateExpression(); + if (this.inputArea) { + this.inputArea.value = expression; + adjustTextareaHeight(this.inputArea); + } + } + + private evaluateExpression(): void { + const expression = this.input.trim(); + if (!expression) { + this.humanReadable = 'Enter a cron expression to generate description.'; + this.nextAt = 'β€”'; + this.alert = null; + return; + } + + try { + const parsed = this.parseCronExpression(expression); + this.humanReadable = this.buildHumanReadable(parsed); + + const nextRun = this.findNextRun(parsed, new Date()); + if (nextRun) { + this.nextAt = this.formatDateTime(nextRun); + this.alert = null; + } else { + this.nextAt = 'Not found'; + this.alert = { + type: 'warning', + message: 'No next execution found in the search window.' + }; + } + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Invalid cron expression.'; + this.humanReadable = 'Unable to parse expression.'; + this.nextAt = 'β€”'; + this.alert = { + type: 'error', + message + }; + } + } + + private parseCronExpression(expression: string): ParsedCron { + const normalized = this.expandAlias(expression); + const parts = normalized.trim().split(/\s+/); + + if (parts.length !== 5) { + throw new Error('Cron expression must contain exactly 5 fields.'); + } + + const minute = this.parseField(parts[0], 0, 59); + const hour = this.parseField(parts[1], 0, 23); + const dayOfMonth = this.parseField(parts[2], 1, 31); + const month = this.parseField(parts[3], 1, 12, CrontabGenerator.MONTH_NAME_MAP); + const dayOfWeek = this.parseField(parts[4], 0, 7, CrontabGenerator.WEEKDAY_NAME_MAP, true); + + return { + rawFields: [parts[0], parts[1], parts[2], parts[3], parts[4]], + minute, + hour, + dayOfMonth, + month, + dayOfWeek + }; + } + + private expandAlias(expression: string): string { + const alias = expression.trim().toLowerCase(); + const aliasMap: Record = { + '@yearly': '0 0 1 1 *', + '@annually': '0 0 1 1 *', + '@monthly': '0 0 1 * *', + '@weekly': '0 0 * * 0', + '@daily': '0 0 * * *', + '@midnight': '0 0 * * *', + '@hourly': '0 * * * *' + }; + + if (alias.startsWith('@')) { + if (!Object.prototype.hasOwnProperty.call(aliasMap, alias)) { + throw new Error(`Unsupported cron alias: ${expression}`); + } + return aliasMap[alias]; + } + + return expression; + } + + private parseField( + raw: string, + min: number, + max: number, + nameMap?: Record, + normalizeSunday = false + ): ParsedField { + const trimmed = raw.trim(); + if (!trimmed) { + throw new Error('Empty cron field detected.'); + } + + const segments = trimmed.split(','); + const collected: number[] = []; + + for (const segment of segments) { + const parsedSegment = this.parseSegment(segment.trim(), min, max, nameMap); + collected.push(...parsedSegment); + } + + const normalized = normalizeSunday + ? collected.map((value) => (value === 7 ? 0 : value)) + : collected; + + const uniqueSorted = Array.from(new Set(normalized)).sort((a, b) => a - b); + if (uniqueSorted.length === 0) { + throw new Error(`Invalid field: "${raw}"`); + } + + const expected = normalizeSunday + ? [0, 1, 2, 3, 4, 5, 6] + : this.buildRange(min, max); + + return { + raw: trimmed, + values: uniqueSorted, + set: new Set(uniqueSorted), + any: this.sameNumberList(uniqueSorted, expected) + }; + } + + private parseSegment( + segment: string, + min: number, + max: number, + nameMap?: Record + ): number[] { + const stepParts = segment.split('/'); + if (stepParts.length > 2) { + throw new Error(`Invalid step syntax: "${segment}"`); + } + + if (stepParts.length === 2) { + const base = stepParts[0]; + const step = this.parsePositiveInteger(stepParts[1], 'step'); + + let start = min; + let end = max; + + if (base !== '*') { + if (base.includes('-')) { + const [startToken, endToken] = base.split('-'); + if (!startToken || !endToken) { + throw new Error(`Invalid range: "${segment}"`); + } + + start = this.parseValue(startToken, min, max, nameMap); + end = this.parseValue(endToken, min, max, nameMap); + if (start > end) { + throw new Error(`Range start must be <= end in "${segment}"`); + } + } else { + start = this.parseValue(base, min, max, nameMap); + end = max; + } + } + + return this.buildSteppedRange(start, end, step); + } + + if (segment === '*') { + return this.buildRange(min, max); + } + + if (segment.includes('-')) { + const [startToken, endToken] = segment.split('-'); + if (!startToken || !endToken) { + throw new Error(`Invalid range: "${segment}"`); + } + + const start = this.parseValue(startToken, min, max, nameMap); + const end = this.parseValue(endToken, min, max, nameMap); + if (start > end) { + throw new Error(`Range start must be <= end in "${segment}"`); + } + + return this.buildRange(start, end); + } + + return [this.parseValue(segment, min, max, nameMap)]; + } + + private parsePositiveInteger(token: string, label: string): number { + if (!/^\d+$/.test(token)) { + throw new Error(`Invalid ${label}: "${token}"`); + } + + const value = Number.parseInt(token, 10); + if (value <= 0) { + throw new Error(`${label} must be greater than 0.`); + } + + return value; + } + + private parseValue( + token: string, + min: number, + max: number, + nameMap?: Record + ): number { + const upperToken = token.toUpperCase(); + let value: number | undefined; + + if (nameMap && Object.prototype.hasOwnProperty.call(nameMap, upperToken)) { + value = nameMap[upperToken]; + } else if (/^\d+$/.test(token)) { + value = Number.parseInt(token, 10); + } + + if (value === undefined) { + throw new Error(`Invalid token: "${token}"`); + } + + if (value < min || value > max) { + throw new Error(`Value out of range: "${token}" (allowed ${min}-${max})`); + } + + return value; + } + + private buildRange(start: number, end: number): number[] { + const length = end - start + 1; + return Array.from({ length }, (_, index) => start + index); + } + + private buildSteppedRange(start: number, end: number, step: number): number[] { + const values: number[] = []; + for (let value = start; value <= end; value += step) { + values.push(value); + } + return values; + } + + private sameNumberList(a: number[], b: number[]): boolean { + if (a.length !== b.length) { + return false; + } + + for (let index = 0; index < a.length; index += 1) { + if (a[index] !== b[index]) { + return false; + } + } + + return true; + } + + private buildHumanReadable(parsed: ParsedCron): string { + const [minuteRaw, hourRaw] = parsed.rawFields; + const timePart = this.describeTimePart(minuteRaw, hourRaw); + const datePart = this.describeDatePart(parsed); + + if (!datePart) { + return timePart; + } + + return `${timePart.slice(0, -1)} ${datePart}.`; + } + + private describeTimePart(minuteRaw: string, hourRaw: string): string { + const minuteIsSingle = this.isSingleNumber(minuteRaw); + const hourIsSingle = this.isSingleNumber(hourRaw); + + if (minuteIsSingle && hourIsSingle) { + const minute = Number.parseInt(minuteRaw, 10); + const hour = Number.parseInt(hourRaw, 10); + return `At ${this.pad2(hour)}:${this.pad2(minute)}.`; + } + + if (minuteIsSingle) { + const minute = Number.parseInt(minuteRaw, 10); + return `At minute ${minute} past ${this.describeHourExpression(hourRaw)}.`; + } + + if (hourIsSingle) { + const hour = Number.parseInt(hourRaw, 10); + return `At ${this.describeMinuteExpression(minuteRaw)} past hour ${hour}.`; + } + + if (minuteRaw === '*' && hourRaw === '*') { + return 'Every minute.'; + } + + if (minuteRaw === '*') { + return `Every minute of ${this.describeHourExpression(hourRaw)}.`; + } + + if (hourRaw === '*') { + return `At ${this.describeMinuteExpression(minuteRaw)} of every hour.`; + } + + return `At ${this.describeMinuteExpression(minuteRaw)} of ${this.describeHourExpression(hourRaw)}.`; + } + + private describeDatePart(parsed: ParsedCron): string { + const clauses: string[] = []; + + if (!parsed.month.any) { + clauses.push(`in ${this.describeMonths(parsed.month.values)}`); + } + + if (!parsed.dayOfMonth.any && !parsed.dayOfWeek.any) { + clauses.push( + `when day-of-month matches "${parsed.dayOfMonth.raw}" or weekday is ${this.describeWeekdays(parsed.dayOfWeek.values)}` + ); + } else if (!parsed.dayOfMonth.any) { + const dayDesc = this.describeDayOfMonth(parsed.dayOfMonth.raw); + const article = dayDesc.startsWith('every') ? '' : 'the '; + clauses.push(`on ${article}${dayDesc} of the month`); + } else if (!parsed.dayOfWeek.any) { + clauses.push(`on ${this.describeWeekdays(parsed.dayOfWeek.values)}`); + } + + return clauses.join(' '); + } + + private describeMinuteExpression(raw: string): string { + return this.describeUnitExpression(raw, 'minute'); + } + + private describeHourExpression(raw: string): string { + return this.describeUnitExpression(raw, 'hour'); + } + + private describeUnitExpression(raw: string, unit: 'minute' | 'hour'): string { + if (raw === '*') { + return `every ${unit}`; + } + + const wildcardStep = raw.match(/^\*\/(\d+)$/); + if (wildcardStep) { + const step = Number.parseInt(wildcardStep[1], 10); + return `every ${step} ${unit}`; + } + + const rangeStep = raw.match(/^(\d+)-(\d+)\/(\d+)$/); + if (rangeStep) { + const start = Number.parseInt(rangeStep[1], 10); + const end = Number.parseInt(rangeStep[2], 10); + const step = Number.parseInt(rangeStep[3], 10); + if (unit === 'hour') { + return `every ${step} ${unit} from ${this.pad2(start)}:00 through ${this.pad2(end)}:00`; + } + return `every ${step} ${unit} from ${start} through ${end}`; + } + + const range = raw.match(/^(\d+)-(\d+)$/); + if (range) { + const start = Number.parseInt(range[1], 10); + const end = Number.parseInt(range[2], 10); + if (unit === 'hour') { + return `every ${unit} from ${this.pad2(start)}:00 through ${this.pad2(end)}:00`; + } + return `every ${unit} from ${start} through ${end}`; + } + + if (raw.includes(',')) { + const values = raw.split(',').map((part) => part.trim()); + return `${unit}s ${this.joinWithAnd(values)}`; + } + + if (this.isSingleNumber(raw)) { + return `${unit} ${Number.parseInt(raw, 10)}`; + } + + return `${unit} pattern "${raw}"`; + } + + private describeDayOfMonth(raw: string): string { + if (this.isSingleNumber(raw)) { + const dayNum = Number.parseInt(raw, 10); + return this.toOrdinal(dayNum); + } + + const wildcardStep = raw.match(/^\*\/(\d+)$/); + if (wildcardStep) { + const step = Number.parseInt(wildcardStep[1], 10); + return `every ${this.toOrdinal(step)} day`; + } + + const rangeStep = raw.match(/^(\d+)-(\d+)\/(\d+)$/); + if (rangeStep) { + const start = Number.parseInt(rangeStep[1], 10); + const end = Number.parseInt(rangeStep[2], 10); + const step = Number.parseInt(rangeStep[3], 10); + return `every ${this.toOrdinal(step)} day from ${start} through ${end}`; + } + + const range = raw.match(/^(\d+)-(\d+)$/); + if (range) { + return `${range[1]} through ${range[2]}`; + } + + if (raw.includes(',')) { + return this.joinWithAnd(raw.split(',').map((part) => part.trim())); + } + + return raw; + } + + private describeMonths(values: number[]): string { + if (values.length >= 12) { + return 'every month'; + } + + const labels = values.map((month) => CrontabGenerator.MONTH_LABELS[month - 1]); + return this.joinWithAnd(labels); + } + + private describeWeekdays(values: number[]): string { + if (values.length >= 7) { + return 'every day of the week'; + } + + const labels = values.map((day) => CrontabGenerator.WEEKDAY_LABELS[day]); + return this.joinWithAnd(labels); + } + + private findNextRun(parsed: ParsedCron, from: Date): Date | null { + const candidate = new Date(from.getTime()); + candidate.setSeconds(0, 0); + candidate.setMinutes(candidate.getMinutes() + 1); + + const maxIterations = 4_300_000; + + for (let index = 0; index < maxIterations; index += 1) { + if (!parsed.month.set.has(candidate.getMonth() + 1)) { + this.jumpToNextAllowedMonth(candidate, parsed.month.values); + continue; + } + + if (!this.matchesDay(candidate, parsed)) { + candidate.setDate(candidate.getDate() + 1); + candidate.setHours(0, 0, 0, 0); + continue; + } + + if (!parsed.hour.set.has(candidate.getHours())) { + const nextHour = this.nextGreaterValue(parsed.hour.values, candidate.getHours()); + if (nextHour === undefined) { + candidate.setDate(candidate.getDate() + 1); + candidate.setHours(parsed.hour.values[0], 0, 0, 0); + } else { + candidate.setHours(nextHour, 0, 0, 0); + } + continue; + } + + if (!parsed.minute.set.has(candidate.getMinutes())) { + const nextMinute = this.nextGreaterValue(parsed.minute.values, candidate.getMinutes()); + if (nextMinute === undefined) { + candidate.setHours(candidate.getHours() + 1, parsed.minute.values[0], 0, 0); + } else { + candidate.setMinutes(nextMinute, 0, 0); + } + continue; + } + + return candidate; + } + + return null; + } + + private matchesDay(date: Date, parsed: ParsedCron): boolean { + const dayOfMonth = date.getDate(); + const dayOfWeek = date.getDay(); + + const domMatch = parsed.dayOfMonth.set.has(dayOfMonth); + const dowMatch = parsed.dayOfWeek.set.has(dayOfWeek); + + if (parsed.dayOfMonth.any && parsed.dayOfWeek.any) { + return true; + } + + if (parsed.dayOfMonth.any) { + return dowMatch; + } + + if (parsed.dayOfWeek.any) { + return domMatch; + } + + return domMatch || dowMatch; + } + + private jumpToNextAllowedMonth(date: Date, allowedMonths: number[]): void { + const currentMonth = date.getMonth() + 1; + const nextMonth = allowedMonths.find((month) => month > currentMonth); + + if (nextMonth === undefined) { + date.setFullYear(date.getFullYear() + 1); + date.setMonth(allowedMonths[0] - 1, 1); + } else { + date.setMonth(nextMonth - 1, 1); + } + + date.setHours(0, 0, 0, 0); + } + + private nextGreaterValue(values: number[], current: number): number | undefined { + return values.find((value) => value > current); + } + + private isSingleNumber(value: string): boolean { + return /^\d+$/.test(value); + } + + private toOrdinal(value: number): string { + const mod100 = value % 100; + if (mod100 >= 11 && mod100 <= 13) { + return `${value}th`; + } + + const mod10 = value % 10; + if (mod10 === 1) return `${value}st`; + if (mod10 === 2) return `${value}nd`; + if (mod10 === 3) return `${value}rd`; + return `${value}th`; + } + + private joinWithAnd(values: string[]): string { + if (values.length === 0) return ''; + if (values.length === 1) return values[0]; + if (values.length === 2) return `${values[0]} and ${values[1]}`; + return `${values.slice(0, -1).join(', ')}, and ${values[values.length - 1]}`; + } + + private formatDateTime(date: Date): string { + const year = date.getFullYear(); + const month = this.pad2(date.getMonth() + 1); + const day = this.pad2(date.getDate()); + const hour = this.pad2(date.getHours()); + const minute = this.pad2(date.getMinutes()); + const second = this.pad2(date.getSeconds()); + return `${year}-${month}-${day} ${hour}:${minute}:${second}`; + } + + private pad2(value: number): string { + return String(value).padStart(2, '0'); + } +} \ No newline at end of file diff --git a/src/tools/components/cubic-bezier/CubicBezier.ts b/src/tools/components/cubic-bezier/CubicBezier.ts index 61bdd7c..533e4b7 100644 --- a/src/tools/components/cubic-bezier/CubicBezier.ts +++ b/src/tools/components/cubic-bezier/CubicBezier.ts @@ -2,7 +2,7 @@ import { html, css } from 'lit'; import { customElement, state, query } from 'lit/decorators.js'; import { BaseTool } from '../../base/BaseTool'; import { renderCopyButton } from '../../../utils/util'; -import BezierEasing from 'bezier-easing'; +import { createCubicBezierEasing } from './cubicBezierUtils'; @customElement('cubic-bezier') export class CubicBezier extends BaseTool { @@ -13,8 +13,8 @@ export class CubicBezier extends BaseTool { @state() private isCopied = false; @state() private animationId: number | null = null; @state() private linearAnimationId: number | null = null; - @state() private easing: any = null; - @state() private linearEasing = BezierEasing(0, 0, 1, 1); + @state() private easing: ((x: number) => number) | null = null; + @state() private linearEasing = createCubicBezierEasing(0, 0, 1, 1); @state() private isDragging = false; @query('.bezier-line') private bezierLine!: HTMLElement; @@ -206,11 +206,10 @@ export class CubicBezier extends BaseTool { private updateEasing() { try { - this.easing = BezierEasing(this.x1, this.y1, this.x2, this.y2); + this.easing = createCubicBezierEasing(this.x1, this.y1, this.x2, this.y2); } catch (e) { - console.error("Invalid bezier values:", e); - // Use a fallback if values are invalid - this.easing = BezierEasing(0.42, 0, 0.58, 1); + console.error('Invalid bezier values:', e); + this.easing = createCubicBezierEasing(0.42, 0, 0.58, 1); } } diff --git a/src/tools/components/cubic-bezier/cubicBezierUtils.ts b/src/tools/components/cubic-bezier/cubicBezierUtils.ts new file mode 100644 index 0000000..7df438e --- /dev/null +++ b/src/tools/components/cubic-bezier/cubicBezierUtils.ts @@ -0,0 +1,137 @@ +/** + * Cubic Bezier easing solver + * -------------------------- + * For animation easing, input is x (time progress), and output is y (eased progress). + * So for each x: + * 1) solve Bx(t) = x --> find t + * 2) compute y = By(t) + * + * Solve Bx(t) = x with Newton-Raphson for speed. + * If Newton is unstable (small slope / out-of-range / poor convergence), fallback to binary subdivision (bisection). + */ + +export type EasingFunction = (x: number) => number; + +const NEWTON_ITERATIONS = 8; +const NEWTON_MIN_SLOPE = 1e-7; +const SUBDIVISION_MAX_ITERATIONS = 12; +const SUBDIVISION_PRECISION = 1e-7; + +function clamp01(v: number): number { + if (v <= 0) return 0; + if (v >= 1) return 1; + return v; +} + +/** + * Cubic bezier polynomial coefficients for: + * B(t) = ((A * t + B) * t + C) * t + * + * Derived from control points with fixed endpoints: + * P0 = 0, P1 = a1, P2 = a2, P3 = 1 + */ +function A(a1: number, a2: number): number { + return 1 - 3 * a2 + 3 * a1; +} + +function B(a1: number, a2: number): number { + return 3 * a2 - 6 * a1; +} + +function C(a1: number): number { + return 3 * a1; +} + +function calcBezier(t: number, a1: number, a2: number): number { + return ((A(a1, a2) * t + B(a1, a2)) * t + C(a1)) * t; +} + +/** + * First derivative dB/dt at t. + * Used by Newton-Raphson: + * t_{n+1} = t_n - (B(t_n) - x) / B'(t_n) + */ +function getSlope(t: number, a1: number, a2: number): number { + return 3 * A(a1, a2) * t * t + 2 * B(a1, a2) * t + C(a1); +} + +/** + * Robust fallback root-finding by bisection. + * Finds t in [a,b] where calcBezier(t, x1, x2) ~= x. + */ +function binarySubdivide(x: number, a: number, b: number, x1: number, x2: number): number { + let currentT = 0; + let currentX = 0; + + for (let i = 0; i < SUBDIVISION_MAX_ITERATIONS; i++) { + currentT = a + (b - a) / 2; + currentX = calcBezier(currentT, x1, x2) - x; + + if (Math.abs(currentX) <= SUBDIVISION_PRECISION) { + return currentT; + } + + if (currentX > 0) { + b = currentT; + } else { + a = currentT; + } + } + + return currentT; +} + +function newtonRaphsonIterate(x: number, guessT: number, x1: number, x2: number): number { + let t = guessT; + + for (let i = 0; i < NEWTON_ITERATIONS; i++) { + const slope = getSlope(t, x1, x2); + + if (Math.abs(slope) < NEWTON_MIN_SLOPE) { + return t; + } + + const currentX = calcBezier(t, x1, x2) - x; + t -= currentX / slope; + } + + return t; +} + +/** + * Create easing function equivalent to CSS cubic-bezier(x1, y1, x2, y2). + * Solves x(t)=x with Newton-Raphson, with binary subdivision fallback. + */ +export function createCubicBezierEasing(x1: number, y1: number, x2: number, y2: number): EasingFunction { + if (![x1, y1, x2, y2].every(Number.isFinite)) { + throw new Error('Invalid cubic-bezier control points'); + } + + // Clamp for resilience (UI already enforces this). + const cx1 = clamp01(x1); + const cx2 = clamp01(x2); + + return (x: number): number => { + const clampedX = clamp01(x); + + if (clampedX === 0) return 0; + if (clampedX === 1) return 1; + + let t = clampedX; // Initial guess + + // 1) Try Newton first + t = newtonRaphsonIterate(clampedX, t, cx1, cx2); + + // 2) If invalid or not accurate enough, fallback to bisection. + if (!Number.isFinite(t) || t < 0 || t > 1) { + t = binarySubdivide(clampedX, 0, 1, cx1, cx2); + } else { + const error = Math.abs(calcBezier(t, cx1, cx2) - clampedX); + if (error > SUBDIVISION_PRECISION) { + t = binarySubdivide(clampedX, 0, 1, cx1, cx2); + } + } + + return calcBezier(t, y1, y2); + }; +} \ No newline at end of file diff --git a/src/tools/components/data-format-convertor/DataFormatConvertorLogic.ts b/src/tools/components/data-format-convertor/DataFormatConvertorLogic.ts index 4522d94..4b36c95 100644 --- a/src/tools/components/data-format-convertor/DataFormatConvertorLogic.ts +++ b/src/tools/components/data-format-convertor/DataFormatConvertorLogic.ts @@ -1,47 +1,79 @@ import * as YAML from 'js-yaml'; -import { XMLParser, XMLBuilder } from 'fast-xml-parser'; -import * as TOML from 'toml'; -import { jsonToToml } from './tomlUtils'; +import { XMLBuilder, XMLParser } from 'fast-xml-parser'; +import { parse as parseToml, stringify as stringifyToml } from 'smol-toml'; let currentFormatFrom = 'json'; let currentFormatTo = 'yaml'; let currentIndentation = 2; let conversionListener: { dispose: () => void } | null = null; -function toJSON(input: string, sourceFormat: string): any { +function normalizeForToml(value: unknown): unknown { + if (value === null || value === undefined) { + return 'null'; + } + + if (Array.isArray(value)) { + return value.map(item => normalizeForToml(item)); + } + + if (value instanceof Date) { + return value; + } + + if (typeof value === 'object') { + const record = value as Record; + const normalized: Record = {}; + for (const [key, item] of Object.entries(record)) { + normalized[key] = normalizeForToml(item); + } + return normalized; + } + + return value; +} + +function toJSON(input: string, sourceFormat: string): unknown { switch (sourceFormat) { case 'json': return JSON.parse(input); case 'yaml': return YAML.load(input); - case 'xml': + case 'xml': { const parser = new XMLParser({ ignoreAttributes: false, - attributeNamePrefix: "@_" + attributeNamePrefix: '@_' }); return parser.parse(input); + } case 'toml': - return TOML.parse(input); + return parseToml(input); default: throw new Error(`Unsupported source format: ${sourceFormat}`); } } -function fromJSON(jsonObj: any, targetFormat: string): string { +function fromJSON(jsonObj: unknown, targetFormat: string): string { switch (targetFormat) { case 'json': return JSON.stringify(jsonObj, null, currentIndentation); case 'yaml': return YAML.dump(jsonObj, { lineWidth: -1, indent: currentIndentation }); - case 'xml': + case 'xml': { const builder = new XMLBuilder({ format: true, ignoreAttributes: false, indentBy: ' '.repeat(currentIndentation) }); return builder.build(jsonObj); - case 'toml': - return jsonToToml(jsonObj, { indent: ' '.repeat(currentIndentation) }); + } + case 'toml': { + if (jsonObj === null || typeof jsonObj !== 'object' || Array.isArray(jsonObj)) { + throw new Error('TOML root must be an object/table.'); + } + + const normalized = normalizeForToml(jsonObj) as Record; + return stringifyToml(normalized); + } default: throw new Error(`Unsupported target format: ${targetFormat}`); } @@ -52,13 +84,12 @@ function performConversion(inputText: string, formatFrom: string, formatTo: stri console.error('Editors not initialized'); return; } - + if (!inputText.trim()) { outputEditor.setValue(''); return; } - // No conversion needed if formats are the same if (formatFrom === formatTo) { outputEditor.setValue(inputText); return; @@ -67,56 +98,60 @@ function performConversion(inputText: string, formatFrom: string, formatTo: stri try { const jsonObj = toJSON(inputText, formatFrom); const result = fromJSON(jsonObj, formatTo); - outputEditor.setValue(result); - } catch (error: any) { + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); console.error('Error during conversion:', error); - outputEditor.setValue(`Error: ${error.message}`); - + outputEditor.setValue(`Error: ${message}`); + (window as any).vscode.postMessage({ type: 'error', - value: `Conversion error: ${error.message}` + value: `Conversion error: ${message}` }); } } -function throttle(func: (...args: any[]) => void, delay: number) { +function throttle(func: (...args: T) => void, delay: number) { let lastCall = 0; - let timeout: NodeJS.Timeout | null = null; + let timeout: ReturnType | null = null; - return function(...args: any[]) { - const now = new Date().getTime(); + return (...args: T) => { + const now = Date.now(); const timeSinceLastCall = now - lastCall; - + if (timeSinceLastCall >= delay) { lastCall = now; - return func(...args); - } else { - clearTimeout(timeout!); - timeout = setTimeout(() => { - lastCall = new Date().getTime(); - func(...args); - }, delay - timeSinceLastCall); + func(...args); + return; + } + + if (timeout) { + clearTimeout(timeout); } + + timeout = setTimeout(() => { + lastCall = Date.now(); + func(...args); + }, delay - timeSinceLastCall); }; } -const convertSmall = throttle((text) => { +const convertSmall = throttle((text: string) => { performConversion(text, currentFormatFrom, currentFormatTo); }, 0); -const convertLarge = throttle((text) => { +const convertLarge = throttle((text: string) => { performConversion(text, currentFormatFrom, currentFormatTo); }, 800); -function updateFormats(args: { formatFrom: string, formatTo: string, indentation?: number }) { +function updateFormats(args: { formatFrom: string; formatTo: string; indentation?: number }) { currentFormatFrom = args.formatFrom; currentFormatTo = args.formatTo; - + if (typeof args.indentation === 'number') { currentIndentation = args.indentation; } - + const text = inputEditor.getValue(); if (text.length > 0) { performConversion(text, currentFormatFrom, currentFormatTo); @@ -125,7 +160,7 @@ function updateFormats(args: { formatFrom: string, formatTo: string, indentation function updateIndentation(args: { indentation: number }) { currentIndentation = args.indentation; - + const text = inputEditor.getValue(); if (text.length > 0) { performConversion(text, currentFormatFrom, currentFormatTo); @@ -145,22 +180,22 @@ function setupRealTimeConversion() { if (!inputEditor || !outputEditor) { return; } - + if (conversionListener) { conversionListener.dispose(); conversionListener = null; } - + conversionListener = inputEditor.onDidChangeModelContent(() => { const text = inputEditor.getValue(); - + if (text.length > 1000) { convertLarge(text); } else { convertSmall(text); } }); - + const initialText = inputEditor.getValue(); if (initialText.length > 0) { performConversion(initialText, currentFormatFrom, currentFormatTo); @@ -177,7 +212,7 @@ function initializeWhenReady() { initializeWhenReady(); -function convert(args: { formatFrom: string, formatTo: string, indentation?: number }) { +function convert(args: { formatFrom: string; formatTo: string; indentation?: number }) { updateFormats(args); } diff --git a/src/tools/components/data-format-convertor/tomlUtils.ts b/src/tools/components/data-format-convertor/tomlUtils.ts deleted file mode 100644 index c79d1ff..0000000 --- a/src/tools/components/data-format-convertor/tomlUtils.ts +++ /dev/null @@ -1,125 +0,0 @@ -export function jsonToToml(obj: any, options: { indent?: string } = {}): string { - const indent = options.indent || ' '; - return convertObjectToToml(obj, '', indent); -} - -function convertObjectToToml(obj: any, path: string = '', indent: string = ' ', depth: number = 0): string { - if (obj === null || obj === undefined) { - return ''; - } - - let result = ''; - - // Process simple key/value pairs first - const simpleEntries: [string, any][] = []; - const objectEntries: [string, object][] = []; - const arrayEntries: [string, any[]][] = []; - - Object.entries(obj).forEach(([key, value]) => { - if (Array.isArray(value)) { - if (value.length > 0 && typeof value[0] === 'object' && value[0] !== null) { - // Array of objects needs special handling - objectEntries.push([key, value]); - } else { - arrayEntries.push([key, value]); - } - } else if (typeof value === 'object' && value !== null) { - objectEntries.push([key, value]); - } else { - simpleEntries.push([key, value]); - } - }); - - simpleEntries.forEach(([key, value]) => { - result += `${escapeKey(key)} = ${formatValue(value)}\n`; - }); - - arrayEntries.forEach(([key, value]) => { - result += `${escapeKey(key)} = ${formatArray(value)}\n`; - }); - - if ((simpleEntries.length > 0 || arrayEntries.length > 0) && objectEntries.length > 0) { - result += '\n'; - } - - objectEntries.forEach(([key, value], index) => { - if (Array.isArray(value)) { - value.forEach((item) => { - if (typeof item === 'object' && item !== null) { - const tableName = path ? `${path}.${key}` : key; - result += `[[${tableName}]]\n`; - result += convertObjectToToml(item, '', indent, depth + 1); - result += '\n'; - } - }); - } else { - const newPath = path ? `${path}.${key}` : key; - result += `[${newPath}]\n`; - result += convertObjectToToml(value, '', indent, depth + 1); - if (index < objectEntries.length - 1) { - result += '\n'; - } - } - }); - - return result; -} - -function escapeKey(key: string): string { - // If the key contains spaces or special characters, wrap it in quotes - if (/[^A-Za-z0-9_-]/.test(key)) { - return `"${key.replace(/"/g, '\\"')}"`; - } - return key; -} - -function formatValue(value: any): string { - if (value === null || value === undefined) { - return 'null'; - } - - const type = typeof value; - - switch (type) { - case 'string': - // Use triple quotes for multiline strings - if (value.includes('\n')) { - return `"""\n${value}\n"""`; - } - return `"${value.replace(/"/g, '\\"')}"`; - - case 'number': - case 'boolean': - return String(value); - - case 'object': - if (value instanceof Date) { - return value.toISOString(); - } - return 'null'; - - default: - return 'null'; - } -} - -function formatArray(arr: any[]): string { - if (arr.length === 0) { - return '[]'; - } - - // Check if all items are of the same primitive type - const firstType = typeof arr[0]; - const allSameType = arr.every(item => typeof item === firstType && - (firstType !== 'object' || item === null)); - - // Simple, single-line array for primitive values - if (allSameType && firstType !== 'object') { - const items = arr.map(item => formatValue(item)).join(', '); - return `[${items}]`; - } - - // Multi-line array for mixed types or complex values - const items = arr.map(item => formatValue(item)).join(',\n '); - return `[\n ${items}\n]`; -} \ No newline at end of file diff --git a/src/tools/components/data-unit-convertor/DataUnitConvertor.ts b/src/tools/components/data-unit-convertor/DataUnitConvertor.ts index 67df254..ed9fd09 100644 --- a/src/tools/components/data-unit-convertor/DataUnitConvertor.ts +++ b/src/tools/components/data-unit-convertor/DataUnitConvertor.ts @@ -1,5 +1,5 @@ import { html, css } from 'lit'; -import { customElement, state } from 'lit/decorators.js'; +import { customElement, state, query } from 'lit/decorators.js'; import { BaseTool } from '../../base/BaseTool'; @customElement('data-unit-convertor') @@ -10,6 +10,12 @@ export class DataUnitConvertor extends BaseTool { @state() private conversions: { unit: string; bitValue: string; bitLabel: string; byteValue: string; byteLabel: string }[] = []; @state() private alert: { type: 'error' | 'warning'; message: string } | null = null; + @query('#input') inputElement!: HTMLInputElement; + + firstUpdated() { + setTimeout(() => this.inputElement?.focus(), 0); + } + private styles = css` ${BaseTool.styles} `; @@ -67,6 +73,7 @@ export class DataUnitConvertor extends BaseTool {
this.inputElement?.focus(), 0); } private styles = css` diff --git a/src/tools/components/jwt-inspector/JwtInspector.ts b/src/tools/components/jwt-inspector/JwtInspector.ts index 054e918..3f4722b 100644 --- a/src/tools/components/jwt-inspector/JwtInspector.ts +++ b/src/tools/components/jwt-inspector/JwtInspector.ts @@ -22,18 +22,22 @@ export class JwtInspector extends BaseTool { @state() private header: Record = {}; @state() private payload: Record = {}; - @query('#input') inputArea!: HTMLTextAreaElement; - @query('#secretKey') secretKeyArea!: HTMLTextAreaElement; - - constructor() { - super(); - } + @query('#input') inputElement!: HTMLTextAreaElement; + @query('#secretKey') secretKeyElement!: HTMLTextAreaElement; async connectedCallback() { super.connectedCallback(); await this.decodeJwt(); } + firstUpdated() { + setTimeout(() => this.inputElement?.focus(), 0); + } + + constructor() { + super(); + } + private styles = css` ${BaseTool.styles} @@ -315,9 +319,8 @@ export class JwtInspector extends BaseTool { this.alertInput = null; this.verificationStatus = 'none'; - const inputTextarea = this.querySelector('#input') as HTMLTextAreaElement; - if (inputTextarea) { - inputTextarea.style.height = '28px'; + if (this.inputElement) { + this.inputElement.style.height = '28px'; } this.requestUpdate(); @@ -328,9 +331,8 @@ export class JwtInspector extends BaseTool { this.alertSecretKey = null; this.verificationStatus = 'none'; - const secretKeyTextarea = this.querySelector('#secretKey') as HTMLTextAreaElement; - if (secretKeyTextarea) { - secretKeyTextarea.style.height = '28px'; + if (this.secretKeyElement) { + this.secretKeyElement.style.height = '28px'; } this.requestUpdate(); diff --git a/src/tools/components/markdown-table-builder/MarkdownTableBuilder.ts b/src/tools/components/markdown-table-builder/MarkdownTableBuilder.ts index 4aae167..70b9b92 100644 --- a/src/tools/components/markdown-table-builder/MarkdownTableBuilder.ts +++ b/src/tools/components/markdown-table-builder/MarkdownTableBuilder.ts @@ -16,7 +16,7 @@ export class MarkdownTableBuilder extends BaseTool { @state() private isCopied = false; @state() private alert: { type: 'error' | 'warning'; message: string } | null = null; - @query('#output') private outputEl!: HTMLTextAreaElement; + @query('#output') private outputElement!: HTMLTextAreaElement; connectedCallback() { super.connectedCallback(); @@ -191,8 +191,8 @@ export class MarkdownTableBuilder extends BaseTool { this.output = markdown; setTimeout(() => { - if (this.outputEl) { - adjustTextareaHeight(this.outputEl); + if (this.outputElement) { + adjustTextareaHeight(this.outputElement); } }, 0); } diff --git a/src/tools/components/number-base-convertor/NumberBaseConvertor.ts b/src/tools/components/number-base-convertor/NumberBaseConvertor.ts index 7105bac..968ecc0 100644 --- a/src/tools/components/number-base-convertor/NumberBaseConvertor.ts +++ b/src/tools/components/number-base-convertor/NumberBaseConvertor.ts @@ -19,7 +19,16 @@ export class NumberBaseConvertor extends BaseTool { @state() private copiedBase: string | null = null; @state() private alert: { type: 'error' | 'warning'; message: string } | null = null; - @query('#input') inputTextarea!: HTMLTextAreaElement; + @query('#input') inputElement!: HTMLTextAreaElement; + + connectedCallback() { + super.connectedCallback(); + this.processInput(); + } + + firstUpdated() { + setTimeout(() => this.inputElement?.focus(), 0); + } private styles = css` ${BaseTool.styles} @@ -59,11 +68,6 @@ export class NumberBaseConvertor extends BaseTool { } `; - connectedCallback() { - super.connectedCallback(); - this.processInput(); - } - protected renderTool() { return html` @@ -288,9 +292,8 @@ export class NumberBaseConvertor extends BaseTool { this.alert = null; this.copiedBase = null; - const inputTextarea = this.querySelector('#input') as HTMLTextAreaElement; - if (inputTextarea) { - inputTextarea.style.height = '28px'; + if (this.inputElement) { + this.inputElement.style.height = '28px'; } this.requestUpdate(); diff --git a/src/tools/components/qr-code-generator/QrCodeGenerator.ts b/src/tools/components/qr-code-generator/QrCodeGenerator.ts index ae5df0a..5f66dd5 100644 --- a/src/tools/components/qr-code-generator/QrCodeGenerator.ts +++ b/src/tools/components/qr-code-generator/QrCodeGenerator.ts @@ -1,5 +1,5 @@ import { html, css } from 'lit'; -import { customElement, state } from 'lit/decorators.js'; +import { customElement, state, query } from 'lit/decorators.js'; import { BaseTool } from '../../base/BaseTool'; import { adjustTextareaHeight } from '../../../utils/util'; import * as QRCode from 'qrcode'; @@ -18,6 +18,12 @@ export class QrCodeGenerator extends BaseTool { @state() private foregroundColor = '#000000'; @state() private maskPattern = 2; + @query('#input-link') inputElement!: HTMLTextAreaElement; + + firstUpdated() { + setTimeout(() => this.inputElement?.focus(), 0); + } + private styles = css` ${BaseTool.styles} diff --git a/src/tools/components/rsa-encryption/RsaEncryption.ts b/src/tools/components/rsa-encryption/RsaEncryption.ts index 07e92c0..e653025 100644 --- a/src/tools/components/rsa-encryption/RsaEncryption.ts +++ b/src/tools/components/rsa-encryption/RsaEncryption.ts @@ -33,6 +33,10 @@ export class RsaEncryption extends BaseTool { @query('#file-input') private fileInput!: HTMLInputElement; @query('#key-file-input') private keyFileInput!: HTMLInputElement; + firstUpdated() { + setTimeout(() => this.input?.focus(), 0); + } + private styles = css` ${BaseTool.styles} `; diff --git a/src/tools/components/sha-hashing/ShaHashing.ts b/src/tools/components/sha-hashing/ShaHashing.ts index ff49ccb..a2123ad 100644 --- a/src/tools/components/sha-hashing/ShaHashing.ts +++ b/src/tools/components/sha-hashing/ShaHashing.ts @@ -27,6 +27,10 @@ export class ShaHashing extends BaseTool { @query('#output') private output!: HTMLTextAreaElement; @query('#file-input') private fileInput!: HTMLInputElement; + firstUpdated() { + setTimeout(() => this.input?.focus(), 0); + } + private styles = css` ${BaseTool.styles} `; diff --git a/src/tools/components/signature-verifier/SignatureVerifier.ts b/src/tools/components/signature-verifier/SignatureVerifier.ts index 1e6fa48..ff4c1f5 100644 --- a/src/tools/components/signature-verifier/SignatureVerifier.ts +++ b/src/tools/components/signature-verifier/SignatureVerifier.ts @@ -40,6 +40,10 @@ export class SignatureVerifier extends BaseTool { @query('#file-input') private fileInput!: HTMLInputElement; @query('#key-file-input') private keyFileInput!: HTMLInputElement; + firstUpdated() { + setTimeout(() => this.input?.focus(), 0); + } + private styles = css` ${BaseTool.styles} `; diff --git a/src/tools/components/slug-generator/SlugGenerator.ts b/src/tools/components/slug-generator/SlugGenerator.ts index 9361cd6..a090ea5 100644 --- a/src/tools/components/slug-generator/SlugGenerator.ts +++ b/src/tools/components/slug-generator/SlugGenerator.ts @@ -15,18 +15,24 @@ export class SlugGenerator extends BaseTool { @state() private alert: { type: 'error' | 'warning'; message: string } | null = null; @state() private isCopied = false; - @query('#output') outputTextarea!: HTMLTextAreaElement; - - private styles = css` - ${BaseTool.styles} - /* Minimal local styling if needed. */ - `; + @query('#input') inputElement!: HTMLTextAreaElement; + @query('#output') outputElement!: HTMLTextAreaElement; connectedCallback() { super.connectedCallback(); this.processInput(); } + firstUpdated() { + this.processInput(); + setTimeout(() => this.inputElement?.focus(), 0); + } + + private styles = css` + ${BaseTool.styles} + /* Minimal local styling if needed. */ + `; + protected renderTool() { return html` @@ -188,9 +194,9 @@ export class SlugGenerator extends BaseTool { this.output = ''; } - if (this.outputTextarea) { + if (this.outputElement) { await this.updateComplete; - adjustTextareaHeight(this.outputTextarea); + adjustTextareaHeight(this.outputElement); } } @@ -199,11 +205,8 @@ export class SlugGenerator extends BaseTool { this.output = ''; this.alert = null; - const inputTextarea = this.querySelector('#input') as HTMLTextAreaElement; - const outputTextarea = this.querySelector('#output') as HTMLTextAreaElement; - - inputTextarea.style.height = `28px`; - outputTextarea.style.height = `auto`; + this.inputElement.style.height = `28px`; + this.outputElement.style.height = `auto`; this.requestUpdate(); } } \ No newline at end of file diff --git a/src/tools/components/unicode-inspector/UnicodeInspector.ts b/src/tools/components/unicode-inspector/UnicodeInspector.ts index b640799..5b268a1 100644 --- a/src/tools/components/unicode-inspector/UnicodeInspector.ts +++ b/src/tools/components/unicode-inspector/UnicodeInspector.ts @@ -1,5 +1,5 @@ import { html, css } from 'lit'; -import { customElement, state, property } from 'lit/decorators.js'; +import { customElement, state, property, query } from 'lit/decorators.js'; import { BaseTool } from '../../base/BaseTool'; import { adjustTextareaHeight, @@ -34,12 +34,18 @@ export class UnicodeInspector extends BaseTool { @property({ type: Array }) formatOptions = FORMAT_OPTIONS; + @query('#input') inputElement!: HTMLTextAreaElement; + constructor() { super(); this.initUnicodeBlocks(); this.processInput(); } + firstUpdated() { + setTimeout(() => this.inputElement?.focus(), 0); + } + private styles = css` ${BaseTool.styles} diff --git a/src/tools/components/unix-path-convertor/UnixPathConvertor.ts b/src/tools/components/unix-path-convertor/UnixPathConvertor.ts index cccdaf7..5cefd50 100644 --- a/src/tools/components/unix-path-convertor/UnixPathConvertor.ts +++ b/src/tools/components/unix-path-convertor/UnixPathConvertor.ts @@ -18,6 +18,10 @@ export class UnixPathConvertor extends BaseTool { @query('#unix-input') unixPathTextarea!: HTMLTextAreaElement; @query('#windows-input') windowsPathTextarea!: HTMLTextAreaElement; + firstUpdated() { + setTimeout(() => this.unixPathTextarea?.focus(), 0); + } + private styles = css` ${BaseTool.styles} /* Minimal local styling if needed. */ diff --git a/src/tools/components/url-encoder/UrlEncoder.ts b/src/tools/components/url-encoder/UrlEncoder.ts index 4cf0e8d..681666a 100644 --- a/src/tools/components/url-encoder/UrlEncoder.ts +++ b/src/tools/components/url-encoder/UrlEncoder.ts @@ -9,13 +9,20 @@ import { @customElement('url-encoder') export class UrlEncoder extends BaseTool { @state() private selectedMode: 'encode' | 'decode' = 'encode'; - @state() private input = ''; + @state() private input = 'https://localhost:3000/naΓ―vetΓ©'; @state() private output = ''; @state() private alert: { type: 'error' | 'warning'; message: string } | null = null; @state() private isCopied = false; - @state() private preserveUrl = true; + @state() private encodePreserveUrl = true; + @state() private decodePreserveUrl = true; - @query('#output') outputTextarea!: HTMLTextAreaElement; + @query('#input') inputElement!: HTMLTextAreaElement; + @query('#output') outputElement!: HTMLTextAreaElement; + + firstUpdated() { + this.processInput(); + setTimeout(() => this.inputElement?.focus(), 0); + } private styles = css` ${BaseTool.styles} @@ -23,6 +30,12 @@ export class UrlEncoder extends BaseTool { `; protected renderTool() { + const isEncode = this.selectedMode === 'encode'; + const preserveCurrentMode = isEncode ? this.encodePreserveUrl : this.decodePreserveUrl; + const switchLabel = isEncode + ? 'Preserve URL structure' + : 'Fully decode URL structure'; + return html`
@@ -56,6 +69,7 @@ export class UrlEncoder extends BaseTool {
+